From 05012067de6d4bd72ff6310962fa1a8156558d2b Mon Sep 17 00:00:00 2001 From: "jonas-lt@live.dk" Date: Tue, 3 Oct 2023 16:59:23 +0200 Subject: [PATCH 1/4] fix schema parsing --- src/custom-operations/anonymous-naming.ts | 9 +- src/custom-operations/index.ts | 19 +-- src/custom-operations/parse-schema.ts | 70 +++++++++++ src/models/v3/schema.ts | 2 +- src/utils.ts | 8 +- test/custom-operations/parse-schema.spec.ts | 130 -------------------- 6 files changed, 94 insertions(+), 144 deletions(-) delete mode 100644 test/custom-operations/parse-schema.spec.ts diff --git a/src/custom-operations/anonymous-naming.ts b/src/custom-operations/anonymous-naming.ts index d29b9e917..88522f3bd 100644 --- a/src/custom-operations/anonymous-naming.ts +++ b/src/custom-operations/anonymous-naming.ts @@ -1,6 +1,6 @@ import { xParserMessageName, xParserSchemaId } from '../constants'; import { traverseAsyncApiDocument } from '../iterator'; -import { setExtension } from '../utils'; +import { setExtension, setExtensionOnJson } from '../utils'; import type { AsyncAPIDocumentInterface, @@ -59,12 +59,15 @@ function assignUidToComponentSchemas(document: AsyncAPIDocumentInterface) { setExtension(xParserSchemaId, schema.id(), schema); }); } - + function assignUidToAnonymousSchemas(doc: AsyncAPIDocumentInterface) { let anonymousSchemaCounter = 0; function callback(schema: SchemaInterface) { + const json = schema.json() as any; + const isMultiFormatSchema = json.schema !== undefined; + const underlyingSchema = isMultiFormatSchema ? json.schema : json; if (!schema.id()) { - setExtension(xParserSchemaId, ``, schema); + setExtensionOnJson(xParserSchemaId, ``, underlyingSchema); } } traverseAsyncApiDocument(doc, callback); diff --git a/src/custom-operations/index.ts b/src/custom-operations/index.ts index e360848b9..28754663a 100644 --- a/src/custom-operations/index.ts +++ b/src/custom-operations/index.ts @@ -1,8 +1,7 @@ import { applyTraitsV2, applyTraitsV3 } from './apply-traits'; -import { checkCircularRefs } from './check-circular-refs'; -import { parseSchemasV2 } from './parse-schema'; -import { anonymousNaming } from './anonymous-naming'; import { resolveCircularRefs } from './resolve-circular-refs'; +import { parseSchemasV2, parseSchemasV3 } from './parse-schema'; +import { anonymousNaming } from './anonymous-naming'; import type { RulesetFunctionContext } from '@stoplight/spectral-core'; import type { Parser } from '../parser'; @@ -10,6 +9,7 @@ import type { ParseOptions } from '../parse'; import type { AsyncAPIDocumentInterface } from '../models'; import type { DetailedAsyncAPI } from '../types'; import type { v2, v3 } from '../spec-types'; +import { checkCircularRefs } from './check-circular-refs'; export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, inventory: RulesetFunctionContext['documentInventory'], options: ParseOptions): Promise { switch (detailed.semver.major) { @@ -28,7 +28,7 @@ async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface, await parseSchemasV2(parser, detailed); } - // anonymous naming and resolving circular refrences should be done after custom schemas parsing + // anonymous naming and resolving circular references should be done after custom schemas parsing if (inventory) { resolveCircularRefs(document, inventory); } @@ -41,9 +41,12 @@ async function operationsV3(parser: Parser, document: AsyncAPIDocumentInterface, if (options.applyTraits) { applyTraitsV3(detailed.parsed as v3.AsyncAPIObject); } - // TODO: Support schema parsing in v3 - // if (options.parseSchemas) { - // await parseSchemasV2(parser, detailed); - // } + if (options.parseSchemas) { + await parseSchemasV3(parser, detailed); + } + // anonymous naming and resolving circular references should be done after custom schemas parsing + if (inventory) { + resolveCircularRefs(document, inventory); + } anonymousNaming(document); } diff --git a/src/custom-operations/parse-schema.ts b/src/custom-operations/parse-schema.ts index 6221af6cf..48420245f 100644 --- a/src/custom-operations/parse-schema.ts +++ b/src/custom-operations/parse-schema.ts @@ -22,6 +22,14 @@ const customSchemasPathsV2 = [ '$.components.messages.*', ]; +const customSchemasPathsV3 = [ + // operations + '$.channels.*.messages.*.payload', + '$.components.channels.*.messages.*.payload', + '$.components.messages.*.payload', + '$.components.schemas.*', +]; + export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI) { const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version); const parseItems: Array = []; @@ -65,6 +73,68 @@ export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI) return Promise.all(parseItems.map(item => parseSchemaV2(parser, item))); } +export async function parseSchemasV3(parser: Parser, detailed: DetailedAsyncAPI) { + const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version); + const parseItems: Array = []; + + const visited: Set = new Set(); + customSchemasPathsV3.forEach(path => { + JSONPath({ + path, + json: detailed.parsed, + resultType: 'all', + callback(result) { + const value = result.value; + if (visited.has(value)) { + return; + } + visited.add(value); + + const schema = value.schema; + if (!schema) { + return; + } + + let schemaFormat = value.schemaFormat; + if (!schemaFormat) { + return; + } + schemaFormat = getSchemaFormat(value.schemaFormat, detailed.semver.version); + + parseItems.push({ + input: { + asyncapi: detailed, + data: schema, + meta: { + message: value, + }, + path: [...splitPath(result.path), 'schema'], + schemaFormat, + defaultSchemaFormat, + }, + value, + }); + }, + }); + }); + + return Promise.all(parseItems.map(item => parseSchemaV3(parser, item))); +} + +async function parseSchemaV3(parser: Parser, item: ToParseItem) { + const originalData = item.input.data; + const parsedData = await parseSchema(parser, item.input); + if (item.value?.schema !== undefined) { + item.value.schema = parsedData; + } else { + item.value = parsedData; + } + // save original payload only when data is different (returned by custom parsers) + if (originalData !== parsedData) { + item.value[xParserOriginalPayload] = originalData; + } +} + async function parseSchemaV2(parser: Parser, item: ToParseItem) { const originalData = item.input.data; const parsedData = item.value.payload = await parseSchema(parser, item.input); diff --git a/src/models/v3/schema.ts b/src/models/v3/schema.ts index 763c91a45..40e3d533e 100644 --- a/src/models/v3/schema.ts +++ b/src/models/v3/schema.ts @@ -47,7 +47,7 @@ export class Schema extends BaseModel 0).toEqual(true); - - expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); - }); - - it('should parse valid default schema format', async function() { - const documentRaw = { - asyncapi: '2.0.0', - info: { - title: 'Valid AsyncApi document', - version: '1.0', - }, - channels: { - channel: { - publish: { - operationId: 'operationId', - message: { - payload: { - type: 'object', - } - } - } - } - } - }; - const { document, diagnostics } = await parser.parse(documentRaw); - - expect(document).toBeInstanceOf(AsyncAPIDocumentV2); - expect(diagnostics.length > 0).toEqual(true); - - expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); - }); - - it('should preserve this same references', async function() { - const documentRaw = { - asyncapi: '2.0.0', - info: { - title: 'Valid AsyncApi document', - version: '1.0', - }, - channels: { - channel: { - publish: { - operationId: 'operationId', - message: { - $ref: '#/components/messages/message' - } - } - } - }, - components: { - messages: { - message: { - payload: { - type: 'object', - } - } - } - } - }; - const { document, diagnostics } = await parser.parse(documentRaw); - - expect(document).toBeInstanceOf(AsyncAPIDocumentV2); - expect(diagnostics.length > 0).toEqual(true); - - expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); - expect((document?.json().components?.messages?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); - // check if logic preserves references - expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload === (document?.json().components?.messages?.message as v2.MessageObject)?.payload).toEqual(true); - }); - - it('should parse invalid schema format', async function() { - const documentRaw = { - asyncapi: '2.0.0', - info: { - title: 'Valid AsyncApi document', - version: '1.0', - }, - channels: { - channel: { - publish: { - operationId: 'operationId', - message: { - schemaFormat: 'not existing', - payload: { - type: 'object', - } - } - } - } - } - }; - const { document, diagnostics } = await parser.parse(documentRaw); - - expect(document).toBeUndefined(); - expect(diagnostics.length > 0).toEqual(true); - }); -}); From 85bcae563eee327f2557063f7d752461fe3fe8f9 Mon Sep 17 00:00:00 2001 From: "jonas-lt@live.dk" Date: Fri, 6 Oct 2023 14:15:36 +0200 Subject: [PATCH 2/4] fix and test --- src/models/v3/channel-parameter.ts | 6 +++++- test/models/v3/channel-parameter.spec.ts | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/models/v3/channel-parameter.ts b/src/models/v3/channel-parameter.ts index 54099a351..1e27d17d3 100644 --- a/src/models/v3/channel-parameter.ts +++ b/src/models/v3/channel-parameter.ts @@ -22,7 +22,11 @@ export class ChannelParameter extends BaseModel({ description: 'test' }); + const d = new ChannelParameter(doc); + expect(d.schema()).toBeInstanceOf(Schema); + expect(d.schema()?.description()).toEqual('test'); + }); + it('should return empty schema with type string when there is no value', function() { const doc = serializeInput({}); const d = new ChannelParameter(doc); - expect(d.schema()).toBeUndefined(); + expect(d.schema()).toBeInstanceOf(Schema); + expect(d.schema()?.type()).toEqual('string'); }); }); From a295e766710914e306d219b886674ee8b10db893 Mon Sep 17 00:00:00 2001 From: "jonas-lt@live.dk" Date: Fri, 6 Oct 2023 14:20:32 +0200 Subject: [PATCH 3/4] fix implementation and test --- src/models/v3/channel-parameter.ts | 12 +----------- test/models/v3/channel-parameter.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/models/v3/channel-parameter.ts b/src/models/v3/channel-parameter.ts index 1e27d17d3..41e46bfab 100644 --- a/src/models/v3/channel-parameter.ts +++ b/src/models/v3/channel-parameter.ts @@ -13,20 +13,10 @@ export class ChannelParameter extends BaseModel({}); const d = new ChannelParameter(doc); - expect(d.hasSchema()).toEqual(false); + expect(d.hasSchema()).toEqual(true); }); }); From 08c9751eca4d576a2b5ee983787b8875f47c8dd4 Mon Sep 17 00:00:00 2001 From: "jonas-lt@live.dk" Date: Fri, 6 Oct 2023 14:27:58 +0200 Subject: [PATCH 4/4] Revert "fix schema parsing" This reverts commit 05012067de6d4bd72ff6310962fa1a8156558d2b. --- src/custom-operations/anonymous-naming.ts | 9 +- src/custom-operations/index.ts | 19 ++- src/custom-operations/parse-schema.ts | 70 ----------- src/models/v3/schema.ts | 2 +- src/utils.ts | 8 +- test/custom-operations/parse-schema.spec.ts | 130 ++++++++++++++++++++ 6 files changed, 144 insertions(+), 94 deletions(-) create mode 100644 test/custom-operations/parse-schema.spec.ts diff --git a/src/custom-operations/anonymous-naming.ts b/src/custom-operations/anonymous-naming.ts index 88522f3bd..d29b9e917 100644 --- a/src/custom-operations/anonymous-naming.ts +++ b/src/custom-operations/anonymous-naming.ts @@ -1,6 +1,6 @@ import { xParserMessageName, xParserSchemaId } from '../constants'; import { traverseAsyncApiDocument } from '../iterator'; -import { setExtension, setExtensionOnJson } from '../utils'; +import { setExtension } from '../utils'; import type { AsyncAPIDocumentInterface, @@ -59,15 +59,12 @@ function assignUidToComponentSchemas(document: AsyncAPIDocumentInterface) { setExtension(xParserSchemaId, schema.id(), schema); }); } - + function assignUidToAnonymousSchemas(doc: AsyncAPIDocumentInterface) { let anonymousSchemaCounter = 0; function callback(schema: SchemaInterface) { - const json = schema.json() as any; - const isMultiFormatSchema = json.schema !== undefined; - const underlyingSchema = isMultiFormatSchema ? json.schema : json; if (!schema.id()) { - setExtensionOnJson(xParserSchemaId, ``, underlyingSchema); + setExtension(xParserSchemaId, ``, schema); } } traverseAsyncApiDocument(doc, callback); diff --git a/src/custom-operations/index.ts b/src/custom-operations/index.ts index 28754663a..e360848b9 100644 --- a/src/custom-operations/index.ts +++ b/src/custom-operations/index.ts @@ -1,7 +1,8 @@ import { applyTraitsV2, applyTraitsV3 } from './apply-traits'; -import { resolveCircularRefs } from './resolve-circular-refs'; -import { parseSchemasV2, parseSchemasV3 } from './parse-schema'; +import { checkCircularRefs } from './check-circular-refs'; +import { parseSchemasV2 } from './parse-schema'; import { anonymousNaming } from './anonymous-naming'; +import { resolveCircularRefs } from './resolve-circular-refs'; import type { RulesetFunctionContext } from '@stoplight/spectral-core'; import type { Parser } from '../parser'; @@ -9,7 +10,6 @@ import type { ParseOptions } from '../parse'; import type { AsyncAPIDocumentInterface } from '../models'; import type { DetailedAsyncAPI } from '../types'; import type { v2, v3 } from '../spec-types'; -import { checkCircularRefs } from './check-circular-refs'; export async function customOperations(parser: Parser, document: AsyncAPIDocumentInterface, detailed: DetailedAsyncAPI, inventory: RulesetFunctionContext['documentInventory'], options: ParseOptions): Promise { switch (detailed.semver.major) { @@ -28,7 +28,7 @@ async function operationsV2(parser: Parser, document: AsyncAPIDocumentInterface, await parseSchemasV2(parser, detailed); } - // anonymous naming and resolving circular references should be done after custom schemas parsing + // anonymous naming and resolving circular refrences should be done after custom schemas parsing if (inventory) { resolveCircularRefs(document, inventory); } @@ -41,12 +41,9 @@ async function operationsV3(parser: Parser, document: AsyncAPIDocumentInterface, if (options.applyTraits) { applyTraitsV3(detailed.parsed as v3.AsyncAPIObject); } - if (options.parseSchemas) { - await parseSchemasV3(parser, detailed); - } - // anonymous naming and resolving circular references should be done after custom schemas parsing - if (inventory) { - resolveCircularRefs(document, inventory); - } + // TODO: Support schema parsing in v3 + // if (options.parseSchemas) { + // await parseSchemasV2(parser, detailed); + // } anonymousNaming(document); } diff --git a/src/custom-operations/parse-schema.ts b/src/custom-operations/parse-schema.ts index 48420245f..6221af6cf 100644 --- a/src/custom-operations/parse-schema.ts +++ b/src/custom-operations/parse-schema.ts @@ -22,14 +22,6 @@ const customSchemasPathsV2 = [ '$.components.messages.*', ]; -const customSchemasPathsV3 = [ - // operations - '$.channels.*.messages.*.payload', - '$.components.channels.*.messages.*.payload', - '$.components.messages.*.payload', - '$.components.schemas.*', -]; - export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI) { const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version); const parseItems: Array = []; @@ -73,68 +65,6 @@ export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI) return Promise.all(parseItems.map(item => parseSchemaV2(parser, item))); } -export async function parseSchemasV3(parser: Parser, detailed: DetailedAsyncAPI) { - const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version); - const parseItems: Array = []; - - const visited: Set = new Set(); - customSchemasPathsV3.forEach(path => { - JSONPath({ - path, - json: detailed.parsed, - resultType: 'all', - callback(result) { - const value = result.value; - if (visited.has(value)) { - return; - } - visited.add(value); - - const schema = value.schema; - if (!schema) { - return; - } - - let schemaFormat = value.schemaFormat; - if (!schemaFormat) { - return; - } - schemaFormat = getSchemaFormat(value.schemaFormat, detailed.semver.version); - - parseItems.push({ - input: { - asyncapi: detailed, - data: schema, - meta: { - message: value, - }, - path: [...splitPath(result.path), 'schema'], - schemaFormat, - defaultSchemaFormat, - }, - value, - }); - }, - }); - }); - - return Promise.all(parseItems.map(item => parseSchemaV3(parser, item))); -} - -async function parseSchemaV3(parser: Parser, item: ToParseItem) { - const originalData = item.input.data; - const parsedData = await parseSchema(parser, item.input); - if (item.value?.schema !== undefined) { - item.value.schema = parsedData; - } else { - item.value = parsedData; - } - // save original payload only when data is different (returned by custom parsers) - if (originalData !== parsedData) { - item.value[xParserOriginalPayload] = originalData; - } -} - async function parseSchemaV2(parser: Parser, item: ToParseItem) { const originalData = item.input.data; const parsedData = item.value.payload = await parseSchema(parser, item.input); diff --git a/src/models/v3/schema.ts b/src/models/v3/schema.ts index 40e3d533e..763c91a45 100644 --- a/src/models/v3/schema.ts +++ b/src/models/v3/schema.ts @@ -47,7 +47,7 @@ export class Schema extends BaseModel 0).toEqual(true); + + expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + }); + + it('should parse valid default schema format', async function() { + const documentRaw = { + asyncapi: '2.0.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: { + channel: { + publish: { + operationId: 'operationId', + message: { + payload: { + type: 'object', + } + } + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); + expect(diagnostics.length > 0).toEqual(true); + + expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + }); + + it('should preserve this same references', async function() { + const documentRaw = { + asyncapi: '2.0.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: { + channel: { + publish: { + operationId: 'operationId', + message: { + $ref: '#/components/messages/message' + } + } + } + }, + components: { + messages: { + message: { + payload: { + type: 'object', + } + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); + expect(diagnostics.length > 0).toEqual(true); + + expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + expect((document?.json().components?.messages?.message as v2.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + // check if logic preserves references + expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload === (document?.json().components?.messages?.message as v2.MessageObject)?.payload).toEqual(true); + }); + + it('should parse invalid schema format', async function() { + const documentRaw = { + asyncapi: '2.0.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0', + }, + channels: { + channel: { + publish: { + operationId: 'operationId', + message: { + schemaFormat: 'not existing', + payload: { + type: 'object', + } + } + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeUndefined(); + expect(diagnostics.length > 0).toEqual(true); + }); +});