diff --git a/src/index.test.ts b/src/index.test.ts index 2a1097e..a2c95ed 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -6,22 +6,26 @@ describe("traverse", () => { mockMutation: any, schema: JSONSchema, isCycle = expect.any(Boolean), - nth?: number + nth?: number, + parent = expect.anything(), ) => { + if (parent === false) { + parent = undefined; + } if (nth) { expect(mockMutation).toHaveBeenNthCalledWith( nth, schema, isCycle, expect.any(String), - expect.anything(), + parent ); } else { expect(mockMutation).toHaveBeenCalledWith( schema, isCycle, expect.any(String), - expect.anything(), + parent ); } }; @@ -126,7 +130,7 @@ describe("traverse", () => { traverse(testSchema, mockMutation); - testCalls(mockMutation, testSchema) + testCalls(mockMutation, testSchema, false, undefined, false) expect(mockMutation).toHaveBeenCalledTimes(1); }); @@ -288,7 +292,7 @@ describe("traverse", () => { traverse(testSchema, mockMutation); testCalls(mockMutation, testSchema.additionalItems); - testCalls(mockMutation, testSchema); + testCalls(mockMutation, testSchema, false, undefined, false); expect(mockMutation).toHaveBeenCalledTimes(2); }); @@ -302,7 +306,7 @@ describe("traverse", () => { traverse(testSchema, mockMutation); testCalls(mockMutation, testSchema.additionalItems); - testCalls(mockMutation, testSchema); + testCalls(mockMutation, testSchema, false, undefined, false); expect(mockMutation).toHaveBeenCalledTimes(2); }); @@ -318,7 +322,7 @@ describe("traverse", () => { testCalls(mockMutation, testSchema.additionalItems); testCalls(mockMutation, testSchema.items[0]); - testCalls(mockMutation, testSchema); + testCalls(mockMutation, testSchema, false, undefined, false); expect(mockMutation).toHaveBeenCalledTimes(3); }); @@ -334,7 +338,7 @@ describe("traverse", () => { testCalls(mockMutation, testSchema.additionalItems); testCalls(mockMutation, testSchema.items); - testCalls(mockMutation, testSchema); + testCalls(mockMutation, testSchema, false, undefined, false); expect(mockMutation).toHaveBeenCalledTimes(3); }); @@ -357,7 +361,7 @@ describe("traverse", () => { testCalls(mockMutation, testSchema.items[0]); testCalls(mockMutation, testSchema.additionalItems.properties.c); testCalls(mockMutation, testSchema.additionalItems.properties.d); - testCalls(mockMutation, testSchema); + testCalls(mockMutation, testSchema, false, undefined, false); expect(mockMutation).toHaveBeenCalledTimes(5); }); @@ -380,7 +384,7 @@ describe("traverse", () => { testCalls(mockMutation, testSchema.items); testCalls(mockMutation, testSchema.additionalItems.properties.c); testCalls(mockMutation, testSchema.additionalItems.properties.d); - testCalls(mockMutation, testSchema); + testCalls(mockMutation, testSchema, false, undefined, false); expect(mockMutation).toHaveBeenCalledTimes(5); }); @@ -439,7 +443,7 @@ describe("traverse", () => { { title: "3", type: "array", - items: { title: "4" }, + items: { title: "replaced with root" }, }, ], }, @@ -479,7 +483,7 @@ describe("traverse", () => { expect(mockMutation).toHaveBeenCalledTimes(4); }); - it("handles chained cycles where the cycle starts in the middle of a different branch of the tree", () => { + it.only("handles chained cycles where the cycle starts in the middle of a different branch of the tree", () => { const schema = { title: "1", type: "object", @@ -493,17 +497,17 @@ describe("traverse", () => { items: { title: "4", properties: { - baz: { title: "5" }, + baz: { title: "replaced with root" }, }, }, }, ], }, bar: { - title: "6", + title: "5", type: "object", allOf: [ - { title: "7", type: "object", properties: { baz: { title: "8" } } }, + { title: "6", type: "object", properties: { baz: { title: "replaced with #3" } } }, ], }, }, @@ -539,7 +543,7 @@ describe("traverse", () => { title: "6", type: "object", allOf: [ - { title: "7", type: "object", properties: { baz: { title: "5" } } }, + { title: "7", type: "object", properties: { baz: { title: "replaced with #5" } } }, ], }, }, diff --git a/src/index.ts b/src/index.ts index f65072c..9626bb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,13 +6,13 @@ import { JSONSchema, JSONSchemaObject, PatternProperties } from "@json-schema-to * @param schema The schema or subschema node being traversed * @param isCycle false if the schema passed is not the root of a detected cycle. Useful for special handling of cycled schemas. * @param path json-path string in dot-notation as per [draft-goessner-dispatch-jsonpath-00](https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html#name-overview-of-jsonpath-expres) - * @param parent if the schema is the root, this will be the same as `schema`. Otherwise, it will be a reference to JSONSchema that is the parent. + * @param parent if the schema is a cycle, parent will be the same as `schema`. Otherwise, it will be a reference to JSONSchema that is the parent. If the schema has no parent (ie is the root), it will be undefined. */ export type MutationFunction = ( schema: JSONSchema, isCycle: boolean, path: string, - parent: JSONSchema, + parent: JSONSchema | undefined, ) => JSONSchema; /** @@ -107,11 +107,12 @@ export default function traverse( if (opts.skipFirstMutation === true && depth === 0) { return schema; } else { + console.log('mutableSchema:', schema, 'last: ', recursiveStack[recursiveStack.length - 1]); return mutation( schema, false, jsonPathStringify(pathStack), - last(recursiveStack) || schema + recursiveStack[recursiveStack.length - 1] ); } } @@ -269,11 +270,28 @@ export default function traverse( return mutableSchema; } else { const isCycle = cycleSet.indexOf(schema) !== -1 + let parent: JSONSchema | undefined = recursiveStack[recursiveStack.length - 2]; + console.log( + 'mutableSchema:', mutableSchema, + 'recursive stack: ', recursiveStack, + 'parent: ', parent, + 'isCycle: ', isCycle, + 'depth:, ', depth + ); + if (depth === 0) { + // console.log('depth 0 is root: root doesnt have a parent'); + parent = undefined; + } + if (isCycle) { + // console.log('isCycle:', isCycle, 'mutableSchema: ', mutableSchema); + parent = recursiveStack[recursiveStack.length - 1]; + } + // recursiveStack.pop(); return mutation( mutableSchema, isCycle, jsonPathStringify(pathStack), - last(recursiveStack, true) || schema + parent ); } } diff --git a/src/parent.test.ts b/src/parent.test.ts index 8889fa0..c38dff1 100644 --- a/src/parent.test.ts +++ b/src/parent.test.ts @@ -3,7 +3,7 @@ import traverse from "./"; import { JSONSchema } from "@json-schema-tools/meta-schema"; describe("traverse parent", () => { - const test = (s: JSONSchema, parents: JSONSchema[]) => { + const test = (s: JSONSchema, parents: (JSONSchema | undefined)[], isCycle = false) => { const mutator = jest.fn((s) => s); traverse(s, mutator); @@ -15,13 +15,22 @@ describe("traverse parent", () => { expect.any(String), parent, ); + + if (!isCycle) { + expect(mutator).not.toHaveBeenCalledWith( + s, + expect.any(Boolean), + expect.any(String), + s + ); + } }); }; describe("schema is a boolean", () => { - it("allows root schema as boolean", () => { + it("allows root schema as boolean, but its parent is undefined", () => { const testSchema: JSONSchema = true; - test(testSchema, [testSchema]); + test(testSchema, [undefined]); }); }); @@ -92,6 +101,29 @@ describe("traverse parent", () => { test(testSchema, [testSchema, testSchema.additionalItems]); }); + + it("parent for additionalItems is correct", () => { + const testSchema: any = { + type: "array", + additionalItems: { + properties: { + c: {}, + d: {}, + }, + }, + }; + + const mutator = jest.fn((s) => s); + + traverse(testSchema, mutator); + + expect(mutator).toHaveBeenCalledWith( + expect.anything(), + false, + expect.any(String), + testSchema.additionalItems + ); + }); }); describe("schema.items", () => { @@ -115,21 +147,76 @@ describe("traverse parent", () => { test(testSchema, [testSchema]); }); + + it("doesnt call mutator with parent being itself when there is no cycle", () => { + const testSchema: any = { + type: "array", + items: { + properties: { + c: {}, + d: {}, + }, + }, + }; + + const mutator = jest.fn((...args) => { + console.log(args); + return args[0]; + }); + + traverse(testSchema, mutator); + + expect(mutator).toHaveBeenCalledWith( + testSchema.items.properties.c, + false, + expect.any(String), + testSchema.items + ); + + // additionalItems is not the root should not be the its own parent + expect(mutator).not.toHaveBeenCalledWith( + testSchema.items, + false, + expect.any(String), + testSchema.items + ); + }); }); describe("schema.oneOf", () => { - it("works with deeply nested oneOfs", () => { + it.only("works with deeply nested oneOfs", () => { const testSchema: any = { + title: '1', oneOf: [ { - oneOf: [{ type: "number" }, { type: "string" }] + title: '2', + oneOf: [ + { + title: '3', + type: "number" + }, + { + title: '4', + type: "string" + } + ] }, { + title: '5', type: "object", properties: { foo: { + title: '6', oneOf: [ - { type: "array", items: true }, { type: "boolean" } + { + title: '7', + type: "array", + items: true + }, + { + title: '8', + type: "boolean" + } ] } } @@ -144,5 +231,17 @@ describe("traverse parent", () => { testSchema.oneOf[1].properties.foo, ]); }); + + it("works with cycle to the root", () => { + const testSchema: any = { + oneOf: [ + {}, + ] + }; + + testSchema.oneOf[0] = testSchema; + + test(testSchema, [testSchema], true); + }); }); });