···11import type { Context } from '~/ir/context';
22import { createOperationKey } from '~/ir/operation';
33import { sanitizeNamespaceIdentifier } from '~/openApi/common/parser/sanitize';
44-import { toCase } from '~/utils/to-case';
44+import { toCase } from '~/utils/naming';
5566import type { State } from '../types/state';
77
···11import type { DefinePlugin, Plugin } from '~/plugins';
22-import type { StringName } from '~/types/case';
22+import type { NameTransformer } from '~/utils/naming';
3344export type UserConfig = Plugin.Name<'@angular/common'> &
55 Plugin.Hooks & {
···2828 * Builds the class name for the generated resource.
2929 * By default, the class name is suffixed with "Resources".
3030 */
3131- classNameBuilder?: StringName;
3131+ classNameBuilder?: NameTransformer;
3232 /**
3333 * Whether or not to create HTTP Request instances.
3434 *
···5959 * Builds the class name for the generated resource.
6060 * By default, the class name is suffixed with "Resources".
6161 */
6262- classNameBuilder?: StringName;
6262+ classNameBuilder?: NameTransformer;
6363 /**
6464 * Whether or not to create HTTP resource APIs.
6565 *
···9898 * Builds the class name for the generated resource.
9999 * By default, the class name is suffixed with "Resources".
100100 */
101101- classNameBuilder: StringName;
101101+ classNameBuilder: NameTransformer;
102102 /**
103103 * Whether or not to create HTTP Request instances.
104104 *
···125125 * Builds the class name for the generated resource.
126126 * By default, the class name is suffixed with "Resources".
127127 */
128128- classNameBuilder: StringName;
128128+ classNameBuilder: NameTransformer;
129129 /**
130130 * Whether or not to create HTTP resource APIs.
131131 *
···11import type { IR } from '~/ir/types';
22import type { PluginInstance } from '~/plugins/shared/utils/instance';
33+import { toCase } from '~/utils/naming';
34import { refToName } from '~/utils/ref';
44-import { toCase } from '~/utils/to-case';
5566import type { Field } from '../../client-core/bundle/params';
77
···11-import type { IR } from '~/ir/types';
21import type { DefinePlugin, Plugin } from '~/plugins';
32import type { PluginClientNames, PluginValidatorNames } from '~/plugins/types';
44-import type { StringName } from '~/types/case';
33+import type { NameTransformer } from '~/utils/naming';
44+55+import type {
66+ StructureConfig,
77+ StructureStrategy,
88+ UserStructureConfig,
99+} from './structure';
510611export type UserConfig = Plugin.Name<'@hey-api/sdk'> &
712 Plugin.Hooks & {
813 /**
99- * Group operation methods into classes? When enabled, you can select which
1010- * classes to export with `sdk.include` and/or transform their names with
1111- * `sdk.classNameBuilder`.
1212- *
1313- * Note that by enabling this option, your SDKs will **NOT**
1414- * support {@link https://developer.mozilla.org/docs/Glossary/Tree_shaking tree-shaking}.
1515- * For this reason, it is disabled by default.
1616- *
1717- * @default false
1818- */
1919- asClass?: boolean;
2020- /**
2114 * Should the generated functions contain auth mechanisms? You may want to
2215 * disable this option if you're handling auth yourself or defining it
2316 * globally on the client and want to reduce the size of generated code.
···2619 */
2720 auth?: boolean;
2821 /**
2929- * Customize the generated class names. The name variable is obtained from
3030- * your OpenAPI specification tags or `instance` value.
3131- *
3232- * This option has no effect if `sdk.asClass` is `false`.
3333- */
3434- classNameBuilder?: StringName;
3535- /**
3636- * How should we structure your SDK? By default, we try to infer the ideal
3737- * structure using `operationId` keywords. If you prefer a flatter structure,
3838- * you can set `classStructure` to `off` to disable this behavior.
3939- *
4040- * @default 'auto'
4141- */
4242- classStructure?: 'auto' | 'off';
4343- /**
4422 * Use an internal client instance to send HTTP requests? This is useful if
4523 * you don't want to manually pass the client to each SDK function.
4624 *
···6038 */
6139 exportFromIndex?: boolean;
6240 /**
6363- * Include only service classes with names matching regular expression
6464- *
6565- * This option has no effect if `sdk.asClass` is `false`.
6666- */
6767- include?: string;
6868- /**
6969- * Set `instance` to create an instantiable SDK. Using `true` will use the
7070- * default instance name; in practice, you want to define your own by passing
7171- * a string value.
7272- *
7373- * @default false
7474- */
7575- instance?: string | boolean;
7676- /**
7777- * Customise the name of methods within the service. By default,
7878- * `operation.id` is used.
7979- */
8080- methodNameBuilder?:
8181- | string
8282- | ((name: string, operation: IR.OperationObject) => string);
8383- // TODO: parser - rename operationId option to something like inferId?: boolean
8484- /**
8585- * Use operation ID to generate operation names?
8686- *
8787- * @default true
8888- */
8989- operationId?: boolean;
9090- /**
9141 * Define how request parameters are structured in generated SDK methods.
9242 *
9343 * - `'flat'` merges parameters into a single object.
···10757 */
10858 responseStyle?: 'data' | 'fields';
10959 /**
6060+ * Define how generated outputs are structurally organized.
6161+ */
6262+ structure?: {
6363+ /**
6464+ * Define the structure of generated SDK operations.
6565+ *
6666+ * String shorthand:
6767+ * - `'flat'` – standalone functions, no container
6868+ * - `'byTags'` – one container per operation tag
6969+ * - `'single'` – all operations in a single container
7070+ * - custom function for full control
7171+ *
7272+ * Use the object form for advanced configuration.
7373+ *
7474+ * @default 'flat'
7575+ */
7676+ operations?: StructureStrategy | UserStructureConfig;
7777+ };
7878+ /**
11079 * Transform response data before returning. This is useful if you want to
11180 * convert for example ISO strings into Date objects. However, transformation
11281 * adds runtime overhead, so it's not recommended to use unless necessary.
···163132 // DEPRECATED OPTIONS BELOW
164133165134 /**
166166- * **This feature works only with the legacy parser**
167167- *
168168- * Filter endpoints to be included in the generated SDK. The provided
169169- * string should be a regular expression where matched results will be
170170- * included in the output. The input pattern this string will be tested
171171- * against is `{method} {path}`. For example, you can match
172172- * `POST /api/v1/foo` with `^POST /api/v1/foo$`.
173173- *
174174- * @deprecated
175175- */
176176- // eslint-disable-next-line typescript-sort-keys/interface
177177- filter?: string;
178178- /**
179179- * Define shape of returned value from service calls
180180- *
181181- * @deprecated
182182- * @default 'body'
183183- */
184184- response?: 'body' | 'response';
185185- };
186186-187187-export type Config = Plugin.Name<'@hey-api/sdk'> &
188188- Plugin.Hooks & {
189189- /**
190135 * Group operation methods into classes? When enabled, you can select which
191136 * classes to export with `sdk.include` and/or transform their names with
192137 * `sdk.classNameBuilder`.
···195140 * support {@link https://developer.mozilla.org/docs/Glossary/Tree_shaking tree-shaking}.
196141 * For this reason, it is disabled by default.
197142 *
143143+ * @deprecated Use `grouping: 'byTags'` or `grouping: 'single'` instead.
198144 * @default false
199145 */
200200- asClass: boolean;
201201- /**
202202- * Should the generated functions contain auth mechanisms? You may want to
203203- * disable this option if you're handling auth yourself or defining it
204204- * globally on the client and want to reduce the size of generated code.
205205- *
206206- * @default true
207207- */
208208- auth: boolean;
146146+ // eslint-disable-next-line typescript-sort-keys/interface
147147+ asClass?: boolean;
209148 /**
210149 * Customize the generated class names. The name variable is obtained from
211150 * your OpenAPI specification tags or `instance` value.
212151 *
213152 * This option has no effect if `sdk.asClass` is `false`.
153153+ *
154154+ * @deprecated Use `grouping.containerName` instead.
214155 */
215215- classNameBuilder: StringName;
156156+ classNameBuilder?: NameTransformer;
216157 /**
217158 * How should we structure your SDK? By default, we try to infer the ideal
218159 * structure using `operationId` keywords. If you prefer a flatter structure,
219160 * you can set `classStructure` to `off` to disable this behavior.
220161 *
162162+ * @deprecated Use `grouping: { nesting: 'operationId' }` or `grouping: { nesting: 'id' }` instead.
221163 * @default 'auto'
222164 */
223223- classStructure: 'auto' | 'off';
165165+ classStructure?: 'auto' | 'off';
224166 /**
225225- * Use an internal client instance to send HTTP requests? This is useful if
226226- * you don't want to manually pass the client to each SDK function.
167167+ * Set `instance` to create an instantiable SDK. Using `true` will use the
168168+ * default instance name; in practice, you want to define your own by passing
169169+ * a string value.
227170 *
228228- * You can customize the selected client output through its plugin. You can
229229- * also set `client` to `true` to automatically choose the client from your
230230- * defined plugins. If we can't detect a client plugin when using `true`, we
231231- * will default to `@hey-api/client-fetch`.
171171+ * @deprecated Use `grouping: { strategy: 'single', as: 'Name', methods: 'instance' }` instead.
172172+ * @default false
173173+ */
174174+ instance?: string | boolean;
175175+ /**
176176+ * Customise the name of methods within the service. By default,
177177+ * `operation.id` is used.
232178 *
233233- * @default true
179179+ * @deprecated Use `grouping.methodName` instead.
234180 */
235235- client: PluginClientNames | false;
181181+ methodNameBuilder?: NameTransformer;
236182 /**
237237- * Should the exports from the generated files be re-exported in the index
238238- * barrel file?
183183+ * Use operation ID to generate operation names?
239184 *
185185+ * @deprecated Use `grouping.nesting: 'operationId'` or `grouping.nesting: 'id'` instead.
240186 * @default true
241187 */
242242- exportFromIndex: boolean;
188188+ operationId?: boolean;
243189 /**
244244- * Include only service classes with names matching regular expression
190190+ * Define shape of returned value from service calls
245191 *
246246- * This option has no effect if `sdk.asClass` is `false`.
192192+ * @deprecated
193193+ * @default 'body'
247194 */
248248- include: string | undefined;
195195+ response?: 'body' | 'response';
196196+ };
197197+198198+export type Config = Plugin.Name<'@hey-api/sdk'> &
199199+ Plugin.Hooks & {
249200 /**
250250- * Set `instance` to create an instantiable SDK. Using `true` will use the
251251- * default instance name; in practice, you want to define your own by passing
252252- * a string value.
201201+ * Should the generated functions contain auth mechanisms? You may want to
202202+ * disable this option if you're handling auth yourself or defining it
203203+ * globally on the client and want to reduce the size of generated code.
204204+ *
205205+ * @default true
253206 */
254254- instance: string;
207207+ auth: boolean;
255208 /**
256256- * Customise the name of methods within the service. By default,
257257- * `operation.id` is used.
209209+ * Use an internal client instance to send HTTP requests? This is useful if
210210+ * you don't want to manually pass the client to each SDK function.
211211+ *
212212+ * You can customize the selected client output through its plugin. You can
213213+ * also set `client` to `true` to automatically choose the client from your
214214+ * defined plugins. If we can't detect a client plugin when using `true`, we
215215+ * will default to `@hey-api/client-fetch`.
216216+ *
217217+ * @default true
258218 */
259259- methodNameBuilder:
260260- | string
261261- | ((name: string, operation: IR.OperationObject) => string);
262262- // TODO: parser - rename operationId option to something like inferId?: boolean
219219+ client: PluginClientNames | false;
263220 /**
264264- * Use operation ID to generate operation names?
221221+ * Should the exports from the generated files be re-exported in the index
222222+ * barrel file?
265223 *
266224 * @default true
267225 */
268268- operationId: boolean;
226226+ exportFromIndex: boolean;
269227 /**
270228 * Define how request parameters are structured in generated SDK methods.
271229 *
···286244 */
287245 responseStyle: 'data' | 'fields';
288246 /**
247247+ * Define how generated outputs are structurally organized.
248248+ */
249249+ structure: {
250250+ /**
251251+ * Define the structure of generated SDK operations.
252252+ */
253253+ operations: StructureConfig;
254254+ };
255255+ /**
289256 * Transform response data before returning. This is useful if you want to
290257 * convert for example ISO strings into Date objects. However, transformation
291258 * adds runtime overhead, so it's not recommended to use unless necessary.
···321288 // DEPRECATED OPTIONS BELOW
322289323290 /**
324324- * **This feature works only with the legacy parser**
291291+ * Group operation methods into classes? When enabled, you can select which
292292+ * classes to export with `sdk.include` and/or transform their names with
293293+ * `sdk.classNameBuilder`.
325294 *
326326- * Filter endpoints to be included in the generated SDK. The provided
327327- * string should be a regular expression where matched results will be
328328- * included in the output. The input pattern this string will be tested
329329- * against is `{method} {path}`. For example, you can match
330330- * `POST /api/v1/foo` with `^POST /api/v1/foo$`.
295295+ * Note that by enabling this option, your SDKs will **NOT**
296296+ * support {@link https://developer.mozilla.org/docs/Glossary/Tree_shaking tree-shaking}.
297297+ * For this reason, it is disabled by default.
331298 *
332332- * @deprecated
299299+ * @deprecated Use `grouping: 'byTags'` or `grouping: 'single'` instead.
300300+ * @default false
333301 */
334302 // eslint-disable-next-line typescript-sort-keys/interface
335335- filter?: string;
303303+ asClass: boolean;
304304+ /**
305305+ * Customize the generated class names. The name variable is obtained from
306306+ * your OpenAPI specification tags or `instance` value.
307307+ *
308308+ * This option has no effect if `sdk.asClass` is `false`.
309309+ *
310310+ * @deprecated Use `grouping.containerName` instead.
311311+ */
312312+ classNameBuilder: NameTransformer;
313313+ /**
314314+ * How should we structure your SDK? By default, we try to infer the ideal
315315+ * structure using `operationId` keywords. If you prefer a flatter structure,
316316+ * you can set `classStructure` to `off` to disable this behavior.
317317+ *
318318+ * @deprecated Use `grouping: { nesting: 'operationId' }` or `grouping: { nesting: 'id' }` instead.
319319+ * @default 'auto'
320320+ */
321321+ classStructure: 'auto' | 'off';
322322+ /**
323323+ * Set `instance` to create an instantiable SDK. Using `true` will use the
324324+ * default instance name; in practice, you want to define your own by passing
325325+ * a string value.
326326+ *
327327+ * @deprecated Use `grouping: { strategy: 'single', as: 'Name', methods: 'instance' }` instead.
328328+ */
329329+ instance: string;
330330+ /**
331331+ * Customise the name of methods within the service. By default,
332332+ * `operation.id` is used.
333333+ *
334334+ * @deprecated Use `grouping.methodName` instead.
335335+ */
336336+ methodNameBuilder: NameTransformer;
337337+ /**
338338+ * Use operation ID to generate operation names?
339339+ *
340340+ * @deprecated Use `grouping.nesting: 'operationId'` or `grouping.nesting: 'id'` instead.
341341+ * @default true
342342+ */
343343+ operationId: boolean;
336344 /**
337345 * Define shape of returned value from service calls
338346 *
···11import type { NameConflictResolver } from '@hey-api/codegen-core';
22import type ts from 'typescript';
3344-import type { StringCase, StringName } from './case';
44+import type { Casing, NameTransformer } from '~/utils/naming';
5566export type Formatters = 'biome' | 'prettier';
77···1616 *
1717 * @default undefined
1818 */
1919- case?: StringCase;
1919+ case?: Casing;
2020 /**
2121 * Clean the `output` folder on every run? If disabled, this folder may
2222 * be used to store additional files. The default option is `true` to
···3434 * @default '{{name}}'
3535 */
3636 fileName?:
3737- | StringName
3737+ | NameTransformer
3838 | {
3939 /**
4040 * The casing convention to use for generated file names.
4141 *
4242 * @default 'preserve'
4343 */
4444- case?: StringCase;
4444+ case?: Casing;
4545 /**
4646 * Custom naming pattern for generated file names.
4747 *
4848 * @default '{{name}}'
4949 */
5050- name?: StringName;
5050+ name?: NameTransformer;
5151 /**
5252 * Suffix to append to file names (before the extension). For example,
5353 * with a suffix of `.gen`, `example.ts` becomes `example.gen.ts`.
···128128 * Defines casing of the output fields. By default, we preserve `input`
129129 * values as data transforms incur a performance penalty at runtime.
130130 */
131131- case: StringCase | undefined;
131131+ case: Casing | undefined;
132132 /**
133133 * Clean the `output` folder on every run? If disabled, this folder may
134134 * be used to store additional files. The default option is `true` to
···146146 /**
147147 * The casing convention to use for generated file names.
148148 */
149149- case: StringCase;
149149+ case: Casing;
150150 /**
151151 * Custom naming pattern for generated file names.
152152 */
153153- name: StringName;
153153+ name: NameTransformer;
154154 /**
155155 * Suffix to append to file names (before the extension). For example,
156156 * with a suffix of `.gen`, `example.ts` becomes `example.gen.ts`.
+15-16
packages/openapi-ts/src/types/parser.d.ts
···77 OpenApiSchemaObject,
88} from '~/openApi/types';
99import type { Hooks } from '~/parser/types/hooks';
1010-1111-import type { StringCase, StringName } from './case';
1010+import type { Casing, NameTransformer } from '~/utils/naming';
12111312type EnumsMode = 'inline' | 'root';
1413···7372 *
7473 * @default 'PascalCase'
7574 */
7676- case?: StringCase;
7575+ case?: Casing;
7776 /**
7877 * Whether to transform all enums.
7978 *
···9291 *
9392 * @default '{{name}}Enum'
9493 */
9595- name?: StringName;
9494+ name?: NameTransformer;
9695 };
9796 /**
9897 * By default, any object schema with a missing `required` keyword is
···137136 * @default '{{name}}Writable'
138137 */
139138 requests?:
140140- | StringName
139139+ | NameTransformer
141140 | {
142141 /**
143142 * The casing convention to use for generated names.
144143 *
145144 * @default 'preserve'
146145 */
147147- case?: StringCase;
146146+ case?: Casing;
148147 /**
149148 * Customize the generated name of schemas used in requests or
150149 * containing write-only fields.
151150 *
152151 * @default '{{name}}Writable'
153152 */
154154- name?: StringName;
153153+ name?: NameTransformer;
155154 };
156155 /**
157156 * Configuration for generated response-specific schemas.
···163162 * @default '{{name}}'
164163 */
165164 responses?:
166166- | StringName
165165+ | NameTransformer
167166 | {
168167 /**
169168 * The casing convention to use for generated names.
170169 *
171170 * @default 'preserve'
172171 */
173173- case?: StringCase;
172172+ case?: Casing;
174173 /**
175174 * Customize the generated name of schemas used in responses or
176175 * containing read-only fields. We default to the original name
···178177 *
179178 * @default '{{name}}'
180179 */
181181- name?: StringName;
180180+ name?: NameTransformer;
182181 };
183182 };
184183 };
···250249 *
251250 * @default 'PascalCase'
252251 */
253253- case: StringCase;
252252+ case: Casing;
254253 /**
255254 * Whether to transform all enums.
256255 *
···269268 *
270269 * @default '{{name}}Enum'
271270 */
272272- name: StringName;
271271+ name: NameTransformer;
273272 };
274273 /**
275274 * By default, any object schema with a missing `required` keyword is
···309308 *
310309 * @default 'preserve'
311310 */
312312- case: StringCase;
311311+ case: Casing;
313312 /**
314313 * Customize the generated name of schemas used in requests or
315314 * containing write-only fields.
316315 *
317316 * @default '{{name}}Writable'
318317 */
319319- name: StringName;
318318+ name: NameTransformer;
320319 };
321320 /**
322321 * Configuration for generated response-specific schemas.
···327326 *
328327 * @default 'preserve'
329328 */
330330- case: StringCase;
329329+ case: Casing;
331330 /**
332331 * Customize the generated name of schemas used in responses or
333332 * containing read-only fields. We default to the original name
···335334 *
336335 * @default '{{name}}'
337336 */
338338- name: StringName;
337337+ name: NameTransformer;
339338 };
340339 };
341340 };
···11import { describe, expect, it } from 'vitest';
2233-import type { StringCase } from '~/types/case';
33+import { toCase } from '../naming';
44+import type { Casing } from '../types';
4555-import { toCase } from '../to-case';
66-77-const cases: ReadonlyArray<StringCase> = [
66+const cases: ReadonlyArray<Casing> = [
87 'camelCase',
98 'PascalCase',
109 'SCREAMING_SNAKE_CASE',
+3-4
packages/openapi-ts/src/utils/exports.ts
···11-import type { StringCase } from '~/types/case';
22-33-import { toCase } from './to-case';
11+import type { Casing } from './naming';
22+import { toCase } from './naming';
4354/**
65 * Utilities shared across the package.
···1413 stripLeadingSeparators,
1514 value,
1615 }: {
1717- readonly case: StringCase | undefined;
1616+ readonly case: Casing | undefined;
1817 /**
1918 * If leading separators have a semantic meaning, we might not want to
2019 * remove them.
+7
packages/openapi-ts/src/utils/naming/index.ts
···11+export { applyNaming, resolveNaming, toCase } from './naming';
22+export type {
33+ Casing,
44+ NameTransformer,
55+ NamingConfig,
66+ NamingRule,
77+} from './types';
+43
packages/openapi-ts/src/utils/naming/types.d.ts
···11+/**
22+ * Available casing strategies.
33+ */
44+export type Casing =
55+ | 'camelCase'
66+ | 'PascalCase'
77+ | 'preserve'
88+ | 'snake_case'
99+ | 'SCREAMING_SNAKE_CASE';
1010+1111+/**
1212+ * Name transformer: template string or function.
1313+ *
1414+ * Template supports `{{name}}` variable.
1515+ */
1616+export type NameTransformer = string | ((name: string) => string);
1717+1818+/**
1919+ * Full naming configuration.
2020+ */
2121+export interface NamingConfig {
2222+ /**
2323+ * Casing strategy applied after transformation.
2424+ *
2525+ * @deprecated Use `casing` instead.
2626+ */
2727+ case?: Casing;
2828+ /**
2929+ * Casing strategy applied after transformation.
3030+ */
3131+ casing?: Casing;
3232+ /**
3333+ * Name template or transformer function.
3434+ *
3535+ * Applied before `casing` transformation.
3636+ */
3737+ name?: NameTransformer;
3838+}
3939+4040+/**
4141+ * Name customization: shorthand or full configuration.
4242+ */
4343+export type NamingRule = NameTransformer | NamingConfig;