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
10 changes: 10 additions & 0 deletions bin/configs/typescript-fetch-split-by-content-type.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
generatorName: typescript-fetch
outputDir: samples/client/petstore/typescript-fetch/builds/split-by-content-type
inputSpec: modules/openapi-generator/src/test/resources/3_0/issue6708-split-by-content-type-sample.yaml
templateDir: modules/openapi-generator/src/main/resources/typescript-fetch
globalProperties:
splitOperationsByContentType: "true"
additionalProperties:
npmVersion: 1.0.0
npmName: '@openapitools/typescript-fetch-split-by-content-type'
snapshot: false
56 changes: 56 additions & 0 deletions docs/global-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,62 @@ title: Global Properties
| modelDocs | Allows the user to define if model docs will be generated. Prefer using the more robust `.openapi-generator-ignore`. | `true` or `false` |
| apiTests | Allows the user to define if api tests will be generated. Prefer using the more robust `.openapi-generator-ignore`. | `true` or `false` |
| modelTests | Allows the user to define if model tests will be generated. Prefer using the more robust `.openapi-generator-ignore`. | `true` or `false` |
| splitOperationsByContentType | Generates one operation per request/response content-type when an operation exposes several with different schemas | `true` or `false` |


## Note on splitOperationsByContentType

An operation may declare several request or response content-types backed by *different* schemas. Only the
first one is normally kept, which leaves the others unreachable. With `splitOperationsByContentType=true`
such an operation is generated once per content-type instead — the cartesian product of the request and
response axes, deduplicated by schema — each with a typed, collision-free operation id built from the base
one: `With<Subtype>` for the request axis, `As<Subtype>` for the response axis, as in
`createReportWithMergePatchAsPdf`.

The content-type declared first on each axis is the default one, consistently with the rest of the
generator. The option is opt-in and off by default, because it changes the shape of the generated API.

Each generated operation carries `x-content-type-variant-*` extensions recording the group it was split
from, the content-type it was narrowed to on each axis and the rank of that content-type in its axis. A
generator whose language can express the whole matrix in a single construct uses them to merge the variants
back together while keeping each one's natively resolved types. `typescript-fetch` does exactly that: it
emits one method whose request type is a union discriminated by `contentType` and whose return type is
selected by overloads on `accept`.

```ts
export type CreateReportRequest = runtime.ExclusiveUnion<
| { contentType?: 'application/json'; report?: Report; }
| { contentType: 'application/merge-patch+json'; reportPatch?: ReportPatch; }
>;

async createReport(requestParameters: CreateReportRequest & { accept?: 'application/json' }, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Receipt>;
async createReport(requestParameters: CreateReportRequest & { accept: 'application/pdf' }, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Blob>;
```

`ExclusiveUnion` makes the members mutually exclusive, by declaring on each of them the keys it does not
have as `never`. Without it nothing stops a caller from handing a patch body to the JSON member and having it
sent under the wrong content-type: excess property checking, which would normally reject the surplus property, treats a
key present in *any* member of a union as known, so it never fires here — for an object literal no more than
for a variable. What rejects most shapes is unrelated: weak type detection when every property of a member
is optional, a missing required property otherwise. A member with a required parameter and an optional body
has neither. The helper is emitted into `runtime.ts` only when this option is on.

A form or multipart content-type is merged like any other: its parameters stay individual rather than
gathered in a single body, so the union member carries them as they are and the body is assembled inside
that content-type's branch of the switch. `Content-Type` is set in each branch rather than once up front,
because a multipart body must not set it at all — `fetch` adds it with the boundary it generates.

The option decides *which* content-types get their own operation; it does not change how a body is
serialised. Each variant is handed to the generator's existing encoders, so a media type the generator has
no encoder for is still sent the way it always was — `typescript-fetch`, for one, has no XML serialiser, and
an `application/xml` body backed by an object schema is JSON-encoded under an XML `Content-Type` exactly as
it is without this option. Splitting makes such a content-type reachable; teaching the generator to encode
it is a separate matter.

One case is left split rather than merged, with a warning: every operation when `useSingleRequestParameter`
is off, since the parameters are then spread over the signature and there is no request object to carry the
discriminant. The separate, individually typed methods the split produced are then generated as they are,
which is what a statically-typed generator emits anyway.


## Note on Global Property declaration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.openapitools.codegen.model.WebhooksMap;

import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -130,6 +131,24 @@ public interface CodegenConfig {

CodegenOperation fromOperation(String resourcePath, String httpMethod, Operation operation, List<Server> servers);

/**
* Divides an operation into one operation per content-type when it exposes several request/response
* content-types with different schemas (opt-in, see {@code splitOperationsByContentType}). Each
* returned operation is self-contained and re-enters {@link #fromOperation}. When the option is off or
* no division applies, the operation is returned unchanged (as a singleton). {@code DefaultCodegen}
* implements the division; the default here keeps the operation whole so that an implementation not
* deriving from {@code DefaultCodegen} keeps compiling and simply opts out of the feature.
*
* @param openAPI the OpenAPI document
* @param path the resource path
* @param httpMethod the HTTP method
* @param operation the operation to (maybe) divide
* @return the operations to generate for {@code operation} (the operation itself when not divided)
*/
default List<Operation> divideOperationsByContentType(OpenAPI openAPI, String path, String httpMethod, Operation operation) {
return Collections.singletonList(operation);
}

List<CodegenSecurity> fromSecurity(Map<String, SecurityScheme> schemas);

List<CodegenServer> fromServers(List<Server> servers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,24 @@ public class CodegenConstants {
public static final String PREPEND_FORM_OR_BODY_PARAMETERS = "prependFormOrBodyParameters";
public static final String PREPEND_FORM_OR_BODY_PARAMETERS_DESC = "Add form or body parameters to the beginning of the parameter list.";

public static final String SPLIT_OPERATIONS_BY_CONTENT_TYPE = "splitOperationsByContentType";

/**
* Extensions set on every operation produced by {@code splitOperationsByContentType}, describing where
* the variant sits in the content-type matrix so that a generator can merge the variants back into a
* single construct instead of emitting one method per combination.
* <p>
* The {@code *-index} ones carry the 0-based rank of the variant's media-type in its axis, in the order
* the spec declares them, so a consumer never has to rely on the order operations happen to reach it in:
* rank 0 is that axis's default content-type, and the variant ranked 0 on both axes is the one a caller
* gets without asking. An axis that was not split has no media-type and ranks 0.
*/
public static final String X_CONTENT_TYPE_VARIANT_GROUP = "x-content-type-variant-group";
public static final String X_CONTENT_TYPE_VARIANT_REQUEST = "x-content-type-variant-request";
public static final String X_CONTENT_TYPE_VARIANT_RESPONSE = "x-content-type-variant-response";
public static final String X_CONTENT_TYPE_VARIANT_REQUEST_INDEX = "x-content-type-variant-request-index";
public static final String X_CONTENT_TYPE_VARIANT_RESPONSE_INDEX = "x-content-type-variant-response-index";

public static final String USE_DATETIME_OFFSET = "useDateTimeOffset";
public static final String USE_DATETIME_OFFSET_DESC = "Use DateTimeOffset to model date-time properties";

Expand Down
Loading
Loading