From 3368f78952f54054298348060aaa92da5f2d8df0 Mon Sep 17 00:00:00 2001 From: Tobias Zipfel Date: Mon, 3 Nov 2025 06:18:24 +0100 Subject: [PATCH 1/5] Add support for AsyncApi v3 reference preservation Signed-off-by: Tobias Zipfel --- .../package.json | 2 +- .../src/lib/bundle.test.ts | 198 +++++++++++++++++- .../src/lib/bundle.ts | 36 +++- yarn.lock | 15 +- 4 files changed, 239 insertions(+), 12 deletions(-) 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..086934d973 100644 --- a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts @@ -34,19 +34,35 @@ 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, read: BundlerRead, resolveUrl: BundlerResolveUrl, ): Promise { + const fileObject = parse(fileWithRefs); + const fileUrlReaderResolver: ResolverOptions = { canRead: file => { const protocol = getProtocol(file.url); 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 +77,28 @@ 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), + ); + }, + }; + } + } + + // Use generic bundler for OpenAPI documents only const bundledObject = await $RefParser.bundle(fileObject, options); return stringify(bundledObject); } diff --git a/yarn.lock b/yarn.lock index 12e6da7e7f..ccbc21a54a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -193,7 +193,7 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:^11.0.0": +"@apidevtools/json-schema-ref-parser@npm:^11.7.0": version: 11.9.3 resolution: "@apidevtools/json-schema-ref-parser@npm:11.9.3" dependencies: @@ -204,13 +204,14 @@ __metadata: 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" +"@apidevtools/json-schema-ref-parser@npm:^14.2.1": + version: 14.2.1 + resolution: "@apidevtools/json-schema-ref-parser@npm:14.2.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 @@ -5070,7 +5071,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:^" From 4492f7340d302dbe7ebdaed169b50b837f69e93b Mon Sep 17 00:00:00 2001 From: Tobias Zipfel Date: Mon, 3 Nov 2025 06:30:42 +0100 Subject: [PATCH 2/5] Remove outdated version of json-schema-ref-parser and consolidate dependencies after rebase Signed-off-by: Tobias Zipfel --- yarn.lock | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index ccbc21a54a..4624be712a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -193,18 +193,7 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:^11.7.0": - version: 11.9.3 - resolution: "@apidevtools/json-schema-ref-parser@npm:11.9.3" - 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.2.1": +"@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: From a5bcb2a2bb6c9b4be3f3359ecb7bdaa85fcc3bb3 Mon Sep 17 00:00:00 2001 From: Tobias Zipfel Date: Mon, 3 Nov 2025 10:19:18 +0100 Subject: [PATCH 3/5] add changeset Signed-off-by: Tobias Zipfel --- .changeset/flat-paws-do.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/flat-paws-do.md 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 From 757a061478d68e4be092fbe247b6f145f6872eed Mon Sep 17 00:00:00 2001 From: Tobias Zipfel Date: Mon, 3 Nov 2025 12:50:37 +0100 Subject: [PATCH 4/5] refactoring Signed-off-by: Tobias Zipfel --- plugins/catalog-backend-module-openapi/src/lib/bundle.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts index 086934d973..2a3df4ab54 100644 --- a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts @@ -54,8 +54,6 @@ export async function bundleFileWithRefs( read: BundlerRead, resolveUrl: BundlerResolveUrl, ): Promise { - const fileObject = parse(fileWithRefs); - const fileUrlReaderResolver: ResolverOptions = { canRead: file => { const protocol = getProtocol(file.url); @@ -84,6 +82,8 @@ export async function bundleFileWithRefs( }, }; + const fileObject = parse(fileWithRefs); + if (fileObject.asyncapi) { const version = parseInt(fileObject.asyncapi, 10); @@ -98,7 +98,6 @@ export async function bundleFileWithRefs( } } - // Use generic bundler for OpenAPI documents only const bundledObject = await $RefParser.bundle(fileObject, options); return stringify(bundledObject); } From 2ad782b00e05aa0b645416307554fc426012e970 Mon Sep 17 00:00:00 2001 From: Tobias Zipfel Date: Mon, 3 Nov 2025 14:20:42 +0100 Subject: [PATCH 5/5] add "dereferencing" to accepted word list Signed-off-by: Tobias Zipfel --- .github/vale/config/vocabularies/Backstage/accept.txt | 1 + 1 file changed, 1 insertion(+) 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