@allgemein/schema-api
Version:
Library for schema api
509 lines • 20.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonSchema7Unserializer = void 0;
const lodash_1 = require("lodash");
const Constants_1 = require("./Constants");
const JsonSchema_1 = require("./JsonSchema");
const RegistryFactory_1 = require("../registry/RegistryFactory");
const Constants_2 = require("../Constants");
const IEntityRef_1 = require("../../api/IEntityRef");
const ClassRef_1 = require("../ClassRef");
const base_1 = require("@allgemein/base");
const MetadataRegistry_1 = require("../registry/MetadataRegistry");
const IParseOptions_1 = require("./IParseOptions");
const SchemaUtils_1 = require("../SchemaUtils");
const skipKeys = ['$id', 'id', 'title', 'type', 'properties', 'allOf', 'anyOf', '$schema', 'patternProperties'];
class JsonSchema7Unserializer {
constructor(opts) {
this.version = Constants_1.DRAFT_07;
this.classRefs = [];
this.fetched = {};
this.reference = {};
this.issues = [];
this.options = opts;
}
uri() {
return `http://json-schema.org/${Constants_1.DRAFT_07}/schema`;
}
async unserialize(data) {
this.data = data;
const isRoot = (0, lodash_1.get)(this.options, 'rootAsEntity', true);
const opts = { isRoot: isRoot };
IParseOptions_1.PARSE_OPTIONS_KEYS.forEach(x => {
if ((0, lodash_1.has)(this.options, x)) {
opts[x] = this.options[x];
}
});
const ret = this.options.return ? this.options.return : 'default';
return this.parse(data, opts).then(refs => {
switch (ret) {
case 'class-refs':
return this.classRefs;
case 'entity-refs':
return this.classRefs.filter(x => x.hasEntityRef()).map(x => x.getEntityRef());
default:
return refs;
}
});
}
/**
* Return namespace if set in passed options or if used in json-schema object.
* If no match return default namespace
*/
getNamespace() {
return (0, lodash_1.get)(this.options, 'namespace', (0, lodash_1.get)(this.data, 'namespace', Constants_2.DEFAULT_NAMESPACE));
}
/**
* Get registry containing elements
*/
getRegistry(ns = null) {
return RegistryFactory_1.RegistryFactory.get(ns ? ns : this.getNamespace());
}
getIssues() {
return this.issues;
}
/**
* If collectors are defined use them before default
*
*
* @param type
* @param key
* @param data
* @param options
* @private
*/
collectOptions(key, data, options = {}) {
const type = (0, lodash_1.get)(options, 'metaType', Constants_2.METATYPE_ENTITY);
let ret = null;
if (!(0, lodash_1.isUndefined)(data[key]) || !(0, lodash_1.isNull)(data[key])) {
ret = {};
ret[key] = data[key];
}
if ((0, lodash_1.has)(this.options, 'collector') && this.options.collector.length > 0) {
const methods = this.options.collector.filter(x => x.type === type && x.key === key);
if (methods) {
ret = (0, lodash_1.assign)(ret, ...methods.map(e => e.fn.apply(null, [key, data, options])));
}
}
return ret;
}
getClassRef(className, namespace) {
if (!namespace) {
namespace = this.getNamespace();
}
return ClassRef_1.ClassRef.get(className, namespace);
}
async parse(data, options = null) {
let ret = null;
if (data.type) {
if (data.type === Constants_2.T_OBJECT) {
ret = await this.parseTypeObject(data, options);
if (ret) {
if (options.$ref) {
// finish reference if waiting
this.reference[options.$ref] = ret;
delete options.$ref;
}
if (this.hasProperties(data)) {
await this.parseProperties(ret, data.properties, options);
}
if (this.hasPatternProperties(data)) {
options.isPattern = true;
await this.parseProperties(ret, data.patternProperties, options);
}
}
}
else {
throw new Error('type is not supported at this place');
}
}
else if (data.$ref) {
return this.parseRef(data, options);
}
else if (data.anyOf && options.isRoot) {
// loading multi schema
ret = [];
for (const anyEntry of data.anyOf) {
ret.push(await this.parse(anyEntry, options));
}
}
else if (data.definitions && (0, lodash_1.keys)(data.definitions).length === 0) {
// doing nothing no entries found
}
else {
throw new Error('no valid schema element for further parse, key with name type or $ref must be present.');
}
return ret;
}
async parseRef(data, options) {
// refers
let ret = null;
if ((0, lodash_1.has)(this.reference, data.$ref)) {
ret = this.reference[data.$ref];
}
else {
const res = await this.followRef(data.$ref, this.data);
const opts = (0, lodash_1.clone)(options);
opts.$ref = data.$ref;
if (!opts.className) {
opts.className = this.getClassNameFromRef(data.$ref);
}
ret = await this.parse(res, opts);
}
return ret;
}
getDefinitionsKey(data) {
for (const k of (0, lodash_1.keys)(data)) {
if (k !== 'properties' && (0, lodash_1.isObjectLike)(data[k])) {
const exists = (0, lodash_1.keys)(data[k]).find(x => (0, lodash_1.has)(data[k][x], 'type'));
if (exists) {
return k;
}
}
}
return 'definition';
}
async followRef($ref, data) {
let [addr, anchor] = $ref.split('#');
if (!anchor) {
anchor = '';
}
if ((0, lodash_1.isEmpty)(addr)) {
// local
if ((0, lodash_1.isEmpty)(anchor)) {
if (data.$ref) {
return this.followRef(data.$ref, data);
}
else {
return data;
}
}
else if (/^\//.test(anchor)) {
// starts with path
const dottedPath = anchor.substr(1).replace(/\//, '.');
const entry = (0, lodash_1.get)(data, dottedPath, null);
if (entry) {
return entry;
}
}
else {
// refs to local id
const refHashed = '#' + anchor;
if (data.$id === refHashed) {
return data;
}
else {
const defKey = this.getDefinitionsKey(data);
if (data[defKey]) {
for (const k of (0, lodash_1.keys)(data[defKey])) {
// @ts-ignore
const x = data[defKey][k];
if (x && x.$id && x.$id === refHashed) {
return x;
}
}
}
}
}
}
else {
// remote fetch url
if (!this.fetched[addr]) {
this.fetched[addr] = await JsonSchema_1.JsonSchema.request(addr, { cwd: this.options.cwd });
}
return this.followRef('#' + anchor, this.fetched[addr]);
}
// TODO create exception
throw new Error($ref + ' not found');
}
async parseProperties(classRef, properties, options = null) {
const _classRef = (0, IEntityRef_1.isEntityRef)(classRef) ? classRef.getClassRef() : classRef;
const propertyNames = (0, lodash_1.keys)(properties);
for (const propertyName of propertyNames) {
await this.parseProperty(_classRef, propertyName, properties[propertyName], options);
}
}
async parseProperty(classRef, propertyName, data, options = null) {
const _classRef = (0, IEntityRef_1.isEntityRef)(classRef) ? classRef.getClassRef() : classRef;
const propRefExits = _classRef.getRegistry()
.find(Constants_2.METATYPE_PROPERTY, (x) => x.getClassRef() === _classRef && x.name === propertyName);
const propOptions = {
metaType: Constants_2.METATYPE_PROPERTY,
propertyName: propertyName,
target: _classRef.getClass(true),
// default type is object
type: Constants_2.T_OBJECT,
};
if (options.isPattern) {
propOptions[Constants_2.K_PATTERN_PROPERTY] = true;
}
if ((0, lodash_1.isObjectLike)(data)) {
const parseOptions = {
isProperty: true,
sourceRef: _classRef,
propertyName: propertyName,
isRoot: false,
metaType: 'property',
// isPattern: options.isPattern ? true : false
};
const dataPointer = data;
let collectOptions = {};
collectOptions = this.collectAndProcess(dataPointer, collectOptions, ['type', '$ref', '$schema', '$id'], parseOptions);
(0, lodash_1.assign)(propOptions, collectOptions);
if (dataPointer.$ref) {
const ref = await this.parse(dataPointer, parseOptions);
propOptions.type = ref;
if (options.isPattern) {
propOptions[Constants_2.K_PATTERN_PROPERTY] = true;
}
}
else if (dataPointer.type) {
await this.onTypes(dataPointer, propOptions, parseOptions);
}
}
else {
// boolean
throw new base_1.NotSupportedError('passed boolean value as definition not supported');
}
if (!propRefExits || (0, lodash_1.get)(this.options, 'forcePropertyRefCreation', false)) {
// remove properties key if exists
delete propOptions.properties;
try {
_classRef.getRegistry().create(Constants_2.METATYPE_PROPERTY, propOptions);
}
catch (e) {
this.issues.push({ msg: e.message, level: 'error' });
}
}
else if (propRefExits) {
propRefExits.setOptions(propOptions);
}
}
hasProperties(datapointer) {
return (0, lodash_1.has)(datapointer, 'properties');
}
hasPatternProperties(datapointer) {
return (0, lodash_1.has)(datapointer, 'patternProperties');
}
async onTypes(dataPointer, propOptions, options) {
let type = dataPointer.type;
if ((0, lodash_1.isString)(type)) {
type = type.toLowerCase();
}
switch (type) {
case Constants_2.T_STRING:
const res = this.onTypeString(dataPointer);
(0, lodash_1.assign)(propOptions, res);
break;
case 'boolean':
propOptions.type = 'boolean';
break;
case 'integer':
propOptions.type = 'number';
break;
case 'number':
propOptions.type = 'number';
break;
case Constants_2.T_OBJECT:
propOptions.type = Constants_2.T_OBJECT;
if (this.hasProperties(dataPointer) || this.hasPatternProperties(dataPointer)) {
propOptions.type = await this.parse(dataPointer, options);
}
break;
case Constants_2.T_ARRAY:
// check items
if (dataPointer.minItems || dataPointer.maxItems) {
propOptions.cardinality = {
min: (0, lodash_1.get)(dataPointer, 'minItems', 0),
max: (0, lodash_1.get)(dataPointer, 'maxItems', 0),
};
}
else {
propOptions.cardinality = 0;
}
if (dataPointer.items) {
if ((0, lodash_1.isArray)(dataPointer.items)) {
throw new base_1.NotSupportedError('tuple values for array is not supported');
}
else if ((0, lodash_1.isObjectLike)(dataPointer.items)) {
options.asArray = true;
const items = dataPointer.items;
if (items.$ref) {
propOptions.type = await this.parse(items, options);
}
else if (items.type === Constants_2.T_OBJECT) {
propOptions.type = items.type;
if (this.hasProperties(items) || this.hasPatternProperties(items)) {
propOptions.type = await this.parse(items, options);
}
}
else {
propOptions.type = items.type;
}
if (options.isPattern) {
propOptions[Constants_2.K_PATTERN_PROPERTY] = true;
}
}
}
break;
case 'null':
throw new base_1.NotSupportedError('null value is not supported for property');
}
}
onTypeString(dataPointer) {
const propOptions = {};
propOptions.type = Constants_2.T_STRING;
// or date check format
if (dataPointer.format) {
propOptions.format = dataPointer.format;
switch (dataPointer.format) {
case 'date-time':
// set date type
propOptions.type = 'date';
break;
case 'date':
// set date type
propOptions.type = 'date';
break;
}
}
return propOptions;
}
async parseInherits(classRef, data, key) {
if ((0, lodash_1.has)(data, key)) {
const values = data[key];
if ((0, lodash_1.isArray)(values)) {
for (const inheritEntry of values) {
const extend = await this.parse(inheritEntry, { isRoot: false });
classRef.addExtend(extend);
}
}
else {
throw new base_1.NotSupportedError('only array is allowed for ' + key);
}
}
}
async parseTypeObject(data, options = null) {
let ret = null;
const id = (0, lodash_1.get)(data, '$id', (0, lodash_1.get)(data, 'id', null));
let metaType = options.isRoot || !(0, lodash_1.isEmpty)(id) ? Constants_2.METATYPE_ENTITY : Constants_2.METATYPE_CLASS_REF;
const title = (0, lodash_1.get)(data, 'title', null);
// get namespace or override
const namespace = !(0, lodash_1.get)(this.options, 'skipNamespace', false) ?
(0, lodash_1.get)(data, '$' + Constants_2.METATYPE_NAMESPACE, this.getNamespace()) : this.getNamespace();
// check if class ref exists else create one or recreate if parse options are set
let classRef = null;
let className = null;
if (options) {
if (options.className) {
className = options.className;
}
if (options.ref) {
classRef = options.ref;
className = options.ref.name;
}
}
// title data override passed className
if (title) {
if (!className || !(0, lodash_1.get)(this.options, 'ignoreDeclared', false)) {
className = (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(title));
}
}
let entityName = metaType === Constants_2.METATYPE_ENTITY && id ? id.replace(/^#/, '') : className;
if (!className && options?.isProperty && options.propertyName) {
if ((0, lodash_1.get)(this.options, 'prependClass', false)) {
className = [options.sourceRef.name, options.propertyName].map(x => (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(x))).join('');
}
else {
className = (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(options.propertyName));
}
// pass property name as entity name if the object is declared as a property
entityName = className;
}
if (className && !classRef) {
const fn = (0, lodash_1.get)(this.options, 'forceClassRefCreation', false) ? SchemaUtils_1.SchemaUtils.clazz(className) : className;
classRef = this.getClassRef(fn, namespace);
}
if (!classRef) {
metaType = Constants_2.METATYPE_CLASS_REF;
classRef = this.getClassRef(SchemaUtils_1.SchemaUtils.clazzAnonymous(), namespace);
}
// TODO check extentions!
for (const inheritsKey of ['allOf', 'anyOf']) {
if ((0, lodash_1.has)(data, inheritsKey)) {
await this.parseInherits(classRef, data, inheritsKey);
}
}
if (classRef) {
// a named class ref exists
if (!this.classRefs.find(x => x === classRef)) {
this.classRefs.push(classRef);
}
const clazz = classRef.getClass(true);
const refOptions = {
name: entityName,
namespace: this.getNamespace(),
target: clazz
};
const collectorOptions = (0, lodash_1.clone)(options);
collectorOptions.metaType = metaType;
collectorOptions.ref = classRef;
this.collectAndProcess(data, refOptions, skipKeys, collectorOptions);
metaType = collectorOptions.metaType;
let entityOptions = MetadataRegistry_1.MetadataRegistry.$().find(metaType, (x) => x.target === clazz);
if (entityOptions) {
// merge existing
(0, lodash_1.assign)(entityOptions, refOptions);
}
else {
entityOptions = refOptions;
refOptions.metaType = Constants_2.METATYPE_CLASS_REF;
const existingOptions = classRef.getOptions();
classRef.setOptions((0, lodash_1.defaults)(refOptions, existingOptions));
}
if (metaType === Constants_2.METATYPE_ENTITY) {
ret = this.getRegistry(namespace).find(metaType, (x) => x.getClassRef() === classRef);
if (!ret || (0, lodash_1.get)(this.options, 'forceEntityRefCreation', false)) {
try {
ret = this.getRegistry(namespace).create(metaType, entityOptions);
}
catch (e) {
this.issues.push({ msg: e.message, level: 'error' });
}
}
}
else {
ret = classRef;
}
}
else {
throw new base_1.NotYetImplementedError();
}
return ret;
}
collectAndProcess(data, collectingObject, skipKeys, collectorOptions) {
const type = (0, lodash_1.get)(collectorOptions, 'metaType', Constants_2.METATYPE_ENTITY);
const _keys = (0, lodash_1.keys)(data);
for (const key of _keys) {
const entry = this.collectOptions(key, data, collectorOptions);
if (skipKeys.includes(key)) {
continue;
}
if (entry && (0, lodash_1.isObjectLike)(entry)) {
collectingObject = (0, lodash_1.assign)(collectingObject, entry);
}
}
if ((0, lodash_1.has)(this.options, 'collector') && this.options.collector.length > 0) {
const methods = this.options.collector.filter(x => x.type === type && (0, lodash_1.isUndefined)(x.key));
if (methods.length > 0) {
collectingObject = (0, lodash_1.assign)(collectingObject, ...methods.map(e => e.fn.apply(null, [null, data, collectorOptions])));
}
}
return collectingObject;
}
getClassNameFromRef(data) {
return data.split(/#|\//).pop();
}
}
exports.JsonSchema7Unserializer = JsonSchema7Unserializer;
//# sourceMappingURL=JsonSchema7Unserializer.js.map