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
3 changes: 3 additions & 0 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
**/target/
# Build outputs from spec-compile.sh and ad-hoc generator runs.
/tmp/spec-compile/
/tmp/spec-compile-target/
/tmp/gen-anthropic/
/tmp/gen-openai/
/tmp/gen-cloudflare/
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "openapi-to-rust"
version = "0.5.3"
version = "0.6.0"
edition = "2024"
rust-version = "1.85.0"
authors = ["James Lal"]
Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ We originally built this internally at [GPU CLI](https://gpu-cli.sh) to generate

It currently compiles cleanly against **54 real-world specs** in `specs/` (Stripe, OpenAI, Anthropic, Cloudflare's 14k-schema spec, GitHub, Discord, Microsoft Graph, Spotify, Twilio, …), guarded by CI.

## What's new in 0.6

- **Typed query parameter serialization** ([#27](https://github.com/gpu-cli/openapi-to-rust/issues/27)) — object and array query params are generated per their OAS `style`/`explode`: form-exploded objects become struct arguments serialized as `?color=red&size=5`, explode=false objects comma-join, deepObject objects emit `?filter[color]=red`, and form arrays become `Vec<T>` (repeated or comma-joined). **Breaking for regenerated clients** — the old `Option<impl AsRef<str>>` passthrough put a single opaque `name=<string>` pair on the wire, which no server expecting the declared style could parse. See [Breaking changes (pre-1.0)](#breaking-changes-pre-10).

## What's new in 0.5

- **Server codegen (Axum)** — opt-in `[server]` section emits a trait per tag, a status-code-typed response enum (with `IntoResponse`), an SSE-aware variant, and a `Router` factory. Pick operations one-by-one or `--all-tag`. Two end-to-end examples ship in `examples/server-{openai-responses,anthropic-messages}/`.
Expand Down Expand Up @@ -551,9 +555,46 @@ cargo run -p openapi-to-rust -- generate --config examples/server-anthropic-mess
cargo run --manifest-path examples/server-anthropic-messages/Cargo.toml
```

## Breaking changes (pre-1.0)

Until 1.0.0, a minor version bump may change the generated API surface —
usually because the previous output was wrong on the wire. Regenerating makes
the compiler point at every affected call site; there is no silent behavior
change without a signature change.

### 0.6.0

Object- and array-schema **query parameters** are now serialized according to
their OpenAPI `style`/`explode` (issue [#27](https://github.com/gpu-cli/openapi-to-rust/issues/27)).
Previously every such parameter was `Option<impl AsRef<str>>` and the caller's
string went out as a single opaque `name=<string>` pair — which no server
expecting the declared style could parse. Signatures change as follows:

| Parameter shape | Old argument | New argument | Wire format |
|---|---|---|---|
| object, form + explode=true (OAS defaults) | `Option<impl AsRef<str>>` | `Option<Struct>` | `?color=red&size=5` |
| object, form + explode=false | `Option<impl AsRef<str>>` | `Option<Struct>` | `?filter=color,red,size,5` |
| object, deepObject | `Option<impl AsRef<str>>` | `Option<Struct>` | `?filter[color]=red` |
| array, form + explode=true (OAS defaults) | `Option<impl AsRef<str>>` | `Option<Vec<T>>` | `?tags=a&tags=b` |
| array, form + explode=false | `Option<impl AsRef<str>>` | `Option<Vec<T>>` | `?tags=a,b,c` |

`Struct` is the referenced component model for `$ref` schemas or a
synthesized `{Operation}{Param}` struct for inline objects. `T` is the scalar
item type (via the same type mapping as properties) or the referenced
string-enum model. Unchanged (still the opaque string passthrough): deepObject
arrays, `spaceDelimited`/`pipeDelimited`, arrays of objects, and server-side
extraction (tracked separately).

## Release notes

### 0.5 (this release)
### 0.6 (this release)

**Typed query parameter serialization** ([#27](https://github.com/gpu-cli/openapi-to-rust/issues/27))
- Object and array query parameters are generated per their OAS `style`/`explode` instead of an opaque `Option<impl AsRef<str>>` passthrough: form-exploded objects (`?color=red&size=5`), explode=false objects (`?filter=color,red,size,5`), deepObject objects (`?filter[color]=red`), and `Vec<T>` form arrays (repeated or comma-joined pairs, scalar or string-enum items).
- **Breaking for regenerated clients** — see [Breaking changes (pre-1.0)](#breaking-changes-pre-10) for the full signature table.
- `scripts/spec-compile.sh` checks all scratch crates as one cargo workspace against a shared persistent target dir — full-sweep verification dropped from hours to minutes.

### 0.5

**Server codegen (Axum)**
- `[server]` TOML section with selector grammar (operationId / `METHOD /path` / `tag:<name>`).
Expand Down
90 changes: 70 additions & 20 deletions scripts/spec-compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
# Smoke-test that generated clients for every spec under specs/ compile cleanly.
#
# Auto-discovers specs/*.yaml and specs/*.json. Each spec produces a separate
# scratch crate; we run the `openapi-to-rust` generator into it and then
# `cargo check`. Any regression here means a real-world spec stops compiling.
# scratch crate; we run the `openapi-to-rust` generator into it, then check
# all scratch crates as ONE cargo workspace: a single dependency resolution,
# a single target-dir lock, and cargo schedules the per-crate checks across
# all cores itself. Any regression here means a real-world spec stops
# compiling.
#
# Usage:
# scripts/spec-compile.sh # all specs in specs/
Expand All @@ -16,6 +19,13 @@
# SPEC_COMPILE_LIMIT=N process only the first N alphabetically-sorted specs
# SPEC_COMPILE_PARSE_ONLY=1 skip cargo check; only verify the generator
# parses+emits without errors. Faster.
# SPEC_COMPILE_TARGET_DIR=path shared cargo target dir for the scratch
# workspace (default tmp/spec-compile-target).
# Dependency artifacts (reqwest, chrono, …)
# compile once and are reused by all specs — and
# by later runs, since the dir survives this
# script's per-run cleanup. Wipe it to force a
# cold build.
set -euo pipefail
cd "$(dirname "$0")/.."

Expand All @@ -34,6 +44,13 @@ ROOT="$WORKSPACE/tmp/spec-compile"
rm -rf "$ROOT"
mkdir -p "$ROOT"

# Shared target dir for the scratch workspace. Deliberately OUTSIDE $ROOT so
# it survives the rm -rf above and stays warm across runs. Only exported for
# the `cargo check` step — the generator build above must keep using the
# workspace target/.
SCRATCH_TARGET="${SPEC_COMPILE_TARGET_DIR:-$WORKSPACE/tmp/spec-compile-target}"
mkdir -p "$SCRATCH_TARGET"

# Discover specs. Sort for deterministic output.
mapfile -t ALL_SPECS < <(find specs -maxdepth 1 -type f \( -name "*.yaml" -o -name "*.json" \) | sort)

Expand Down Expand Up @@ -63,10 +80,12 @@ fi
echo "[spec-compile] running ${#SPECS[@]} spec(s)"
echo

# ---- Phase 1: generate a scratch crate per spec -------------------------
passed=()
failed_gen=()
failed_check=()
skipped=()
gen_ok=()
for entry in "${SPECS[@]}"; do
IFS='|' read -r name spec_path <<<"$entry"

Expand Down Expand Up @@ -138,26 +157,57 @@ EOF
continue
fi

if [ "${SPEC_COMPILE_PARSE_ONLY:-}" = "1" ]; then
echo "GEN-OK"
passed+=("$name")
[ "${SPEC_COMPILE_KEEP:-}" != "1" ] && rm -rf "$dir"
continue
fi
echo "GEN-OK"
gen_ok+=("$name")
done

# Cargo check step
log="$dir/check.log"
if ! ( cd "$dir" && cargo check $OFFLINE ) >"$log" 2>&1; then
err_count=$(grep -cE "^error" "$log" || true)
echo "CHECK-FAIL ($err_count errs)"
failed_check+=("$name")
continue
if [ "${SPEC_COMPILE_PARSE_ONLY:-}" = "1" ]; then
passed=("${gen_ok[@]}")
[ "${SPEC_COMPILE_KEEP:-}" != "1" ] && rm -rf "$ROOT"
elif [ ${#gen_ok[@]} -gt 0 ]; then
# ---- Phase 2: check everything as one workspace ------------------------
{
echo "[workspace]"
echo "resolver = \"2\""
echo "members = ["
for name in "${gen_ok[@]}"; do
echo " \"$name\","
done
echo "]"
} >"$ROOT/Cargo.toml"

echo
echo "[spec-compile] cargo check (workspace of ${#gen_ok[@]} crates)..."
ws_log="$ROOT/check.log"
if ( cd "$ROOT" && CARGO_TARGET_DIR="$SCRATCH_TARGET" cargo check --workspace --keep-going $OFFLINE ) >"$ws_log" 2>&1; then
passed=("${gen_ok[@]}")
for name in "${gen_ok[@]}"; do
printf "%-30s PASS\n" "$name"
done
[ "${SPEC_COMPILE_KEEP:-}" != "1" ] && rm -rf "$ROOT"
else
# Attribute failures per crate. Everything that compiles is already
# cached from the workspace pass, so these re-checks are cheap. Passing
# crates are cleaned up only after the loop — they must stay on disk
# while they're still members of the workspace being checked.
for name in "${gen_ok[@]}"; do
log="$ROOT/$name/check.log"
if ( cd "$ROOT" && CARGO_TARGET_DIR="$SCRATCH_TARGET" cargo check -p "spec-compile-$name" $OFFLINE ) >"$log" 2>&1; then
printf "%-30s PASS\n" "$name"
passed+=("$name")
else
err_count=$(grep -cE "^error" "$log" || true)
printf "%-30s CHECK-FAIL (%s errs)\n" "$name" "$err_count"
failed_check+=("$name")
fi
done
if [ "${SPEC_COMPILE_KEEP:-}" != "1" ]; then
for name in "${passed[@]}"; do
rm -rf "$ROOT/$name"
done
fi
fi

echo "PASS"
passed+=("$name")
[ "${SPEC_COMPILE_KEEP:-}" != "1" ] && rm -rf "$dir"
done
fi

echo
echo "[spec-compile] summary: ${#passed[@]} passed, ${#failed_gen[@]} gen-failed, ${#failed_check[@]} check-failed, ${#skipped[@]} skipped"
Expand Down
Loading
Loading