From 8937d3d926acb806d4ea13b7260faaf46afefb78 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Fri, 13 Aug 2021 10:27:59 +0200 Subject: [PATCH 1/3] feat: filter only complex types --- README.md | 2 +- src/index.ts | 86 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index ff05478..d374f24 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ -Referencer is a package that exports a single function - a function that accepts and returns a JSON Schema. The returned schema is 'flat', as in, any subschemas of the schema have been converted into $refs. Further, any of the subschemas' subschema are also $reffed, recurively until everything is a $ref, and the definition section is fully populated. +Referencer is a package that exports a single function - a function that accepts and returns a JSON Schema. The returned schema is 'flat', as in, any subschemas of the schema have been converted into $refs. Further, any of the subschemas' subschema are also $reffed, recursively until everything is a $ref, and the definition section is fully populated. The input schema may have refs, but the refs must already be in the definitions section. The input schema, as well as all of its subschemas must have titles. Their titles must also be unique for their content. We would like to use $id, but it has special meaning and therefor title is used as the unique identifier on-which schemas will be referenced. diff --git a/src/index.ts b/src/index.ts index 74f38c1..ce8ebec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,24 @@ import traverse from "@json-schema-tools/traverse"; -import { JSONSchema } from "@json-schema-tools/meta-schema"; +import { JSONSchema, JSONSchemaObject } from "@json-schema-tools/meta-schema"; const deleteAllProps = (o: { [k: string]: any }) => { Object.keys(o) .forEach((k) => { delete o[k]; }); }; +const isObject = (obj?: JSONSchema) => { + return obj && obj === Object(obj) +}; + +const isComplex = (obj: JSONSchemaObject) => { + // TODO: evaluate if `array` should be considered complex + return (obj.type === 'object' && typeof obj.properties === 'object'); +}; + +const hasReference = (obj: JSONSchemaObject) => { + return typeof obj.$ref === 'string' && obj.$ref.length > 0 && obj.$ref.indexOf("#") !== -1; +}; + export const stringifyCircular = (obj: any) => { const cache: any[] = []; return JSON.stringify(obj, (key, value) => { @@ -52,6 +65,14 @@ export class NoTitleError implements Error { } } +export interface Options { + /** + * By setting this to `true`, only complex types (`object`) will be replaced. + * @default false + */ + onlyComplex?: boolean; +} + /** * Returns the schema where all subschemas have been replaced with $refs and added to definitions. * @@ -60,6 +81,7 @@ export class NoTitleError implements Error { * exists in the input schema's definitions object. * * @param s The schema to create references for (ie 'flatten' it) + * @param options The custom options * * @returns input schema where subschemas are turned into refs (recursively) * @@ -67,55 +89,63 @@ export class NoTitleError implements Error { * @category SchemaImprover * */ -export default function referencer(s: JSONSchema): JSONSchema { +export default function referencer(s: JSONSchema, options: Options = {}): JSONSchema { + const { + onlyComplex = false, + } = options; const definitions: any = {}; traverse( s, (subSchema: JSONSchema, isRootCycle: boolean) => { let t = ""; - if (isRootCycle && subSchema !== true && subSchema !== false) { - if (subSchema.$ref) { - const title = subSchema.$ref.replace("#/definitions/", ""); + if (!isObject(subSchema)) { // For schema that is boolean + if (subSchema === true) { + t = "AlwaysTrue"; + definitions[t as string] = true; + } else if (subSchema === false) { + t = "AlwaysFalse"; + definitions[t as string] = false; + } + return subSchema; + } + + // Otherwise it is a object schema + const objectSchema = subSchema as JSONSchemaObject; + if (isRootCycle) { + if (objectSchema.$ref) { + const title = objectSchema.$ref.replace("#/definitions/", ""); const hasDefForRef = definitions[title]; if (hasDefForRef === undefined) { - throw new Error(`Encountered unknown $ref: ${subSchema.$ref}`); + throw new Error(`Encountered unknown $ref: ${objectSchema.$ref}`); } - return subSchema; + return objectSchema; } - if (subSchema === s) { + if (objectSchema === s) { definitions[s.title as string] = { $ref: `#` }; return { $ref: `#/definitions/${s.title}` }; } - definitions[subSchema.title as string] = { ...subSchema }; - deleteAllProps(subSchema); - subSchema.$ref = `#/definitions/${subSchema.title}`; - return subSchema; + definitions[objectSchema.title as string] = { ...objectSchema }; + deleteAllProps(objectSchema); + objectSchema.$ref = `#/definitions/${objectSchema.title}`; + return objectSchema; } - if (subSchema === true) { - t = "AlwaysTrue"; - definitions[t as string] = true; - } else if (subSchema === false) { - t = "AlwaysFalse"; - definitions[t as string] = false; - } else if (subSchema.$ref !== undefined && subSchema.$ref.indexOf("#") !== -1) { - return subSchema; - } else { - if (typeof subSchema.title !== "string") { - throw new NoTitleError(subSchema, s); + if (!hasReference(objectSchema) && (!onlyComplex || isComplex(objectSchema))) { + if (typeof objectSchema.title !== "string") { + throw new NoTitleError(objectSchema, s); } - t = subSchema.title as string; - definitions[t as string] = { ...subSchema }; - deleteAllProps(subSchema); - subSchema.$ref = `#/definitions/${t}`; + t = objectSchema.title as string; + definitions[t as string] = { ...objectSchema }; + deleteAllProps(objectSchema); + objectSchema.$ref = `#/definitions/${t}`; } - return subSchema; + return objectSchema; }, { mutable: true, skipFirstMutation: true }, ); From b336e4c2d5bdf1cf5aa7f18b6526afcb56f4819f Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Fri, 13 Aug 2021 11:27:45 +0200 Subject: [PATCH 2/3] chore: remove boolean if only complex is set --- src/index.test.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 3 +++ 2 files changed, 47 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 99d93c5..1b3c606 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -163,4 +163,48 @@ describe("referencer", () => { expect(defs.allOfFoo.allOf[1].$ref).toBe("#/definitions/baz"); expect(defs.baz.properties.cba.$ref).toBe("#/definitions/cba"); }); + + it.only("flatten only complex types", () => { + const testSchema = { + title: "filterComplexTypes", + properties: { + bar: { + type: "string", + }, + qux: true, + baz: { + title: "baz", + type: "array", + items: [ + { + title: "complexType", + type: "object", + properties: { + foo: { + type: "number", + }, + }, + }, + ], + }, + }, + }; + const reffed = referencer(testSchema, { + onlyComplex: true, + }) as JSONSchemaObject; + + const props = reffed.properties as Properties; + const defs = reffed.definitions as Definitions; + + expect(props.bar.type).toBe("string"); + expect(props.bar.$ref).toBeUndefined(); + expect(props.qux).toBe(true); + expect(props.baz.type).toBe("array"); + expect(props.baz.$ref).toBeUndefined(); + expect(props.baz.title).toBe("baz"); + const bazItems = props.baz.items as JSONSchema[]; + expect(bazItems).toHaveLength(1); + expect((bazItems[0] as JSONSchemaObject).$ref).toBe("#/definitions/complexType"); + expect(defs.complexType.title).toBe("complexType"); + }); }); diff --git a/src/index.ts b/src/index.ts index ce8ebec..df17034 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,6 +100,9 @@ export default function referencer(s: JSONSchema, options: Options = {}): JSONSc (subSchema: JSONSchema, isRootCycle: boolean) => { let t = ""; if (!isObject(subSchema)) { // For schema that is boolean + if (onlyComplex) { + return subSchema; + } if (subSchema === true) { t = "AlwaysTrue"; definitions[t as string] = true; From c0f2c6d0c9a387d3e514d180dc8a94e5c267a2b8 Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues Date: Fri, 13 Aug 2021 11:51:33 +0200 Subject: [PATCH 3/3] chore: improve unit test for complex types --- src/index.test.ts | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 1b3c606..dc3a124 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -164,20 +164,19 @@ describe("referencer", () => { expect(defs.baz.properties.cba.$ref).toBe("#/definitions/cba"); }); - it.only("flatten only complex types", () => { + it("flatten only complex types", () => { const testSchema = { - title: "filterComplexTypes", + title: "FilterComplexTypes", properties: { bar: { type: "string", }, - qux: true, baz: { title: "baz", type: "array", items: [ { - title: "complexType", + title: "ComplexType1", type: "object", properties: { foo: { @@ -187,6 +186,19 @@ describe("referencer", () => { }, ], }, + qux: { + title: "ComplexType2", + type: "object", + properties: { + quux: { + title: "ComplexType3", + type: "object", + properties: { + quuz: true, + }, + }, + }, + }, }, }; const reffed = referencer(testSchema, { @@ -198,13 +210,18 @@ describe("referencer", () => { expect(props.bar.type).toBe("string"); expect(props.bar.$ref).toBeUndefined(); - expect(props.qux).toBe(true); expect(props.baz.type).toBe("array"); expect(props.baz.$ref).toBeUndefined(); expect(props.baz.title).toBe("baz"); const bazItems = props.baz.items as JSONSchema[]; expect(bazItems).toHaveLength(1); - expect((bazItems[0] as JSONSchemaObject).$ref).toBe("#/definitions/complexType"); - expect(defs.complexType.title).toBe("complexType"); + expect((bazItems[0] as JSONSchemaObject).$ref).toBe("#/definitions/ComplexType1"); + expect(defs.ComplexType1.title).toBe("ComplexType1"); + expect(props.qux.$ref).toBe("#/definitions/ComplexType2"); + expect(defs.ComplexType2.title).toBe("ComplexType2"); + expect(defs.ComplexType2.properties.quux.$ref).toBe("#/definitions/ComplexType3"); + expect(defs.ComplexType3.title).toBe("ComplexType3"); + expect(defs.ComplexType3.properties.quuz).toBe(true); + }); });