Skip to content

feat: add export-openapi subcommand (RFC companion) - #73

Draft
gsmith85 wants to merge 5 commits into
mainfrom
feat/export-openapi
Draft

feat: add export-openapi subcommand (RFC companion)#73
gsmith85 wants to merge 5 commits into
mainfrom
feat/export-openapi

Conversation

@gsmith85

@gsmith85 gsmith85 commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Companion RFC: Universal-Commerce-Protocol/ucp#800

This PR introduces the export-openapi subcommand and library API to ucp-schema, providing the reference compiler implementation for the UCP Interface-Definition Strategy RFC.

It compiles raw UCP JSON Schemas (Draft 2020-12) into a canonical, self-contained OpenAPI 3.1.0 specification. This provides the ecosystem with standardized client/server stubs and strongly-typed DTOs, retiring >3,500 lines of brittle AST preprocessing scripts currently maintained across language SDKs (python-sdk, js-sdk).


Core Compiler Pipeline (5 Lowering Passes)

  1. Pass 1 — Scoped $defs Hoisting (mod.rs, normalizer.rs):
    • Hoists internal definitions to components.schemas with <Parent><Def> qualification, eliminating naming collisions while rewriting # self-refs and distributing properties into bare anyOf constraint unions.
  2. Pass 2 — Capability Composition & Directional Slicing (mod.rs, normalizer.rs):
    • Composes active capability extensions (discount.json, fulfillment.json) into root resources (checkout.json, cart.json).
    • Slices schemas by (direction, op) into explicit models (CheckoutCreateRequest, CheckoutUpdateRequest, CheckoutCompleteRequest, Checkout).
    • Prunes empty container schemas (CatalogLookup, CatalogSearch, Pagination) from the registry.
  3. Pass 3 — Directional Reference Alignment (mod.rs):
    • Recursively traverses sliced request models, oneOf unions, and discriminator.mapping dictionaries (e.g. updating LocationDestination to LocationDestinationCreateRequest).
  4. Pass 4 — Polymorphic Discriminator Synthesis (discriminator.rs):
    • Synthesizes explicit OpenAPI 3.1 discriminator.mapping tables from conditional allOf + if/then branches.
  5. Pass 5 — Wire Protocol Projection (operations.rs):
    • Projects canonical routes (/checkout-sessions, /carts, /orders, /catalog) driven by x-ucp-path and x-ucp-lifecycle.
    • Attaches standard protocol parameters (UCP-Agent, Idempotency-Key) and RFC 9421 HTTP Message Signature security schemes.

Built for Zero-Maintenance Extensibility

To eliminate ongoing maintenance overhead as UCP evolves, export-openapi is 100% schema-driven:

  • Zero-Touch for New Verticals: Single-object resources and RPC container operations are detected structurally (is_container_schema, is_extension_schema, {op}_request conventions). Adding new verticals (Lodging, Services) requires zero Rust compiler code edits.
  • Declarative Routing: Canonical paths are governed in schema space via "x-ucp-path" (e.g. "x-ucp-path": "/checkout-sessions"), decoupling route authorship from compiler code.
  • Graph-Driven Profile Down-Scoping: Profiles (--profile shopping) compute transitive $ref dependency closures rather than static file allowlists, ensuring new schema properties are automatically resolved.
  • Hermetic CI Drift Gate: export-openapi --check verifies byte-for-byte spec parity in GitHub Actions, preventing schema drift with zero human review overhead.

Verification & Test Status

  • Cargo Test: 126/126 passed across all unit, conformance, container, and OpenAPI integration tests (tests/export_openapi_test.rs).
  • Cargo Clippy: 0 warnings (cargo clippy --all-targets -- -D warnings).
  • Cryptographic Determinism: Independently compiled against Universal-Commerce-Protocol/ucp with byte-for-byte identical SHA-256 (02668cd8...), zero broken $refs across 447 references, 172 component schemas, and 12 canonical REST routes.

