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
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 24 additions & 8 deletions docs/src/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
187 changes: 187 additions & 0 deletions docs/src/multipart_and_binary.md
Original file line number Diff line number Diff line change
@@ -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<u8>` |
| `rust-ureq` | `Vec<u8>` |
| `rust-aioduct` | `Vec<u8>` |

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.<part>.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.
31 changes: 31 additions & 0 deletions examples/multipart-binary/README.md
Original file line number Diff line number Diff line change
@@ -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`.
Loading
Loading