From d81eddd7a68ddf3f43b622bd0eeef315ae33dccd Mon Sep 17 00:00:00 2001 From: Martin Karkowski Date: Mon, 21 Mar 2022 15:56:06 +0100 Subject: [PATCH] Adding a Helper to idenfitify valid json-schemas. --- lib/helpers/jsonSchemaMethods.spec.ts | 264 ++++++++++++++++++++++++++ lib/helpers/jsonSchemaMethods.ts | 89 +++++++++ 2 files changed, 353 insertions(+) create mode 100644 lib/helpers/jsonSchemaMethods.spec.ts diff --git a/lib/helpers/jsonSchemaMethods.spec.ts b/lib/helpers/jsonSchemaMethods.spec.ts new file mode 100644 index 0000000..281489e --- /dev/null +++ b/lib/helpers/jsonSchemaMethods.spec.ts @@ -0,0 +1,264 @@ +/** + * @author Martin Karkowski + * @email m.karkowski@zema.de + */ + +import { assert } from "chai"; +import { describe, it } from "mocha"; +import { INopeDescriptor } from "../index.browser"; +import { isJsonSchema } from "./jsonSchemaMethods"; + +describe("jsonSchemaMethods", function () { + // Describe the required Test: + + const tests: [string, INopeDescriptor, boolean][] = [ + [ + "function", + { + type: "function", + inputs: [ + { + name: "parameter", + schema: { + type: "string", + }, + }, + ], + }, + false, + ], + [ + "nested-function", + { + type: "object", + properties: { + function: { + type: "function", + inputs: [], + }, + }, + }, + false, + ], + [ + "array => items", + { + $id: "https://example.com/arrays.schema.json", + $schema: "https://json-schema.org/draft/2020-12/schema", + description: + "A representation of a person, company, organization, or place", + type: "object", + properties: { + fruits: { + type: "array", + items: { + type: "function", + }, + }, + vegetables: { + type: "array", + items: { $ref: "#/$defs/veggie" }, + }, + }, + }, + false, + ], + [ + "array => additionalItems", + { + $id: "https://example.com/arrays.schema.json", + $schema: "https://json-schema.org/draft/2020-12/schema", + description: + "A representation of a person, company, organization, or place", + type: "object", + properties: { + fruits: { + type: "array", + items: { + type: "string", + }, + additionalItems: { + type: "function", + }, + }, + vegetables: { + type: "array", + items: { $ref: "#/$defs/veggie" }, + }, + }, + }, + false, + ], + [ + "anyOf", + { + $id: "https://example.com/arrays.schema.json", + $schema: "https://json-schema.org/draft/2020-12/schema", + description: + "A representation of a person, company, organization, or place", + type: "object", + properties: { + fruits: { + type: "array", + items: { + type: "string", + }, + }, + }, + anyOf: [ + { + type: "string", + }, + { + type: "function", + }, + ], + }, + false, + ], + [ + "allOf", + { + $id: "https://example.com/arrays.schema.json", + $schema: "https://json-schema.org/draft/2020-12/schema", + description: + "A representation of a person, company, organization, or place", + type: "object", + properties: { + fruits: { + type: "array", + items: { + type: "string", + }, + }, + }, + allOf: [ + { + type: "function", + }, + ], + }, + false, + ], + [ + "oneOf", + { + $id: "https://example.com/arrays.schema.json", + $schema: "https://json-schema.org/draft/2020-12/schema", + description: + "A representation of a person, company, organization, or place", + type: "object", + properties: { + fruits: { + type: "array", + items: { + type: "string", + }, + }, + }, + oneOf: [ + { + type: "function", + }, + ], + }, + false, + ], + ]; + + describe("isJsonSchema", function () { + for (const [name, test, expectedResult] of tests) { + it(name, function () { + const result = isJsonSchema(test); + assert.isTrue(result == expectedResult, "Test Failed"); + }); + } + + it("detect-nope-schema -> true", function () { + const shouldFalse: INopeDescriptor[] = [ + { + $id: "https://example.com/person.schema.json", + $schema: "https://json-schema.org/draft/2020-12/schema", + title: "Person", + type: "object", + properties: { + firstName: { + type: "string", + description: "The person's first name.", + }, + lastName: { + type: "string", + description: "The person's last name.", + }, + age: { + description: + "Age in years which must be equal to or greater than zero.", + type: "integer", + minimum: 0, + }, + }, + }, + { + $id: "https://example.com/geographical-location.schema.json", + $schema: "https://json-schema.org/draft/2020-12/schema", + title: "Longitude and Latitude Values", + description: "A geographical coordinate.", + required: ["latitude", "longitude"], + type: "object", + properties: { + latitude: { + type: "number", + minimum: -90, + maximum: 90, + }, + longitude: { + type: "number", + minimum: -180, + maximum: 180, + }, + }, + }, + { + $id: "https://example.com/arrays.schema.json", + $schema: "https://json-schema.org/draft/2020-12/schema", + description: + "A representation of a person, company, organization, or place", + type: "object", + properties: { + fruits: { + type: "array", + items: { + type: "string", + }, + }, + vegetables: { + type: "array", + items: { $ref: "#/$defs/veggie" }, + }, + }, + definitions: { + veggie: { + type: "object", + required: ["veggieName", "veggieLike"], + properties: { + veggieName: { + type: "string", + description: "The name of the vegetable.", + }, + veggieLike: { + type: "boolean", + description: "Do I like this vegetable?", + }, + }, + }, + }, + }, + ]; + + for (const test of shouldFalse) { + const result = isJsonSchema(test); + assert.isTrue(result, "Test should be true"); + } + }); + }); +}); diff --git a/lib/helpers/jsonSchemaMethods.ts b/lib/helpers/jsonSchemaMethods.ts index 5e5e01d..27d687c 100644 --- a/lib/helpers/jsonSchemaMethods.ts +++ b/lib/helpers/jsonSchemaMethods.ts @@ -7,6 +7,7 @@ */ import { IJsonSchema } from "../types/IJSONSchema"; +import { INopeDescriptor } from "../types/nope"; import { flattenObject, rgetattr, rsetattr, SPLITCHAR } from "./objectMethods"; /** @@ -59,3 +60,91 @@ export function flattenSchema(schema: IJsonSchema) { export function schemaGetDefinition(schema: IJsonSchema, reference: string) { return rgetattr(schema, reference.replace("#/", ""), null, "/"); } + +const _isNopeDescriptor: Array< + [keyof INopeDescriptor, (value: any) => boolean] +> = [ + ["type", (value) => value === "function"], + ["inputs", (value) => typeof value === "object"], + ["outputs", (value) => 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: INopeDescriptor | IJsonSchema): boolean { + 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] as INopeDescriptor) + ) { + 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; +}