diff --git a/.changeset/flat-paws-do.md b/.changeset/flat-paws-do.md new file mode 100644 index 0000000000..13d78c1423 --- /dev/null +++ b/.changeset/flat-paws-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-openapi': patch +--- + +fix wrong dereferencing for AsyncApi 3 documents diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index c2b7d8c2ad..fa1b10add8 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -110,6 +110,7 @@ dependabot deps dequeue dequeueing +dereferencing deserialization destructured destructuring diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index 87106fdf10..96f2451352 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -37,7 +37,7 @@ "test": "backstage-cli package test" }, "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.0.0", + "@apidevtools/json-schema-ref-parser": "^14.2.1", "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/integration": "workspace:^", diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts index a34b1c9a5b..ba6793ef63 100644 --- a/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts @@ -119,7 +119,7 @@ describe('bundleFileWithRefs', () => { expect(result).toEqual(expectedResult.trimStart()); }); - it('should return the bundled asyncapi specification', async () => { + it('should return the bundled asyncapi 2.5.0 specification', async () => { const spec = ` asyncapi: 2.5.0 info: @@ -179,6 +179,202 @@ channels: expect(result).toEqual(expectedSchema.trimStart()); }); + + it('should return the bundled asyncapi 3.0.0 specification with preserved references', async () => { + const spec = ` +asyncapi: 3.0.0 +info: + version: 1.0.0 + title: AsyncAPI 3.0 Sample + description: Sample AsyncAPI 3.0 with operations and replies +servers: + test: + host: api.example.com:5672 + protocol: kafka +channels: + userSignup: + address: user/signedup + servers: + - $ref: "#/servers/test" + messages: + UserSignedUp: + $ref: "#/components/messages/UserSignedUp" + ServiceUserSignup: + $ref: "#/components/messages/ServiceUserSignup" + userSignupReply: + - $ref: "#/components/channels/userSignupReply" +operations: + sendUserSignup: + action: send + channel: + $ref: "#/channels/userSignup" + messages: + - $ref: "#/channels/userSignup/messages/UserSignedUp" + reply: + channel: + $ref: "#/channels/userSignupReply" + messages: + - $ref: "#/channels/userSignupReply/messages/UserSignedUpReply" + sendServiceUserSignup: + $ref: "#/components/operations/sendServiceUserSignup" +components: + channels: + userSignupReply: + servers: + - $ref: "#/servers/test" + address: user/signedup/reply + messages: + UserSignedUpReply: + $ref: "#/components/messages/UserSignedUpReply" + ServiceUserSignupReply: + $ref: "#/components/messages/ServiceUserSignupReply" + operations: + sendServiceUserSignup: + action: send + channel: + $ref: "#/channels/userSignup" + messages: + - $ref: "#/channels/userSignup/messages/ServiceUserSignup" + reply: + channel: + $ref: "#/channels/userSignupReply" + messages: + - $ref: "#/channels/userSignupReply/messages/ServiceUserSignupReply" + messages: + UserSignedUp: + $ref: "./messages/UserSignedUp.yaml" + ServiceUserSignup: + payload: + type: object + properties: + serviceId: + type: string + UserSignedUpReply: + $ref: "./messages/UserSignedUpReply.yaml" + ServiceUserSignupReply: + payload: + type: object + properties: + success: + type: boolean + `; + + const userSignedUpMessage = ` +payload: + type: object + properties: + userId: + type: string +`; + + const userSignedUpReplyMessage = ` +payload: + type: object + properties: + success: + type: boolean +`; + + const expectedBundledSpec = ` +asyncapi: 3.0.0 +info: + version: 1.0.0 + title: AsyncAPI 3.0 Sample + description: Sample AsyncAPI 3.0 with operations and replies +servers: + test: + host: api.example.com:5672 + protocol: kafka +channels: + userSignup: + address: user/signedup + servers: + - $ref: "#/servers/test" + messages: + UserSignedUp: + $ref: "#/components/messages/UserSignedUp" + ServiceUserSignup: + $ref: "#/components/messages/ServiceUserSignup" + userSignupReply: + - $ref: "#/components/channels/userSignupReply" +operations: + sendUserSignup: + action: send + channel: + $ref: "#/channels/userSignup" + messages: + - $ref: "#/channels/userSignup/messages/UserSignedUp" + reply: + channel: + $ref: "#/channels/userSignupReply" + messages: + - $ref: "#/channels/userSignupReply/messages/UserSignedUpReply" + sendServiceUserSignup: + $ref: "#/components/operations/sendServiceUserSignup" +components: + channels: + userSignupReply: + servers: + - $ref: "#/servers/test" + address: user/signedup/reply + messages: + UserSignedUpReply: + $ref: "#/components/messages/UserSignedUpReply" + ServiceUserSignupReply: + $ref: "#/components/messages/ServiceUserSignupReply" + operations: + sendServiceUserSignup: + action: send + channel: + $ref: "#/channels/userSignup" + messages: + - $ref: "#/channels/userSignup/messages/ServiceUserSignup" + reply: + channel: + $ref: "#/channels/userSignupReply" + messages: + - $ref: "#/channels/userSignupReply/messages/ServiceUserSignupReply" + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string + ServiceUserSignup: + payload: + type: object + properties: + serviceId: + type: string + UserSignedUpReply: + payload: + type: object + properties: + success: + type: boolean + ServiceUserSignupReply: + payload: + type: object + properties: + success: + type: boolean +`; + + read + .mockResolvedValueOnce(userSignedUpMessage) + .mockResolvedValueOnce(userSignedUpReplyMessage); + + const result = await bundleFileWithRefs( + spec, + 'https://github.com/owner/repo/blob/main/catalog-info.yaml', + read, + resolveUrl, + ); + + expect(read).toHaveBeenCalledTimes(2); + expect(result).toEqual(expectedBundledSpec.trimStart()); + }); }); describe('bundleFileWithRefs - Testing getRelativePath scenarios', () => { diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts index 32dac30681..2a3df4ab54 100644 --- a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts @@ -34,6 +34,20 @@ export type BundlerRead = (url: string) => Promise; export type BundlerResolveUrl = (url: string, base: string) => string; +// Preserved references paths for AsyncAPI v3 documents +const asyncApiV3PreservedPaths = [ + /#\/channels\/.*\/servers/, + /#\/operations\/.*\/channel/, + /#\/operations\/.*\/messages/, + /#\/operations\/.*\/reply\/channel/, + /#\/operations\/.*\/reply\/messages/, + /#\/components\/channels\/.*\/servers/, + /#\/components\/operations\/.*\/channel/, + /#\/components\/operations\/.*\/messages/, + /#\/components\/operations\/.*\/reply\/channel/, + /#\/components\/operations\/.*\/reply\/messages/, +]; + export async function bundleFileWithRefs( fileWithRefs: string, baseUrl: string, @@ -46,7 +60,7 @@ export async function bundleFileWithRefs( return protocol === undefined || protocol === 'file'; }, read: async file => { - const relativePath = path.relative('.', file.url); + const relativePath = path.relative('.', file.url).replace(/\\/g, '/'); const url = resolveUrl(relativePath, baseUrl); return await read(url); }, @@ -61,14 +75,29 @@ export async function bundleFileWithRefs( return await read(url); }, }; - const options: ParserOptions = { resolve: { file: fileUrlReaderResolver, http: httpUrlReaderResolver, }, }; + const fileObject = parse(fileWithRefs); + + if (fileObject.asyncapi) { + const version = parseInt(fileObject.asyncapi, 10); + + if (version === 3) { + options.bundle = { + excludedPathMatcher: (refPath: string): any => { + return asyncApiV3PreservedPaths.some(pattern => + pattern.test(refPath), + ); + }, + }; + } + } + const bundledObject = await $RefParser.bundle(fileObject, options); return stringify(bundledObject); } diff --git a/yarn.lock b/yarn.lock index 90cc32debd..056de1c49a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -193,24 +193,14 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:^11.0.0": - version: 11.9.3 - resolution: "@apidevtools/json-schema-ref-parser@npm:11.9.3" +"@apidevtools/json-schema-ref-parser@npm:^14.0.3, @apidevtools/json-schema-ref-parser@npm:^14.2.1": + version: 14.2.1 + resolution: "@apidevtools/json-schema-ref-parser@npm:14.2.1" dependencies: - "@jsdevtools/ono": "npm:^7.1.3" - "@types/json-schema": "npm:^7.0.15" js-yaml: "npm:^4.1.0" - checksum: 10/3d3618dbb611d1296b99bdee4ff0dde664dad47632d30e0310c6d10de8081f6378ccb58329ea4e03103eca9347d5143671d03f0527b1c3f0916d95f8c09215e2 - languageName: node - linkType: hard - -"@apidevtools/json-schema-ref-parser@npm:^14.0.3": - version: 14.1.1 - resolution: "@apidevtools/json-schema-ref-parser@npm:14.1.1" - dependencies: - "@types/json-schema": "npm:^7.0.15" - js-yaml: "npm:^4.1.0" - checksum: 10/c4332faf164c19764838e33cd8a7ef7c233ecaf3348e7c4470ef92d0c8c1c7ec2dbb1ce535d569bba52a4b1ef104d3f747da11f7a0f0bafdaee11b54ec826b49 + peerDependencies: + "@types/json-schema": ^7.0.15 + checksum: 10/c3f6d97c0e885f9543b0654258ee16b2dd75463c8496499563c278089043317f89010e89eb51699c7fb38dfb83cc8592f0b0c4983b764b56789dc3329b25ebfd languageName: node linkType: hard @@ -5069,7 +5059,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-openapi@workspace:plugins/catalog-backend-module-openapi" dependencies: - "@apidevtools/json-schema-ref-parser": "npm:^11.0.0" + "@apidevtools/json-schema-ref-parser": "npm:^14.2.1" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^"