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.test.ts b/src/index.test.ts index 99d93c5..dc3a124 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -163,4 +163,65 @@ describe("referencer", () => { expect(defs.allOfFoo.allOf[1].$ref).toBe("#/definitions/baz"); expect(defs.baz.properties.cba.$ref).toBe("#/definitions/cba"); }); + + it("flatten only complex types", () => { + const testSchema = { + title: "FilterComplexTypes", + properties: { + bar: { + type: "string", + }, + baz: { + title: "baz", + type: "array", + items: [ + { + title: "ComplexType1", + type: "object", + properties: { + foo: { + type: "number", + }, + }, + }, + ], + }, + qux: { + title: "ComplexType2", + type: "object", + properties: { + quux: { + title: "ComplexType3", + type: "object", + properties: { + quuz: true, + }, + }, + }, + }, + }, + }; + 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.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/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); + + }); }); diff --git a/src/index.ts b/src/index.ts index 74f38c1..df17034 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,66 @@ 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 (onlyComplex) { + return subSchema; + } + 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 }, );