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..d33c017b4 100644 --- a/src/custom-operations/parse-schema.ts +++ b/src/custom-operations/parse-schema.ts @@ -22,6 +22,24 @@ const customSchemasPathsV2 = [ '$.components.messages.*', ]; +const customSchemasPathsV3 = [ + // channels + '$.channels.*.messages.*.payload', + '$.channels.*.messages.*.headers', + '$.components.channels.*.messages.*.payload', + '$.components.channels.*.messages.*.headers', + // operations + '$.operations.*.messages.*.payload', + '$.operations.*.messages.*.headers', + '$.components.operations.*.messages.*.payload', + '$.components.operations.*.messages.*.headers', + // messages + '$.components.messages.*.payload', + '$.components.messages.*.headers.*', + // schemas + '$.components.schemas.*', +]; + export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI) { const defaultSchemaFormat = getDefaultSchemaFormat(detailed.semver.version); const parseItems: Array = []; @@ -65,6 +83,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' }); + expect(((((document?.json() as any).operations?.operation as v3.OperationObject).channel as v3.ChannelObject)?.messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + expect((((document?.json() as any).operations?.operation as v3.OperationObject).messages?.[0] as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + expect(((document?.json()?.components?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + expect((document?.json()?.components?.messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + expect((((document?.json() as any).components?.messages?.message as v3.MessageObject)?.headers as v3.MultiFormatObject).schema).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + expect((document?.json() as any).components?.schemas?.schema?.schema).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + }); + + it('should parse valid default schema format', async function() { + const documentRaw = { + asyncapi: '3.0.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0' + }, + channels: { + channel: { + address: 'channel', + messages: { + message: { + payload: { + type: 'object' + } + } + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV3); + expect(diagnostics.length === 0).toEqual(true); + + expect(((document?.json()?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload).toEqual({ type: 'object', 'x-parser-schema-id': '' }); + }); + + it('should parse invalid schema format', async function() { + const documentRaw = { + asyncapi: '3.0.0', + info: { + title: 'Valid AsyncApi document', + version: '1.0' + }, + channels: { + channel: { + address: 'channel', + messages: { + message: { + payload: { + schemaFormat: 'not existing', + schema: { + type: 'object' + } + } + } + } + } + } + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeUndefined(); + expect(diagnostics.length > 0).toEqual(true); + }); +});