-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(json-schema-parser): add traverseJsonSchema
- Loading branch information
1 parent
2fea7a1
commit 5449357
Showing
4 changed files
with
386 additions
and
0 deletions.
There are no files selected for viewing
223 changes: 223 additions & 0 deletions
223
packages/parser/json-schema-parser/src/jsonSchema/202012/actions/traverseJsonSchema.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; | ||
|
||
import { | ||
JsonRootSchemaObject, | ||
JsonSchema, | ||
} from '@cuaklabs/json-schema-types/2020-12'; | ||
|
||
import { JsonRootSchemaFixtures } from '../fixtures/JsonRootSchemaFixtures'; | ||
import { TraverseJsonSchemaCallbackParams } from '../models/TraverseJsonSchemaCallbackParams'; | ||
import { traverseJsonSchema } from './traverseJsonSchema'; | ||
|
||
describe(traverseJsonSchema.name, () => { | ||
let callbackMock: jest.Mock< | ||
(params: TraverseJsonSchemaCallbackParams) => void | ||
>; | ||
|
||
beforeAll(() => { | ||
callbackMock = jest.fn(); | ||
}); | ||
|
||
describe('when called', () => { | ||
beforeAll(() => { | ||
traverseJsonSchema({ schema: JsonRootSchemaFixtures.any }, callbackMock); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should call callback() with the schema', () => { | ||
const expectedTraverseJsonSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: '', | ||
parentJsonPointer: undefined, | ||
parentSchema: undefined, | ||
schema: JsonRootSchemaFixtures.any, | ||
}; | ||
|
||
expect(callbackMock).toHaveBeenCalledTimes(1); | ||
expect(callbackMock).toHaveBeenCalledWith( | ||
expectedTraverseJsonSchemaCallbackParams, | ||
); | ||
}); | ||
}); | ||
|
||
describe.each<[string, JsonRootSchemaObject]>([ | ||
['$defs', JsonRootSchemaFixtures.with$DefsOne], | ||
['dependentSchemas', JsonRootSchemaFixtures.withDependentSchemasOne], | ||
['patternProperties', JsonRootSchemaFixtures.withPatternProperiesOne], | ||
['properties', JsonRootSchemaFixtures.withProperiesOne], | ||
])( | ||
'(key to schema map) having a schema with "%s"', | ||
(schemaKey: string, schemaFixture: JsonRootSchemaObject): void => { | ||
beforeAll(() => { | ||
traverseJsonSchema({ schema: schemaFixture }, callbackMock); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should call callback() with the schema', () => { | ||
const expectedTraverseJsonSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: '', | ||
parentJsonPointer: undefined, | ||
parentSchema: undefined, | ||
schema: schemaFixture, | ||
}; | ||
|
||
expect(callbackMock).toHaveBeenNthCalledWith( | ||
1, | ||
expectedTraverseJsonSchemaCallbackParams, | ||
); | ||
}); | ||
|
||
it('should call callback() with every subschema', () => { | ||
const subschemaMap: Record<string, JsonSchema> = schemaFixture[ | ||
schemaKey | ||
] as Record<string, JsonSchema>; | ||
|
||
const subschemaMapEntries: [string, JsonSchema][] = | ||
Object.entries(subschemaMap); | ||
|
||
expect(callbackMock).toHaveBeenCalledTimes( | ||
subschemaMapEntries.length + 1, | ||
); | ||
|
||
for (const [ | ||
index, | ||
[subschemaKey, subschema], | ||
] of subschemaMapEntries.entries()) { | ||
const expectedTraverseJsonSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: `/${schemaKey}/${subschemaKey}`, | ||
parentJsonPointer: '', | ||
parentSchema: schemaFixture, | ||
schema: subschema, | ||
}; | ||
|
||
expect(callbackMock).toHaveBeenNthCalledWith( | ||
index + 2, | ||
expectedTraverseJsonSchemaCallbackParams, | ||
); | ||
} | ||
}); | ||
}, | ||
); | ||
|
||
describe.each<[string, JsonRootSchemaObject]>([ | ||
['allOf', JsonRootSchemaFixtures.withAllOfTwo], | ||
['anyOf', JsonRootSchemaFixtures.withAnyOfTwo], | ||
['oneOf', JsonRootSchemaFixtures.withOneOfTwo], | ||
['prefixItems', JsonRootSchemaFixtures.withPrefixItemsOne], | ||
])( | ||
'(schema array) having a schema with "%s"', | ||
(schemaKey: string, schemaFixture: JsonRootSchemaObject): void => { | ||
beforeAll(() => { | ||
traverseJsonSchema({ schema: schemaFixture }, callbackMock); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should call callback() with the schema', () => { | ||
const expectedTraverseJsonSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: '', | ||
parentJsonPointer: undefined, | ||
parentSchema: undefined, | ||
schema: schemaFixture, | ||
}; | ||
|
||
expect(callbackMock).toHaveBeenNthCalledWith( | ||
1, | ||
expectedTraverseJsonSchemaCallbackParams, | ||
); | ||
}); | ||
|
||
it('should call callback() with every subschema', () => { | ||
const schemaArrays: JsonSchema[] = schemaFixture[ | ||
schemaKey | ||
] as JsonSchema[]; | ||
|
||
expect(callbackMock).toHaveBeenCalledTimes(schemaArrays.length + 1); | ||
|
||
for (const [subschemaIndex, subschema] of schemaArrays.entries()) { | ||
const expectedTraverseJsonSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: `/${schemaKey}/${subschemaIndex}`, | ||
parentJsonPointer: '', | ||
parentSchema: schemaFixture, | ||
schema: subschema, | ||
}; | ||
|
||
expect(callbackMock).toHaveBeenNthCalledWith( | ||
subschemaIndex + 2, | ||
expectedTraverseJsonSchemaCallbackParams, | ||
); | ||
} | ||
}); | ||
}, | ||
); | ||
|
||
describe.each<[string, JsonRootSchemaObject]>([ | ||
['additionalProperties', JsonRootSchemaFixtures.withAdditionalProperties], | ||
['contains', JsonRootSchemaFixtures.withContains], | ||
['else', JsonRootSchemaFixtures.withElse], | ||
['if', JsonRootSchemaFixtures.withIf], | ||
['items', JsonRootSchemaFixtures.withItems], | ||
['not', JsonRootSchemaFixtures.withNot], | ||
['propertyNames', JsonRootSchemaFixtures.withProperyNames], | ||
['then', JsonRootSchemaFixtures.withThen], | ||
['unevaluatedItems', JsonRootSchemaFixtures.withUnevaluatedItems], | ||
['unevaluatedProperties', JsonRootSchemaFixtures.withUnevaluatedProperties], | ||
])( | ||
'(schema) having a schema with "%s"', | ||
(schemaKey: string, schemaFixture: JsonRootSchemaObject): void => { | ||
beforeAll(() => { | ||
traverseJsonSchema({ schema: schemaFixture }, callbackMock); | ||
}); | ||
|
||
afterAll(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should call callback() with the schema', () => { | ||
const expectedTraverseJsonSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: '', | ||
parentJsonPointer: undefined, | ||
parentSchema: undefined, | ||
schema: schemaFixture, | ||
}; | ||
|
||
expect(callbackMock).toHaveBeenNthCalledWith( | ||
1, | ||
expectedTraverseJsonSchemaCallbackParams, | ||
); | ||
}); | ||
|
||
it('should call callback() with the subschema', () => { | ||
const subschema: JsonSchema = schemaFixture[schemaKey] as JsonSchema; | ||
|
||
expect(callbackMock).toHaveBeenCalledTimes(2); | ||
|
||
const expectedTraverseJsonSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: `/${schemaKey}`, | ||
parentJsonPointer: '', | ||
parentSchema: schemaFixture, | ||
schema: subschema, | ||
}; | ||
|
||
expect(callbackMock).toHaveBeenNthCalledWith( | ||
2, | ||
expectedTraverseJsonSchemaCallbackParams, | ||
); | ||
}); | ||
}, | ||
); | ||
}); |
146 changes: 146 additions & 0 deletions
146
packages/parser/json-schema-parser/src/jsonSchema/202012/actions/traverseJsonSchema.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { | ||
JsonRootSchemaObject, | ||
JsonSchema, | ||
JsonRootSchemaKnownPropertiesObject, | ||
} from '@cuaklabs/json-schema-types/2020-12'; | ||
|
||
import { TraverseJsonSchemaCallbackParams } from '../models/TraverseJsonSchemaCallbackParams'; | ||
import { TraverseJsonSchemaParams } from '../models/TraverseJsonSchemaParams'; | ||
|
||
type JsonRootSchemaSchemaProperty = | ||
| JsonSchema | ||
| JsonSchema[] | ||
| Record<string, JsonRootSchemaObject>; | ||
|
||
type JsonRootSchemaSchemaPropertyHandler = ( | ||
params: TraverseJsonSchemaCallbackParams, | ||
childSchema: JsonRootSchemaSchemaProperty, | ||
key: string, | ||
callback: (params: TraverseJsonSchemaCallbackParams) => void, | ||
) => void; | ||
|
||
const jsonRootSchemaObjectPropertyToHandlerMap: { | ||
[TKey in keyof JsonRootSchemaKnownPropertiesObject]?: ( | ||
params: TraverseJsonSchemaCallbackParams, | ||
childSchema: Exclude<JsonRootSchemaObject[TKey], undefined>, | ||
key: string, | ||
callback: (params: TraverseJsonSchemaCallbackParams) => void, | ||
) => void; | ||
} = { | ||
$defs: traverseDirectChildSchemaMap, | ||
additionalProperties: traverseDirectChildSchema, | ||
allOf: traverseDirectChildSchemaArray, | ||
anyOf: traverseDirectChildSchemaArray, | ||
contains: traverseDirectChildSchema, | ||
dependentSchemas: traverseDirectChildSchemaMap, | ||
else: traverseDirectChildSchema, | ||
if: traverseDirectChildSchema, | ||
items: traverseDirectChildSchema, | ||
not: traverseDirectChildSchema, | ||
oneOf: traverseDirectChildSchemaArray, | ||
patternProperties: traverseDirectChildSchemaMap, | ||
prefixItems: traverseDirectChildSchemaArray, | ||
properties: traverseDirectChildSchemaMap, | ||
propertyNames: traverseDirectChildSchema, | ||
then: traverseDirectChildSchema, | ||
unevaluatedItems: traverseDirectChildSchema, | ||
unevaluatedProperties: traverseDirectChildSchema, | ||
}; | ||
|
||
export function traverseJsonSchema( | ||
params: TraverseJsonSchemaParams, | ||
callback: (params: TraverseJsonSchemaCallbackParams) => void, | ||
): void { | ||
traverseJsonSchemaFromParams( | ||
{ | ||
jsonPointer: params.jsonPointer ?? '', | ||
parentJsonPointer: undefined, | ||
parentSchema: undefined, | ||
schema: params.schema, | ||
}, | ||
callback, | ||
); | ||
} | ||
|
||
function traverseJsonSchemaFromParams( | ||
params: TraverseJsonSchemaCallbackParams, | ||
callback: (params: TraverseJsonSchemaCallbackParams) => void, | ||
): void { | ||
callback(params); | ||
|
||
if (params.schema !== true && params.schema !== false) { | ||
for (const key of Object.keys(params.schema)) { | ||
const handler: JsonRootSchemaSchemaPropertyHandler | undefined = | ||
jsonRootSchemaObjectPropertyToHandlerMap[ | ||
key as keyof JsonRootSchemaKnownPropertiesObject | ||
] as JsonRootSchemaSchemaPropertyHandler | undefined; | ||
|
||
if (handler !== undefined) { | ||
handler( | ||
params, | ||
params.schema[key] as JsonRootSchemaSchemaProperty, | ||
key, | ||
callback, | ||
); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function traverseDirectChildSchema( | ||
params: TraverseJsonSchemaCallbackParams, | ||
childSchema: JsonSchema, | ||
key: string, | ||
callback: (params: TraverseJsonSchemaCallbackParams) => void, | ||
): void { | ||
const traverseChildSchemaCallbackParams: TraverseJsonSchemaCallbackParams = { | ||
jsonPointer: `${params.jsonPointer}/${escapeJsonPtr(key)}`, | ||
parentJsonPointer: params.jsonPointer, | ||
parentSchema: params.schema, | ||
schema: childSchema, | ||
}; | ||
|
||
traverseJsonSchemaFromParams(traverseChildSchemaCallbackParams, callback); | ||
} | ||
|
||
function traverseDirectChildSchemaArray( | ||
params: TraverseJsonSchemaCallbackParams, | ||
childSchemas: JsonSchema[], | ||
key: string, | ||
callback: (params: TraverseJsonSchemaCallbackParams) => void, | ||
): void { | ||
for (const [index, schema] of childSchemas.entries()) { | ||
const traverseChildSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: `${params.jsonPointer}/${escapeJsonPtr(key)}/${index}`, | ||
parentJsonPointer: params.jsonPointer, | ||
parentSchema: params.schema, | ||
schema, | ||
}; | ||
|
||
traverseJsonSchemaFromParams(traverseChildSchemaCallbackParams, callback); | ||
} | ||
} | ||
|
||
function traverseDirectChildSchemaMap( | ||
params: TraverseJsonSchemaCallbackParams, | ||
schemasMap: Record<string, JsonSchema>, | ||
key: string, | ||
callback: (params: TraverseJsonSchemaCallbackParams) => void, | ||
): void { | ||
for (const [mapKey, schema] of Object.entries(schemasMap)) { | ||
const traverseChildSchemaCallbackParams: TraverseJsonSchemaCallbackParams = | ||
{ | ||
jsonPointer: `${params.jsonPointer}/${escapeJsonPtr(key)}/${mapKey}`, | ||
parentJsonPointer: params.jsonPointer, | ||
parentSchema: params.schema, | ||
schema, | ||
}; | ||
|
||
traverseJsonSchemaFromParams(traverseChildSchemaCallbackParams, callback); | ||
} | ||
} | ||
|
||
function escapeJsonPtr(str: string): string { | ||
return str.replace(/~/g, '~0').replace(/\//g, '~1'); | ||
} |
11 changes: 11 additions & 0 deletions
11
...arser/json-schema-parser/src/jsonSchema/202012/models/TraverseJsonSchemaCallbackParams.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { | ||
JsonRootSchema, | ||
JsonSchema, | ||
} from '@cuaklabs/json-schema-types/2020-12'; | ||
|
||
export interface TraverseJsonSchemaCallbackParams { | ||
jsonPointer: string; | ||
parentJsonPointer: string | undefined; | ||
parentSchema: JsonSchema | undefined; | ||
schema: JsonSchema | JsonRootSchema; | ||
} |
6 changes: 6 additions & 0 deletions
6
packages/parser/json-schema-parser/src/jsonSchema/202012/models/TraverseJsonSchemaParams.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { JsonSchema } from '@cuaklabs/json-schema-types/2020-12'; | ||
|
||
export interface TraverseJsonSchemaParams { | ||
jsonPointer?: string; | ||
schema: JsonSchema; | ||
} |