feat: OpenAPI 3.1/3.2 conformance — 38 of 51 beads#15
Merged
Conversation
…acker Establishes the conformance infrastructure that backs OpenAPI 3.1/3.2 work: - catalog-gen: parses OAS 3.2 spec markdown into tests/conformance/catalog.yaml (30 objects, 141 fields, 57 JSON Schema 2020-12 keywords, 7 style combos). This is the denominator for "100% coverage". - conformance harness (tests/conformance.rs): walks fixtures, runs L0 lossless-parse check, supports layered fails_at: markers so fixtures for layers we haven't built yet (L1/L3/L5) are recorded as deferred. - 10 seed fixtures across the audit's top-impact gaps (webhooks, additional-operations, query method, deepObject, header params, type-array nullability, components/pathItems, components/mediaTypes, mutualTLS, $dynamicRef). - JSON Schema 2020-12 corpus runner via git submodule (json-schema-org/JSON-Schema-Test-Suite). 47 test files exercised. - APIs.guru lazy-clone + smoke runner (off by default). - status.toml: honest support claims, derived from harness results. - beads.yaml + file-beads bin: source-of-truth for the work plan; emitted as GitHub issue #14 with parallel-execution batches computed from dependency depth and file-set disjointness. Cross-cutting findings live in tmp/openapi-specs/reports/00-SUMMARY.md. Tracking issue: #14 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bead F1 from issue #14. Replaces the silent serde(flatten) extra: BTreeMap pattern with a typed Extensions newtype that rejects any non-`x-` key at deserialize time, so unknown spec fields surface loudly instead of being swallowed. Structs converted to Extensions: OpenApiSpec, Info, Components, PathItem, Operation, Parameter, RequestBody, Response, MediaType, Discriminator. Adds typed fields for previously-flattened spec fields so that valid OAS documents continue to parse: OpenApiSpec jsonSchemaDialect, servers, webhooks, security, tags, externalDocs, $self Info summary, description, termsOfService, contact, license Components responses, examples, requestBodies, headers, securitySchemes, links, callbacks, pathItems (3.1+), mediaTypes (3.2) PathItem summary, description, servers, $ref Operation tags, deprecated, callbacks, security, servers, externalDocs Parameter deprecated, allowEmptyValue, style, explode, allowReserved, content, example, examples, $ref Response headers, links, $ref MediaType example, examples, encoding, $ref RequestBody $ref Schema and SchemaDetails are intentionally left with the loose `extra: BTreeMap`. They hold JSON Schema 2020-12 keywords (patternProperties, propertyNames, dependentRequired, if/then/else, …) that analysis.rs already reads from extras. Those graduate to typed fields under the J5–J8 beads (Phase 2b). Note on serde: deny_unknown_fields cannot combine with #[serde(flatten)] — the former rejects fields before the flatten target sees them, including x-* extensions. Using Extensions with custom Deserialize is the canonical workaround: any non-x-* unknown lands in Extensions and fails its check. Adds CLI version gate: 3.0.x and 3.1.x accepted; 3.2.x emits an "experimental" warning; everything else is a hard error. Conformance harness now passes the deepObject, mediaTypes, pathItems, $dynamicAnchor, mutualTLS, and webhooks fixtures at L0; they advance to fails_at: L3 since codegen still ignores the typed values. All 205 tests still pass. Refs #14 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bead F2 from issue #14. Adds Schema::TypedMulti variant ahead of Schema::Typed in the untagged enum so `type: ["string", "null"]` deserializes into a typed multi-element array instead of falling through to Untyped. - Schema::TypedMulti { schema_types: Vec<SchemaType>, details } - Schema::schema_type() returns the primary non-null type for TypedMulti. - Schema::type_array_contains_null() exposes nullability cleanly. - Schema::details() / details_mut() / inferred_type() handle TypedMulti. - analyze_single_schema combines the 3.0 `nullable: true` field with the 3.1 type-array null marker, so AnalyzedSchema.nullable is correct regardless of which form the spec used. The 3.0 `nullable` field stays for compatibility (the openai-responses fixture is OAS 3.0.0 and uses it). 3.1+ specs should use the type array. Conformance: schema/type-string-or-null.yaml flips from fails_at:L3 to fully passing. status.toml entry for objects.Schema.type[array] flips to "supported". All 205 tests still pass. Refs #14 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five beads from issue #14, all in Phase 1 (top silent failures). T1: Wire header request parameters through codegen - Header params parsed from `Parameter.in == "header"` now flow into the method signature (required → bare arg, optional → Option<T>). - generate_header_params emits `req = req.header(name, value)` for each, gated on Some(_) for optional headers. T2: Emit operations for HEAD/OPTIONS/PATCH/TRACE methods - Replaces the "_ => get" silent fallback in client_generator.rs:711 with explicit match arms. - HEAD uses reqwest's named method; OPTIONS/TRACE go through Client::request(Method::OPTIONS, ...) (reqwest doesn't expose those as named methods). - registry_generator.rs's HttpMethod enum extended with Head/Options/Trace. T6: Detect operationId collisions - analysis.rs:3514 silently overwrote duplicate operationIds. Now returns a clear error pointing at both colliding paths. T11: requestBody required:false → Option<T> - OperationInfo gains request_body_required field, populated from operation.request_body.required (defaults to false per OAS spec). - generate_request_param wraps the body argument in Option<T> when not required. Existing tests with explicit request bodies were updated to set request_body_required: true. F4: Accept paths-less specs (components-only / webhooks-only) - extract_schemas in analysis.rs no longer errors when components.schemas is missing. OAS 3.1+ allows specs with only `webhooks` or only `components`. Conformance: parameter/header-required.yaml flips to passing (header params now reach the wire). All 205 tests still pass; insta snapshots updated for the new request_body_required field. Refs #14 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four more beads from Phase 1 (top silent failures).
T13: Surface description/summary into rustdoc
- generate_operation_doc_comment now prepends operation.summary and
operation.description as rustdoc lines, then the method+path in
backticks. Tests updated for the new format.
T5: Path templating percent-encoding
- generate_url_with_params now wraps each path-template substitution in
__pct_encode_path_segment, a private helper emitted into the generated
client. Encodes per RFC3986 §3.3 (only ALPHA/DIGIT/`-._~` pass through).
Without this, path values containing `/`, `?`, `#`, or non-ASCII broke
URLs.
T8: Range status codes (1XX/2XX/3XX/4XX/5XX)
- generate_error_match_arms now emits guarded match arms for range-keyed
response codes (`2XX`, `4XX`, `5XX`, etc.) instead of falling through
to the generic default. Specific codes still take precedence — concrete
match arms come first in the generated match.
T3: Honor auth_config ApiKey / Custom variants
- generate_auth_application replaces the hardcoded `req.bearer_auth(...)`
in every operation with a config-driven emission.
- Bearer + Authorization: bearer_auth (unchanged behavior).
- Bearer + custom header: format!("Bearer {}", token).
- ApiKey: req.header(name, token) — no Bearer prefix.
- Custom: req.header(name, format!("{prefix}{token}")) when prefix set.
- README claim of multi-scheme auth is now actually true at runtime.
All 205 tests still pass.
Refs #14
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o-detect
T4: Walk webhooks: in analysis
- analyze_operations now extracts operations from spec.webhooks too,
using a synthetic `__webhook__/<name>` path. Their request bodies and
responses become typed schemas like any other operation; the typed
Webhook enum + dispatcher is a follow-up.
- Refactored the path/operation walker into ingest_path_item_operations
so paths and webhooks share the same code path.
T10: Resolve $ref-typed parameter types
- get_param_rust_type now consults ParameterInfo.schema_ref first and
emits the resolved Rust type. Previously $ref-ed enum parameters
silently fell back to String.
T15: SSE auto-detection from text/event-stream responses
- analyze_single_operation now detects `text/event-stream` content
types on responses and sets supports_streaming = true. If a
`stream`-named parameter is also present it's recorded as
stream_parameter. The user can still override via config.
All 205 tests still pass.
Refs #14
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…refs
Phase 3 + 2b beads for the OAS 3.2.0 / JSON Schema 2020-12 surface area.
D1: PathItem.query + additionalOperations
- PathItem now has fields for the 3.2 `query` HTTP method and the
`additionalOperations` extension map for arbitrary verbs (LINK, UNLINK,
PROPFIND, etc.).
- PathItem::operations() yields all of them so analysis sees them.
- http_method_call falls through to reqwest::Method::from_bytes(...)
for verbs other than the 8 well-known ones, so 3.2 query and
custom verbs become real client methods.
- Conformance fixtures pathitem/query-method-3.2.yaml and
pathitem/additional-operations-3.2.yaml flip to fully passing.
D9: Discriminator.defaultMapping
- Field added to the Discriminator struct (3.2 §"Discriminator Object").
Captured today; emission of a `_Other(Value)` fallback variant in
generated enums is a follow-up.
J1: $dynamicRef / $dynamicAnchor (JSON Schema 2020-12)
- Schema gains a DynamicRef variant with the $dynamicRef field.
- SchemaDetails gains $dynamicAnchor and $id fields.
- SchemaDetails now derives Default; the once_cell EMPTY_DETAILS
used by Reference/RecursiveRef/DynamicRef variants collapses to
a single SchemaDetails::default().
- analyze_single_schema treats DynamicRef like RecursiveRef for now;
full anchor-scope resolution is a follow-up.
All 205 tests still pass.
Refs #14
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…amilies + 3.2 deltas
A big batch covering most of the previously-unmodelled top-level OAS objects.
All structs are typed with Extensions for x-* fields; 3.2-only fields are
added as the spec defines them.
H1: Server + ServerVariable Object
- OpenApiSpec.servers, PathItem.servers, Operation.servers all become
Option<Vec<Server>>. Server has typed url/description/variables and
3.2's `name` field (D8).
- ServerVariable carries default/enum/description.
H2: Security Scheme Object (full set)
- SecurityScheme enum with #[serde(tag = "type")] covering apiKey, http,
mutualTLS (3.1+), oauth2, openIdConnect.
- OAuthFlows + OAuthFlow modeled with all four standard flows + 3.2's
deviceAuthorization (D4).
- OAuth Flow gains deviceAuthorizationUrl (D4) and SecurityScheme::OAuth2
gains oauth2MetadataUrl (D4).
- Every variant carries a `deprecated` field (D10).
- Components.security_schemes is now BTreeMap<String, SecurityScheme>.
H4: Encoding Object
- contentType, headers, style, explode, allowReserved.
- 3.2 itemEncoding (D3) for streaming/array body parts.
- MediaType.encoding becomes BTreeMap<String, Encoding>.
H5: Header Object
- Same shape as Parameter minus name/in. Used in Response.headers
(T9), Encoding.headers, Components.headers.
H6: Example Object
- summary, description, value, externalValue.
- 3.2 dataValue and serializedValue.
- Components.examples and MediaType.examples are now typed.
H7: Link Object
- operationRef, operationId, parameters, requestBody, server.
- Components.links typed.
H8: Callback Object
- Newtype wrapper around BTreeMap<String, PathItem>.
- Operation.callbacks and Components.callbacks are typed.
H9: Tag Object
- name, description, externalDocs.
- 3.2 summary, parent, kind (D5).
- OpenApiSpec.tags is Vec<Tag>.
H10: ExternalDocs Object
- url + description. Used by OpenApiSpec, Operation.
D3: MediaType.itemSchema, prefixEncoding, itemEncoding (3.2)
Spec-level and Operation-level `security` are now typed
Vec<BTreeMap<String, Vec<String>>> instead of opaque Value.
All 205 tests still pass.
Refs #14
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflect the wave of beads landed in this PR. All 33 entries are now fixture-backed or note their precise scope (typed-but-codegen-TODO). Refs #14
J4: prefixItems
J5: patternProperties + propertyNames
J6: unevaluatedProperties + unevaluatedItems
J7: dependentRequired + dependentSchemas
J8: contains + minContains + maxContains + contentEncoding +
contentMediaType + contentSchema
All landed as typed fields on SchemaDetails, plus the conditional
keywords (`if`/`then`/`else`/`not`), the `$defs` map (J2 partial),
`$comment`, `$schema`, `examples`, `example`, `title`, `deprecated`,
`readOnly`/`writeOnly`. Spec keywords no longer hide in `extra`.
Updated `should_use_dynamic_json` in analysis.rs to read these typed
fields instead of doing string lookups in `details.extra`.
H11: Path Item $ref resolution
- resolve_path_item follows `$ref: "#/components/pathItems/X"` against
spec.components.path_items at analysis time so referenced path items
contribute their operations to the generated client.
All 205 tests still pass; insta snapshots updated for the new typed fields
where they show up in pretty-printed schemas.
Refs #14
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- cargo fmt across files touched in this PR. - Replace the always-breaking while-let in strip_markdown with a straight if-let; same behavior, no clippy warning. cargo clippy --lib --bins is now clean (one expect_used warning, not denied). The remaining clippy errors live in pre-existing tests and examples this PR doesn't touch. Refs #14
Two clippy errors caught only with `--all-features -- -D warnings`: - large_enum_variant: SecurityScheme::OAuth2 carried OAuthFlows directly (~817 bytes); other variants ~97 bytes. Box it so the enum's variants are similarly sized. - manual_contains: schema_types.iter().any(|t| *t == X) → contains(&X). Plus drop the only `expect_used` in catalog-gen.rs by replacing the `get_mut(...).expect(...)` with an `if let Some(...)` guard. `cargo clippy --all-features -- -D warnings` is now clean. Refs #14
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the bulk of the OpenAPI 3.1 / 3.2 conformance work tracked in #14. 38 of 51 beads landed (~75%) across 10 commits.
cargo test --testsis 205/205 green; all 10 conformance fixtures pass at every active layer.Summary
serde(flatten) extra: BTreeMappattern (the audit's Axum handler support #1 architectural finding) with a typedExtensionsnewtype that rejects any non-x-*key at deserialize time.Valueblob:Server,ServerVariable,Tag,ExternalDocs,Header,Example,Link,Callback,Encoding,SecurityScheme(all 5 types) +OAuthFlows+OAuthFlow.type: ["string","null"]via a newSchema::TypedMultivariant; nullability propagates toAnalyzedSchema.additionalOperations,queryHTTP method,$self, Tagsummary/parent/kind, Servername, DiscriminatordefaultMapping, MediaTypeitemSchema/prefixEncoding/itemEncoding, OAuthdeviceAuthorizationflow +oauth2MetadataUrl, SecuritySchemedeprecated, ComponentspathItems(3.1+) andmediaTypes(3.2).prefixItems,patternProperties,propertyNames,unevaluatedProperties,unevaluatedItems,dependentRequired,dependentSchemas,contains/minContains/maxContains,contentEncoding/contentMediaType/contentSchema,if/then/else/not,$dynamicRef/$dynamicAnchor,$id/$defs/$schema/$comment.__pct_encode_path_segmenthelper to the generated client and used it for every path-template substitution (RFC 3986 §3.3).Conformance scaffold (also in this PR)
Built first so progress is verifiable:
tests/conformance/specs/— committed copies of OAS 3.1.2 and 3.2.0 markdown.cargo run --bin catalog-gen— parses the 3.2 spec intotests/conformance/catalog.yaml(30 objects, 141 fields, 57 JSON Schema keywords, 7 style combos). The denominator for "100% coverage".tests/conformance.rs— fixture harness with layeredfails_at: L0|L1|L3|...markers; onlyACTIVE_LAYERSare enforced, deferring fixtures targeting unimplemented layers.tests/conformance/external/json-schema-test-suite/— git submodule of the canonical JSON Schema 2020-12 corpus.tests/conformance/external/apis-guru-sync.sh— lazy-clone for nightly real-world smoke (APIS_GURU_SMOKE=1).tests/conformance/status.toml— honest support claims (33 entries: 22 supported, 6 partial, 5 unsupported), each with a precise pointer to remaining work.tests/conformance/beads.yaml+cargo run --bin file-beads— source of truth for the work plan; can re-render the master tracking issue idempotently.Status by phase
Remaining (deferred to follow-up PRs, all noted in #14)
Group 1 (response-shape refactor): T7 (non-JSON success bodies), T9 (success-path response headers), T12 (typed
additionalProperties: <schema>).Group 2 (parameter serialization): T14 (full RFC6570 style/explode matrix — best done as its own PR with a wiremock test per combo).
Group 3 (schema-keyword codegen): J2 ($defs walking), J3 (validator runtime checks), J9 (
format→chrono::DateTime/uuid::Uuidwith feature-gated deps), J10 ($self resolution per Appendix F).Plus small ones: F3 (formal
MaybeRef<T>sum type), D2 (in: "querystring"codegen), H3 (per-op security override emission), H12 (allOf-parent polymorphism +notcodegen), D6 ($self resolution).Test plan
cargo test --tests— 205/205 passingcargo test --test conformance— 10/10 fixtures pass at active layerscargo test --test conformance_json_schema— 44 of 47 JSON Schema 2020-12 corpus files parse (skipped:dynamicRef,vocabulary,unknownKeywordper documented limits)cargo build— clean, no new warningstests/fixtures/anthropic.yml,openai-responses.json, etc.) still passINSTA_UPDATE=alwaysand reviewed in commitsCloses most of #14. Remaining work tracked in the issue's progress comments.
🤖 Generated with Claude Code