Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</span>
</center>

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.
Expand Down
61 changes: 61 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

});
});
89 changes: 61 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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.
*
Expand All @@ -60,62 +81,74 @@ 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)
*
* @category Utils
* @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 },
);
Expand Down