ui-modules/utils/yaml-editor/addon/hint/schema-matcher.js (122 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const TYPE = {
null: 'null',
boolean: 'boolean',
object: 'object',
array: 'array',
number: 'number',
string: 'string'
};
const CONDITION = {
anyOf: 'anyOf',
oneOf: 'oneOf',
allOf: 'allOf'
};
/**
* Find the related schema, based on an input JSON and the level (how deep are we in the json) and return the list of
* defined properties.
*/
export class SchemaMatcher {
constructor() {
this.schemas = new Map();
}
/**
* Register a JSON schema, to be referenced by other schemas. Will throw an exception if the field "$id" is not
* defined, as per as the specification.
*
* @param {Object} schema The schema to register
*/
registerSchema(schema) {
if (!schema.hasOwnProperty('$id')) {
throw new Error('Schema miss "$id" property');
}
this.schemas.set(schema.$id, schema);
}
/**
* Find and retrieve the properties, based on the matching schema for the given json and level. Returns a promise
* that will be resolved with an array of properties. If for the given level, a schema is defined by a operator
* such as "oneOf", "anyOf", etc, the resolved properties array will contains all properties for all schemas
* defined by this operator.
*
* The promise will be rejected if a referenced schema is encountered but not registered.
*
* @param {Object} json The JSON object
* @param {Object} schema The root schema defining the given json
* @param {Integer} level The level to get the properties from
* @return {Promise}
*/
findProperties(json, schema, level) {
return new Promise((resolve, reject) => {
try {
resolve(this._findProperties(json, schema, level, 0));
} catch (ex) {
reject(ex);
}
})
}
_findProperties(json, schema, level, currentLevel) {
// We are at the right level, let's return the resolved properties
if (currentLevel === level) {
return this._resolveProperties(schema);
}
// Retrieve the last key of the JSON to get the properties from
let keys = Object.keys(json || {});
let key = keys[keys.length - 1];
// If the schema declare a type, then we resolve the properties based on that
if (schema.hasOwnProperty('type')) {
let subSchema;
let subLevel = currentLevel;
let subJson = json[key] || {};
switch (schema.type) {
case TYPE.object:
subSchema = schema.properties[key];
break;
case TYPE.array:
subSchema = schema.items;
break;
}
if (!subSchema) {
return [];
}
if (subSchema.type === TYPE.array) {
subLevel = subLevel - 1;
}
if (subSchema.hasOwnProperty('$ref')) {
return this._findProperties(subJson, Object.assign({}, this._resolveSchema(schema, subSchema.$ref), subSchema), level, subLevel + 1);
}
let condition = Object.keys(subSchema).find(key => Object.keys(CONDITION).some(condition => condition === key));
if (condition) {
return subSchema[condition].reduce((properties, subSchema) => {
let resolvedSubSchema = subSchema;
if (subSchema.hasOwnProperty('$ref')) {
resolvedSubSchema = Object.assign({}, this._resolveSchema(schema, subSchema.$ref), subSchema);
}
return properties.concat(this._findProperties(subJson, resolvedSubSchema, level, subLevel + 1));
}, []);
}
return this._findProperties(subJson, Object.assign({definitions: schema.definitions}, subSchema), level, subLevel + 1);
}
// If we have any conditions (i.e. anyOf, oneOf, etc) we iterate over the sub-schemas, resolve them
// if we need to and concat all their properties
let condition = Object.keys(schema).find(key => Object.keys(CONDITION).some(condition => condition === key));
if (condition) {
return schema[condition].reduce((properties, subSchema) => {
let resolvedSubSchema = subSchema;
if (subSchema.hasOwnProperty('$ref')) {
resolvedSubSchema = Object.assign({}, this._resolveSchema(schema, subSchema.$ref), subSchema);
}
return properties.concat(this._findProperties(json, resolvedSubSchema, level, currentLevel));
}, []);
}
// If we arrive here, it means tht the schema does not have anything useful so return an empty array
return [];
}
_resolveSchema(baseSchema, $ref) {
if ($ref.startsWith('#/')) {
let refSchema = baseSchema;
let path = $ref.replace('#/', '').split('/');
for (let i = 0; i < path.length; i++) {
if (!refSchema.hasOwnProperty(path[i])) {
throw new Error(`Schema with id "${$ref.replace('#/', '')}" is not registered`)
}
refSchema = refSchema[path[i]];
}
return Object.assign({definitions: baseSchema.definitions}, refSchema);
}
if (!this.schemas.has($ref)) {
throw new Error(`Schema with id "${$ref}" is not registered`);
}
return this.schemas.get($ref);
}
_resolveProperties(schema) {
// If the schema has some properties, we resolve them is we need to and return them
if (schema.hasOwnProperty('properties')) {
return Object.keys(schema.properties).map(key => {
return Object.assign({'$key': key},
schema.properties[key].hasOwnProperty('$ref') ?
this._resolveSchema(schema, schema.properties[key]['$ref']) :
schema.properties[key]);
});
}
// If we have any conditions (i.e. anyOf, oneOf, etc) we iterate over the sub-schemas, resolve them
// if we need to and concat all their properties
let condition = Object.keys(schema).find(key => Object.keys(CONDITION).some(condition => condition === key));
if (condition) {
return schema[condition].reduce((properties, subSchema) => {
let resolvedSubSchema = subSchema;
if (subSchema.hasOwnProperty('$ref')) {
resolvedSubSchema = Object.assign({}, this._resolveSchema(schema, subSchema.$ref), subSchema);
}
return properties.concat(this._resolveProperties(resolvedSubSchema));
}, []);
}
return [];
}
}