@gsmith85
gsmith85 force-pushed the feat/export-openapi branch 2 times, most recently from db7343b to 7529723 Compare September 2, 2026 21:53
Add `export-openapi` subcommand and library API to compile canonical UCP
JSON Schemas (Draft 2020-12) into valid, self-contained OpenAPI 3.1.0 specifications.

Key capabilities:
- Capability extension composition (`compose_extension_into_root`, `merge_extension_object`)
  prior to directional slicing, seamlessly merging extensions (e.g. discounts, fulfillment)
- Directional Request & Response model slicing (`CreateRequest`, `UpdateRequest`, etc.)
- Dynamic container capability inspection (`{op}_request`/`{op}_response`) and routing
- Transitive `$ref` reachability graph calculation with canonical URL resolution for down-scoping
- Two-pass `$defs` hoisting with `<Parent><Def>` namespacing, eliminating collision race conditions
- Synthesizes polymorphic `discriminator` mappings from `allOf` + `if`/`then` conditionals
- Distributes properties across bare `anyOf` branches (e.g. `ValueConstraint`)
- Zero domain hardcoding: schema-driven `x-ucp-path` routing and `x-ucp-lifecycle` action synthesis
- Projects normative REST operations (`POST`, `GET`, `PUT`) with standard UCP headers and RFC 9421 security
- Supports declarative `x-ucp-path` overrides and RFC 8615 well-known discovery (`GET /.well-known/ucp`)
- Provides declarative `--check` mode to enforce zero drift in CI
- Public library ergonomics (`OpenApiExportOptions` alias, safe error typing, CLI exit codes)
Greg Smith added 3 commits September 10, 2026 03:08
…ype discovery

- Add hoist_inline_conditional_variants to hoist inline if/then conditional variants into discrete components with discriminator mapping
- Add register_extended_subtypes to discover and register decentralized allOf extension subtypes without modifying upstream schemas
- Replace multi-pass disk reads in export_openapi with single-pass in-memory AST loading
- Clean profile reachability to use pure AST metadata (reverse-domain segments, $id URI segments, x-ucp-profile)
- Generalize capability group extraction to handle arbitrary reverse-domain namespace depths and operation annotations
Adds fallback routing for Checkout (/checkout-sessions) and lifecycle actions (cancel) when declarative x-ucp-* schema extensions are not yet present in upstream schemas. Tracked for removal via ucp#817.
…al variant hoisting

- Strip properties and required on union schemas when lowered to oneOf, preventing code generators from synthesizing wrapper classes with null property overrides
- Refine hoist_inline_conditional_variants to scope hoisting to abstract discriminator bases or multi-branch variants, keeping concrete resource models intact
- Ensure discriminator properties are marked required in synthesized variant schemas
@vishkaty

Copy link
Copy Markdown

Ran this against release/2026-08-25 to see what the intermediate
representation preserves. The semantic keywords all survive, including the ones
an SDK generator normally drops: not 2 to 4, contains 2 to 6,
propertyNames 14 to 22, if/then 16 to 25, dependentRequired 2 to 4,
counts rising because request and response variants expand.

One property comes through narrower than it is authored. capability.json
declares extends as a choice:

"extends": { "oneOf": [
  { "$ref": "common/types/reverse_domain_name.json" },
  { "type": "array", "items": { "$ref": "common/types/reverse_domain_name.json" }, "minItems": 1 }
]}

export-openapi emits the array branch only:

"extends": { "type": "array", "items": { "$ref": "#/components/schemas/ReverseDomainName" } }

Validating three documents against the exported CapabilityBase:

input exported IR
"extends": "dev.ucp.shopping.checkout" rejected, not of type array
"extends": ["dev.ucp.shopping.cart", ...] accepted
"extends": [] accepted

The first is the specification example at
docs/specification/shopping/checkout/mcp.md:58, and the third is the
minItems bound going missing. Both forms are handled in @ucp-js/sdk today
through #55 and #63.

The same scalar or array union shape appears once more, on
transports/jsonrpc.json request.params, though I found no published example
there that would be affected.

Happy to open a pull request if the fix direction is obvious to you, or to
leave it if the exporter is meant to normalise unions and the consumers are
expected to widen them again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants