Skip to content

Commit 3b1c4a8

Browse files
committed
feat(core): allow operationName override to decouple type names from method names (#3684)
The override.operationName callback can now return [methodName, typeNameBase] to independently control generated function/hook names from TypeScript type identifier names (*Params, *Body, *Error, *Result, *Accept, zod/hono/effect schema names). Returning a string (existing behavior) keeps both identical. This enables gateway-aggregated specs with tags-split + splitByTags + indexFiles where bare method names are safe per-tag but type names need global uniqueness to avoid barrel-level TS2300 collisions. Fully backward compatible — existing callbacks returning string work unchanged. The tuple form is opt-in.
1 parent 2ddd0a5 commit 3b1c4a8

35 files changed

Lines changed: 749 additions & 78 deletions

File tree

docs/content/docs/reference/configuration/output.mdx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2192,7 +2192,9 @@ Override by OpenAPI tag (same options as `operations`).
21922192

21932193
**Type:** `Function`
21942194

2195-
Custom function to override generated operation names:
2195+
Custom function to override generated operation names.
2196+
2197+
**Return `string`** to control both the method name and the type-name base together:
21962198

21972199
```ts title="orval.config.ts"
21982200
export default defineConfig({
@@ -2208,6 +2210,46 @@ export default defineConfig({
22082210
});
22092211
```
22102212

2213+
**Return `[methodName, typeNameBase]`** to decouple method names from type-identifier names. This is useful for gateway-aggregated specs where multiple services share the same REST patterns (`GET /products`, `GET /orders`) — bare method names are safe per-tag (each service class scopes them), but type names (`*Params`, `*Body`, `*Error`, `*Result`) need to be globally unique to avoid barrel-level collisions with `tags-split` + `splitByTags` + `indexFiles`:
2214+
2215+
```ts title="orval.config.ts"
2216+
import { pascal } from '@orval/core';
2217+
2218+
export default defineConfig({
2219+
api: {
2220+
output: {
2221+
mode: 'tags-split',
2222+
schemas: { path: './model', splitByTags: true },
2223+
override: {
2224+
operationName: (_operation, route, verb) => {
2225+
const segments = route.split('/').filter(Boolean);
2226+
return [
2227+
`${verb}${pascal(segments.slice(2).join('-'))}`, // getProducts
2228+
`${verb}${pascal(segments.slice(1).join('-'))}`, // getCatalogProducts
2229+
];
2230+
},
2231+
},
2232+
},
2233+
},
2234+
});
2235+
```
2236+
2237+
Result:
2238+
2239+
```ts
2240+
// catalog/catalog.service.ts
2241+
class CatalogService {
2242+
getProducts = (params: GetCatalogProductsParams) => ...;
2243+
}
2244+
2245+
// inventory/inventory.service.ts
2246+
class InventoryService {
2247+
getProducts = (params: GetInventoryProductsParams) => ...;
2248+
}
2249+
```
2250+
2251+
The first element controls the function/hook name. The second controls the base for all operation-specific TypeScript type identifiers (`*Params`, `*Body`, `*Error`, `*Result`, `*Accept`, zod/hono/effect schema names).
2252+
22112253
---
22122254

22132255
## override.components

packages/angular/src/http-client.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ const createVerbOption = (
201201
({
202202
operationId: 'getPetById',
203203
operationName: 'getPetById',
204+
typeName: 'getPetById',
204205
verb: 'get',
205206
route: '/pets/${petId}',
206207
pathRoute: '/pets/{petId}',
@@ -553,6 +554,7 @@ describe('angular HttpClient generator', () => {
553554
it('still emits the shared helper when at least one operation lacks paramsFilter', () => {
554555
const verbWithFilter = createVerbOption({
555556
operationName: 'a',
557+
typeName: 'a',
556558
queryParams: createQueryParams({
557559
schema: { name: 'AParams', model: '', imports: [] },
558560
}),
@@ -569,6 +571,7 @@ describe('angular HttpClient generator', () => {
569571
});
570572
const verbWithoutFilter = createVerbOption({
571573
operationName: 'b',
574+
typeName: 'b',
572575
queryParams: createQueryParams({
573576
schema: { name: 'BParams', model: '', imports: [] },
574577
}),
@@ -870,6 +873,7 @@ describe('angular HttpClient generator', () => {
870873
const verbOption = createVerbOption({
871874
operationId: 'createPet',
872875
operationName: 'createPet',
876+
typeName: 'createPet',
873877
verb: 'post',
874878
route: '/pets',
875879
pathRoute: '/pets',
@@ -911,6 +915,7 @@ describe('angular HttpClient generator', () => {
911915
const verbOption = createVerbOption({
912916
operationId: 'updatePet',
913917
operationName: 'updatePet',
918+
typeName: 'updatePet',
914919
verb: 'put',
915920
route: '/pets/${petId}',
916921
pathRoute: '/pets/{petId}',
@@ -960,6 +965,7 @@ describe('angular HttpClient generator', () => {
960965
const verbOption = createVerbOption({
961966
operationId: 'deletePet',
962967
operationName: 'deletePet',
968+
typeName: 'deletePet',
963969
verb: 'delete',
964970
route: '/pets/${petId}',
965971
pathRoute: '/pets/{petId}',
@@ -1031,6 +1037,7 @@ describe('angular HttpClient generator', () => {
10311037
const verbOption = createVerbOption({
10321038
operationId: 'authenticate',
10331039
operationName: 'authenticate',
1040+
typeName: 'authenticate',
10341041
verb: 'post',
10351042
route: '/api/auth',
10361043
pathRoute: '/api/auth',
@@ -1125,6 +1132,7 @@ describe('angular HttpClient generator', () => {
11251132
const verbOption = createVerbOption({
11261133
operationId: 'getPetFile',
11271134
operationName: 'getPetFile',
1135+
typeName: 'getPetFile',
11281136
response: baseResponse({
11291137
definition: { success: 'Pet | string', errors: 'Error' },
11301138
types: {
@@ -1158,6 +1166,7 @@ describe('angular HttpClient generator', () => {
11581166
const verbOption = createVerbOption({
11591167
operationId: 'updatePet',
11601168
operationName: 'updatePet',
1169+
typeName: 'updatePet',
11611170
verb: 'put',
11621171
route: '/pets/${petId}',
11631172
pathRoute: '/pets/{petId}',
@@ -1219,6 +1228,7 @@ describe('angular HttpClient generator', () => {
12191228
const verbOption = createVerbOption({
12201229
operationId: 'confirmReservation',
12211230
operationName: 'confirmReservation',
1231+
typeName: 'confirmReservation',
12221232
verb: 'post',
12231233
route: '/reservations/${token}/confirm',
12241234
pathRoute: '/reservations/{token}/confirm',
@@ -1303,6 +1313,7 @@ describe('angular HttpClient generator', () => {
13031313
const verbOption = createVerbOption({
13041314
operationId: 'updatePet',
13051315
operationName: 'updatePet',
1316+
typeName: 'updatePet',
13061317
verb: 'put',
13071318
route: '/pets/${petId}',
13081319
pathRoute: '/pets/{petId}',
@@ -1371,6 +1382,7 @@ describe('angular HttpClient generator', () => {
13711382
const verbOption = createVerbOption({
13721383
operationId: 'listPets',
13731384
operationName: 'listPets',
1385+
typeName: 'listPets',
13741386
route: '/pets',
13751387
pathRoute: '/pets',
13761388
params: [],
@@ -1420,6 +1432,7 @@ describe('angular HttpClient generator', () => {
14201432
const verbOption = createVerbOption({
14211433
operationId: 'deletePet',
14221434
operationName: 'deletePet',
1435+
typeName: 'deletePet',
14231436
verb: 'delete',
14241437
route: '/pets/${petId}',
14251438
pathRoute: '/pets/{petId}',
@@ -1480,6 +1493,7 @@ describe('angular HttpClient generator', () => {
14801493
const verbOption = createVerbOption({
14811494
operationId: 'listPets',
14821495
operationName: 'listPets',
1496+
typeName: 'listPets',
14831497
route: '/pets',
14841498
pathRoute: '/pets',
14851499
params: [],
@@ -1707,6 +1721,7 @@ describe('angular HttpClient generator', () => {
17071721
const verbOption = createVerbOption({
17081722
operationId: 'createPet',
17091723
operationName: 'createPet',
1724+
typeName: 'createPet',
17101725
verb: 'post',
17111726
route: '/pets',
17121727
pathRoute: '/pets',
@@ -1827,6 +1842,7 @@ describe('angular HttpClient generator', () => {
18271842
getPetFile: createVerbOption({
18281843
operationId: 'getPetFile',
18291844
operationName: 'getPetFile',
1845+
typeName: 'getPetFile',
18301846
response: baseResponse({
18311847
definition: { success: 'Pet | string', errors: 'Error' },
18321848
types: {
@@ -1909,6 +1925,7 @@ describe('angular HttpClient generator', () => {
19091925
const verbOption = createVerbOption({
19101926
operationId: 'listPets',
19111927
operationName: 'listPets',
1928+
typeName: 'listPets',
19121929
route: '/pets',
19131930
pathRoute: '/pets',
19141931
params: [],

packages/angular/src/http-client.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ export const getAngularDependencies: ClientDependenciesBuilder = () => [
132132
*
133133
* @returns A PascalCase helper type/const name for the operation's `Accept` values.
134134
*/
135-
export const getAcceptHelperName = (operationName: string) =>
136-
`${pascal(operationName)}Accept`;
135+
export const getAcceptHelperName = (typeName: string) =>
136+
`${pascal(typeName)}Accept`;
137137

138138
/**
139139
* Collects the distinct successful response content types for a single
@@ -155,11 +155,11 @@ const toAcceptHelperKey = (contentType: string): string =>
155155
.toLowerCase();
156156

157157
const buildAcceptHelper = (
158-
operationName: string,
158+
typeName: string,
159159
contentTypes: string[],
160160
output: ContextSpec['output'],
161161
): string => {
162-
const acceptHelperName = getAcceptHelperName(operationName);
162+
const acceptHelperName = getAcceptHelperName(typeName);
163163
const unionValue = contentTypes
164164
.map((contentType) => `'${contentType}'`)
165165
.join(' | ');
@@ -200,9 +200,7 @@ export const buildAcceptHelpers = (
200200
);
201201
if (contentTypes.length <= 1) return [];
202202

203-
return [
204-
buildAcceptHelper(verbOption.operationName, contentTypes, output),
205-
];
203+
return [buildAcceptHelper(verbOption.typeName, contentTypes, output)];
206204
})
207205
.join('\n\n');
208206

@@ -306,6 +304,7 @@ export const generateHttpClientImplementation = (
306304
headers,
307305
queryParams,
308306
operationName,
307+
typeName,
309308
response,
310309
mutator,
311310
body,
@@ -404,7 +403,7 @@ export const generateHttpClientImplementation = (
404403
returnTypesRegistry.set(
405404
operationName,
406405
`export type ${pascal(
407-
operationName,
406+
typeName,
408407
)}ClientResult = NonNullable<${resultAliasType}>`,
409408
);
410409

@@ -476,7 +475,7 @@ export const generateHttpClientImplementation = (
476475
const uniqueContentTypes = getUniqueContentTypes(successTypes);
477476
const hasMultipleContentTypes = uniqueContentTypes.length > 1;
478477
const acceptTypeName = hasMultipleContentTypes
479-
? getAcceptHelperName(operationName)
478+
? getAcceptHelperName(typeName)
480479
: undefined;
481480

482481
const needsObserveBranching = isRequestOptions && !hasMultipleContentTypes;

0 commit comments

Comments
 (0)