Skip to content

Commit 2faab85

Browse files
authored
Merge pull request #3436 from hey-api/copilot/fix-path-collision-prefix
fix: merge paths with non-conflicting HTTP methods instead of prefixing
2 parents 55ccacf + b1a419a commit 2faab85

3 files changed

Lines changed: 122 additions & 10 deletions

File tree

.changeset/thin-peaches-doubt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hey-api/json-schema-ref-parser": patch
3+
"@hey-api/openapi-ts": patch
4+
---
5+
6+
**input**: fix: avoid prefixing sources if paths do not collide on operations

packages/json-schema-ref-parser/src/__tests__/bundle.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,86 @@ describe('bundle', () => {
308308
}
309309
});
310310
});
311+
312+
describe('mergeMany', () => {
313+
it('merges paths with non-conflicting methods under the same path', async () => {
314+
const refParser = new $RefParser();
315+
const spec1 = {
316+
info: { title: 'Spec 1', version: '1.0.0' },
317+
paths: {
318+
'/pet/{petId}': {
319+
post: {
320+
operationId: 'updatePetWithForm',
321+
responses: { '405': { description: 'Invalid input' } },
322+
},
323+
},
324+
},
325+
swagger: '2.0',
326+
};
327+
const spec2 = {
328+
info: { title: 'Spec 2', version: '1.0.0' },
329+
paths: {
330+
'/pet/{petId}': {
331+
delete: {
332+
operationId: 'deletePet',
333+
responses: {
334+
'400': { description: 'Invalid ID supplied' },
335+
'404': { description: 'Pet not found' },
336+
},
337+
},
338+
},
339+
},
340+
swagger: '2.0',
341+
};
342+
343+
const merged = (await refParser.bundleMany({ pathOrUrlOrSchemas: [spec1, spec2] })) as any;
344+
345+
// Both methods should be under the same path (no prefix added)
346+
expect(merged.paths['/pet/{petId}']).toBeDefined();
347+
expect(merged.paths['/pet/{petId}'].post).toBeDefined();
348+
expect(merged.paths['/pet/{petId}'].delete).toBeDefined();
349+
350+
// No prefixed path should be created
351+
const pathKeys = Object.keys(merged.paths);
352+
expect(pathKeys).toHaveLength(1);
353+
});
354+
355+
it('adds prefix to path when HTTP methods conflict', async () => {
356+
const refParser = new $RefParser();
357+
const spec1 = {
358+
info: { title: 'Spec 1', version: '1.0.0' },
359+
paths: {
360+
'/pet/{petId}': {
361+
get: {
362+
operationId: 'getPetById',
363+
responses: { '200': { description: 'OK' } },
364+
},
365+
},
366+
},
367+
swagger: '2.0',
368+
};
369+
const spec2 = {
370+
info: { title: 'Spec 2', version: '1.0.0' },
371+
paths: {
372+
'/pet/{petId}': {
373+
get: {
374+
operationId: 'getPet',
375+
responses: { '200': { description: 'Success' } },
376+
},
377+
},
378+
},
379+
swagger: '2.0',
380+
};
381+
382+
const merged = (await refParser.bundleMany({ pathOrUrlOrSchemas: [spec1, spec2] })) as any;
383+
384+
// The conflicting path should be prefixed
385+
const pathKeys = Object.keys(merged.paths);
386+
expect(pathKeys).toHaveLength(2);
387+
expect(merged.paths['/pet/{petId}']).toBeDefined();
388+
const prefixedKey = pathKeys.find((k) => k !== '/pet/{petId}');
389+
expect(prefixedKey).toBeDefined();
390+
expect(merged.paths[prefixedKey!].get).toBeDefined();
391+
});
392+
});
311393
});

packages/json-schema-ref-parser/src/index.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -527,20 +527,44 @@ export class $RefParser {
527527
}
528528
}
529529

530+
const HTTP_METHODS = new Set([
531+
'delete',
532+
'get',
533+
'head',
534+
'options',
535+
'patch',
536+
'post',
537+
'put',
538+
'trace',
539+
]);
540+
530541
const srcPaths = (schema.paths || {}) as Record<string, any>;
531542
for (const [p, item] of Object.entries(srcPaths)) {
532-
let targetPath = p;
533543
if (merged.paths[p]) {
534-
const trimmed = p.startsWith('/') ? p.substring(1) : p;
535-
targetPath = `/${prefix}/${trimmed}`;
544+
const newMethods = Object.keys(item as object).filter((k) => HTTP_METHODS.has(k));
545+
const hasMethodConflict = newMethods.some((m) => merged.paths[p][m] !== undefined);
546+
const rewritten = cloneAndRewrite(
547+
item,
548+
refMap,
549+
tagMap,
550+
prefix,
551+
url.stripHash(sourcePath),
552+
);
553+
if (hasMethodConflict) {
554+
const trimmed = p.startsWith('/') ? p.substring(1) : p;
555+
merged.paths[`/${prefix}/${trimmed}`] = rewritten;
556+
} else {
557+
Object.assign(merged.paths[p], rewritten);
558+
}
559+
} else {
560+
merged.paths[p] = cloneAndRewrite(
561+
item,
562+
refMap,
563+
tagMap,
564+
prefix,
565+
url.stripHash(sourcePath),
566+
);
536567
}
537-
merged.paths[targetPath] = cloneAndRewrite(
538-
item,
539-
refMap,
540-
tagMap,
541-
prefix,
542-
url.stripHash(sourcePath),
543-
);
544568
}
545569
}
546570

0 commit comments

Comments
 (0)