Skip to content

Commit

Permalink
Merge pull request #17 from cuaklabs/refactor/add-traverse-json-schema
Browse files Browse the repository at this point in the history
Add traverseJsonSchema
  • Loading branch information
notaphplover committed Sep 6, 2023
2 parents 2fea7a1 + 5449357 commit af39db5
Show file tree
Hide file tree
Showing 4 changed files with 386 additions and 0 deletions.
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,
);
});
},
);
});
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');
}
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;
}
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;
}

0 comments on commit af39db5

Please sign in to comment.