Skip to content

Commit b78cf4b

Browse files
feat(respect-core): generate operationPath when operationId is missing (#1986)
1 parent ef25df8 commit b78cf4b

File tree

4 files changed

+131
-25
lines changed

4 files changed

+131
-25
lines changed

.changeset/old-lamps-taste.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@redocly/respect-core": minor
3+
"@redocly/cli": minor
4+
---
5+
6+
Added support for generating workflows from OpenAPI operations without operationIds. The `generate-arazzo` command now automatically generates operationPaths using the URL pattern: `{$sourceDescriptions.<name>.url}#/paths/<path>/<method>`.

packages/respect-core/src/modules/__tests__/arazzo-description-generator/generate-arazzo-description.test.ts

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,36 @@ const BUNDLED_DESCRIPTION_MOCK = {
4141
},
4242
};
4343

44+
const BUNDLED_DESCRIPTION_MOCK_WITHOUT_OPERATION_ID = {
45+
paths: {
46+
'/pet': {
47+
get: {
48+
responses: {
49+
200: {
50+
description: 'OK',
51+
},
52+
},
53+
},
54+
},
55+
'/fact': {
56+
get: {
57+
responses: {},
58+
},
59+
},
60+
},
61+
servers: [
62+
{
63+
url: 'https://petstore.swagger.io/v1',
64+
},
65+
],
66+
info: {
67+
title: 'Swagger Petstore',
68+
version: '1.0.0',
69+
},
70+
};
71+
4472
describe('generateArazzoDescription', () => {
45-
it('should generate test config when output file is provided', async () => {
73+
it('should generate arazzo description when output file is provided', async () => {
4674
(bundleOpenApi as jest.Mock).mockReturnValue(BUNDLED_DESCRIPTION_MOCK);
4775
expect(
4876
await generateArazzoDescription({
@@ -86,7 +114,7 @@ describe('generateArazzoDescription', () => {
86114
});
87115
});
88116

89-
it('should generate test config with', async () => {
117+
it('should generate arazzo description with operationId', async () => {
90118
(bundleOpenApi as jest.Mock).mockReturnValue(BUNDLED_DESCRIPTION_MOCK);
91119
(getOperationFromDescription as jest.Mock).mockReturnValue({
92120
responses: {
@@ -149,7 +177,7 @@ describe('generateArazzoDescription', () => {
149177
});
150178
});
151179

152-
it('should generate test config with not existing description', async () => {
180+
it('should generate arazzo description with not existing description', async () => {
153181
(bundleOpenApi as jest.Mock).mockReturnValue(undefined);
154182
expect(
155183
await generateArazzoDescription({
@@ -172,4 +200,67 @@ describe('generateArazzoDescription', () => {
172200
workflows: [],
173201
});
174202
});
203+
204+
it('should generate arazzo description with operationPath', async () => {
205+
(bundleOpenApi as jest.Mock).mockReturnValue(BUNDLED_DESCRIPTION_MOCK_WITHOUT_OPERATION_ID);
206+
(getOperationFromDescription as jest.Mock).mockReturnValue({
207+
responses: {
208+
200: {
209+
description: 'OK',
210+
},
211+
},
212+
});
213+
(getRequestDataFromOpenApi as jest.Mock).mockReturnValue({
214+
parameters: [
215+
{
216+
in: 'query',
217+
name: 'limit',
218+
schema: {
219+
type: 'integer',
220+
format: 'int32',
221+
},
222+
},
223+
],
224+
});
225+
226+
expect(
227+
await generateArazzoDescription({
228+
descriptionPath: 'description.yaml',
229+
})
230+
).toEqual({
231+
arazzo: '1.0.1',
232+
info: {
233+
title: 'Swagger Petstore',
234+
version: '1.0.0',
235+
},
236+
sourceDescriptions: [
237+
{
238+
name: 'description',
239+
type: 'openapi',
240+
url: 'description.yaml',
241+
},
242+
],
243+
workflows: [
244+
{
245+
workflowId: 'get-pet-workflow',
246+
steps: [
247+
{
248+
operationPath: '{$sourceDescriptions.description.url}#/paths/~1pet/get',
249+
stepId: 'get-pet-step',
250+
successCriteria: [{ condition: '$statusCode == 200' }],
251+
},
252+
],
253+
},
254+
{
255+
workflowId: 'get-fact-workflow',
256+
steps: [
257+
{
258+
operationPath: '{$sourceDescriptions.description.url}#/paths/~1fact/get',
259+
stepId: 'get-fact-step',
260+
},
261+
],
262+
},
263+
],
264+
});
265+
});
175266
});

packages/respect-core/src/modules/arazzo-description-generator/generate-workflows-from-description.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ export function generateWorkflowsFromDescription({
6464

6565
const operation = descriptionPaths[pathItemKey][methodToCheck.toLowerCase() as HttpMethod];
6666
const operationSecurity = operation?.security || undefined;
67-
const operationId = operation?.operationId || generateOperationId(pathItemKey, method);
67+
const operationId = generateOperationId(sourceDescriptionName, operation?.operationId);
68+
const operationPath = !operationId
69+
? generateOperationPath(sourceDescriptionName, pathItemKey, method)
70+
: undefined;
6871
const workflowSecurityInputs = generateWorkflowSecurityInputs(
6972
inputsComponents,
7073
operationSecurity || rootSecurity || []
@@ -84,7 +87,7 @@ export function generateWorkflowsFromDescription({
8487
steps: [
8588
{
8689
stepId: pathKey ? `${method}-${pathKey}-step` : `${method}-step`,
87-
operationId: `$sourceDescriptions.${sourceDescriptionName}.${operationId}`,
90+
...(operationId ? { operationId } : { operationPath }),
8891
...generateParametersWithSuccessCriteria(
8992
descriptionPaths[pathItemKey][methodToCheck.toLowerCase() as HttpMethod]?.responses
9093
),
@@ -111,6 +114,21 @@ function generateParametersWithSuccessCriteria(
111114
return { successCriteria: [{ condition: `$statusCode == ${firstResponseCode}` }] };
112115
}
113116

114-
function generateOperationId(path: string, method: OperationMethod) {
115-
return `${method}@${path}`;
117+
function generateOperationId(sourceDescriptionName: string, operationId?: string) {
118+
if (!operationId) {
119+
return undefined;
120+
}
121+
122+
return `$sourceDescriptions.${sourceDescriptionName}.${operationId}`;
123+
}
124+
125+
function generateOperationPath(
126+
sourceDescriptionName: string,
127+
path: string,
128+
method: OperationMethod
129+
) {
130+
return `{$sourceDescriptions.${sourceDescriptionName}.url}#/paths/~1${path.replace(
131+
/^\//,
132+
''
133+
)}/${method}`;
116134
}

packages/respect-core/src/modules/description-parser/get-operation-by-id.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,15 @@ export function getOperationById(
4040
}
4141

4242
for (const [method, operationDetails] of Object.entries(pathDetails)) {
43-
// @ indicates autogenerated operationId
44-
if (operationId.includes('@/')) {
45-
// TODO: replace this with construct of path
46-
const [methodPrefix, pathSuffix] = operationId.split('@');
47-
if (methodPrefix === method && pathSuffix === path) {
48-
return { ...operationDetails, path, method, descriptionName };
49-
}
50-
} else {
51-
if (operationDetails.operationId === operationId) {
52-
return {
53-
servers: (pathDetails as any).servers || rootServers,
54-
...operationDetails,
55-
pathParameters: operationDetails.parameters || [],
56-
path,
57-
method,
58-
descriptionName,
59-
};
60-
}
43+
if (operationDetails.operationId === operationId) {
44+
return {
45+
servers: (pathDetails as any).servers || rootServers,
46+
...operationDetails,
47+
pathParameters: operationDetails.parameters || [],
48+
path,
49+
method,
50+
descriptionName,
51+
};
6152
}
6253
}
6354
}

0 commit comments

Comments
 (0)