diff --git a/.changeset/fix-bindings-ref-filter.md b/.changeset/fix-bindings-ref-filter.md new file mode 100644 index 000000000..42e1edd6a --- /dev/null +++ b/.changeset/fix-bindings-ref-filter.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/parser": patch +--- + +fix: filter out $ref keys in bindings() to prevent undefined parser output diff --git a/packages/parser/src/models/v2/components.ts b/packages/parser/src/models/v2/components.ts index 168cedf4d..89064f521 100644 --- a/packages/parser/src/models/v2/components.ts +++ b/packages/parser/src/models/v2/components.ts @@ -133,9 +133,11 @@ export class Components extends BaseModel implements Compon const asyncapi = this.meta('asyncapi'); const pointer = `components/${itemsName}/${name}`; bindings[name] = new Bindings( - Object.entries(bindingsData).map(([protocol, binding]) => - this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) - ), + Object.entries(bindingsData) + .filter(([key]) => !key.startsWith('$')) + .map(([protocol, binding]) => + this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) + ), { originalData: bindingsData as any, asyncapi, pointer } ); return bindings; diff --git a/packages/parser/src/models/v2/mixins.ts b/packages/parser/src/models/v2/mixins.ts index 37ebc7927..68b33cb1d 100644 --- a/packages/parser/src/models/v2/mixins.ts +++ b/packages/parser/src/models/v2/mixins.ts @@ -21,9 +21,11 @@ import type { v2 } from '../../spec-types'; export function bindings(model: BaseModel<{ bindings?: Record }>): BindingsInterface { const bindings = model.json('bindings') || {}; return new Bindings( - Object.entries(bindings || {}).map(([protocol, binding]) => - createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) - ), + Object.entries(bindings || {}) + .filter(([key]) => !key.startsWith('$')) + .map(([protocol, binding]) => + createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) + ), { originalData: bindings, asyncapi: model.meta('asyncapi'), pointer: model.jsonPath('bindings') } ); } diff --git a/packages/parser/src/models/v3/components.ts b/packages/parser/src/models/v3/components.ts index 9931b0b70..12471e2f4 100644 --- a/packages/parser/src/models/v3/components.ts +++ b/packages/parser/src/models/v3/components.ts @@ -157,9 +157,11 @@ export class Components extends BaseModel implements Compon const asyncapi = this.meta('asyncapi'); const pointer = `components/${itemsName}/${name}`; bindings[name] = new Bindings( - Object.entries(bindingsData).map(([protocol, binding]) => - this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) - ), + Object.entries(bindingsData) + .filter(([key]) => !key.startsWith('$')) + .map(([protocol, binding]) => + this.createModel(Binding, binding, { protocol, pointer: `${pointer}/${protocol}` }) + ), { originalData: bindingsData as any, asyncapi, pointer } ); return bindings; diff --git a/packages/parser/src/models/v3/mixins.ts b/packages/parser/src/models/v3/mixins.ts index 9191b4008..b1b081adf 100644 --- a/packages/parser/src/models/v3/mixins.ts +++ b/packages/parser/src/models/v3/mixins.ts @@ -83,9 +83,11 @@ export abstract class CoreModel): BindingsInterface { const bindings = model.json('bindings') || {}; return new Bindings( - Object.entries(bindings || {}).map(([protocol, binding]) => - createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) - ), + Object.entries(bindings || {}) + .filter(([key]) => !key.startsWith('$')) + .map(([protocol, binding]) => + createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) + ), { originalData: bindings as Record, asyncapi: model.meta('asyncapi'), pointer: model.jsonPath('bindings') } ); } diff --git a/packages/parser/src/schema-parser/index.ts b/packages/parser/src/schema-parser/index.ts index a40f944c0..ee7d08931 100644 --- a/packages/parser/src/schema-parser/index.ts +++ b/packages/parser/src/schema-parser/index.ts @@ -45,10 +45,10 @@ export async function validateSchema(parser: Parser, input: ValidateSchemaInput) return schemaParser.validate(input); } -export async function parseSchema(parser: Parser, input: ParseSchemaInput) { +export async function parseSchema(parser: Parser, input: ParseSchemaInput): Promise { const schemaParser = parser.parserRegistry.get(input.schemaFormat); if (schemaParser === undefined) { - throw new Error('Unknown schema format'); + return input.data as AsyncAPISchema; } return schemaParser.parse(input); } diff --git a/packages/parser/test/custom-operations/parse-schema-v2.spec.ts b/packages/parser/test/custom-operations/parse-schema-v2.spec.ts index 8504bb64d..00175c93c 100644 --- a/packages/parser/test/custom-operations/parse-schema-v2.spec.ts +++ b/packages/parser/test/custom-operations/parse-schema-v2.spec.ts @@ -101,7 +101,7 @@ describe('custom operations for v2 - parse schemas', function() { 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() { + it('should skip parsing unknown schema format and still return document', async function() { const documentRaw = { asyncapi: '2.0.0', info: { @@ -122,9 +122,16 @@ describe('custom operations for v2 - parse schemas', function() { } } }; - const { document, diagnostics } = await parser.parse(documentRaw); + const { document, diagnostics } = await parser.parse(documentRaw, { + validateOptions: { allowedSeverity: { error: true } }, + }); - expect(document).toBeUndefined(); + expect(document).toBeInstanceOf(AsyncAPIDocumentV2); expect(diagnostics.length > 0).toEqual(true); + expect(diagnostics.some(d => d.message?.includes('Unknown schema format'))).toEqual(true); + expect((document?.json()?.channels?.channel?.publish?.message as v2.MessageObject)?.payload).toEqual({ + type: 'object', + 'x-parser-schema-id': '', + }); }); }); diff --git a/packages/parser/test/custom-operations/parse-schema-v3.spec.ts b/packages/parser/test/custom-operations/parse-schema-v3.spec.ts index c8c3ea87f..becf4a1f0 100644 --- a/packages/parser/test/custom-operations/parse-schema-v3.spec.ts +++ b/packages/parser/test/custom-operations/parse-schema-v3.spec.ts @@ -105,7 +105,7 @@ describe('custom operations for v3 - parse schemas', function() { 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() { + it('should skip parsing unknown schema format and still return document', async function() { const documentRaw = { asyncapi: '3.0.0', info: { @@ -130,7 +130,70 @@ describe('custom operations for v3 - parse schemas', function() { }; const { document, diagnostics } = await parser.parse(documentRaw); - expect(document).toBeUndefined(); + expect(document).toBeInstanceOf(AsyncAPIDocumentV3); expect(diagnostics.length > 0).toEqual(true); + expect(((document?.json()?.channels?.channel as v3.ChannelObject).messages?.message as v3.MessageObject)?.payload?.schema).toEqual({ + type: 'object', + 'x-parser-schema-id': '', + }); + }); + + it('should parse avro schema format without registering AvroSchemaParser', async function() { + const documentRaw = { + asyncapi: '3.1.0', + info: { + title: 'Adeo AsyncAPI Case Study', + version: '1.0.0', + }, + channels: { + costingRequestChannel: { + address: 'adeo-{env}-case-study-COSTING-REQUEST-{version}', + bindings: { + kafka: { + replicas: 3, + partitions: 3, + }, + }, + messages: { + CostingRequest: { + payload: { + schemaFormat: 'application/vnd.apache.avro;version=1.9.0', + schema: { + type: 'record', + name: 'CostingRequest', + fields: [], + }, + }, + }, + }, + }, + }, + operations: { + receiveACostingRequest: { + action: 'receive', + channel: { + $ref: '#/channels/costingRequestChannel', + }, + bindings: { + kafka: { + groupId: { + type: 'string', + }, + }, + }, + }, + }, + }; + const { document, diagnostics } = await parser.parse(documentRaw); + + expect(document).toBeInstanceOf(AsyncAPIDocumentV3); + expect(filterLastVersionDiagnostics(diagnostics).length === 0).toEqual(true); + expect(document!.allChannels().get('costingRequestChannel')?.bindings().get('kafka')?.json()).toEqual({ + replicas: 3, + partitions: 3, + }); + expect(document!.allOperations().get('receiveACostingRequest')?.bindings().get('kafka')?.json()?.groupId).toEqual({ + type: 'string', + }); }); }); diff --git a/packages/parser/test/models/v2/mixins.spec.ts b/packages/parser/test/models/v2/mixins.spec.ts index e5c4d0e70..18daf8b83 100644 --- a/packages/parser/test/models/v2/mixins.spec.ts +++ b/packages/parser/test/models/v2/mixins.spec.ts @@ -24,6 +24,13 @@ describe('mixins', function() { expect(bindings(d3)).toBeInstanceOf(BindingsV2); expect(bindings(d3).length).toEqual(0); }); + + it('should ignore JSON reference keys in bindings', function() { + const doc = { bindings: { $ref: '#/components/messageBindings/myBindings', amqp: { test: 'test1' } } }; + const model = new Model(doc); + expect(bindings(model).length).toEqual(1); + expect(bindings(model).all()[0].protocol()).toEqual('amqp'); + }); }); describe('hasDescription', function() { diff --git a/packages/parser/test/models/v3/mixins.spec.ts b/packages/parser/test/models/v3/mixins.spec.ts new file mode 100644 index 000000000..ae30299bf --- /dev/null +++ b/packages/parser/test/models/v3/mixins.spec.ts @@ -0,0 +1,17 @@ +import { BaseModel } from '../../../src/models/base'; +import { bindings } from '../../../src/models/v3/mixins'; +import { BindingsV3 } from '../../../src/models/v3'; + +describe('mixins', function() { + describe('bindings', function() { + class Model extends BaseModel {} + + it('should ignore JSON reference keys in bindings', function() { + const doc = { bindings: { $ref: '#/components/messageBindings/myBindings', kafka: { bindingVersion: '0.4.0' } } }; + const model = new Model(doc); + expect(bindings(model)).toBeInstanceOf(BindingsV3); + expect(bindings(model).length).toEqual(1); + expect(bindings(model).all()[0].protocol()).toEqual('kafka'); + }); + }); +});