From 37df3e73ac0c31eaa89e5a903b934eaa3ae7637a Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Wed, 17 Jun 2026 05:30:33 +0800 Subject: [PATCH] docs: add multipart binary usage examples - Document multipart UploadFile wrappers and raw binary body behavior - Add a reusable multipart and octet-stream OpenAPI example - Correct generator CLI examples to use repeatable generator flags --- README.md | 19 +-- docs/src/SUMMARY.md | 1 + docs/src/getting_started.md | 32 +++-- docs/src/multipart_and_binary.md | 187 +++++++++++++++++++++++++ examples/multipart-binary/README.md | 31 ++++ examples/multipart-binary/openapi.yaml | 111 +++++++++++++++ 6 files changed, 365 insertions(+), 16 deletions(-) create mode 100644 docs/src/multipart_and_binary.md create mode 100644 examples/multipart-binary/README.md create mode 100644 examples/multipart-binary/openapi.yaml diff --git a/README.md b/README.md index 8e7480a18..edc4a24bf 100644 --- a/README.md +++ b/README.md @@ -50,22 +50,23 @@ Requires Rust 1.90+. ```bash # TypeScript client -openapi-nexus generate -i spec.yaml -o output -g typescript-fetch +openapi-nexus generate -i spec.yaml -o output --generators typescript-fetch # Go client -openapi-nexus generate -i spec.yaml -o output -g go-http +openapi-nexus generate -i spec.yaml -o output --generators go-http # Rust client (reqwest) -openapi-nexus generate -i spec.yaml -o output -g rust-reqwest +openapi-nexus generate -i spec.yaml -o output --generators rust-reqwest # Python client (httpx) -openapi-nexus generate -i spec.yaml -o output -g python-httpx +openapi-nexus generate -i spec.yaml -o output --generators python-httpx # Java client -openapi-nexus generate -i spec.yaml -o output -g java-okhttp +openapi-nexus generate -i spec.yaml -o output --generators java-okhttp -# Multiple generators at once -openapi-nexus generate -i spec.yaml -o output -g typescript-fetch,go-http,rust-reqwest +# Generate another target into its own directory +openapi-nexus generate -i spec.yaml -o output/go --generators go-http +openapi-nexus generate -i spec.yaml -o output/rust --generators rust-reqwest ``` ## Configuration @@ -76,7 +77,7 @@ Configuration resolves in order: CLI args > environment variables (`OPENAPI_NEXU # Environment variables export OPENAPI_NEXUS_INPUT="spec.yaml" export OPENAPI_NEXUS_OUTPUT="generated" -export OPENAPI_NEXUS_GENERATOR="typescript-fetch" +export OPENAPI_NEXUS_GENERATORS="typescript-fetch" ``` Generator-specific options go in the config file: @@ -121,6 +122,8 @@ Parsing auto-detects OAS version (3.0, 3.1, 3.2). Lowering produces a version-ag Full documentation is available at the [project docs site](https://adamcavendish.github.io/openapi-nexus/). +Examples live under [`examples/`](examples/), including multipart file upload and raw binary body usage in [`examples/multipart-binary`](examples/multipart-binary/). + ## Development ```bash diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 6cbfab0b8..680fa71ff 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -3,6 +3,7 @@ - [Introduction](introduction.md) - [Getting Started](getting_started.md) - [Authentication](auth.md) +- [Multipart and Binary Bodies](multipart_and_binary.md) - [Rust Generator Configuration](rust_config.md) - [TypeScript Generator Configuration](ts_config.md) - [Architecture](architecture.md) diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md index dca7fd2ba..d31c2dd25 100644 --- a/docs/src/getting_started.md +++ b/docs/src/getting_started.md @@ -30,23 +30,39 @@ Generate a TypeScript fetch client: openapi-nexus generate \ --input path/to/openapi.yaml \ --output generated \ - --generator typescript-fetch + --generators typescript-fetch ``` -Generate clients for multiple languages at once: +Generate another target language into a separate directory: ```bash openapi-nexus generate \ --input spec.yaml \ - --output output \ - --generators typescript-fetch,go-http,rust-reqwest,python-httpx + --output output/go \ + --generators go-http + +openapi-nexus generate \ + --input spec.yaml \ + --output output/python \ + --generators python-httpx ``` -All nine generators: +All nine generators, each with its own output directory: ```bash -openapi-nexus generate -i spec.yaml -o output \ - -g typescript-fetch,go-http,rust-reqwest,rust-ureq,rust-aioduct,python-httpx,python-requests,java-okhttp,kotlin-okhttp +for generator in \ + typescript-fetch \ + go-http \ + rust-reqwest \ + rust-ureq \ + rust-aioduct \ + python-httpx \ + python-requests \ + java-okhttp \ + kotlin-okhttp +do + openapi-nexus generate -i spec.yaml -o "output/${generator}" -g "${generator}" +done ``` ## Configuration @@ -63,7 +79,7 @@ Configuration is resolved with the following precedence (highest to lowest): ```bash export OPENAPI_NEXUS_INPUT="spec.yaml" export OPENAPI_NEXUS_OUTPUT="generated" -export OPENAPI_NEXUS_GENERATOR="typescript-fetch" +export OPENAPI_NEXUS_GENERATORS="typescript-fetch" ``` ### Configuration File diff --git a/docs/src/multipart_and_binary.md b/docs/src/multipart_and_binary.md new file mode 100644 index 000000000..5efd1989a --- /dev/null +++ b/docs/src/multipart_and_binary.md @@ -0,0 +1,187 @@ +# Multipart and Binary Bodies + +OpenAPI uses the same `type: string`, `format: binary` schema shape in several places, but generated clients intentionally expose different APIs depending on the HTTP body. + +- Multipart binary parts use a generated upload wrapper so callers can provide a filename. +- Raw `application/octet-stream` request bodies stay as raw bytes or blob values. +- Binary response bodies stay as raw bytes or blob values. +- JSON and text multipart parts keep their normal generated model or scalar types. + +See [`examples/multipart-binary/openapi.yaml`](https://github.com/adamcavendish/openapi-nexus/blob/master/examples/multipart-binary/openapi.yaml) for a complete spec. + +## Multipart Uploads + +For object-shaped `multipart/form-data` request bodies, openapi-nexus generates an operation-specific request body model. Binary properties in that model use an upload wrapper instead of the normal raw binary type. + +Given this request body: + +```yaml +requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file, profile, purpose] + properties: + file: + type: string + format: binary + profile: + $ref: '#/components/schemas/ProfileAttributes' + purpose: + type: string + encoding: + file: + contentType: image/png + profile: + contentType: application/json + purpose: + contentType: text/plain +``` + +The generated request model has a binary `file` upload field, a normal JSON `profile` field, and a normal text `purpose` field. The multipart field name remains `file`; the filename is read from the upload wrapper. If the caller does not provide a filename, generated clients fall back to the field name. + +## Upload Filenames + +Use the wrapper when the filename matters, for example when the multipart field name is `file` but the uploaded filename should be `avatar.png`. + +### TypeScript + +TypeScript accepts browser-native `File` values directly. It also accepts `{ data: Blob, filename?: string }` for runtimes or tests that have `Blob` but not `File`. + +```ts +import { AvatarsApi } from './generated/apis/AvatarsApi'; + +const api = new AvatarsApi(); + +await api.uploadAvatar({ + body: { + file: { data: new Blob([bytes], { type: 'image/png' }), filename: 'avatar.png' }, + profile: { display_name: 'Ada Lovelace' }, + purpose: 'profile', + }, +}); + +await api.uploadAvatar({ + body: { + file: new File([bytes], 'avatar.png', { type: 'image/png' }), + profile: { display_name: 'Ada Lovelace' }, + purpose: 'profile', + }, +}); +``` + +Passing a plain `Blob` is still accepted, but the filename falls back to the multipart field name. + +### Go + +```go +body := &models.UploadAvatarMultipartRequestBody{ + File: runtime.NewUploadFile(pngBytes, "avatar.png"), + Profile: models.ProfileAttributes{ + DisplayName: "Ada Lovelace", + }, + Purpose: "profile", +} + +resp, err := avatars.UploadAvatar(ctx, body) +``` + +Use `runtime.NewUploadFileBytes(pngBytes)` when the fallback filename is acceptable. + +### Python + +The same `UploadFile` wrapper is generated for `python-httpx` and `python-requests`. + +```python +from generated.models.upload_avatar_multipart_request_body import UploadAvatarMultipartRequestBody +from generated.models.profile_attributes import ProfileAttributes +from generated.runtime import UploadFile + +body = UploadAvatarMultipartRequestBody( + file=UploadFile.from_bytes(png_bytes, filename="avatar.png"), + profile=ProfileAttributes(display_name="Ada Lovelace"), + purpose="profile", +) + +client.avatars.upload_avatar(body=body) +``` + +Use `UploadFile.from_bytes(png_bytes)` when the fallback filename is acceptable. + +### Java + +```java +UploadAvatarMultipartRequestBody body = new UploadAvatarMultipartRequestBody( + UploadFile.of(pngBytes, "avatar.png"), + new ProfileAttributes(null, "Ada Lovelace"), + "profile" +); + +UploadAvatarResponse response = avatarsApi.uploadAvatar(body); +``` + +Use `UploadFile.ofBytes(pngBytes)` when the fallback filename is acceptable. + +### Kotlin + +```kotlin +val body = UploadAvatarMultipartRequestBody( + file = UploadFile(pngBytes, "avatar.png"), + profile = ProfileAttributes(displayName = "Ada Lovelace"), + purpose = "profile", +) + +val response = avatarsApi.uploadAvatar(body) +``` + +Use `UploadFile(pngBytes)` when the fallback filename is acceptable. + +### Rust + +The same `UploadFile` wrapper is generated for `rust-reqwest`, `rust-ureq`, and `rust-aioduct`. + +```rust +let body = UploadAvatarMultipartRequestBody { + file: UploadFile::new(png_bytes, "avatar.png"), + profile: ProfileAttributes { + display_name: "Ada Lovelace".to_string(), + alt_text: None, + }, + purpose: "profile".to_string(), +}; + +let response = avatars_api.upload_avatar(&body).await?; +``` + +Use `UploadFile::from_bytes(png_bytes)` when the fallback filename is acceptable. + +## Raw Binary Bodies + +For non-multipart `application/octet-stream` bodies, openapi-nexus does not use the upload wrapper. The method body type remains the normal binary type for the target language: + +| Generator | Request body type | +|---|---| +| `typescript-fetch` | `Blob \| File` | +| `go-http` | `[]byte` | +| `python-httpx` | `bytes` | +| `python-requests` | `bytes` | +| `java-okhttp` | `byte[]` | +| `kotlin-okhttp` | `ByteArray` | +| `rust-reqwest` | `Vec` | +| `rust-ureq` | `Vec` | +| `rust-aioduct` | `Vec` | + +This keeps ordinary binary request bodies and binary responses separate from multipart filename handling. + +## Supported Multipart Shape + +Multipart request bodies must be object-shaped. Each object property becomes one multipart part. + +- Binary parts use `UploadFile` or `UploadFileInput`. +- String, number, boolean, and enum parts are emitted as text. +- Object and array parts are emitted as JSON. +- `encoding..contentType` controls the per-part `Content-Type` when present. + +Schemas that do not describe an object-shaped multipart body are rejected by generators with an explicit unsupported multipart error. diff --git a/examples/multipart-binary/README.md b/examples/multipart-binary/README.md new file mode 100644 index 000000000..51669556a --- /dev/null +++ b/examples/multipart-binary/README.md @@ -0,0 +1,31 @@ +# Multipart and Binary Example + +This example shows the two binary paths that openapi-nexus treats differently: + +- `multipart/form-data` request bodies use operation-specific request body models. Binary parts use the generated `UploadFile` wrapper so callers can provide the HTTP `filename`. +- `application/octet-stream` request and response bodies stay as raw binary values for the target language. + +Generate a client from this spec: + +```bash +openapi-nexus generate \ + --input examples/multipart-binary/openapi.yaml \ + --output generated/multipart-binary/typescript \ + --generators typescript-fetch +``` + +Use a separate output directory per generator when generating more than one target language. + +In the generated clients, the multipart `file` field remains the wire field name, while the filename can be supplied separately: + +```ts +await new AvatarsApi().uploadAvatar({ + body: { + file: { data: new Blob([bytes], { type: 'image/png' }), filename: 'avatar.png' }, + profile: { display_name: 'Ada Lovelace' }, + purpose: 'profile', + }, +}); +``` + +If no filename is provided, generated clients fall back to the multipart field name. In this example the fallback is `file`. diff --git a/examples/multipart-binary/openapi.yaml b/examples/multipart-binary/openapi.yaml new file mode 100644 index 000000000..b2401b0d0 --- /dev/null +++ b/examples/multipart-binary/openapi.yaml @@ -0,0 +1,111 @@ +openapi: 3.1.0 +info: + title: Multipart and Binary Example + version: 1.0.0 + description: Shows multipart file uploads and raw octet-stream bodies. +paths: + /avatars: + post: + tags: + - avatars + operationId: upload_avatar + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + - profile + - purpose + properties: + file: + type: string + format: binary + description: Avatar image bytes. + profile: + $ref: '#/components/schemas/ProfileAttributes' + purpose: + type: string + description: Why the avatar is being uploaded. + encoding: + file: + contentType: image/png + profile: + contentType: application/json + purpose: + contentType: text/plain + responses: + '200': + description: Avatar accepted. + content: + application/json: + schema: + $ref: '#/components/schemas/AvatarUpload' + /avatars/{avatar_id}/content: + get: + tags: + - avatars + operationId: download_avatar + parameters: + - name: avatar_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Raw avatar content. + content: + application/octet-stream: + schema: + type: string + format: binary + '404': + description: Avatar not found. + put: + tags: + - avatars + operationId: replace_avatar_content + parameters: + - name: avatar_id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '204': + description: Avatar content replaced. +components: + schemas: + ProfileAttributes: + type: object + required: + - display_name + properties: + display_name: + type: string + alt_text: + type: string + AvatarUpload: + type: object + required: + - id + - filename + - bytes + properties: + id: + type: string + filename: + type: string + bytes: + type: integer + format: int64