Merge pull request #31563 from TobZip/fix-async-api-deref

Add support for AsyncApi v3 reference preservation
This commit is contained in:
Fredrik Adelöw
2025-11-04 13:26:40 +01:00
committed by GitHub
6 changed files with 242 additions and 21 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-openapi': patch
---
fix wrong dereferencing for AsyncApi 3 documents
@@ -110,6 +110,7 @@ dependabot
deps
dequeue
dequeueing
dereferencing
deserialization
destructured
destructuring
@@ -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:^",
@@ -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', () => {
@@ -34,6 +34,20 @@ export type BundlerRead = (url: string) => Promise<Buffer>;
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);
}
+7 -17
View File
@@ -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:^"