Skip to content

Commit 833185d

Browse files
Merge pull request #28 from gpu-cli/issue-27-exploded-query-params
feat(client): typed query parameter serialization (form/deepObject styles) (#27)
2 parents 3aaa847 + 191cf00 commit 833185d

9 files changed

Lines changed: 919 additions & 40 deletions

File tree

.beads/issues.jsonl

Lines changed: 3 additions & 0 deletions
Large diffs are not rendered by default.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
**/target/
33
# Build outputs from spec-compile.sh and ad-hoc generator runs.
44
/tmp/spec-compile/
5+
/tmp/spec-compile-target/
56
/tmp/gen-anthropic/
67
/tmp/gen-openai/
78
/tmp/gen-cloudflare/

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "openapi-to-rust"
3-
version = "0.5.3"
3+
version = "0.6.0"
44
edition = "2024"
55
rust-version = "1.85.0"
66
authors = ["James Lal"]

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ We originally built this internally at [GPU CLI](https://gpu-cli.sh) to generate
1111

1212
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.
1313

14+
## What's new in 0.6
15+
16+
- **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).
17+
1418
## What's new in 0.5
1519

1620
- **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}/`.
@@ -551,9 +555,46 @@ cargo run -p openapi-to-rust -- generate --config examples/server-anthropic-mess
551555
cargo run --manifest-path examples/server-anthropic-messages/Cargo.toml
552556
```
553557

