Skip to content

Commit dcb4bb9

Browse files
committed
fix: sort handler
1 parent c4334b5 commit dcb4bb9

6 files changed

Lines changed: 98 additions & 37 deletions

File tree

examples/openapi-ts-fetch/src/client/msw.gen.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -484,25 +484,25 @@ export const createMswHandlerFactory = (config?: { baseUrl?: string }): MswHandl
484484
}
485485
}
486486
};
487-
addRequiredHandler(mocks.addPetMock, overrides?.addPetMock);
488-
addRequiredHandler(mocks.updatePetMock, overrides?.updatePetMock);
487+
handlers.push(mocks.deleteOrderMock(overrides?.deleteOrderMock));
488+
addRequiredHandler(mocks.getOrderByIdMock, overrides?.getOrderByIdMock);
489+
addRequiredHandler(mocks.uploadFileMock, overrides?.uploadFileMock);
489490
addRequiredHandler(mocks.findPetsByStatusMock, overrides?.findPetsByStatusMock);
490491
addRequiredHandler(mocks.findPetsByTagsMock, overrides?.findPetsByTagsMock);
491-
handlers.push(mocks.deletePetMock(overrides?.deletePetMock));
492-
addRequiredHandler(mocks.getPetByIdMock, overrides?.getPetByIdMock);
493-
addRequiredHandler(mocks.updatePetWithFormMock, overrides?.updatePetWithFormMock);
494-
addRequiredHandler(mocks.uploadFileMock, overrides?.uploadFileMock);
495492
addRequiredHandler(mocks.getInventoryMock, overrides?.getInventoryMock);
496493
addRequiredHandler(mocks.placeOrderMock, overrides?.placeOrderMock);
497-
handlers.push(mocks.deleteOrderMock(overrides?.deleteOrderMock));
498-
addRequiredHandler(mocks.getOrderByIdMock, overrides?.getOrderByIdMock);
499-
addRequiredHandler(mocks.createUserMock, overrides?.createUserMock);
500494
addRequiredHandler(mocks.createUsersWithListInputMock, overrides?.createUsersWithListInputMock);
501495
addRequiredHandler(mocks.loginUserMock, overrides?.loginUserMock);
502496
handlers.push(mocks.logoutUserMock(overrides?.logoutUserMock));
497+
handlers.push(mocks.deletePetMock(overrides?.deletePetMock));
498+
addRequiredHandler(mocks.getPetByIdMock, overrides?.getPetByIdMock);
499+
addRequiredHandler(mocks.updatePetWithFormMock, overrides?.updatePetWithFormMock);
503500
handlers.push(mocks.deleteUserMock(overrides?.deleteUserMock));
504501
addRequiredHandler(mocks.getUserByNameMock, overrides?.getUserByNameMock);
505502
handlers.push(mocks.updateUserMock(overrides?.updateUserMock));
503+
addRequiredHandler(mocks.addPetMock, overrides?.addPetMock);
504+
addRequiredHandler(mocks.updatePetMock, overrides?.updatePetMock);
505+
addRequiredHandler(mocks.createUserMock, overrides?.createUserMock);
506506
return handlers;
507507
};
508508
return { ...mocks, getAllMocks };

packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/msw/default/msw.gen.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ export const createMswHandlerFactory = (config?: {
165165
}
166166
}
167167
};
168+
handlers.push(mocks.callWithWeirdParameterNamesMock(overrides?.callWithWeirdParameterNamesMock));
169+
addRequiredHandler(mocks.callWithResponseAndNoContentResponseMock, overrides?.callWithResponseAndNoContentResponseMock);
170+
handlers.push(mocks.dummyAMock(overrides?.dummyAMock));
171+
handlers.push(mocks.dummyBMock(overrides?.dummyBMock));
172+
handlers.push(mocks.callWithParametersMock(overrides?.callWithParametersMock));
168173
handlers.push(mocks.serviceWithEmptyTagMock(overrides?.serviceWithEmptyTagMock));
169174
handlers.push(mocks.patchApiVbyApiVersionNoTagMock(overrides?.patchApiVbyApiVersionNoTagMock));
170175
handlers.push(mocks.fooWowMock(overrides?.fooWowMock));
@@ -176,8 +181,6 @@ export const createMswHandlerFactory = (config?: {
176181
handlers.push(mocks.postCallWithoutParametersAndResponseMock(overrides?.postCallWithoutParametersAndResponseMock));
177182
handlers.push(mocks.putCallWithoutParametersAndResponseMock(overrides?.putCallWithoutParametersAndResponseMock));
178183
handlers.push(mocks.callWithDescriptionsMock(overrides?.callWithDescriptionsMock));
179-
handlers.push(mocks.callWithParametersMock(overrides?.callWithParametersMock));
180-
handlers.push(mocks.callWithWeirdParameterNamesMock(overrides?.callWithWeirdParameterNamesMock));
181184
handlers.push(mocks.callWithDefaultParametersMock(overrides?.callWithDefaultParametersMock));
182185
handlers.push(mocks.callWithDefaultOptionalParametersMock(overrides?.callWithDefaultOptionalParametersMock));
183186
handlers.push(mocks.callToTestOrderOfParamsMock(overrides?.callToTestOrderOfParamsMock));
@@ -186,9 +189,6 @@ export const createMswHandlerFactory = (config?: {
186189
handlers.push(mocks.duplicateName3Mock(overrides?.duplicateName3Mock));
187190
handlers.push(mocks.duplicateName4Mock(overrides?.duplicateName4Mock));
188191
handlers.push(mocks.callWithNoContentResponseMock(overrides?.callWithNoContentResponseMock));
189-
addRequiredHandler(mocks.callWithResponseAndNoContentResponseMock, overrides?.callWithResponseAndNoContentResponseMock);
190-
handlers.push(mocks.dummyAMock(overrides?.dummyAMock));
191-
handlers.push(mocks.dummyBMock(overrides?.dummyBMock));
192192
handlers.push(mocks.callWithResponseMock(overrides?.callWithResponseMock));
193193
addRequiredHandler(mocks.callWithDuplicateResponsesMock, overrides?.callWithDuplicateResponsesMock);
194194
addRequiredHandler(mocks.callWithResponsesMock, overrides?.callWithResponsesMock);

packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/msw/default/msw.gen.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,20 @@ export const createMswHandlerFactory = (config?: {
228228
}
229229
}
230230
};
231+
handlers.push(mocks.deleteFooMock(overrides?.deleteFooMock));
232+
handlers.push(mocks.callWithWeirdParameterNamesMock(overrides?.callWithWeirdParameterNamesMock));
233+
addRequiredHandler(mocks.apiVVersionODataControllerCountMock, overrides?.apiVVersionODataControllerCountMock);
234+
handlers.push(mocks.deprecatedCallMock(overrides?.deprecatedCallMock));
235+
addRequiredHandler(mocks.callWithResponseAndNoContentResponseMock, overrides?.callWithResponseAndNoContentResponseMock);
236+
addRequiredHandler(mocks.dummyAMock, overrides?.dummyAMock);
237+
handlers.push(mocks.dummyBMock(overrides?.dummyBMock));
238+
handlers.push(mocks.callWithParametersMock(overrides?.callWithParametersMock));
239+
addRequiredHandler(mocks.fileResponseMock, overrides?.fileResponseMock);
240+
addRequiredHandler(mocks.complexParamsMock, overrides?.complexParamsMock);
231241
handlers.push(mocks.exportMock(overrides?.exportMock));
232242
handlers.push(mocks.patchApiVbyApiVersionNoTagMock(overrides?.patchApiVbyApiVersionNoTagMock));
233243
addRequiredHandler(mocks.importMock, overrides?.importMock);
234244
handlers.push(mocks.fooWowMock(overrides?.fooWowMock));
235-
addRequiredHandler(mocks.apiVVersionODataControllerCountMock, overrides?.apiVVersionODataControllerCountMock);
236245
addRequiredHandler(mocks.getApiVbyApiVersionSimpleOperationMock, overrides?.getApiVbyApiVersionSimpleOperationMock);
237246
handlers.push(mocks.deleteCallWithoutParametersAndResponseMock(overrides?.deleteCallWithoutParametersAndResponseMock));
238247
handlers.push(mocks.getCallWithoutParametersAndResponseMock(overrides?.getCallWithoutParametersAndResponseMock));
@@ -241,11 +250,7 @@ export const createMswHandlerFactory = (config?: {
241250
handlers.push(mocks.patchCallWithoutParametersAndResponseMock(overrides?.patchCallWithoutParametersAndResponseMock));
242251
handlers.push(mocks.postCallWithoutParametersAndResponseMock(overrides?.postCallWithoutParametersAndResponseMock));
243252
handlers.push(mocks.putCallWithoutParametersAndResponseMock(overrides?.putCallWithoutParametersAndResponseMock));
244-
handlers.push(mocks.deleteFooMock(overrides?.deleteFooMock));
245253
handlers.push(mocks.callWithDescriptionsMock(overrides?.callWithDescriptionsMock));
246-
handlers.push(mocks.deprecatedCallMock(overrides?.deprecatedCallMock));
247-
handlers.push(mocks.callWithParametersMock(overrides?.callWithParametersMock));
248-
handlers.push(mocks.callWithWeirdParameterNamesMock(overrides?.callWithWeirdParameterNamesMock));
249254
handlers.push(mocks.getCallWithOptionalParamMock(overrides?.getCallWithOptionalParamMock));
250255
addRequiredHandler(mocks.postCallWithOptionalParamMock, overrides?.postCallWithOptionalParamMock);
251256
handlers.push(mocks.postApiVbyApiVersionRequestBodyMock(overrides?.postApiVbyApiVersionRequestBodyMock));
@@ -258,20 +263,15 @@ export const createMswHandlerFactory = (config?: {
258263
handlers.push(mocks.duplicateName3Mock(overrides?.duplicateName3Mock));
259264
handlers.push(mocks.duplicateName4Mock(overrides?.duplicateName4Mock));
260265
handlers.push(mocks.callWithNoContentResponseMock(overrides?.callWithNoContentResponseMock));
261-
addRequiredHandler(mocks.callWithResponseAndNoContentResponseMock, overrides?.callWithResponseAndNoContentResponseMock);
262-
addRequiredHandler(mocks.dummyAMock, overrides?.dummyAMock);
263-
handlers.push(mocks.dummyBMock(overrides?.dummyBMock));
264266
handlers.push(mocks.callWithResponseMock(overrides?.callWithResponseMock));
265267
addRequiredHandler(mocks.callWithDuplicateResponsesMock, overrides?.callWithDuplicateResponsesMock);
266268
addRequiredHandler(mocks.callWithResponsesMock, overrides?.callWithResponsesMock);
267269
handlers.push(mocks.collectionFormatMock(overrides?.collectionFormatMock));
268270
addRequiredHandler(mocks.typesMock, overrides?.typesMock);
269271
addRequiredHandler(mocks.uploadFileMock, overrides?.uploadFileMock);
270-
addRequiredHandler(mocks.fileResponseMock, overrides?.fileResponseMock);
271272
addRequiredHandler(mocks.complexTypesMock, overrides?.complexTypesMock);
272273
addRequiredHandler(mocks.multipartResponseMock, overrides?.multipartResponseMock);
273274
handlers.push(mocks.multipartRequestMock(overrides?.multipartRequestMock));
274-
addRequiredHandler(mocks.complexParamsMock, overrides?.complexParamsMock);
275275
handlers.push(mocks.callWithResultFromHeaderMock(overrides?.callWithResultFromHeaderMock));
276276
handlers.push(mocks.testErrorCodeMock(overrides?.testErrorCodeMock));
277277
addRequiredHandler(mocks.nonAsciiæøåÆøÅöôêÊ字符串Mock, overrides?.nonAsciiæøåÆøÅöôêÊ字符串Mock);

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/msw/default/msw.gen.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,20 @@ export const createMswHandlerFactory = (config?: {
228228
}
229229
}
230230
};
231+
handlers.push(mocks.deleteFooMock(overrides?.deleteFooMock));
232+
handlers.push(mocks.callWithWeirdParameterNamesMock(overrides?.callWithWeirdParameterNamesMock));
233+
addRequiredHandler(mocks.apiVVersionODataControllerCountMock, overrides?.apiVVersionODataControllerCountMock);
234+
handlers.push(mocks.deprecatedCallMock(overrides?.deprecatedCallMock));
235+
addRequiredHandler(mocks.callWithResponseAndNoContentResponseMock, overrides?.callWithResponseAndNoContentResponseMock);
236+
addRequiredHandler(mocks.dummyAMock, overrides?.dummyAMock);
237+
handlers.push(mocks.dummyBMock(overrides?.dummyBMock));
238+
handlers.push(mocks.callWithParametersMock(overrides?.callWithParametersMock));
239+
addRequiredHandler(mocks.fileResponseMock, overrides?.fileResponseMock);
240+
addRequiredHandler(mocks.complexParamsMock, overrides?.complexParamsMock);
231241
handlers.push(mocks.exportMock(overrides?.exportMock));
232242
handlers.push(mocks.patchApiVbyApiVersionNoTagMock(overrides?.patchApiVbyApiVersionNoTagMock));
233243
addRequiredHandler(mocks.importMock, overrides?.importMock);
234244
handlers.push(mocks.fooWowMock(overrides?.fooWowMock));
235-
addRequiredHandler(mocks.apiVVersionODataControllerCountMock, overrides?.apiVVersionODataControllerCountMock);
236245
addRequiredHandler(mocks.getApiVbyApiVersionSimpleOperationMock, overrides?.getApiVbyApiVersionSimpleOperationMock);
237246
handlers.push(mocks.deleteCallWithoutParametersAndResponseMock(overrides?.deleteCallWithoutParametersAndResponseMock));
238247
handlers.push(mocks.getCallWithoutParametersAndResponseMock(overrides?.getCallWithoutParametersAndResponseMock));
@@ -241,11 +250,7 @@ export const createMswHandlerFactory = (config?: {
241250
handlers.push(mocks.patchCallWithoutParametersAndResponseMock(overrides?.patchCallWithoutParametersAndResponseMock));
242251
handlers.push(mocks.postCallWithoutParametersAndResponseMock(overrides?.postCallWithoutParametersAndResponseMock));
243252
handlers.push(mocks.putCallWithoutParametersAndResponseMock(overrides?.putCallWithoutParametersAndResponseMock));
244-
handlers.push(mocks.deleteFooMock(overrides?.deleteFooMock));
245253
handlers.push(mocks.callWithDescriptionsMock(overrides?.callWithDescriptionsMock));
246-
handlers.push(mocks.deprecatedCallMock(overrides?.deprecatedCallMock));
247-
handlers.push(mocks.callWithParametersMock(overrides?.callWithParametersMock));
248-
handlers.push(mocks.callWithWeirdParameterNamesMock(overrides?.callWithWeirdParameterNamesMock));
249254
handlers.push(mocks.getCallWithOptionalParamMock(overrides?.getCallWithOptionalParamMock));
250255
addRequiredHandler(mocks.postCallWithOptionalParamMock, overrides?.postCallWithOptionalParamMock);
251256
handlers.push(mocks.postApiVbyApiVersionRequestBodyMock(overrides?.postApiVbyApiVersionRequestBodyMock));
@@ -258,20 +263,15 @@ export const createMswHandlerFactory = (config?: {
258263
handlers.push(mocks.duplicateName3Mock(overrides?.duplicateName3Mock));
259264
handlers.push(mocks.duplicateName4Mock(overrides?.duplicateName4Mock));
260265
handlers.push(mocks.callWithNoContentResponseMock(overrides?.callWithNoContentResponseMock));
261-
addRequiredHandler(mocks.callWithResponseAndNoContentResponseMock, overrides?.callWithResponseAndNoContentResponseMock);
262-
addRequiredHandler(mocks.dummyAMock, overrides?.dummyAMock);
263-
handlers.push(mocks.dummyBMock(overrides?.dummyBMock));
264266
handlers.push(mocks.callWithResponseMock(overrides?.callWithResponseMock));
265267
addRequiredHandler(mocks.callWithDuplicateResponsesMock, overrides?.callWithDuplicateResponsesMock);
266268
addRequiredHandler(mocks.callWithResponsesMock, overrides?.callWithResponsesMock);
267269
handlers.push(mocks.collectionFormatMock(overrides?.collectionFormatMock));
268270
addRequiredHandler(mocks.typesMock, overrides?.typesMock);
269271
addRequiredHandler(mocks.uploadFileMock, overrides?.uploadFileMock);
270-
addRequiredHandler(mocks.fileResponseMock, overrides?.fileResponseMock);
271272
addRequiredHandler(mocks.complexTypesMock, overrides?.complexTypesMock);
272273
addRequiredHandler(mocks.multipartResponseMock, overrides?.multipartResponseMock);
273274
handlers.push(mocks.multipartRequestMock(overrides?.multipartRequestMock));
274-
addRequiredHandler(mocks.complexParamsMock, overrides?.complexParamsMock);
275275
handlers.push(mocks.callWithResultFromHeaderMock(overrides?.callWithResultFromHeaderMock));
276276
handlers.push(mocks.testErrorCodeMock(overrides?.testErrorCodeMock));
277277
addRequiredHandler(mocks.nonAsciiæøåÆøÅöôêÊ字符串Mock, overrides?.nonAsciiæøåÆøÅöôêÊ字符串Mock);

packages/openapi-ts/src/plugins/msw/plugin.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { parseUrl } from '@hey-api/shared';
22

33
import { $ } from '../../ts-dsl';
44
import { operationToHandlerCreator } from './handlerCreator';
5+
import { sortHandlersBySpecificity } from './sortHandlersBySpecificity';
56
import type { MswPlugin } from './types';
67

78
export const handler: MswPlugin['Handler'] = ({ plugin }) => {
@@ -85,7 +86,7 @@ export const handler: MswPlugin['Handler'] = ({ plugin }) => {
8586
const symbolFactory = plugin.symbol('createMswHandlerFactory');
8687
const ofObject = $.object().pretty();
8788
const singleHandlerFactoriesType = $.type.object();
88-
const handlerMeta: Array<{ isOptional: boolean; name: string }> = [];
89+
const handlerMeta: Array<{ isOptional: boolean; name: string; path: string }> = [];
8990

9091
plugin.forEach(
9192
'operation',
@@ -98,7 +99,11 @@ export const handler: MswPlugin['Handler'] = ({ plugin }) => {
9899
if (handlerCreator) {
99100
ofObject.prop(handlerCreator.name, handlerCreator.value);
100101
singleHandlerFactoriesType.prop(handlerCreator.name, (p) => p.type(handlerCreator.type));
101-
handlerMeta.push({ isOptional: handlerCreator.isOptional, name: handlerCreator.name });
102+
handlerMeta.push({
103+
isOptional: handlerCreator.isOptional,
104+
name: handlerCreator.name,
105+
path: operation.path,
106+
});
102107
}
103108
},
104109
{
@@ -236,7 +241,7 @@ export const handler: MswPlugin['Handler'] = ({ plugin }) => {
236241
);
237242
}
238243

239-
for (const handler of handlerMeta) {
244+
for (const handler of sortHandlersBySpecificity(handlerMeta)) {
240245
if (handler.isOptional) {
241246
getAllMocksBodyStmts.push(
242247
$.stmt(
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
interface HandlerMeta {
2+
name: string;
3+
path: string;
4+
}
5+
6+
/**
7+
* Returns 1 for a static segment, 0 for a parameterized segment.
8+
* Works with OpenAPI `{param}` format.
9+
*/
10+
const segmentScore = (segment: string): number => (/\{[^}]+\}/.test(segment) ? 0 : 1);
11+
12+
/**
13+
* Sorts handler metadata by route specificity so that more specific routes
14+
* appear before less specific ones in the generated `getAllMocks` handler array.
15+
*
16+
* MSW matches handlers top-to-bottom, so without sorting a dynamic route like
17+
* `/api/permissions/:userId` would shadow `/api/permissions/all`.
18+
*
19+
* Algorithm inspired by React Router v6 and route-sort:
20+
* @see https://reactrouter.com/explanation/route-matching
21+
* @see https://github.com/lukeed/route-sort
22+
*
23+
* Rules, applied in priority order:
24+
* 1. More path segments (deeper routes) come first.
25+
* 2. At equal depth, static segments beat dynamic ones (left-to-right).
26+
* 3. At equal specificity, preserve original declaration order (stable sort).
27+
*/
28+
export const sortHandlersBySpecificity = <T extends HandlerMeta>(
29+
handlers: ReadonlyArray<T>,
30+
): Array<T> => {
31+
const indexed = handlers.map((handler, index) => ({ handler, index }));
32+
33+
indexed.sort((a, b) => {
34+
const segmentsA = a.handler.path.split('/').filter(Boolean);
35+
const segmentsB = b.handler.path.split('/').filter(Boolean);
36+
37+
// Rule 1: deeper routes are more specific
38+
if (segmentsA.length !== segmentsB.length) {
39+
return segmentsB.length - segmentsA.length;
40+
}
41+
42+
// Rule 2: at equal depth, static segments beat dynamic ones (left-to-right)
43+
for (let i = 0; i < segmentsA.length; i++) {
44+
const scoreA = segmentScore(segmentsA[i]!);
45+
const scoreB = segmentScore(segmentsB[i]!);
46+
if (scoreA !== scoreB) {
47+
return scoreB - scoreA;
48+
}
49+
}
50+
51+
// Rule 3: preserve declaration order
52+
return a.index - b.index;
53+
});
54+
55+
return indexed.map((entry) => entry.handler);
56+
};

0 commit comments

Comments
 (0)