diff --git a/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/dereferenceJsonSchema.spec.ts b/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/dereferenceJsonSchema.spec.ts new file mode 100644 index 0000000..6e0ddfb --- /dev/null +++ b/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/dereferenceJsonSchema.spec.ts @@ -0,0 +1,186 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'; + +jest.mock('../actions/traverseJsonSchema'); +jest.mock('./getJsonSchemaBaseUri'); + +import { + JsonSchema, + JsonSchemaObject, +} from '@cuaklabs/json-schema-types/2020-12'; + +import { traverseJsonSchema } from '../actions/traverseJsonSchema'; +import { JsonRootSchemaFixtures } from '../fixtures/JsonRootSchemaFixtures'; +import { DereferenceFunction } from '../models/DereferenceFunction'; +import { TraverseJsonSchemaCallbackParams } from '../models/TraverseJsonSchemaCallbackParams'; +import { TraverseJsonSchemaParams } from '../models/TraverseJsonSchemaParams'; +import { UriOptions } from '../models/UriOptions'; +import { dereferenceJsonSchema } from './dereferenceJsonSchema'; +import { getJsonSchemaBaseUri } from './getJsonSchemaBaseUri'; + +describe(dereferenceJsonSchema.name, () => { + let derefMock: jest.Mock; + let schemaFixture: JsonSchema; + let uriOptionsFixture: UriOptions; + + beforeAll(() => { + derefMock = jest.fn(); + schemaFixture = JsonRootSchemaFixtures.any; + uriOptionsFixture = {}; + }); + + describe('when called, and traverseJsonSchema() does not call callback', () => { + let baseUriFixture: string; + let referenceMapFixture: Map; + + let result: unknown; + + beforeAll(async () => { + baseUriFixture = 'base://fixture'; + referenceMapFixture = new Map(); + + ( + getJsonSchemaBaseUri as jest.Mock + ).mockReturnValueOnce(baseUriFixture); + + ( + traverseJsonSchema as jest.Mock + ).mockReturnValueOnce(undefined); + + result = await dereferenceJsonSchema( + derefMock, + schemaFixture, + referenceMapFixture, + uriOptionsFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call getJsonSchemaBaseUri()', () => { + expect(getJsonSchemaBaseUri).toHaveBeenCalledTimes(1); + expect(getJsonSchemaBaseUri).toHaveBeenCalledWith( + schemaFixture, + uriOptionsFixture, + ); + }); + + it('should call traverseJsonSchema()', () => { + const expectedParams: TraverseJsonSchemaParams = { + schema: schemaFixture, + }; + + expect(traverseJsonSchema).toHaveBeenCalledTimes(1); + expect(traverseJsonSchema).toHaveBeenCalledWith( + expectedParams, + expect.any(Function), + ); + }); + + it('should resolve to undefined', () => { + expect(result).toBeUndefined(); + }); + }); + + describe('when called, and traverseJsonSchema() calls callback twice with an schema with a reference', () => { + let dereferencedSchemaFixture: JsonSchema; + let subSchemaFixture: JsonSchemaObject; + let baseUriFixture: string; + let referenceMapFixture: Map; + + let result: unknown; + + beforeAll(async () => { + dereferencedSchemaFixture = JsonRootSchemaFixtures.any; + subSchemaFixture = JsonRootSchemaFixtures.withRef; + baseUriFixture = 'base://fixture'; + referenceMapFixture = new Map(); + + ( + getJsonSchemaBaseUri as jest.Mock + ).mockReturnValueOnce(baseUriFixture); + + (traverseJsonSchema as jest.Mock) + .mockImplementationOnce( + ( + params: TraverseJsonSchemaParams, + callback: (params: TraverseJsonSchemaCallbackParams) => void, + ) => { + callback({ + jsonPointer: params.jsonPointer ?? '', + parentJsonPointer: params.jsonPointer, + parentSchema: schemaFixture, + schema: subSchemaFixture, + }); + callback({ + jsonPointer: params.jsonPointer ?? '', + parentJsonPointer: params.jsonPointer, + parentSchema: schemaFixture, + schema: subSchemaFixture, + }); + }, + ) + .mockImplementationOnce(() => undefined); + + derefMock.mockResolvedValueOnce(dereferencedSchemaFixture); + + result = await dereferenceJsonSchema( + derefMock, + schemaFixture, + referenceMapFixture, + uriOptionsFixture, + ); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call getJsonSchemaBaseUri()', () => { + expect(getJsonSchemaBaseUri).toHaveBeenCalledTimes(2); + expect(getJsonSchemaBaseUri).toHaveBeenNthCalledWith( + 1, + schemaFixture, + uriOptionsFixture, + ); + expect(getJsonSchemaBaseUri).toHaveBeenNthCalledWith( + 2, + dereferencedSchemaFixture, + uriOptionsFixture, + ); + }); + + it('should call traverseJsonSchema()', () => { + const expectedFirstParams: TraverseJsonSchemaParams = { + schema: schemaFixture, + }; + + const expectedSecondParams: TraverseJsonSchemaParams = { + schema: dereferencedSchemaFixture, + }; + + expect(traverseJsonSchema).toHaveBeenCalledTimes(2); + expect(traverseJsonSchema).toHaveBeenNthCalledWith( + 1, + expectedFirstParams, + expect.any(Function), + ); + expect(traverseJsonSchema).toHaveBeenNthCalledWith( + 2, + expectedSecondParams, + expect.any(Function), + ); + }); + + it('should push references', () => { + expect([...referenceMapFixture.entries()]).toStrictEqual([ + [subSchemaFixture.$ref, dereferencedSchemaFixture], + ]); + }); + + it('should resolve to undefined', () => { + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/dereferenceJsonSchema.ts b/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/dereferenceJsonSchema.ts new file mode 100644 index 0000000..6fd13ad --- /dev/null +++ b/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/dereferenceJsonSchema.ts @@ -0,0 +1,64 @@ +import { JsonSchema } from '@cuaklabs/json-schema-types/2020-12'; +import { Uri } from '@cuaklabs/uri'; + +import { traverseJsonSchema } from '../actions/traverseJsonSchema'; +import { DereferenceFunction } from '../models/DereferenceFunction'; +import { TraverseJsonSchemaCallbackParams } from '../models/TraverseJsonSchemaCallbackParams'; +import { UriOptions } from '../models/UriOptions'; +import { getJsonSchemaBaseUri } from './getJsonSchemaBaseUri'; + +export async function dereferenceJsonSchema( + deref: DereferenceFunction, + schema: JsonSchema, + referenceMap: Map, + uriOptions: UriOptions | undefined, +): Promise { + const baseUri: string = getJsonSchemaBaseUri(schema, uriOptions); + + const schemaUris: string[] = [...new Set(getSchemaUris(schema, baseUri))]; + + const missingSchemaUris: string[] = schemaUris.filter( + (schemaUri: string) => !referenceMap.has(schemaUri), + ); + + await Promise.all( + missingSchemaUris.map(async (schemaUri: string): Promise => { + const dereferencedSchema: JsonSchema = await deref( + schema, + baseUri, + schemaUri, + ); + + referenceMap.set(schemaUri, dereferencedSchema); + + await dereferenceJsonSchema( + deref, + dereferencedSchema, + referenceMap, + uriOptions, + ); + }), + ); +} + +function getSchemaUris(schema: JsonSchema, baseUri: string): string[] { + const schemaUris: string[] = []; + + traverseJsonSchema( + { + schema, + }, + (params: TraverseJsonSchemaCallbackParams): void => { + if ( + typeof params.schema !== 'boolean' && + params.schema.$ref !== undefined + ) { + const refUri: string = new Uri(params.schema.$ref, baseUri).toString(); + + schemaUris.push(refUri); + } + }, + ); + + return schemaUris; +} diff --git a/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/getJsonSchemaBaseUri.ts b/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/getJsonSchemaBaseUri.ts index 19afdf5..a94d44f 100644 --- a/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/getJsonSchemaBaseUri.ts +++ b/packages/parser/json-schema-parser/src/jsonSchema/202012/calculations/getJsonSchemaBaseUri.ts @@ -1,14 +1,11 @@ import { JsonSchema } from '@cuaklabs/json-schema-types/2020-12'; import { getBaseUri } from '@cuaklabs/uri'; -export interface GetJsonSchemaBaseUriOptions { - encapsulatingDocumentBaseUri?: string | undefined; - retrievalUri?: string | undefined; -} +import { UriOptions } from '../models/UriOptions'; export function getJsonSchemaBaseUri( schema: JsonSchema, - options?: GetJsonSchemaBaseUriOptions, + options?: UriOptions, ): string { const documentBaseUri: string | undefined = typeof schema === 'boolean' ? undefined : schema.$id; diff --git a/packages/parser/json-schema-parser/src/jsonSchema/202012/fixtures/JsonRootSchemaFixtures.ts b/packages/parser/json-schema-parser/src/jsonSchema/202012/fixtures/JsonRootSchemaFixtures.ts index 8be3bff..86e0725 100644 --- a/packages/parser/json-schema-parser/src/jsonSchema/202012/fixtures/JsonRootSchemaFixtures.ts +++ b/packages/parser/json-schema-parser/src/jsonSchema/202012/fixtures/JsonRootSchemaFixtures.ts @@ -17,6 +17,13 @@ export class JsonRootSchemaFixtures { }; } + public static get withRef(): JsonRootSchemaObject { + return { + ...JsonRootSchemaFixtures.any, + $ref: 'https://schema.id', + }; + } + public static get withNoId(): JsonRootSchemaObject { const fixture: JsonRootSchemaObject = JsonRootSchemaFixtures.any; diff --git a/packages/parser/json-schema-parser/src/jsonSchema/202012/models/DereferenceFunction.ts b/packages/parser/json-schema-parser/src/jsonSchema/202012/models/DereferenceFunction.ts new file mode 100644 index 0000000..9411ea6 --- /dev/null +++ b/packages/parser/json-schema-parser/src/jsonSchema/202012/models/DereferenceFunction.ts @@ -0,0 +1,7 @@ +import { JsonSchema } from '@cuaklabs/json-schema-types/2020-12'; + +export type DereferenceFunction = ( + schema: JsonSchema, + baseUri: string, + uri: string, +) => Promise; diff --git a/packages/parser/json-schema-parser/src/jsonSchema/202012/models/UriOptions.ts b/packages/parser/json-schema-parser/src/jsonSchema/202012/models/UriOptions.ts new file mode 100644 index 0000000..4031044 --- /dev/null +++ b/packages/parser/json-schema-parser/src/jsonSchema/202012/models/UriOptions.ts @@ -0,0 +1,4 @@ +export interface UriOptions { + encapsulatingDocumentBaseUri?: string | undefined; + retrievalUri?: string | undefined; +}