558+
## Breaking changes (pre-1.0)
559+
560+
Until 1.0.0, a minor version bump may change the generated API surface —
561+
usually because the previous output was wrong on the wire. Regenerating makes
562+
the compiler point at every affected call site; there is no silent behavior
563+
change without a signature change.
564+
565+
### 0.6.0
566+
567+
Object- and array-schema **query parameters** are now serialized according to
568+
their OpenAPI `style`/`explode` (issue [#27](https://github.com/gpu-cli/openapi-to-rust/issues/27)).
569+
Previously every such parameter was `Option<impl AsRef<str>>` and the caller's
570+
string went out as a single opaque `name=<string>` pair — which no server
571+
expecting the declared style could parse. Signatures change as follows:
572+
573+
| Parameter shape | Old argument | New argument | Wire format |
574+
|---|---|---|---|
575+
| object, form + explode=true (OAS defaults) | `Option<impl AsRef<str>>` | `Option<Struct>` | `?color=red&size=5` |
576+
| object, form + explode=false | `Option<impl AsRef<str>>` | `Option<Struct>` | `?filter=color,red,size,5` |
577+
| object, deepObject | `Option<impl AsRef<str>>` | `Option<Struct>` | `?filter[color]=red` |
578+
| array, form + explode=true (OAS defaults) | `Option<impl AsRef<str>>` | `Option<Vec<T>>` | `?tags=a&tags=b` |
579+
| array, form + explode=false | `Option<impl AsRef<str>>` | `Option<Vec<T>>` | `?tags=a,b,c` |
580+
581+
`Struct` is the referenced component model for `$ref` schemas or a
582+
synthesized `{Operation}{Param}` struct for inline objects. `T` is the scalar
583+
item type (via the same type mapping as properties) or the referenced
584+
string-enum model. Unchanged (still the opaque string passthrough): deepObject
585+
arrays, `spaceDelimited`/`pipeDelimited`, arrays of objects, and server-side
586+
extraction (tracked separately).
587+
554588
## Release notes
555589

556-
### 0.5 (this release)
590+
### 0.6 (this release)
591+
592+
**Typed query parameter serialization** ([#27](https://github.com/gpu-cli/openapi-to-rust/issues/27))
593+
- 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).
594+
- **Breaking for regenerated clients** — see [Breaking changes (pre-1.0)](#breaking-changes-pre-10) for the full signature table.
595+
- `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.
596+
597+
### 0.5
557598

558599
**Server codegen (Axum)**
559600
- `[server]` TOML section with selector grammar (operationId / `METHOD /path` / `tag:<name>`).

scripts/spec-compile.sh

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
# Smoke-test that generated clients for every spec under specs/ compile cleanly.
33
#
44
# Auto-discovers specs/*.yaml and specs/*.json. Each spec produces a separate
5-
# scratch crate; we run the `openapi-to-rust` generator into it and then
6-
# `cargo check`. Any regression here means a real-world spec stops compiling.
5+
# scratch crate; we run the `openapi-to-rust` generator into it, then check
6+
# all scratch crates as ONE cargo workspace: a single dependency resolution,
7+
# a single target-dir lock, and cargo schedules the per-crate checks across
8+
# all cores itself. Any regression here means a real-world spec stops
9+
# compiling.
710
#
811
# Usage:
912
# scripts/spec-compile.sh # all specs in specs/
@@ -16,6 +19,13 @@
1619
# SPEC_COMPILE_LIMIT=N process only the first N alphabetically-sorted specs
1720
# SPEC_COMPILE_PARSE_ONLY=1 skip cargo check; only verify the generator
1821
# parses+emits without errors. Faster.
22+
# SPEC_COMPILE_TARGET_DIR=path shared cargo target dir for the scratch
23+
# workspace (default tmp/spec-compile-target).
24+
# Dependency artifacts (reqwest, chrono, …)
25+
# compile once and are reused by all specs — and
26+
# by later runs, since the dir survives this
27+
# script's per-run cleanup. Wipe it to force a
28+
# cold build.
1929
set -euo pipefail
2030
cd "$(dirname "$0")/.."
2131

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

47+
# Shared target dir for the scratch workspace. Deliberately OUTSIDE $ROOT so
48+
# it survives the rm -rf above and stays warm across runs. Only exported for
49+
# the `cargo check` step — the generator build above must keep using the
50+
# workspace target/.
51+
SCRATCH_TARGET="${SPEC_COMPILE_TARGET_DIR:-$WORKSPACE/tmp/spec-compile-target}"
52+
mkdir -p "$SCRATCH_TARGET"
53+
3754
# Discover specs. Sort for deterministic output.
3855
mapfile -t ALL_SPECS < <(find specs -maxdepth 1 -type f \( -name "*.yaml" -o -name "*.json" \) | sort)
3956

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

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

@@ -138,26 +157,57 @@ EOF
138157
continue
139158
fi
140159

141-
if [ "${SPEC_COMPILE_PARSE_ONLY:-}" = "1" ]; then
142-
echo "GEN-OK"
143-
passed+=("$name")
144-
[ "${SPEC_COMPILE_KEEP:-}" != "1" ] && rm -rf "$dir"
145-
continue
146-
fi
160+
echo "GEN-OK"
161+
gen_ok+=("$name")
162+
done
147163

148-
# Cargo check step
149-
log="$dir/check.log"
150-
if ! ( cd "$dir" && cargo check $OFFLINE ) >"$log" 2>&1; then
151-
err_count=$(grep -cE "^error" "$log" || true)
152-
echo "CHECK-FAIL ($err_count errs)"
153-
failed_check+=("$name")
154-
continue
164+
if [ "${SPEC_COMPILE_PARSE_ONLY:-}" = "1" ]; then
165+
passed=("${gen_ok[@]}")
166+
[ "${SPEC_COMPILE_KEEP:-}" != "1" ] && rm -rf "$ROOT"
167+
elif [ ${#gen_ok[@]} -gt 0 ]; then
168+
# ---- Phase 2: check everything as one workspace ------------------------
169+
{
170+
echo "[workspace]"
171+
echo "resolver = \"2\""
172+
echo "members = ["
173+
for name in "${gen_ok[@]}"; do
174+
echo " \"$name\","
175+
done
176+
echo "]"
177+
} >"$ROOT/Cargo.toml"
178+
179+
echo
180+
echo "[spec-compile] cargo check (workspace of ${#gen_ok[@]} crates)..."
181+
ws_log="$ROOT/check.log"
182+
if ( cd "$ROOT" && CARGO_TARGET_DIR="$SCRATCH_TARGET" cargo check --workspace --keep-going $OFFLINE ) >"$ws_log" 2>&1; then
183+
passed=("${gen_ok[@]}")
184+
for name in "${gen_ok[@]}"; do
185+
printf "%-30s PASS\n" "$name"
186+
done
187+
[ "${SPEC_COMPILE_KEEP:-}" != "1" ] && rm -rf "$ROOT"
188+
else
189+
# Attribute failures per crate. Everything that compiles is already
190+
# cached from the workspace pass, so these re-checks are cheap. Passing
191+
# crates are cleaned up only after the loop — they must stay on disk
192+
# while they're still members of the workspace being checked.
193+
for name in "${gen_ok[@]}"; do
194+
log="$ROOT/$name/check.log"
195+
if ( cd "$ROOT" && CARGO_TARGET_DIR="$SCRATCH_TARGET" cargo check -p "spec-compile-$name" $OFFLINE ) >"$log" 2>&1; then
196+
printf "%-30s PASS\n" "$name"
197+
passed+=("$name")
198+
else
199+
err_count=$(grep -cE "^error" "$log" || true)
200+
printf "%-30s CHECK-FAIL (%s errs)\n" "$name" "$err_count"
201+
failed_check+=("$name")
202+
fi
203+
done
204+
if [ "${SPEC_COMPILE_KEEP:-}" != "1" ]; then
205+
for name in "${passed[@]}"; do
206+
rm -rf "$ROOT/$name"
207+
done
208+
fi
155209
fi
156-
157-
echo "PASS"
158-
passed+=("$name")
159-
[ "${SPEC_COMPILE_KEEP:-}" != "1" ] && rm -rf "$dir"
160-
done
210+
fi
161211

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

0 commit comments

Comments
 (0)