From 3fb8ae0b2a07031962e551a32ee7abe00c7ce9ae Mon Sep 17 00:00:00 2001 From: Derek Thompson <dotslashderek@gmail.com> Date: Tue, 17 May 2022 07:45:06 -0400 Subject: [PATCH 01/11] fix(mergeallof): make sure fragment is fully merged (#16) Right now we merge on a fragment with multiple nested refs at most twice; we have some that require more merge operations to be fully merged. Co-authored-by: Derek Thompson <derekthompson@Dereks-MacBook-Pro.local> --- src/mergers/mergeAllOf.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mergers/mergeAllOf.ts b/src/mergers/mergeAllOf.ts index 627bb8a..9d759c5 100644 --- a/src/mergers/mergeAllOf.ts +++ b/src/mergers/mergeAllOf.ts @@ -59,9 +59,9 @@ export function mergeAllOf(fragment: SchemaFragment, path: string[], walkingOpti store.set(walkingOptions.resolveRef, new WeakMap()); } - const merged = _mergeAllOf(fragment, path, walkingOptions.resolveRef); - if ('allOf' in merged) { - return _mergeAllOf(merged, path, walkingOptions.resolveRef); + let merged = _mergeAllOf(fragment, path, walkingOptions.resolveRef); + while ('allOf' in merged) { + merged = _mergeAllOf(merged, path, walkingOptions.resolveRef); } return merged; From a14a678e68206bfc02d69ac0697310c5bd4b4a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= <jakub@stoplight.io> Date: Tue, 2 Aug 2022 22:11:04 +0200 Subject: [PATCH 02/11] fix(walker): handle $ref siblings (#18) --- src/__tests__/tree.spec.ts | 80 ++++++++++++++++++++++++++++++++++++++ src/walker/walker.ts | 9 ++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/__tests__/tree.spec.ts b/src/__tests__/tree.spec.ts index 335b7a1..f001544 100644 --- a/src/__tests__/tree.spec.ts +++ b/src/__tests__/tree.spec.ts @@ -677,6 +677,86 @@ describe('SchemaTree', () => { it('given empty schema, should output empty tree', () => { expect(printTree({})).toEqual(''); }); + + it('should override description', () => { + const schema = { + type: 'object', + properties: { + caves: { + type: 'array', + items: { + summary: 'Bear cave', + $ref: '#/$defs/Cave', + description: 'Apparently Tom likes bears', + }, + }, + greatestBear: { + $ref: '#/$defs/Bear', + description: 'The greatest bear!', + }, + bestBear: { + $ref: '#/$defs/Bear', + summary: 'The best bear!', + }, + }, + $defs: { + Bear: { + type: 'string', + summary: "Tom's favorite bear", + }, + Cave: { + type: 'string', + summary: 'A cave', + description: '_Everyone_ ~hates~ loves caves', + }, + }, + }; + + const tree = new SchemaTree(schema, {}); + tree.populate(); + + expect(tree.root).toEqual( + expect.objectContaining({ + children: [ + expect.objectContaining({ + primaryType: 'object', + types: ['object'], + children: [ + expect.objectContaining({ + primaryType: 'array', + subpath: ['properties', 'caves'], + types: ['array'], + children: [ + expect.objectContaining({ + primaryType: 'string', + types: ['string'], + subpath: ['items'], + annotations: { + description: 'Apparently Tom likes bears', + }, + }), + ], + }), + expect.objectContaining({ + primaryType: 'string', + types: ['string'], + subpath: ['properties', 'greatestBear'], + annotations: { + description: 'The greatest bear!', + }, + }), + expect.objectContaining({ + primaryType: 'string', + types: ['string'], + subpath: ['properties', 'bestBear'], + annotations: {}, + }), + ], + }), + ], + }), + ); + }); }); describe('position', () => { diff --git a/src/walker/walker.ts b/src/walker/walker.ts index 172b803..28c5462 100644 --- a/src/walker/walker.ts +++ b/src/walker/walker.ts @@ -275,7 +275,14 @@ export class Walker extends EventEmitter<WalkerEmitter> { return [new ReferenceNode(fragment, '$ref is not a string'), fragment]; } else if (walkingOptions.resolveRef !== null) { try { - fragment = walkingOptions.resolveRef(path, fragment.$ref); + let newFragment = walkingOptions.resolveRef(path, fragment.$ref); + + if (typeof fragment.description === 'string') { + newFragment = { ...newFragment }; + Object.assign(newFragment, { description: fragment.description }); + } + + fragment = newFragment; } catch (ex) { super.emit('error', createMagicError(ex)); return [new ReferenceNode(fragment, ex?.message ?? 'Unknown resolving error'), fragment]; From 7d1bc9d0de8a6e345e450d18a917cce4e09ef3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= <jakub@stoplight.io> Date: Mon, 15 May 2023 23:34:50 +0200 Subject: [PATCH 03/11] perf: improve handling of under compound keyword combo (#20) --- package.json | 2 +- .../combiners/allOfs/nested-refs.json | 60 ++++++++ src/__tests__/__snapshots__/tree.spec.ts.snap | 138 ++++++++++++++++++ src/mergers/mergeAllOf.ts | 38 +++-- src/mergers/mergeOneOrAnyOf.ts | 32 ++-- src/walker/walker.ts | 8 +- yarn.lock | 8 +- 7 files changed, 252 insertions(+), 34 deletions(-) create mode 100644 src/__tests__/__fixtures__/combiners/allOfs/nested-refs.json diff --git a/package.json b/package.json index bf9a074..844c8a0 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "peerDependencies": {}, "dependencies": { "@stoplight/json": "^3.12.0", - "@stoplight/json-schema-merge-allof": "^0.7.7", + "@stoplight/json-schema-merge-allof": "^0.7.8", "@stoplight/lifecycle": "^2.3.2", "@types/json-schema": "^7.0.7", "magic-error": "0.0.1" diff --git a/src/__tests__/__fixtures__/combiners/allOfs/nested-refs.json b/src/__tests__/__fixtures__/combiners/allOfs/nested-refs.json new file mode 100644 index 0000000..d44142c --- /dev/null +++ b/src/__tests__/__fixtures__/combiners/allOfs/nested-refs.json @@ -0,0 +1,60 @@ +{ + "required": ["limit", "order"], + "type": "object", + "properties": { + "dimensions": { "type": "array", "items": { "type": "string" } }, + "measures": { "type": "array", "items": { "type": "string" } }, + "limit": { + "maximum": 2000, + "minimum": 0, + "type": "integer", + "format": "int32" + }, + "offset": { + "minimum": 0, + "type": "integer", + "format": "int32", + "maximum": 2147483647 + }, + "filters": { + "type": "array", + "items": { "oneOf": [{ "$ref": "#/$defs/Logical" }, { "$ref": "#/$defs/Plain" }] } + }, + "timeDimensions": { + "maxItems": 1, + "minItems": 0, + "type": "array", + "items": { "$ref": "#/$defs/TimeDimension" } + }, + "order": { "type": "object", "additionalProperties": { "type": "string", "enum": ["ASC", "DESC"] } }, + "nextToken": { "type": "string" } + }, + "$defs": { + "Logical": { "type": "object", "allOf": [{ "$ref": "#/$defs/Filter" }] }, + "Filter": { "type": "object", "anyOf": [{ "$ref": "#/$defs/Logical" }, { "$ref": "#/$defs/Plain" }] }, + "Plain": { + "required": ["member", "operator"], + "type": "object", + "allOf": [ + { "$ref": "#/$defs/Filter" }, + { + "type": "object", + "properties": { + "member": { "type": "string" }, + "operator": { "pattern": "equals|notEquals|gt|gte|lt|lte|set|notSet|inDateRange", "type": "string" }, + "values": { "type": "array", "items": { "type": "object" } } + } + } + ] + }, + "TimeDimension": { + "required": ["dateRange", "dimension", "granularity"], + "type": "object", + "properties": { + "dimension": { "type": "string" }, + "granularity": { "pattern": "second|minute|hour|day", "type": "string" }, + "dateRange": { "type": "object", "example": ["2022-04-19T16:00:00.000Z", "2022-04-19T17:00:00.000Z"] } + } + } + } +} diff --git a/src/__tests__/__snapshots__/tree.spec.ts.snap b/src/__tests__/__snapshots__/tree.spec.ts.snap index f414438..6402999 100644 --- a/src/__tests__/__snapshots__/tree.spec.ts.snap +++ b/src/__tests__/__snapshots__/tree.spec.ts.snap @@ -694,6 +694,144 @@ exports[`SchemaTree output should generate valid tree for combiners/allOfs/compl " `; +exports[`SchemaTree output should generate valid tree for combiners/allOfs/nested-refs.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + ├─ 0 + │ └─ #/properties/dimensions + │ ├─ types + │ │ └─ 0: array + │ ├─ primaryType: array + │ └─ children + │ └─ 0 + │ └─ #/properties/dimensions/items + │ ├─ types + │ │ └─ 0: string + │ └─ primaryType: string + ├─ 1 + │ └─ #/properties/measures + │ ├─ types + │ │ └─ 0: array + │ ├─ primaryType: array + │ └─ children + │ └─ 0 + │ └─ #/properties/measures/items + │ ├─ types + │ │ └─ 0: string + │ └─ primaryType: string + ├─ 2 + │ └─ #/properties/limit + │ ├─ types + │ │ └─ 0: integer + │ └─ primaryType: integer + ├─ 3 + │ └─ #/properties/offset + │ ├─ types + │ │ └─ 0: integer + │ └─ primaryType: integer + ├─ 4 + │ └─ #/properties/filters + │ ├─ types + │ │ └─ 0: array + │ ├─ primaryType: array + │ └─ children + │ └─ 0 + │ └─ #/properties/filters/items + │ ├─ combiners + │ │ └─ 0: oneOf + │ └─ children + │ ├─ 0 + │ │ └─ #/properties/filters/items/oneOf/0 + │ │ ├─ types + │ │ │ └─ 0: object + │ │ ├─ primaryType: object + │ │ ├─ combiners + │ │ │ └─ 0: anyOf + │ │ └─ children + │ │ ├─ 0 + │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/0 + │ │ │ └─ mirrors: #/properties/filters/items/oneOf/0 + │ │ └─ 1 + │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1 + │ │ ├─ types + │ │ │ └─ 0: object + │ │ ├─ primaryType: object + │ │ ├─ combiners + │ │ │ └─ 0: anyOf + │ │ └─ children + │ │ ├─ 0 + │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/anyOf/0 + │ │ │ └─ mirrors: #/properties/filters/items/oneOf/0 + │ │ ├─ 1 + │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/anyOf/1 + │ │ │ └─ mirrors: #/properties/filters/items/oneOf/0/anyOf/1 + │ │ ├─ 2 + │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/properties/member + │ │ │ ├─ types + │ │ │ │ └─ 0: string + │ │ │ └─ primaryType: string + │ │ ├─ 3 + │ │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/properties/operator + │ │ │ ├─ types + │ │ │ │ └─ 0: string + │ │ │ └─ primaryType: string + │ │ └─ 4 + │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/properties/values + │ │ ├─ types + │ │ │ └─ 0: array + │ │ ├─ primaryType: array + │ │ └─ children + │ │ └─ 0 + │ │ └─ #/properties/filters/items/oneOf/0/anyOf/1/properties/values/items + │ │ ├─ types + │ │ │ └─ 0: object + │ │ └─ primaryType: object + │ └─ 1 + │ └─ #/properties/filters/items/oneOf/1 + │ └─ mirrors: #/properties/filters/items/oneOf/0/anyOf/1 + ├─ 5 + │ └─ #/properties/timeDimensions + │ ├─ types + │ │ └─ 0: array + │ ├─ primaryType: array + │ └─ children + │ └─ 0 + │ └─ #/properties/timeDimensions/items + │ ├─ types + │ │ └─ 0: object + │ ├─ primaryType: object + │ └─ children + │ ├─ 0 + │ │ └─ #/properties/timeDimensions/items/properties/dimension + │ │ ├─ types + │ │ │ └─ 0: string + │ │ └─ primaryType: string + │ ├─ 1 + │ │ └─ #/properties/timeDimensions/items/properties/granularity + │ │ ├─ types + │ │ │ └─ 0: string + │ │ └─ primaryType: string + │ └─ 2 + │ └─ #/properties/timeDimensions/items/properties/dateRange + │ ├─ types + │ │ └─ 0: object + │ └─ primaryType: object + ├─ 6 + │ └─ #/properties/order + │ ├─ types + │ │ └─ 0: object + │ └─ primaryType: object + └─ 7 + └─ #/properties/nextToken + ├─ types + │ └─ 0: string + └─ primaryType: string +" +`; + exports[`SchemaTree output should generate valid tree for combiners/allOfs/todo-full.json 1`] = ` "└─ # ├─ types diff --git a/src/mergers/mergeAllOf.ts b/src/mergers/mergeAllOf.ts index 9d759c5..24d89dd 100644 --- a/src/mergers/mergeAllOf.ts +++ b/src/mergers/mergeAllOf.ts @@ -1,4 +1,4 @@ -import { pathToPointer, stringify } from '@stoplight/json'; +import { pathToPointer } from '@stoplight/json'; import { ResolvingError } from '../errors'; import type { SchemaFragment } from '../types'; @@ -8,8 +8,18 @@ const resolveAllOf = require('@stoplight/json-schema-merge-allof'); const store = new WeakMap<WalkerRefResolver, WeakMap<SchemaFragment, string[]>>(); -function _mergeAllOf(fragment: SchemaFragment, path: string[], resolveRef: WalkerRefResolver | null): SchemaFragment { - return resolveAllOf(fragment, { +function _mergeAllOf( + fragment: SchemaFragment, + path: string[], + resolveRef: WalkerRefResolver | null, + seen: WeakMap<SchemaFragment, SchemaFragment>, +): SchemaFragment { + const cached = seen.get(fragment); + if (cached !== void 0) { + return cached; + } + + const merged = resolveAllOf(fragment, { deep: false, resolvers: resolveAllOf.stoplightResolvers, ...(resolveRef !== null @@ -30,8 +40,8 @@ function _mergeAllOf(fragment: SchemaFragment, path: string[], resolveRef: Walke schemaRefs = [$ref]; allRefs.set(fragment, schemaRefs); } else if (schemaRefs.includes($ref)) { - const safelyResolved = JSON.parse(stringify(resolveRef(null, $ref))); - return 'allOf' in safelyResolved ? _mergeAllOf(safelyResolved, path, resolveRef) : safelyResolved; + const resolved = resolveRef(null, $ref); + return 'allOf' in resolved ? _mergeAllOf(resolved, path, resolveRef, seen) : resolved; } else { schemaRefs.push($ref); } @@ -52,17 +62,25 @@ function _mergeAllOf(fragment: SchemaFragment, path: string[], resolveRef: Walke } : null), }); + + seen.set(fragment, merged); + return merged; } -export function mergeAllOf(fragment: SchemaFragment, path: string[], walkingOptions: WalkingOptions) { +export function mergeAllOf( + fragment: SchemaFragment, + path: string[], + walkingOptions: WalkingOptions, + seen: WeakMap<SchemaFragment, SchemaFragment>, +) { if (walkingOptions.resolveRef !== null && !store.has(walkingOptions.resolveRef)) { store.set(walkingOptions.resolveRef, new WeakMap()); } - let merged = _mergeAllOf(fragment, path, walkingOptions.resolveRef); - while ('allOf' in merged) { - merged = _mergeAllOf(merged, path, walkingOptions.resolveRef); - } + let merged = fragment; + do { + merged = _mergeAllOf(merged, path, walkingOptions.resolveRef, seen); + } while ('allOf' in merged); return merged; } diff --git a/src/mergers/mergeOneOrAnyOf.ts b/src/mergers/mergeOneOrAnyOf.ts index 3e1e320..6730737 100644 --- a/src/mergers/mergeOneOrAnyOf.ts +++ b/src/mergers/mergeOneOrAnyOf.ts @@ -7,6 +7,7 @@ export function mergeOneOrAnyOf( fragment: SchemaFragment, path: string[], walkingOptions: WalkingOptions, + mergedAllOfs: WeakMap<SchemaFragment, SchemaFragment>, ): SchemaFragment[] { const combiner = SchemaCombinerName.OneOf in fragment ? SchemaCombinerName.OneOf : SchemaCombinerName.AnyOf; const items = fragment[combiner]; @@ -15,7 +16,7 @@ export function mergeOneOrAnyOf( const merged: SchemaFragment[] = []; - if (Array.isArray(fragment.allOf) && Array.isArray(items)) { + if (Array.isArray(fragment.allOf)) { for (const item of items) { merged.push({ allOf: [...fragment.allOf, item], @@ -24,26 +25,23 @@ export function mergeOneOrAnyOf( return merged; } else { - for (const item of items) { - const prunedSchema = { ...fragment }; - delete prunedSchema[combiner]; + const prunedSchema = { ...fragment }; + delete prunedSchema[combiner]; + for (const item of items) { if (Object.keys(prunedSchema).length === 0) { merged.push(item); } else { - const resolvedItem = - typeof item.$ref === 'string' && walkingOptions.resolveRef !== null - ? walkingOptions.resolveRef(null, item.$ref) - : item; - const mergedSchema = { - allOf: [prunedSchema, resolvedItem], - }; - - try { - merged.push(mergeAllOf(mergedSchema, path, walkingOptions)); - } catch { - merged.push(mergedSchema); - } + merged.push( + mergeAllOf( + { + allOf: [prunedSchema, item], + }, + path, + walkingOptions, + mergedAllOfs, + ), + ); } } } diff --git a/src/walker/walker.ts b/src/walker/walker.ts index 28c5462..521a1f0 100644 --- a/src/walker/walker.ts +++ b/src/walker/walker.ts @@ -28,7 +28,9 @@ export class Walker extends EventEmitter<WalkerEmitter> { protected fragment: SchemaFragment; protected schemaNode: RegularNode | RootNode; + private mergedAllOfs: WeakMap<SchemaFragment, SchemaFragment>; private processedFragments: WeakMap<ProcessedFragment, SchemaNode>; + private readonly hooks: Partial<Dictionary<WalkerHookHandler, WalkerHookAction>>; constructor(protected readonly root: RootNode, protected readonly walkingOptions: WalkingOptions) { @@ -39,6 +41,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { this.fragment = root.fragment; this.schemaNode = root; this.processedFragments = new WeakMap<SchemaFragment, SchemaNode>(); + this.mergedAllOfs = new WeakMap(); this.hooks = {}; } @@ -49,6 +52,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { this.fragment = this.root.fragment; this.schemaNode = this.root; this.processedFragments = new WeakMap<SchemaFragment, RegularNode | ReferenceNode>(); + this.mergedAllOfs = new WeakMap(); } public loadSnapshot(snapshot: WalkerSnapshot) { @@ -299,7 +303,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { initialFragment = fragment.allOf; } - fragment = mergeAllOf(fragment, path, walkingOptions); + fragment = mergeAllOf(fragment, path, walkingOptions, this.mergedAllOfs); } catch (ex) { initialFragment = fragment; super.emit('error', createMagicError(new MergingError(ex?.message ?? 'Unknown merging error'))); @@ -309,7 +313,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { if (SchemaCombinerName.OneOf in fragment || SchemaCombinerName.AnyOf in fragment) { try { - const merged = mergeOneOrAnyOf(fragment, path, walkingOptions); + const merged = mergeOneOrAnyOf(fragment, path, walkingOptions, this.mergedAllOfs); if (merged.length === 1) { return [new RegularNode(merged[0], { originalFragment }), initialFragment]; } else { diff --git a/yarn.lock b/yarn.lock index 6cc6e3c..0ec7f5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -936,10 +936,10 @@ dependencies: eslint-config-prettier "^7.1.0" -"@stoplight/json-schema-merge-allof@^0.7.7": - version "0.7.7" - resolved "https://registry.yarnpkg.com/@stoplight/json-schema-merge-allof/-/json-schema-merge-allof-0.7.7.tgz#d79ca8729aa5d6420e40cc545b1854b518b9240f" - integrity sha512-3QrZ+2hhTvsPAjl8HD9rQI3MPHSCl/kCf0rEkKM4YjLumDsfX1TSrhESDIt4lSzpSdNyT2oXSJx0ADVGE2gTyA== +"@stoplight/json-schema-merge-allof@^0.7.8": + version "0.7.8" + resolved "https://registry.yarnpkg.com/@stoplight/json-schema-merge-allof/-/json-schema-merge-allof-0.7.8.tgz#7efe5e0086dff433eb011f617e82f7295c3de061" + integrity sha512-JTDt6GYpCWQSb7+UW1P91IAp/pcLWis0mmEzWVFcLsrNgtUYK7JLtYYz0ZPSR4QVL0fJ0YQejM+MPq5iNDFO4g== dependencies: compute-lcm "^1.1.0" json-schema-compare "^0.2.2" From c16711d1a040334bf1bedc0a9284db2f6398439c Mon Sep 17 00:00:00 2001 From: Kayla Chun <58235624+kaylachun@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:53:59 -0700 Subject: [PATCH 04/11] fix(gettypes.ts): add null type if nullable is true (#21) --- src/accessors/getTypes.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/accessors/getTypes.ts b/src/accessors/getTypes.ts index 16db198..17d13d0 100644 --- a/src/accessors/getTypes.ts +++ b/src/accessors/getTypes.ts @@ -1,20 +1,36 @@ -import type { SchemaNodeKind } from '../nodes/types'; +import { SchemaNodeKind } from '../nodes/types'; import type { SchemaFragment } from '../types'; import { isValidType } from './guards/isValidType'; import { inferType } from './inferType'; export function getTypes(fragment: SchemaFragment): SchemaNodeKind[] | null { + const types: SchemaNodeKind[] = []; + let isNullable = false; + + if ('nullable' in fragment) { + if (fragment.nullable === true) { + isNullable = true; + } + } if ('type' in fragment) { if (Array.isArray(fragment.type)) { - return fragment.type.filter(isValidType); + types.push(...fragment.type.filter(isValidType)); } else if (isValidType(fragment.type)) { - return [fragment.type]; + types.push(fragment.type); } + if (isNullable && !types.includes(SchemaNodeKind.Null)) { + types.push(SchemaNodeKind.Null); + } + return types; } const inferredType = inferType(fragment); if (inferredType !== null) { - return [inferredType]; + types.push(inferredType); + if (isNullable && !types.includes(SchemaNodeKind.Null)) { + types.push(SchemaNodeKind.Null); + } + return types; } return null; From 12b56230eb096140e7870dcfadfa1ac7cbaecd4e Mon Sep 17 00:00:00 2001 From: paulatulis <47359669+paulatulis@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:26:17 -0500 Subject: [PATCH 05/11] fix (walker): show description for $ref in fragments of type array (#23) --- src/__tests__/tree.spec.ts | 95 ++++++++++++++++++++++++++++++++++++++ src/walker/walker.ts | 24 +++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/__tests__/tree.spec.ts b/src/__tests__/tree.spec.ts index f001544..5f80a29 100644 --- a/src/__tests__/tree.spec.ts +++ b/src/__tests__/tree.spec.ts @@ -757,6 +757,101 @@ describe('SchemaTree', () => { }), ); }); + + it('node of type array should adopt description of referenced node', () => { + const schema = { + definitions: { + Cave: { + type: 'string', + summary: 'A cave', + description: '_Everyone_ ~hates~ loves caves', + }, + }, + type: 'object', + properties: { + caves: { + type: 'array', + items: { + $ref: '#/definitions/Cave', + }, + }, + }, + }; + + const tree = new SchemaTree(schema); + tree.populate(); + + expect( + // @ts-ignore + tree.root.children[0].children[0].annotations.description, + ).toEqual('_Everyone_ ~hates~ loves caves'); + }); + + it('node of type array should keep its own description even when referenced node has a description', () => { + const schema = { + definitions: { + Cave: { + type: 'string', + summary: 'A cave', + description: '_Everyone_ ~hates~ loves caves', + }, + }, + type: 'object', + properties: { + caves: { + type: 'array', + description: 'I have my own description', + items: { + $ref: '#/definitions/Cave', + }, + }, + }, + }; + + const tree = new SchemaTree(schema); + tree.populate(); + + expect( + // @ts-ignore + tree.root.children[0].children[0].annotations.description, + ).toEqual('I have my own description'); + }); + + it('referenced node description should appear for all properties with that ref', () => { + const schema = { + definitions: { + Cave: { + type: 'string', + summary: 'A cave', + description: '_Everyone_ ~hates~ loves caves', + }, + }, + type: 'object', + properties: { + caves: { + type: 'array', + items: { + $ref: '#/definitions/Cave', + }, + }, + bear: { + $ref: '#/definitions/Cave', + }, + }, + }; + + const tree = new SchemaTree(schema); + tree.populate(); + + expect( + // @ts-ignore + tree.root.children[0].children[0].annotations.description, + ).toEqual('_Everyone_ ~hates~ loves caves'); + expect( + // @ts-ignore + tree.root.children[0].children[1].annotations.description, + ).toEqual('_Everyone_ ~hates~ loves caves'); + }); }); describe('position', () => { diff --git a/src/walker/walker.ts b/src/walker/walker.ts index 521a1f0..b8eedeb 100644 --- a/src/walker/walker.ts +++ b/src/walker/walker.ts @@ -295,7 +295,29 @@ export class Walker extends EventEmitter<WalkerEmitter> { return [new ReferenceNode(fragment, null), fragment]; } } - + //fragment with type 'array' and no description should adopt description of $ref if it exists + if (fragment.type === 'array' && fragment.description === void 0) { + if (fragment.items !== void 0 && isObjectLiteral(fragment.items)) { + for (const key of Object.keys(fragment.items)) { + if (key === '$ref') { + const refToResolve = fragment.items[key]; + if (typeof refToResolve !== 'string') { + return [new ReferenceNode(fragment, '$ref is not a string'), fragment]; + } else if (walkingOptions.resolveRef !== null) { + try { + let newFragment = walkingOptions.resolveRef(path, refToResolve); + if (newFragment.description !== void 0) { + newFragment = { ...newFragment }; + Object.assign(fragment, { description: newFragment.description }); + } + } catch (ex) { + super.emit('error', createMagicError(ex)); + } + } + } + } + } + } let initialFragment: ProcessedFragment = fragment; if (walkingOptions.mergeAllOf && SchemaCombinerName.AllOf in fragment) { try { From 5cf28016dae7da482d2c3bf2899f483920cd50c1 Mon Sep 17 00:00:00 2001 From: paulatulis <47359669+paulatulis@users.noreply.github.com> Date: Wed, 9 Aug 2023 11:26:04 -0500 Subject: [PATCH 06/11] fix(walker): show description for $ref in fragments of type array (#24) --- src/walker/walker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/walker/walker.ts b/src/walker/walker.ts index b8eedeb..4e144aa 100644 --- a/src/walker/walker.ts +++ b/src/walker/walker.ts @@ -295,7 +295,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { return [new ReferenceNode(fragment, null), fragment]; } } - //fragment with type 'array' and no description should adopt description of $ref if it exists + //fragment with type 'array' and no description should adopt description of $ref if it exists. if (fragment.type === 'array' && fragment.description === void 0) { if (fragment.items !== void 0 && isObjectLiteral(fragment.items)) { for (const key of Object.keys(fragment.items)) { From 628627cb4fde4c0ed4472a4dc5040b36eb58d2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= <jakub@stoplight.io> Date: Wed, 15 Nov 2023 19:52:38 +0100 Subject: [PATCH 07/11] feat(tree): account for resolveInlineRef behavior when $ref has siblings (#25) * fix(tree): account for resolveInlineRef behavior when $ref has siblings * feat(walker): adds a max depth option for refs --------- Co-authored-by: Daniel A. White <daniel.white@stoplight.io> --- .../__fixtures__/recursive-schema.json | 24 +++++++++++++++++++ .../references/with-overrides.json | 23 ++++++++++++++++++ src/__tests__/__snapshots__/tree.spec.ts.snap | 23 ++++++++++++++++++ src/__tests__/tree.spec.ts | 15 +++++++++++- src/tree/tree.ts | 9 +++++++ src/tree/types.ts | 3 +++ src/walker/types.ts | 3 +++ src/walker/walker.ts | 21 ++++++++++++---- 8 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/__fixtures__/recursive-schema.json create mode 100644 src/__tests__/__fixtures__/references/with-overrides.json diff --git a/src/__tests__/__fixtures__/recursive-schema.json b/src/__tests__/__fixtures__/recursive-schema.json new file mode 100644 index 0000000..610dfc1 --- /dev/null +++ b/src/__tests__/__fixtures__/recursive-schema.json @@ -0,0 +1,24 @@ +{ + "title": "Thing", + "allOf": [ + { + "$ref": "#/definitions/User" + } + ], + "description": "baz", + "definitions": { + "User": { + "type": "object", + "description": "user", + "properties": { + "manager": { + "$ref": "#/definitions/Boss" + } + } + }, + "Boss": { + "$ref": "#/definitions/User", + "description": "xyz" + } + } +} diff --git a/src/__tests__/__fixtures__/references/with-overrides.json b/src/__tests__/__fixtures__/references/with-overrides.json new file mode 100644 index 0000000..85ed288 --- /dev/null +++ b/src/__tests__/__fixtures__/references/with-overrides.json @@ -0,0 +1,23 @@ +{ + "oneOf": [ + { + "$ref": "#/definitions/User" + } + ], + "description": "User Model", + "definitions": { + "User": { + "type": "object", + "description": "Plain User", + "properties": { + "manager": { + "$ref": "#/definitions/Admin" + } + } + }, + "Admin": { + "$ref": "#/definitions/User", + "description": "Admin User" + } + } +} diff --git a/src/__tests__/__snapshots__/tree.spec.ts.snap b/src/__tests__/__snapshots__/tree.spec.ts.snap index 6402999..2708563 100644 --- a/src/__tests__/__snapshots__/tree.spec.ts.snap +++ b/src/__tests__/__snapshots__/tree.spec.ts.snap @@ -1293,6 +1293,29 @@ exports[`SchemaTree output should generate valid tree for references/nullish.jso " `; +exports[`SchemaTree output should generate valid tree for references/with-overrides.json 1`] = ` +"└─ # + ├─ combiners + │ └─ 0: oneOf + └─ children + └─ 0 + └─ #/oneOf/0 + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/oneOf/0/properties/manager + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/oneOf/0/properties/manager/properties/manager + └─ mirrors: #/oneOf/0/properties/manager +" +`; + exports[`SchemaTree output should generate valid tree for tickets.schema.json 1`] = ` "└─ # ├─ types diff --git a/src/__tests__/tree.spec.ts b/src/__tests__/tree.spec.ts index 5f80a29..4216bad 100644 --- a/src/__tests__/tree.spec.ts +++ b/src/__tests__/tree.spec.ts @@ -13,7 +13,7 @@ describe('SchemaTree', () => { it.each( fastGlob.sync('**/*.json', { cwd: path.join(__dirname, '__fixtures__'), - ignore: ['stress-schema.json'], + ignore: ['stress-schema.json', 'recursive-schema.json'], }), )('should generate valid tree for %s', async filename => { const schema = JSON.parse(await fs.promises.readFile(path.resolve(__dirname, '__fixtures__', filename), 'utf8')); @@ -985,4 +985,17 @@ describe('SchemaTree', () => { }); }); }); + + describe('recursive walking', () => { + it('should load with a max depth', async () => { + const schema = JSON.parse( + await fs.promises.readFile(path.resolve(__dirname, '__fixtures__', 'recursive-schema.json'), 'utf8'), + ); + + const w = new SchemaTree(schema, { + maxRefDepth: 1000, + }); + w.populate(); + }); + }); }); diff --git a/src/tree/tree.ts b/src/tree/tree.ts index 761e1ae..e81be64 100644 --- a/src/tree/tree.ts +++ b/src/tree/tree.ts @@ -11,18 +11,22 @@ import type { SchemaTreeOptions } from './types'; export class SchemaTree { public walker: Walker; public root: RootNode; + private readonly resolvedRefs = new Map(); constructor(public schema: SchemaFragment, protected readonly opts?: Partial<SchemaTreeOptions>) { this.root = new RootNode(schema); + this.resolvedRefs = new Map(); this.walker = new Walker(this.root, { mergeAllOf: this.opts?.mergeAllOf !== false, resolveRef: opts?.refResolver === null ? null : this.resolveRef, + maxRefDepth: opts?.maxRefDepth, }); } public destroy() { this.root.children.length = 0; this.walker.destroy(); + this.resolvedRefs.clear(); } public populate() { @@ -34,6 +38,10 @@ export class SchemaTree { } protected resolveRef: WalkerRefResolver = (path, $ref) => { + if (this.resolvedRefs.has($ref)) { + return this.resolvedRefs.get($ref); + } + const seenRefs: string[] = []; let cur$ref: unknown = $ref; let resolvedValue!: SchemaFragment; @@ -48,6 +56,7 @@ export class SchemaTree { cur$ref = resolvedValue.$ref; } + this.resolvedRefs.set($ref, resolvedValue); return resolvedValue; }; diff --git a/src/tree/types.ts b/src/tree/types.ts index 9d76dce..1034782 100644 --- a/src/tree/types.ts +++ b/src/tree/types.ts @@ -2,7 +2,10 @@ import type { SchemaFragment } from '../types'; export type SchemaTreeOptions = { mergeAllOf: boolean; + /** Resolves references to the schemas. If providing a custom implementation, it must return the same object reference for the same reference string. */ refResolver: SchemaTreeRefDereferenceFn | null; + /** Controls the level of recursion of refs. Prevents overly complex trees and running out of stack depth. */ + maxRefDepth?: number | null; }; export type SchemaTreeRefInfo = { diff --git a/src/walker/types.ts b/src/walker/types.ts index aa67f2f..34f7b74 100644 --- a/src/walker/types.ts +++ b/src/walker/types.ts @@ -6,7 +6,10 @@ export type WalkerRefResolver = (path: string[] | null, $ref: string) => SchemaF export type WalkingOptions = { mergeAllOf: boolean; + /** Resolves references to the schemas. If providing a custom implementation, it must return the same object reference for the same reference string. */ resolveRef: WalkerRefResolver | null; + /** Controls the level of recursion of refs. Prevents overly complex trees and running out of stack depth. */ + maxRefDepth?: number | null; }; export type WalkerSnapshot = { diff --git a/src/walker/walker.ts b/src/walker/walker.ts index 4e144aa..af4e0c0 100644 --- a/src/walker/walker.ts +++ b/src/walker/walker.ts @@ -36,11 +36,22 @@ export class Walker extends EventEmitter<WalkerEmitter> { constructor(protected readonly root: RootNode, protected readonly walkingOptions: WalkingOptions) { super(); + let maxRefDepth = walkingOptions.maxRefDepth ?? null; + if (typeof maxRefDepth === 'number') { + if (maxRefDepth < 1) { + maxRefDepth = null; + } else if (maxRefDepth > 1000) { + // experimented with 1500 and the recursion limit is still lower than that + maxRefDepth = 1000; + } + } + walkingOptions.maxRefDepth = maxRefDepth; + this.path = []; this.depth = -1; this.fragment = root.fragment; this.schemaNode = root; - this.processedFragments = new WeakMap<SchemaFragment, SchemaNode>(); + this.processedFragments = new WeakMap(); this.mergedAllOfs = new WeakMap(); this.hooks = {}; @@ -51,7 +62,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { this.depth = -1; this.fragment = this.root.fragment; this.schemaNode = this.root; - this.processedFragments = new WeakMap<SchemaFragment, RegularNode | ReferenceNode>(); + this.processedFragments = new WeakMap(); this.mergedAllOfs = new WeakMap(); } @@ -265,7 +276,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { } protected processFragment(): [SchemaNode, ProcessedFragment] { - const { walkingOptions, path, fragment: originalFragment } = this; + const { walkingOptions, path, fragment: originalFragment, depth } = this; let { fragment } = this; let retrieved = isNonNullable(fragment) ? this.retrieveFromFragment(fragment, originalFragment) : null; @@ -275,7 +286,9 @@ export class Walker extends EventEmitter<WalkerEmitter> { } if ('$ref' in fragment) { - if (typeof fragment.$ref !== 'string') { + if (typeof walkingOptions.maxRefDepth === 'number' && walkingOptions.maxRefDepth < depth) { + return [new ReferenceNode(fragment, `max $ref depth limit reached`), fragment]; + } else if (typeof fragment.$ref !== 'string') { return [new ReferenceNode(fragment, '$ref is not a string'), fragment]; } else if (walkingOptions.resolveRef !== null) { try { From 46d92ce6252cf645111dec702b09624ec1a65dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= <jakub@stoplight.io> Date: Fri, 17 Nov 2023 17:30:23 +0100 Subject: [PATCH 08/11] fix(walker): support circular JSON $refs with overrides (#26) --- .../references/circular-with-overrides.json | 38 +++++++++++++++++++ src/__tests__/__snapshots__/tree.spec.ts.snap | 30 +++++++++++++++ src/walker/walker.ts | 8 +++- 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/__fixtures__/references/circular-with-overrides.json diff --git a/src/__tests__/__fixtures__/references/circular-with-overrides.json b/src/__tests__/__fixtures__/references/circular-with-overrides.json new file mode 100644 index 0000000..49ce245 --- /dev/null +++ b/src/__tests__/__fixtures__/references/circular-with-overrides.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "properties": { + "order": { + "$ref": "#/definitions/Order", + "description": "My Order" + } + }, + "definitions": { + "Cart": { + "type": "object", + "properties": { + "order": { + "$ref": "#/definitions/Order", + "description": "My Order" + } + } + }, + "Order": { + "type": "object", + "properties": { + "member": { + "$ref": "#/definitions/Member", + "description": "Member" + } + } + }, + "Member": { + "type": "object", + "properties": { + "referredMember": { + "$ref": "#/definitions/Member", + "description": "Member" + } + } + } + } +} diff --git a/src/__tests__/__snapshots__/tree.spec.ts.snap b/src/__tests__/__snapshots__/tree.spec.ts.snap index 2708563..15893f9 100644 --- a/src/__tests__/__snapshots__/tree.spec.ts.snap +++ b/src/__tests__/__snapshots__/tree.spec.ts.snap @@ -1279,6 +1279,36 @@ exports[`SchemaTree output should generate valid tree for references/base.json 1 " `; +exports[`SchemaTree output should generate valid tree for references/circular-with-overrides.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/properties/order + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/properties/order/properties/member + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/properties/order/properties/member/properties/referredMember + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/properties/order/properties/member/properties/referredMember/properties/referredMember + └─ mirrors: #/properties/order/properties/member/properties/referredMember +" +`; + exports[`SchemaTree output should generate valid tree for references/nullish.json 1`] = ` "└─ # ├─ types diff --git a/src/walker/walker.ts b/src/walker/walker.ts index af4e0c0..7989d90 100644 --- a/src/walker/walker.ts +++ b/src/walker/walker.ts @@ -285,6 +285,8 @@ export class Walker extends EventEmitter<WalkerEmitter> { return retrieved; } + let initialFragment: ProcessedFragment = fragment; + if ('$ref' in fragment) { if (typeof walkingOptions.maxRefDepth === 'number' && walkingOptions.maxRefDepth < depth) { return [new ReferenceNode(fragment, `max $ref depth limit reached`), fragment]; @@ -297,6 +299,11 @@ export class Walker extends EventEmitter<WalkerEmitter> { if (typeof fragment.description === 'string') { newFragment = { ...newFragment }; Object.assign(newFragment, { description: fragment.description }); + } else { + retrieved = this.retrieveFromFragment(newFragment, originalFragment); + if (retrieved) { + return retrieved; + } } fragment = newFragment; @@ -331,7 +338,6 @@ export class Walker extends EventEmitter<WalkerEmitter> { } } } - let initialFragment: ProcessedFragment = fragment; if (walkingOptions.mergeAllOf && SchemaCombinerName.AllOf in fragment) { try { if (Array.isArray(fragment.allOf)) { From 44abda77236b9d14c584426d7b00c4777c2cb5a8 Mon Sep 17 00:00:00 2001 From: Brenda Rearden <brendarearden@gmail.com> Date: Thu, 14 Dec 2023 11:52:41 -0700 Subject: [PATCH 09/11] fix(json-schema-merge-allof): bump json-schema-merge-allOf version (#28) BREAKING CHANGE: description and summary reference siblings will not get overwritten by description and summary from the referenced object --- package.json | 2 +- src/__tests__/tree.spec.ts | 55 ++++++++++++++++++++++++++++++++++++++ yarn.lock | 8 +++--- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 844c8a0..a4b078e 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "peerDependencies": {}, "dependencies": { "@stoplight/json": "^3.12.0", - "@stoplight/json-schema-merge-allof": "^0.7.8", + "@stoplight/json-schema-merge-allof": "^0.8.0", "@stoplight/lifecycle": "^2.3.2", "@types/json-schema": "^7.0.7", "magic-error": "0.0.1" diff --git a/src/__tests__/tree.spec.ts b/src/__tests__/tree.spec.ts index 4216bad..721cf98 100644 --- a/src/__tests__/tree.spec.ts +++ b/src/__tests__/tree.spec.ts @@ -787,6 +787,61 @@ describe('SchemaTree', () => { ).toEqual('_Everyone_ ~hates~ loves caves'); }); + it('should not override description reference siblings', () => { + const schema = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + AAAAA: { + allOf: [{ description: 'AAAAA', type: 'string' }, { examples: ['AAAAA'] }], + }, + BBBBB: { + allOf: [ + { + $ref: '#/properties/AAAAA/allOf/0', + description: 'BBBBB', + }, + { examples: ['BBBBB'] }, + ], + }, + }, + }; + + const tree = new SchemaTree(schema, {}); + tree.populate(); + + expect(tree.root).toEqual( + expect.objectContaining({ + children: [ + expect.objectContaining({ + primaryType: 'object', + types: ['object'], + children: [ + expect.objectContaining({ + primaryType: 'string', + subpath: ['properties', 'AAAAA'], + types: ['string'], + annotations: { + description: 'AAAAA', + examples: ['AAAAA'], + }, + }), + expect.objectContaining({ + primaryType: 'string', + subpath: ['properties', 'BBBBB'], + types: ['string'], + annotations: { + description: 'BBBBB', + examples: ['BBBBB'], + }, + }), + ], + }), + ], + }), + ); + }); + it('node of type array should keep its own description even when referenced node has a description', () => { const schema = { definitions: { diff --git a/yarn.lock b/yarn.lock index 0ec7f5f..0a95cbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -936,10 +936,10 @@ dependencies: eslint-config-prettier "^7.1.0" -"@stoplight/json-schema-merge-allof@^0.7.8": - version "0.7.8" - resolved "https://registry.yarnpkg.com/@stoplight/json-schema-merge-allof/-/json-schema-merge-allof-0.7.8.tgz#7efe5e0086dff433eb011f617e82f7295c3de061" - integrity sha512-JTDt6GYpCWQSb7+UW1P91IAp/pcLWis0mmEzWVFcLsrNgtUYK7JLtYYz0ZPSR4QVL0fJ0YQejM+MPq5iNDFO4g== +"@stoplight/json-schema-merge-allof@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@stoplight/json-schema-merge-allof/-/json-schema-merge-allof-0.8.0.tgz#62f8116f59d9df5a910d037b1965decd2efab472" + integrity sha512-g8e0s43v96Xbzvd8d6KKUuJTO16CS2oJglJrviUi8ASIUxzFvAJqTHWLtGmpTryisQopqg1evXGJfi0+164+Qw== dependencies: compute-lcm "^1.1.0" json-schema-compare "^0.2.2" From 8300b1224d1a44215506481f0574235bc10011ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= <jakub@stoplight.io> Date: Tue, 23 Jan 2024 11:22:23 +0100 Subject: [PATCH 10/11] feat: support booleanish schemas & represent additional{Items,Properties} (#31) BREAKING CHANGE: additionalProperties/additionalItems keywords are now processed as schemas BREAKING CHANGE: true/false schemas are now represented --- jest.config.js | 1 + .../__fixtures__/arrays/additional-empty.json | 4 + .../__fixtures__/arrays/additional-false.json | 4 + .../arrays/additional-schema.json | 11 ++ .../__fixtures__/arrays/additional-true.json | 4 + .../arrays/with-multiple-arrayish-items.json | 5 +- .../arrays/with-single-arrayish-items.json | 5 +- .../objects/additional-empty.json | 4 + .../objects/additional-false.json | 4 + .../objects/additional-schema.json | 11 ++ .../__fixtures__/objects/additional-true.json | 4 + src/__tests__/__snapshots__/tree.spec.ts.snap | 121 +++++++++++++++++- src/__tests__/tree.spec.ts | 28 ++++ src/__tests__/utils/printTree.ts | 29 +++-- src/accessors/getValidations.ts | 4 +- src/guards/nodes.ts | 5 + src/nodes/BaseNode.ts | 3 +- src/nodes/BooleanishNode.ts | 7 + src/nodes/ReferenceNode.ts | 4 +- src/nodes/RegularNode.ts | 5 +- src/nodes/RootNode.ts | 2 +- src/nodes/index.ts | 1 + src/nodes/mirrored/MirroredReferenceNode.ts | 6 +- src/nodes/mirrored/MirroredRegularNode.ts | 24 +++- src/nodes/types.ts | 3 +- src/utils/guards.ts | 6 + src/walker/types.ts | 4 +- src/walker/walker.ts | 67 +++++++--- 28 files changed, 317 insertions(+), 59 deletions(-) create mode 100644 src/__tests__/__fixtures__/arrays/additional-empty.json create mode 100644 src/__tests__/__fixtures__/arrays/additional-false.json create mode 100644 src/__tests__/__fixtures__/arrays/additional-schema.json create mode 100644 src/__tests__/__fixtures__/arrays/additional-true.json create mode 100644 src/__tests__/__fixtures__/objects/additional-empty.json create mode 100644 src/__tests__/__fixtures__/objects/additional-false.json create mode 100644 src/__tests__/__fixtures__/objects/additional-schema.json create mode 100644 src/__tests__/__fixtures__/objects/additional-true.json create mode 100644 src/nodes/BooleanishNode.ts diff --git a/jest.config.js b/jest.config.js index 58f0a45..5346809 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,7 @@ module.exports = { rootDir: process.cwd(), testEnvironment: 'node', + roots: ['<rootDir>/src'], setupFilesAfterEnv: ['./setupTests.ts'], testMatch: ['<rootDir>/src/**/__tests__/*.(ts|js)?(x)'], transform: { diff --git a/src/__tests__/__fixtures__/arrays/additional-empty.json b/src/__tests__/__fixtures__/arrays/additional-empty.json new file mode 100644 index 0000000..b968368 --- /dev/null +++ b/src/__tests__/__fixtures__/arrays/additional-empty.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "additionalItems": {} +} diff --git a/src/__tests__/__fixtures__/arrays/additional-false.json b/src/__tests__/__fixtures__/arrays/additional-false.json new file mode 100644 index 0000000..024923c --- /dev/null +++ b/src/__tests__/__fixtures__/arrays/additional-false.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "additionalItems": false +} diff --git a/src/__tests__/__fixtures__/arrays/additional-schema.json b/src/__tests__/__fixtures__/arrays/additional-schema.json new file mode 100644 index 0000000..d795cb4 --- /dev/null +++ b/src/__tests__/__fixtures__/arrays/additional-schema.json @@ -0,0 +1,11 @@ +{ + "type": "array", + "additionalItems": { + "type": "object", + "properties": { + "baz": { + "type": "number" + } + } + } +} diff --git a/src/__tests__/__fixtures__/arrays/additional-true.json b/src/__tests__/__fixtures__/arrays/additional-true.json new file mode 100644 index 0000000..01777c1 --- /dev/null +++ b/src/__tests__/__fixtures__/arrays/additional-true.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "additionalItems": true +} diff --git a/src/__tests__/__fixtures__/arrays/with-multiple-arrayish-items.json b/src/__tests__/__fixtures__/arrays/with-multiple-arrayish-items.json index e45d26d..6a9e8ef 100644 --- a/src/__tests__/__fixtures__/arrays/with-multiple-arrayish-items.json +++ b/src/__tests__/__fixtures__/arrays/with-multiple-arrayish-items.json @@ -17,10 +17,7 @@ "type": "string" } }, - "required": [ - "code", - "msg" - ] + "required": ["code", "msg"] } ] } diff --git a/src/__tests__/__fixtures__/arrays/with-single-arrayish-items.json b/src/__tests__/__fixtures__/arrays/with-single-arrayish-items.json index f15e8d3..b5b53c5 100644 --- a/src/__tests__/__fixtures__/arrays/with-single-arrayish-items.json +++ b/src/__tests__/__fixtures__/arrays/with-single-arrayish-items.json @@ -14,10 +14,7 @@ "type": "string" } }, - "required": [ - "code", - "msg" - ] + "required": ["code", "msg"] } ] } diff --git a/src/__tests__/__fixtures__/objects/additional-empty.json b/src/__tests__/__fixtures__/objects/additional-empty.json new file mode 100644 index 0000000..08c87bb --- /dev/null +++ b/src/__tests__/__fixtures__/objects/additional-empty.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "additionalProperties": {} +} diff --git a/src/__tests__/__fixtures__/objects/additional-false.json b/src/__tests__/__fixtures__/objects/additional-false.json new file mode 100644 index 0000000..97ea2a4 --- /dev/null +++ b/src/__tests__/__fixtures__/objects/additional-false.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "additionalProperties": false +} diff --git a/src/__tests__/__fixtures__/objects/additional-schema.json b/src/__tests__/__fixtures__/objects/additional-schema.json new file mode 100644 index 0000000..66de7a3 --- /dev/null +++ b/src/__tests__/__fixtures__/objects/additional-schema.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "baz": { + "type": "number" + } + } + } +} diff --git a/src/__tests__/__fixtures__/objects/additional-true.json b/src/__tests__/__fixtures__/objects/additional-true.json new file mode 100644 index 0000000..9bc2c95 --- /dev/null +++ b/src/__tests__/__fixtures__/objects/additional-true.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "additionalProperties": true +} diff --git a/src/__tests__/__snapshots__/tree.spec.ts.snap b/src/__tests__/__snapshots__/tree.spec.ts.snap index 15893f9..28d8a9e 100644 --- a/src/__tests__/__snapshots__/tree.spec.ts.snap +++ b/src/__tests__/__snapshots__/tree.spec.ts.snap @@ -296,6 +296,61 @@ exports[`SchemaTree output compound keywords given oneOf combiner placed next to " `; +exports[`SchemaTree output should generate valid tree for arrays/additional-empty.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: array + ├─ primaryType: array + └─ children + └─ 0 + └─ #/additionalItems +" +`; + +exports[`SchemaTree output should generate valid tree for arrays/additional-false.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: array + ├─ primaryType: array + └─ children + └─ 0 + └─ #/additionalItems + └─ value: false +" +`; + +exports[`SchemaTree output should generate valid tree for arrays/additional-schema.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: array + ├─ primaryType: array + └─ children + └─ 0 + └─ #/additionalItems + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/additionalItems/properties/baz + ├─ types + │ └─ 0: number + └─ primaryType: number +" +`; + +exports[`SchemaTree output should generate valid tree for arrays/additional-true.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: array + ├─ primaryType: array + └─ children + └─ 0 + └─ #/additionalItems + └─ value: true +" +`; + exports[`SchemaTree output should generate valid tree for arrays/of-allofs.json 1`] = ` "└─ # ├─ types @@ -823,7 +878,16 @@ exports[`SchemaTree output should generate valid tree for combiners/allOfs/neste │ └─ #/properties/order │ ├─ types │ │ └─ 0: object - │ └─ primaryType: object + │ ├─ primaryType: object + │ └─ children + │ └─ 0 + │ └─ #/properties/order/additionalProperties + │ ├─ types + │ │ └─ 0: string + │ ├─ primaryType: string + │ └─ enum + │ ├─ 0: ASC + │ └─ 1: DESC └─ 7 └─ #/properties/nextToken ├─ types @@ -1246,6 +1310,61 @@ exports[`SchemaTree output should generate valid tree for formats-schema.json 1` " `; +exports[`SchemaTree output should generate valid tree for objects/additional-empty.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/additionalProperties +" +`; + +exports[`SchemaTree output should generate valid tree for objects/additional-false.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/additionalProperties + └─ value: false +" +`; + +exports[`SchemaTree output should generate valid tree for objects/additional-schema.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/additionalProperties + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/additionalProperties/properties/baz + ├─ types + │ └─ 0: number + └─ primaryType: number +" +`; + +exports[`SchemaTree output should generate valid tree for objects/additional-true.json 1`] = ` +"└─ # + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + └─ 0 + └─ #/additionalProperties + └─ value: true +" +`; + exports[`SchemaTree output should generate valid tree for references/base.json 1`] = ` "└─ # ├─ types diff --git a/src/__tests__/tree.spec.ts b/src/__tests__/tree.spec.ts index 721cf98..0f88231 100644 --- a/src/__tests__/tree.spec.ts +++ b/src/__tests__/tree.spec.ts @@ -907,6 +907,34 @@ describe('SchemaTree', () => { tree.root.children[0].children[1].annotations.description, ).toEqual('_Everyone_ ~hates~ loves caves'); }); + + it('should render true/false schemas', () => { + const schema = { + type: 'object', + properties: { + bear: true, + cave: false, + }, + }; + + const tree = new SchemaTree(schema); + tree.populate(); + + expect(printTree(schema)).toMatchInlineSnapshot(` + "└─ # + ├─ types + │ └─ 0: object + ├─ primaryType: object + └─ children + ├─ 0 + │ └─ #/properties/bear + │ └─ value: true + └─ 1 + └─ #/properties/cave + └─ value: false + " + `); + }); }); describe('position', () => { diff --git a/src/__tests__/utils/printTree.ts b/src/__tests__/utils/printTree.ts index 18d7cbb..f329a91 100644 --- a/src/__tests__/utils/printTree.ts +++ b/src/__tests__/utils/printTree.ts @@ -2,8 +2,9 @@ import { pathToPointer } from '@stoplight/json'; import type { Dictionary } from '@stoplight/types'; import * as treeify from 'treeify'; -import { isMirroredNode, isReferenceNode, isRegularNode } from '../../guards'; +import { isBooleanishNode, isMirroredNode, isReferenceNode, isRegularNode, isRootNode } from '../../guards'; import type { MirroredSchemaNode, ReferenceNode, RegularNode, SchemaNode } from '../../nodes'; +import type { BooleanishNode } from '../../nodes/BooleanishNode'; import type { SchemaTreeOptions } from '../../tree'; import { SchemaTree } from '../../tree'; import type { SchemaFragment } from '../../types'; @@ -41,6 +42,12 @@ function printReferenceNode(node: ReferenceNode) { }; } +function printBooleanishNode(node: BooleanishNode) { + return { + value: node.fragment, + }; +} + function printMirrorNode(node: MirroredSchemaNode): any { return { mirrors: pathToPointer(node.mirroredNode.path as string[]), @@ -48,15 +55,17 @@ function printMirrorNode(node: MirroredSchemaNode): any { } function printNode(node: SchemaNode) { - return isMirroredNode(node) - ? printMirrorNode(node) - : isRegularNode(node) - ? printRegularNode(node) - : isReferenceNode(node) - ? printReferenceNode(node) - : { - kind: 'unknown node', - }; + if (isMirroredNode(node)) { + return printMirrorNode(node); + } else if (isRegularNode(node)) { + return printRegularNode(node); + } else if (isReferenceNode(node)) { + return printReferenceNode(node); + } else if (isBooleanishNode(node)) { + return printBooleanishNode(node); + } else if (isRootNode(node)) { + return {}; + } } function prepareTree(node: SchemaNode) { diff --git a/src/accessors/getValidations.ts b/src/accessors/getValidations.ts index 2d5394d..6425248 100644 --- a/src/accessors/getValidations.ts +++ b/src/accessors/getValidations.ts @@ -12,8 +12,8 @@ const VALIDATION_TYPES: Partial<Dictionary<(keyof SchemaFragment)[], SchemaNodeK get integer() { return this.number; }, - object: ['additionalProperties', 'minProperties', 'maxProperties'], - array: ['additionalItems', 'minItems', 'maxItems', 'uniqueItems'], + object: ['minProperties', 'maxProperties'], + array: ['minItems', 'maxItems', 'uniqueItems'], }; function getTypeValidations(types: SchemaNodeKind[]): (keyof SchemaFragment)[] | null { diff --git a/src/guards/nodes.ts b/src/guards/nodes.ts index 0557b41..55c3e48 100644 --- a/src/guards/nodes.ts +++ b/src/guards/nodes.ts @@ -7,6 +7,7 @@ import { RootNode, SchemaNode, } from '../nodes'; +import type { BooleanishNode } from '../nodes/BooleanishNode'; export function isSchemaNode(node: unknown): node is SchemaNode { const name = Object.getPrototypeOf(node).constructor.name; @@ -34,3 +35,7 @@ export function isMirroredNode(node: SchemaNode): node is MirroredSchemaNode { export function isReferenceNode(node: SchemaNode): node is ReferenceNode { return 'external' in node && 'value' in node; } + +export function isBooleanishNode(node: SchemaNode): node is BooleanishNode { + return typeof node.fragment === 'boolean'; +} diff --git a/src/nodes/BaseNode.ts b/src/nodes/BaseNode.ts index 29d50b6..9546f80 100644 --- a/src/nodes/BaseNode.ts +++ b/src/nodes/BaseNode.ts @@ -1,4 +1,3 @@ -import type { SchemaFragment } from '../types'; import type { MirroredRegularNode } from './mirrored'; import type { RegularNode } from './RegularNode'; import type { RootNode } from './RootNode'; @@ -35,7 +34,7 @@ export abstract class BaseNode { return this.pos === this.parentChildren.length - 1; } - protected constructor(public readonly fragment: SchemaFragment) { + protected constructor() { this.id = String(SEED++); this.subpath = []; } diff --git a/src/nodes/BooleanishNode.ts b/src/nodes/BooleanishNode.ts new file mode 100644 index 0000000..57fad0e --- /dev/null +++ b/src/nodes/BooleanishNode.ts @@ -0,0 +1,7 @@ +import { BaseNode } from './BaseNode'; + +export class BooleanishNode extends BaseNode { + constructor(public readonly fragment: boolean) { + super(); + } +} diff --git a/src/nodes/ReferenceNode.ts b/src/nodes/ReferenceNode.ts index b9401b7..b05bff1 100644 --- a/src/nodes/ReferenceNode.ts +++ b/src/nodes/ReferenceNode.ts @@ -7,8 +7,8 @@ import { BaseNode } from './BaseNode'; export class ReferenceNode extends BaseNode { public readonly value: string | null; - constructor(fragment: SchemaFragment, public readonly error: string | null) { - super(fragment); + constructor(public readonly fragment: SchemaFragment, public readonly error: string | null) { + super(); this.value = unwrapStringOrNull(fragment.$ref); } diff --git a/src/nodes/RegularNode.ts b/src/nodes/RegularNode.ts index 865ac24..ed8aa7a 100644 --- a/src/nodes/RegularNode.ts +++ b/src/nodes/RegularNode.ts @@ -10,6 +10,7 @@ import { isDeprecated } from '../accessors/isDeprecated'; import { unwrapArrayOrNull, unwrapStringOrNull } from '../accessors/unwrap'; import type { SchemaFragment } from '../types'; import { BaseNode } from './BaseNode'; +import type { BooleanishNode } from './BooleanishNode'; import type { ReferenceNode } from './ReferenceNode'; import { MirroredSchemaNode, SchemaAnnotations, SchemaCombinerName, SchemaNodeKind } from './types'; @@ -25,14 +26,14 @@ export class RegularNode extends BaseNode { public readonly title: string | null; public readonly deprecated: boolean; - public children: (RegularNode | ReferenceNode | MirroredSchemaNode)[] | null | undefined; + public children: (RegularNode | BooleanishNode | ReferenceNode | MirroredSchemaNode)[] | null | undefined; public readonly annotations: Readonly<Partial<Dictionary<unknown, SchemaAnnotations>>>; public readonly validations: Readonly<Dictionary<unknown>>; public readonly originalFragment: SchemaFragment; constructor(public readonly fragment: SchemaFragment, context?: { originalFragment?: SchemaFragment }) { - super(fragment); + super(); this.$id = unwrapStringOrNull('id' in fragment ? fragment.id : fragment.$id); this.types = getTypes(fragment); diff --git a/src/nodes/RootNode.ts b/src/nodes/RootNode.ts index 6c744b4..9d7f8f1 100644 --- a/src/nodes/RootNode.ts +++ b/src/nodes/RootNode.ts @@ -7,7 +7,7 @@ export class RootNode extends BaseNode { public readonly children: SchemaNode[]; constructor(public readonly fragment: SchemaFragment) { - super(fragment); + super(); this.children = []; } } diff --git a/src/nodes/index.ts b/src/nodes/index.ts index 4bd4715..b60c970 100644 --- a/src/nodes/index.ts +++ b/src/nodes/index.ts @@ -1,4 +1,5 @@ export { BaseNode } from './BaseNode'; +export { BooleanishNode } from './BooleanishNode'; export * from './mirrored'; export { ReferenceNode } from './ReferenceNode'; export { RegularNode } from './RegularNode'; diff --git a/src/nodes/mirrored/MirroredReferenceNode.ts b/src/nodes/mirrored/MirroredReferenceNode.ts index d1404c8..89cccf4 100644 --- a/src/nodes/mirrored/MirroredReferenceNode.ts +++ b/src/nodes/mirrored/MirroredReferenceNode.ts @@ -1,9 +1,13 @@ +import type { SchemaFragment } from '../../types'; import { BaseNode } from '../BaseNode'; import type { ReferenceNode } from '../ReferenceNode'; export class MirroredReferenceNode extends BaseNode implements ReferenceNode { + public readonly fragment: SchemaFragment; + constructor(public readonly mirroredNode: ReferenceNode) { - super(mirroredNode.fragment); + super(); + this.fragment = mirroredNode.fragment; } get error() { diff --git a/src/nodes/mirrored/MirroredRegularNode.ts b/src/nodes/mirrored/MirroredRegularNode.ts index 7eb07b7..873c15f 100644 --- a/src/nodes/mirrored/MirroredRegularNode.ts +++ b/src/nodes/mirrored/MirroredRegularNode.ts @@ -1,15 +1,17 @@ import type { Dictionary } from '@stoplight/types'; -import { isRegularNode } from '../../guards'; +import { isReferenceNode, isRegularNode } from '../../guards'; import type { SchemaFragment } from '../../types'; import { isNonNullable } from '../../utils'; import { BaseNode } from '../BaseNode'; +import { BooleanishNode } from '../BooleanishNode'; import type { ReferenceNode } from '../ReferenceNode'; import type { RegularNode } from '../RegularNode'; import type { SchemaAnnotations, SchemaCombinerName, SchemaNodeKind } from '../types'; import { MirroredReferenceNode } from './MirroredReferenceNode'; export class MirroredRegularNode extends BaseNode implements RegularNode { + public readonly fragment: SchemaFragment; public readonly $id!: string | null; public readonly types!: SchemaNodeKind[] | null; public readonly primaryType!: SchemaNodeKind | null; @@ -28,10 +30,14 @@ export class MirroredRegularNode extends BaseNode implements RegularNode { public readonly simple!: boolean; public readonly unknown!: boolean; - private readonly cache: WeakMap<RegularNode | ReferenceNode, MirroredRegularNode | MirroredReferenceNode>; + private readonly cache: WeakMap< + RegularNode | BooleanishNode | ReferenceNode, + MirroredRegularNode | BooleanishNode | MirroredReferenceNode + >; constructor(public readonly mirroredNode: RegularNode, context?: { originalFragment?: SchemaFragment }) { - super(mirroredNode.fragment); + super(); + this.fragment = mirroredNode.fragment; this.originalFragment = context?.originalFragment ?? mirroredNode.originalFragment; this.cache = new WeakMap(); @@ -59,9 +65,9 @@ export class MirroredRegularNode extends BaseNode implements RegularNode { private readonly _this: MirroredRegularNode; - private _children?: (MirroredRegularNode | MirroredReferenceNode)[]; + private _children?: (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[]; - public get children(): (MirroredRegularNode | MirroredReferenceNode)[] | null | undefined { + public get children(): (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[] | null | undefined { const referencedChildren = this.mirroredNode.children; if (!isNonNullable(referencedChildren)) { @@ -74,7 +80,7 @@ export class MirroredRegularNode extends BaseNode implements RegularNode { this._children.length = 0; } - const children: (MirroredRegularNode | MirroredReferenceNode)[] = this._children; + const children: (MirroredRegularNode | BooleanishNode | MirroredReferenceNode)[] = this._children; for (const child of referencedChildren) { // this is to avoid pointing at nested mirroring const cached = this.cache.get(child); @@ -84,7 +90,11 @@ export class MirroredRegularNode extends BaseNode implements RegularNode { continue; } - const mirroredChild = isRegularNode(child) ? new MirroredRegularNode(child) : new MirroredReferenceNode(child); + const mirroredChild = isRegularNode(child) + ? new MirroredRegularNode(child) + : isReferenceNode(child) + ? new MirroredReferenceNode(child) + : new BooleanishNode(child.fragment); mirroredChild.parent = this._this; mirroredChild.subpath = child.subpath; diff --git a/src/nodes/types.ts b/src/nodes/types.ts index 5d69517..2723d4a 100644 --- a/src/nodes/types.ts +++ b/src/nodes/types.ts @@ -1,3 +1,4 @@ +import type { BooleanishNode } from './BooleanishNode'; import type { MirroredReferenceNode } from './mirrored/MirroredReferenceNode'; import type { MirroredRegularNode } from './mirrored/MirroredRegularNode'; import type { ReferenceNode } from './ReferenceNode'; @@ -6,7 +7,7 @@ import type { RootNode } from './RootNode'; export type MirroredSchemaNode = MirroredRegularNode | MirroredReferenceNode; -export type SchemaNode = RootNode | RegularNode | ReferenceNode | MirroredSchemaNode; +export type SchemaNode = RootNode | RegularNode | BooleanishNode | ReferenceNode | MirroredSchemaNode; export enum SchemaNodeKind { Any = 'any', diff --git a/src/utils/guards.ts b/src/utils/guards.ts index 085f14a..778deae 100644 --- a/src/utils/guards.ts +++ b/src/utils/guards.ts @@ -1,5 +1,7 @@ import type { Dictionary } from '@stoplight/types'; +import type { SchemaFragment } from '../types'; + export function isStringOrNumber(value: unknown): value is number | string { return typeof value === 'string' || typeof value === 'number'; } @@ -23,3 +25,7 @@ export function isObjectLiteral(maybeObj: unknown): maybeObj is Dictionary<unkno export function isNonNullable<T = unknown>(maybeNullable: T): maybeNullable is NonNullable<T> { return maybeNullable !== void 0 && maybeNullable !== null; } + +export function isValidSchemaFragment(maybeSchemaFragment: unknown): maybeSchemaFragment is SchemaFragment { + return typeof maybeSchemaFragment === 'boolean' || isObjectLiteral(maybeSchemaFragment); +} diff --git a/src/walker/types.ts b/src/walker/types.ts index 34f7b74..284f73d 100644 --- a/src/walker/types.ts +++ b/src/walker/types.ts @@ -13,7 +13,7 @@ export type WalkingOptions = { }; export type WalkerSnapshot = { - readonly fragment: SchemaFragment; + readonly fragment: SchemaFragment | boolean; readonly depth: number; readonly schemaNode: RegularNode | RootNode; readonly path: string[]; @@ -23,7 +23,7 @@ export type WalkerHookAction = 'filter' | 'stepIn'; export type WalkerHookHandler = (node: SchemaNode) => boolean; export type WalkerNodeEventHandler = (node: SchemaNode) => void; -export type WalkerFragmentEventHandler = (node: SchemaFragment) => void; +export type WalkerFragmentEventHandler = (node: SchemaFragment | boolean) => void; export type WalkerErrorEventHandler = (ex: Error) => void; export type WalkerEmitter = { diff --git a/src/walker/walker.ts b/src/walker/walker.ts index 7989d90..02efbd5 100644 --- a/src/walker/walker.ts +++ b/src/walker/walker.ts @@ -7,10 +7,11 @@ import { isMirroredNode, isReferenceNode, isRegularNode, isRootNode } from '../g import { mergeAllOf } from '../mergers/mergeAllOf'; import { mergeOneOrAnyOf } from '../mergers/mergeOneOrAnyOf'; import { MirroredReferenceNode, MirroredRegularNode, MirroredSchemaNode, ReferenceNode, RegularNode } from '../nodes'; +import { BooleanishNode } from '../nodes/BooleanishNode'; import type { RootNode } from '../nodes/RootNode'; import { SchemaCombinerName, SchemaNode, SchemaNodeKind } from '../nodes/types'; import type { SchemaFragment } from '../types'; -import { isNonNullable, isObjectLiteral } from '../utils/guards'; +import { isNonNullable, isObjectLiteral, isValidSchemaFragment } from '../utils/guards'; import type { WalkerEmitter, WalkerHookAction, WalkerHookHandler, WalkerSnapshot, WalkingOptions } from './types'; type InternalWalkerState = { @@ -25,7 +26,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { public readonly path: string[]; public depth: number; - protected fragment: SchemaFragment; + protected fragment: SchemaFragment | boolean; protected schemaNode: RegularNode | RootNode; private mergedAllOfs: WeakMap<SchemaFragment, SchemaFragment>; @@ -124,12 +125,19 @@ export class Walker extends EventEmitter<WalkerEmitter> { super.emit('enterNode', schemaNode); const actualNode = isMirroredNode(schemaNode) ? schemaNode.mirroredNode : schemaNode; - this.processedFragments.set(schemaNode.fragment, actualNode); - this.processedFragments.set(initialFragment, actualNode); + if (typeof schemaNode.fragment !== 'boolean' && initialFragment !== null) { + this.processedFragments.set(schemaNode.fragment, actualNode); + this.processedFragments.set(initialFragment, actualNode); + } this.fragment = schemaNode.fragment; this.depth = initialDepth + 1; + if (!isRootNode(schemaNode)) { + schemaNode.parent = initialSchemaNode; + schemaNode.subpath = this.path.slice(initialSchemaNode.path.length); + } + const isIncluded = this.hooks.filter?.(schemaNode); if (isIncluded === false) { @@ -137,11 +145,6 @@ export class Walker extends EventEmitter<WalkerEmitter> { return; } - if (!isRootNode(schemaNode)) { - schemaNode.parent = initialSchemaNode; - schemaNode.subpath = this.path.slice(initialSchemaNode.path.length); - } - if ('children' in initialSchemaNode && !isRootNode(schemaNode)) { if (initialSchemaNode.children === void 0) { (initialSchemaNode as RegularNode).children = [schemaNode]; @@ -186,7 +189,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { protected walkNodeChildren(): void { const { fragment, schemaNode } = this; - if (!isRegularNode(schemaNode)) return; + if (!isRegularNode(schemaNode) || typeof fragment === 'boolean') return; const state = this.dumpInternalWalkerState(); @@ -213,17 +216,26 @@ export class Walker extends EventEmitter<WalkerEmitter> { let i = -1; for (const item of fragment.items) { i++; - if (!isObjectLiteral(item)) continue; + if (!isValidSchemaFragment(item)) continue; this.fragment = item; this.restoreInternalWalkerState(state); this.path.push('items', String(i)); this.walk(); } - } else if (isObjectLiteral(fragment.items)) { - this.fragment = fragment.items; - this.restoreInternalWalkerState(state); - this.path.push('items'); - this.walk(); + } else { + if (isObjectLiteral(fragment.items)) { + this.fragment = fragment.items; + this.restoreInternalWalkerState(state); + this.path.push('items'); + this.walk(); + } + + if (isValidSchemaFragment(fragment.additionalItems)) { + this.fragment = fragment.additionalItems; + this.restoreInternalWalkerState(state); + this.path.push('additionalItems'); + this.walk(); + } } break; @@ -231,7 +243,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { if (isObjectLiteral(fragment.properties)) { for (const key of Object.keys(fragment.properties)) { const value = fragment.properties[key]; - if (!isObjectLiteral(value)) continue; + if (!isValidSchemaFragment(value)) continue; this.fragment = value; this.restoreInternalWalkerState(state); this.path.push('properties', key); @@ -242,7 +254,7 @@ export class Walker extends EventEmitter<WalkerEmitter> { if (isObjectLiteral(fragment.patternProperties)) { for (const key of Object.keys(fragment.patternProperties)) { const value = fragment.patternProperties[key]; - if (!isObjectLiteral(value)) continue; + if (!isValidSchemaFragment(value)) continue; this.fragment = value; this.restoreInternalWalkerState(state); this.path.push('patternProperties', key); @@ -250,6 +262,13 @@ export class Walker extends EventEmitter<WalkerEmitter> { } } + if (isValidSchemaFragment(fragment.additionalProperties)) { + this.fragment = fragment.additionalProperties; + this.restoreInternalWalkerState(state); + this.path.push('additionalProperties'); + this.walk(); + } + break; } @@ -275,11 +294,19 @@ export class Walker extends EventEmitter<WalkerEmitter> { } } - protected processFragment(): [SchemaNode, ProcessedFragment] { + protected processFragment(): [SchemaNode, ProcessedFragment | null] { const { walkingOptions, path, fragment: originalFragment, depth } = this; let { fragment } = this; - let retrieved = isNonNullable(fragment) ? this.retrieveFromFragment(fragment, originalFragment) : null; + if (typeof fragment === 'boolean') { + return [new BooleanishNode(fragment), null]; + } + + if (typeof originalFragment === 'boolean') { + throw new TypeError('Original fragment cannot be a boolean'); + } + + let retrieved = isNonNullable(fragment) ? this.retrieveFromFragment(fragment, fragment) : null; if (retrieved) { return retrieved; From 636840c263b426d62e699aa6ca83da9acf283e67 Mon Sep 17 00:00:00 2001 From: SB-harshitajadhav <165134656+SB-harshitajadhav@users.noreply.github.com> Date: Wed, 8 Jan 2025 17:34:39 +0530 Subject: [PATCH 11/11] chore(deps): bump cross-spawn 7.0.5 (#38) --- package.json | 3 +++ yarn.lock | 69 ++++++---------------------------------------------- 2 files changed, 10 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index a4b078e..f4b6090 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,9 @@ "ts-jest": "^26.4.4", "typescript": "^4.1.3" }, + "resolutions": { + "cross-spawn": "7.0.5" + }, "lint-staged": { "*.{ts,tsx}": [ "eslint --fix --cache --cache-location .cache" diff --git a/yarn.lock b/yarn.lock index 0a95cbc..786cc24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2441,30 +2441,10 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^6.0.0, cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^7.0.0, cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== +cross-spawn@7.0.5, cross-spawn@^5.0.1, cross-spawn@^6.0.0, cross-spawn@^6.0.5, cross-spawn@^7.0.0, cross-spawn@^7.0.2: + version "7.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" + integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -5688,14 +5668,6 @@ lowercase-keys@^1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -6078,11 +6050,6 @@ nerf-dart@^1.0.0: resolved "https://registry.yarnpkg.com/nerf-dart/-/nerf-dart-1.0.0.tgz#e6dab7febf5ad816ea81cf5c629c5a0ebde72c1a" integrity sha1-5tq3/r9a2Bbqgc9cYpxaDr3nLBo= -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - node-emoji@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" @@ -6856,7 +6823,7 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= -path-key@^2.0.0, path-key@^2.0.1: +path-key@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= @@ -7052,11 +7019,6 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - psl@^1.1.28: version "1.3.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd" @@ -7713,7 +7675,7 @@ semver-regex@^2.0.0: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.1, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -7762,13 +7724,6 @@ sha@^3.0.0: dependencies: graceful-fs "^4.1.2" -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -7776,11 +7731,6 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" @@ -8957,7 +8907,7 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= -which@1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: +which@1, which@^1.2.14, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -9112,11 +9062,6 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"