Skip to content
Closed
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ when correcting output that was wrong or incomplete on the wire.

### Fixed

- Schemas that carried enough information to type no longer degrade to
`serde_json::Value`. Across the 57-spec corpus this types 3,341 fields that
were previously opaque (#62):
- `anyOf: [$ref, {type: object, nullable: true}]` — how OData spells "that
type, or null" — becomes `Option<T>` instead of an untyped union (2,127
fields in Microsoft Graph alone);
- an inline object, union, enum, or merged `allOf` in a field or element
position is hoisted to a named type instead of being dropped by the
generator, which could not render one inline;
- `allOf` with a single member takes that member's type, and `allOf` inside
array items is analyzed instead of ignored;
- a `$ref` to any local JSON Pointer resolves — a parameter's schema, one
member of another schema's composition — not only
`#/components/schemas/<name>`;
- `type: null` becomes `()`, which serde reads and writes as `null`;
- a union whose branch list is empty takes the schema's declared type; a
union of one branch is that branch; branches differing only in constraints
share one type; branches that only alternate `required` describe the
object their properties declare; and branches that are local pointers are
expanded before the union is built.
- `items: false` and `items: true` — 2020-12 boolean schemas, and the canonical
way to close a tuple — now parse instead of failing the document with "data
did not match any variant of untagged enum Schema" (#62).
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,17 @@ Also run the relevant distribution or corpus gate when touching these areas:
scripts/install-smoke.sh # packaging, CLI, or dependencies
scripts/spec-compile.sh anthropic openai # generator/client output
scripts/spec-compile.sh # broad generator/type changes
scripts/untyped-census.sh # anything that changes which fields get typed
```

`scripts/untyped-census.sh` rewrites `tests/conformance/untyped-report.md`,
which counts every generated field that carries `serde_json::Value` and says
why. Regenerate it when a change types fields that used to be opaque (or stops
typing ones that were), so the corpus delta is visible in review;
`scripts/untyped-census.sh --check` fails when it is stale. A **recoverable**
row means the schema carried type information the generator dropped — those are
defects with a fix, not shapes the spec left open.

The full corpus generates and compile-checks 55 OpenAPI documents and can take
several minutes. CI runs a fast generation tier on pull requests and the full
compile tier weekly or on manual dispatch.
Expand Down
102 changes: 102 additions & 0 deletions scripts/untyped-census.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Report how much of the generated surface is still `serde_json::Value`, and
# why, across every spec under specs/.
#
# `serde_json::Value` in generated code means one of two things: the schema
# declared an unconstrained value, or the generator dropped type information the
# schema carried. Only the second is a defect, so the report separates them and
# ranks the recoverable causes — that ranking is what says where typing work is
# worth doing next.
#
# Usage:
# scripts/untyped-census.sh # rewrite the checked-in report
# scripts/untyped-census.sh --check # fail if the report is out of date
#
# Env:
# CENSUS_SPECS="a b" limit to named specs (default: everything in specs/)
set -euo pipefail
cd "$(dirname "$0")/.."

REPORT="tests/conformance/untyped-report.md"
CHECK=0
[ "${1:-}" = "--check" ] && CHECK=1

echo "[untyped-census] building openapi-to-rust binary..." >&2
cargo build --quiet --bin openapi-to-rust

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

specs=()
if [ -n "${CENSUS_SPECS:-}" ]; then
for name in $CENSUS_SPECS; do
for candidate in "specs/$name.yaml" "specs/$name.json"; do
[ -f "$candidate" ] && specs+=("$candidate")
done
done
else
for candidate in specs/*.yaml specs/*.json; do
[ -f "$candidate" ] && specs+=("$candidate")
done
fi

for spec in "${specs[@]}"; do
name="$(basename "$spec")"; name="${name%.*}"
if ! target/debug/openapi-to-rust generate "$spec" \
--output-dir "$WORK/$name" --types-only --report-untyped --json \
> "$WORK/$name.json" 2>/dev/null; then
echo "[untyped-census] skipped $name (generation failed)" >&2
rm -f "$WORK/$name.json"
continue
fi
# The findings array is followed by the generation summary; keep the array.
jq -s '.[0]' "$WORK/$name.json" > "$WORK/$name.findings.json"
done

{
echo "# Untyped Output Census"
echo
echo "Generated by \`scripts/untyped-census.sh\`. Every generated field that"
echo "carries \`serde_json::Value\`, grouped by why."
echo
echo "**Faithful** means the schema declared an unconstrained value and there is"
echo "no better Rust type. **Recoverable** means the schema carried type"
echo "information that did not survive; those are defects with a fix."
echo
echo "## Corpus totals"
echo
echo "| Reason | Count | Verdict |"
echo "|---|---:|---|"
jq -s -r '
add
| group_by(.reason)
| map({reason: .[0].reason, count: length})
| sort_by(-.count)
| .[]
| "| `\(.reason)` | \(.count) | " +
(if (.reason | test("^(any-schema|opaque-object|untyped-additional-properties|array-without-items|open-positional-items)$"))
then "faithful" else "**recoverable**" end) + " |"
' "$WORK"/*.findings.json
echo
echo "## Per spec"
echo
echo "| Spec | Untyped | Recoverable |"
echo "|---|---:|---:|"
for findings in "$WORK"/*.findings.json; do
name="$(basename "$findings")"; name="${name%.findings.json}"
total="$(jq 'length' "$findings")"
recoverable="$(jq '[.[] | select(.reason | test("^(any-schema|opaque-object|untyped-additional-properties|array-without-items|open-positional-items)$") | not)] | length' "$findings")"
echo "| \`$name\` | $total | $recoverable |"
done
} > "$WORK/report.md"

if [ "$CHECK" = "1" ]; then
if ! diff -u "$REPORT" "$WORK/report.md"; then
echo "[untyped-census] $REPORT is out of date; run scripts/untyped-census.sh" >&2
exit 1
fi
echo "[untyped-census] ✅ report is up to date"
else
cp "$WORK/report.md" "$REPORT"
echo "[untyped-census] wrote $REPORT"
fi
Loading
Loading