UNPKG

nope-js-browser

Version:

NoPE Runtime for the Browser. For nodejs please use nope-js-node

328 lines (327 loc) 12.1 kB
/** * @author Martin Karkowski * @email m.karkowski@zema.de * @desc [description] */ import { deepEqual, flattenObject, rgetattr, rsetattr, SPLITCHAR, } from "./objectMethods"; import { replaceAll } from "./stringMethods"; /** * Function to Flatten a JSON-Schema. * @param schema */ export function nestSchema(schema) { let counter = 10000; let flattenSchema = flattenObject(schema); const getRefKeys = (flattenSchema) => { const relevantKeys = []; for (const [key, value] of flattenSchema) { if (key.endsWith("$ref")) { relevantKeys.push({ schemaPath: key, searchPath: value.replace("#/", "").replace("/", SPLITCHAR), }); } } return relevantKeys; }; let refs = getRefKeys(flattenSchema); while (refs.length > 0) { counter--; if (counter === 0) { throw Error("Max amount of Recursions performed"); } for (const ref of refs) { const subSchema = rgetattr(schema, ref.searchPath, null, "."); rsetattr(schema, ref.schemaPath.replace(".$ref", ""), subSchema); } flattenSchema = flattenObject(schema); refs = getRefKeys(flattenSchema); } return schema; } /** * Function to get a Schemas Definition * @param schema the JSON-Schema * @param reference the path of the relevant definition. */ export function schemaGetDefinition(schema, reference) { return rgetattr(schema, reference.replace("#/", ""), null, "/"); } /** * A Helper to flatten a schema. This will add additional "$ref" items instead of nested items. * This will perhaps help to reduce the amount of data. * * @author M.Karkowski * @export * @param {IJsonSchema} schema The Schema used as input. This will be flattend * @param {string} [prePath="root"] The Name of the Schema. It is used for the "main" definition * @param {string} [postPath=""] An additional path for every item which is added to the name. example "msg" * @param {*} [splitChar=SPLITCHAR] The char to split the elements. * @param {IJsonSchema} [definitions={ definitions: {} }] A Set of defintions to be used. * @return {IJsonSchema} The Adapted Item. */ export function flattenSchema(schema, prePath = "root", postPath = "", splitChar = SPLITCHAR, definitions = { definitions: {} }) { const _postPath = postPath ? splitChar + postPath : postPath; if (Array.isArray(schema.items)) { for (const [idx, item] of schema.items.entries()) { // We only want to adapt more complex datatypes. if (item.type === "object" || item.type === "array") { const ref = prePath + splitChar + "items" + splitChar + idx.toString(); definitions = flattenSchema(item, ref, postPath, splitChar, definitions); schema.items[idx] = { type: item.type, $ref: ref + _postPath, }; } } } else if (schema.items) { if (schema.items.type === "object" || schema.items.type === "array") { const ref = prePath + splitChar + "items"; definitions = flattenSchema(schema.items, ref, postPath, splitChar, definitions); schema.items = { type: schema.items.type, $ref: ref + _postPath, }; } } if (typeof schema.additionalItems === "object" && (schema.additionalItems.type === "object" || schema.additionalItems.type === "array")) { const ref = prePath + splitChar + "additionalItems"; definitions = flattenSchema(schema.additionalItems, ref, postPath, splitChar, definitions); schema.additionalProperties = { type: schema.additionalItems.type, $ref: ref, }; } for (const key in schema.properties || {}) { const item = schema.properties[key]; if (item.type === "object" || item.type === "array") { const ref = prePath + splitChar + key; definitions = flattenSchema(item, ref, postPath, splitChar, definitions); schema.properties[key] = { type: item.type, $ref: ref + _postPath, }; } } if (typeof schema.additionalProperties === "object" && (schema.additionalProperties.type === "object" || schema.additionalProperties.type === "array")) { const ref = prePath + splitChar + "additionalProperties"; definitions = flattenSchema(schema.additionalProperties, ref, postPath, splitChar, definitions); schema.additionalProperties = { type: schema.additionalProperties.type, $ref: ref, }; } if (schema.oneOf) { for (const [idx, item] of schema.oneOf.entries()) { if (item.type === "object" || item.type === "array") { const ref = prePath + splitChar + "oneOf" + splitChar + idx.toString(); definitions = flattenSchema(item, ref, postPath, splitChar, definitions); schema.items[idx] = { type: item.type, $ref: ref, }; } } } if (schema.allOf) { for (const [idx, item] of schema.allOf.entries()) { if (item.type === "object" || item.type === "array") { const ref = prePath + splitChar + "allOf" + splitChar + idx.toString(); definitions = flattenSchema(item, ref, postPath, splitChar, definitions); schema.items[idx] = { type: item.type, $ref: ref, }; } } } if (schema.anyOf) { for (const [idx, item] of schema.anyOf.entries()) { if (item.type === "object" || item.type === "array") { const ref = prePath + splitChar + "anyOf" + splitChar + idx.toString(); definitions = flattenSchema(item, ref, postPath, splitChar, definitions); schema.items[idx] = { type: item.type, $ref: ref, }; } } } definitions.definitions[prePath + splitChar + postPath] = schema; return definitions; } /** * Helper to generate a name for the combined schemas. * * @author M.Karkowski * @param {IJsonSchema} schema The base schema, containing all definitions. * @param {string[]} names The names of the defintions, which should be combined. * @return {string} The combined name. */ function _defaultCombiner(schema, names) { for (const name of names) { const item = schema.definitions[name]; if (item.title) { return item.title; } } return names[0]; } /** * Helper Function to reduce the Schema and remove multiple definitions. * @param schema * @param getName * @returns */ export function reduceSchema(schema, getName = _defaultCombiner) { if (schema.definitions) { /** * Helper to find equals definitions. * @param candidateName Name of the candidate to use as reference * @returns */ function _findDuplicates(candidateName) { const equals = []; const candidate = schema.definitions[candidateName]; for (const name in schema.definitions) { const item = schema.definitions[name]; if (candidateName === name) { continue; } if (deepEqual(item, candidate)) { equals.push(name); } } if (equals.length > 0) { equals.push(candidateName); } return equals; } // We want to check every definition, whether there exists a // a pair that matches. let toTest = Object.keys(schema.definitions); // As long as we have items to test, we try to look for enties, // that are equal. while (toTest.length) { const candidateName = toTest.pop(); // Therefore we look for equal elements. const equalDefintionIds = _findDuplicates(candidateName); // If we found some, we define the new name and remove double enteties. if (equalDefintionIds.length) { const newName = getName(schema, equalDefintionIds); // Our first loop will remove the double enteties: let first = true; for (const name of equalDefintionIds) { if (first) { continue; } first = false; delete schema.default[name]; } // The Second loop is used to update the references. for (const name of equalDefintionIds) { schema = JSON.parse(replaceAll(JSON.stringify(schema), JSON.stringify(name), JSON.stringify(newName))); } toTest = Object.keys(schema.definitions); } } return schema; } else { return schema; } } const _isNopeDescriptor = [ [ "type", (value) => { return value === "function"; }, ], [ "inputs", (value) => { return typeof value === "object"; }, ], [ "outputs", (value) => { return typeof value === "object"; }, ], ]; /** * A Helper Function, to test, if the given schema is a JSON Schema or whether it contains a method description * * @param { INopeDescriptor | IJsonSchema } schema The Schema to Test * @returns {boolean} */ export function isJsonSchema(schema) { for (const [attr, test] of _isNopeDescriptor) { if (test(schema[attr])) { return false; } } // Object-Related Test-Cases. if (schema.type == "object" || (Array.isArray(schema.type) && schema.type.includes("object"))) { if (schema.properties) { for (const key in schema.properties) { if (!isJsonSchema(schema.properties[key])) { return false; } } } if (schema.patternProperties) { for (const key in schema.patternProperties) { if (!isJsonSchema(schema.patternProperties[key])) { return false; } } } if (schema.dependencies) { for (const key in schema.dependencies) { if (typeof schema.dependencies[key] !== "string" && !isJsonSchema(schema.dependencies[key])) { return false; } } } } // for (const key of ["allOf", "anyOf", "oneOf"]) { if (schema[key]) { for (const subSchema of schema[key]) { if (!isJsonSchema(subSchema)) { return false; } } } } if (schema.type == "array" || (Array.isArray(schema.type) && schema.type.includes("array"))) { if (Array.isArray(schema.items)) { for (const key in schema.items) { if (!isJsonSchema(schema.items[key])) { return false; } } } else if (!isJsonSchema(schema.items)) { return false; } // Test the Additional Items. if (typeof schema.additionalItems === "object") { if (!isJsonSchema(schema.additionalItems)) { return false; } } } return true; }