Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/content/docs/reference/configuration/output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,37 @@ export default defineConfig({
});
```

### splitByContentType

**Type:** `Boolean` **Default:** `false`

When an endpoint's `requestBody` supports multiple content types (e.g. `application/json` and `multipart/form-data`), generate a separate function for each content type instead of combining them into a single function with a union type parameter.

Each generated function is suffixed with the content type name (e.g. `WithJson`, `WithFormData`).

```ts
// Default (false) — single function with union body
updateProfile(body: FormDataType | JsonType) => { ... }

// With splitByContentType: true — separate function per content type
updateProfileWithFormData(body: FormDataType) => { ... }
updateProfileWithJson(body: JsonType) => { ... }
```

```ts title="orval.config.ts"
export default defineConfig({
petstore: {
output: {
override: {
splitByContentType: true,
},
},
},
});
```

> If the endpoint only has a single content type, no suffix is added and the behavior is the same as the default.

### formData

**Type:** `Boolean | String | Object`
Expand Down
1 change: 1 addition & 0 deletions packages/angular/src/http-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const createOutput = (
runtimeValidation: false,
},
enumGenerationType: 'const',
splitByContentType: false,
aliasCombinedTypes: false,
suppressReadonlyModifier: false,
},
Expand Down
1 change: 1 addition & 0 deletions packages/angular/src/http-resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const createOutput = (
runtimeValidation: false,
},
enumGenerationType: 'const',
splitByContentType: false,
aliasCombinedTypes: false,
suppressReadonlyModifier: false,
},
Expand Down
217 changes: 155 additions & 62 deletions packages/core/src/generators/verbs-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
getBodiesByContentType,
getBody,
getOperationId,
getParameters,
Expand All @@ -11,6 +12,7 @@ import type {
ContextSpec,
GeneratorVerbOptions,
GeneratorVerbsOptions,
GetterBody,
NormalizedInputOptions,
NormalizedMutator,
NormalizedOperationOptions,
Expand Down Expand Up @@ -46,79 +48,48 @@ export interface GenerateVerbOptionsParams {
context: ContextSpec;
}

export async function generateVerbOptions({
async function buildVerbOption({
verb,
output,
operation,
route,
pathRoute,
verbParameters = [],
context,
}: GenerateVerbOptionsParams): Promise<GeneratorVerbOptions> {
const {
responses,
requestBody,
parameters: operationParameters,
tags: rawTags,
deprecated: rawDeprecated,
description: rawDescription,
summary: rawSummary,
} = operation;

// Bridge assertions: OpenApiOperationObject has AnyOtherAttribute index signature
// which makes all destructured properties `any`. Assert to their declared types.
const tags = (rawTags ?? []) as string[];
const deprecated = rawDeprecated as boolean | undefined;
const description = rawDescription as string | undefined;
const summary = rawSummary as string | undefined;
const operationId = getOperationId(operation, route, verb);
const overrideOperation = output.override.operations[operationId];
let overrideTag: NormalizedOperationOptions = {};
for (const [tag, options] of Object.entries(output.override.tags)) {
if (tags.includes(tag) && options) {
overrideTag = mergeDeep(overrideTag, options);
}
}

const override = mergeDeep(
mergeDeep(output.override, overrideTag),
overrideOperation ?? {},
) as NormalizedOverrideOutput;

const overrideOperationName =
overrideOperation?.operationName ?? output.override.operationName;
const operationName = overrideOperationName
? overrideOperationName(operation, route, verb)
: sanitize(camel(operationId), { es5keyword: true });

body,
operationName,
operationId,
override,
tags,
deprecated,
description,
summary,
}: {
verb: Verbs;
output: NormalizedOutputOptions;
operation: OpenApiOperationObject;
route: string;
pathRoute: string;
verbParameters: OpenApiPathItemObject['parameters'];
context: ContextSpec;
body: GetterBody;
operationName: string;
operationId: string;
override: NormalizedOverrideOutput;
tags: string[];
deprecated: boolean | undefined;
description: string | undefined;
summary: string | undefined;
}): Promise<GeneratorVerbOptions> {
const response = getResponse({
responses: responses ?? {},
responses: operation.responses ?? {},
operationName,
context,
contentType: override.contentType,
});

const body = requestBody
? getBody({
requestBody,
operationName,
context,
contentType: override.contentType,
})
: {
originalSchema: {} as OpenApiRequestBodyObject,
definition: '',
implementation: '',
imports: [],
schemas: [],
formData: '',
formUrlEncoded: '',
contentType: '',
isOptional: false,
};

const parameters = getParameters({
parameters: [...verbParameters, ...(operationParameters ?? [])],
parameters: [...verbParameters, ...(operation.parameters ?? [])],
context,
});

Expand All @@ -140,7 +111,7 @@ export async function generateVerbOptions({
const params = getParams({
route,
pathParams: parameters.path,
operationId: operationId,
operationId,
context,
output,
});
Expand Down Expand Up @@ -208,7 +179,7 @@ export async function generateVerbOptions({
const doc = jsDoc({ description, deprecated, summary });

const verbOption: GeneratorVerbOptions = {
verb: verb,
verb,
tags,
route,
pathRoute,
Expand Down Expand Up @@ -240,6 +211,128 @@ export async function generateVerbOptions({
return transformer ? transformer(verbOption) : verbOption;
}

export async function generateVerbOptions({
verb,
output,
operation,
route,
pathRoute,
verbParameters = [],
context,
}: GenerateVerbOptionsParams): Promise<GeneratorVerbOptions[]> {
const {
requestBody,
tags: rawTags,
deprecated: rawDeprecated,
description: rawDescription,
summary: rawSummary,
} = operation;

// Bridge assertions: OpenApiOperationObject has AnyOtherAttribute index signature
// which makes all destructured properties `any`. Assert to their declared types.
const tags = (rawTags ?? []) as string[];
const deprecated = rawDeprecated as boolean | undefined;
const description = rawDescription as string | undefined;
const summary = rawSummary as string | undefined;
const operationId = getOperationId(operation, route, verb);
const overrideOperation = output.override.operations[operationId];
let overrideTag: NormalizedOperationOptions = {};
for (const [tag, options] of Object.entries(output.override.tags)) {
if (tags.includes(tag) && options) {
overrideTag = mergeDeep(overrideTag, options);
}
}

const override = mergeDeep(
mergeDeep(output.override, overrideTag),
overrideOperation ?? {},
) as NormalizedOverrideOutput;

const overrideOperationName =
overrideOperation?.operationName ?? output.override.operationName;
const operationName = overrideOperationName
? overrideOperationName(operation, route, verb)
: sanitize(camel(operationId), { es5keyword: true });

const splitByContentType = override.splitByContentType;

if (splitByContentType && requestBody) {
const bodies = getBodiesByContentType({
requestBody,
operationName,
context,
contentType: override.contentType,
});

const results: GeneratorVerbOptions[] = [];
for (const bodyEntry of bodies) {
const { contentTypeSuffix, ...body } = bodyEntry;
const suffixedName = contentTypeSuffix
? `${operationName}With${contentTypeSuffix}`
: operationName;

const verbOption = await buildVerbOption({
verb,
output,
operation,
route,
pathRoute,
verbParameters,
context,
body,
operationName: suffixedName,
operationId,
override,
tags,
deprecated,
description,
summary,
});
results.push(verbOption);
}
return results;
}

const body = requestBody
? getBody({
requestBody,
operationName,
context,
contentType: override.contentType,
})
: {
originalSchema: {} as OpenApiRequestBodyObject,
definition: '',
implementation: '',
imports: [],
schemas: [],
formData: '',
formUrlEncoded: '',
contentType: '',
isOptional: false,
};

const verbOption = await buildVerbOption({
verb,
output,
operation,
route,
pathRoute,
verbParameters,
context,
body,
operationName,
operationId,
override,
tags,
deprecated,
description,
summary,
});

return [verbOption];
}

export interface GenerateVerbsOptionsParams {
verbs: OpenApiPathItemObject;
input: NormalizedInputOptions;
Expand Down Expand Up @@ -271,7 +364,7 @@ export function generateVerbsOptions({
context,
});

acc.push(verbOptions);
acc.push(...verbOptions);
}

return acc;
Expand Down
Loading