Repository navigation
feat(codegen): lower open string discriminator unions into ordered anyOf with <Union>Base - #87
Conversation
63a971e to
8ebe945
Compare
33e031a to
233fbe2
Compare
233fbe2 to
9971a9c
Compare
a9d9753 to
57d290f
Compare
7940a92 to
cd8c9b3
Compare
57d290f to
63aa281
Compare
63aa281 to
831c2df
Compare
ff6d9c1 to
792c4ed
Compare
831c2df to
2d102e4
Compare
damaz91
left a comment
There was a problem hiding this comment.
Clean implementation of Stage 7 conditional union lowering (OU-1 through OU-4), $ref discriminator inlining on <Union>Base, and request-slice *Base pruning! Left a few inline notes on edge cases and clippy lints.
2d102e4 to
b338761
Compare
792c4ed to
61c5803
Compare
b338761 to
6ff6704
Compare
61c5803 to
5df86d0
Compare
6ff6704 to
6ccf31b
Compare
6ccf31b to
aca7679
Compare
84e4665 to
68e158b
Compare
aca7679 to
e2d223a
Compare
68e158b to
7f79b07
Compare
e2d223a to
816d836
Compare
7f79b07 to
592e88a
Compare
592e88a to
f0bdaea
Compare
5767e71 to
f53e59e
Compare
f0bdaea to
a8b8d6d
Compare
ptiper
left a comment
There was a problem hiding this comment.
Approving. The OU-1 to OU-4 rules hold on every union in the corpus, and inlining the $ref string constraints on <Union>Base is a nice touch.
I used an AI assistant (Antigravity) to help with this review. It compared this PR against its own base branch (feat/codegen-ast-reify), not main, and I ran the full test suite and clippy on the branch: all green. The comments below are about union shapes that do not exist today but could show up as new verticals are added.
Two inline comments, each a small fix plus a test. One smaller note here so it does not get lost:
- If a union's
allOfhas even one branch that is not anif/then.$refvariant (say one extraif/thenvalue rule),extract_conditional_union_branchesreturnsNoneand the whole union quietly keeps its oldallOfform, with noBaseand none of theOU-*handling. A safe default, but invisible. Either lower the variant branches and copy the other branches into each variant and intoBase, or add a corpus test that fails if any$defsentry still has anallOfbranch withthen.$refafter Stage 7.
All fine to defer if you want to land the stack first; say so and I will open issues to track them.
| "required", | ||
| "type", | ||
| "additionalProperties", | ||
| "dependentRequired", |
There was a problem hiding this comment.
dependentRequired is removed from the parent here, but inherit_parent_into_union_variant only copies properties and required into each variant. build_union_base_schema does keep it on <Union>Base. So after lowering, a rule like "if hours is set, timezone is required" still applies to unknown tags but no longer to the known variants, which is the opposite of what the author meant.
It does not show today because the only unions with dependentRequired (FulfillmentMethod*) have variants that were hoisted in Stage 4 and already copied it, and the test at generate_types_test.rs:1589 only checks the Base. Any union whose variants are separate $defs or files (ShippingDestination, MediaImage, CancellationItem, or a future one) would lose it.
Suggestion: in inherit_parent_into_union_variant, also copy the parent's dependentRequired (dropping entries already covered by the variant's required, as build_hoisted_variant_schema does). minProperties / maxProperties / propertyNames could travel the same way. A unit test with a $ref variant and a parent dependentRequired would pin it.
There was a problem hiding this comment.
Good catch. Extracted prune_satisfied_dependent_required in ba88937 so both Stage 4 and Stage 7's inherit_parent_into_union_variant merge dependentRequired (pruning entries already satisfied by the variant's required list) along with minProperties, maxProperties, and propertyNames, covered by lower_conditional_unions_handles_partial_enums_dependent_required_and_non_variant_allof.
| ); | ||
| } | ||
|
|
||
| let open_disc_target = is_open_string_discriminator(&parent_schema, &disc_prop, defs); |
There was a problem hiding this comment.
When the discriminator is a closed enum, the parent becomes anyOf: [<variants>] with no Base. That is only correct if every enum value has a then.$ref branch. With status: ["a","b","c"] and variants for a and b only, { "status": "c" } was valid before (no if matched, parent properties applied) and is invalid after (no anyOf branch matches).
No union in today's corpus is closed, so nothing changes now, but "a few special shapes out of a longer status enum" is a very common pattern for new verticals.
Suggestion: when the enum has values with no variant, emit a <Union>Base whose discriminator is { "type": "string", "enum": [<the remaining values>] } and append it last, like the open case. If every value has a variant, keep the current behaviour. Adding a third enum value to the existing ClosedUnion unit test would cover it.
There was a problem hiding this comment.
Done in ba88937. classify_union_discriminator now returns UnionBaseKind::PartialEnum when a closed enum has uncovered tags and emits <Union>Base with enum: [<remaining_tags>] appended last in anyOf (and omits <Union>Base when every enum tag is covered).
f53e59e to
e9bad62
Compare
0211640 to
b35f537
Compare
e9bad62 to
b7b8c96
Compare
a211ebb to
a741904
Compare
11d30b1 to
0941a3f
Compare
…yOf with <Union>Base
a741904 to
eb1bdf1
Compare
…rt partial enum union bases
eb1bdf1 to
ba88937
Compare
|
Thanks @ptiper! Addressed all three points in
|
Working...
Summary
Implements Task 7 (PR 3b) of the two-layer
ucp-schemacode-generation pipeline: open string discriminator union lowering (lower_conditional_unionsinsrc/codegen/reify.rs), stacked on #86.Changes
allOfif/then.$refUnion Lowering (src/codegen/reify.rs):allOfbranches consist ofif: { properties: { <disc>: { const | enum([single]) } } }paired withthen: { "$ref": "#/$defs/<Variant>" }.propertiesandrequiredinto each referenced variant definition, removes any back-reference{"$ref": "#/$defs/<ParentName>"}from the variant'sallOf, sets the variant's discriminator property to{"type": "string", "const": "<tag>"}, and strips"default"from variant discriminator properties (invariantOU-4).OU-1throughOU-4):type == "string"withoutconstorenum, or a$refto such a string schema likeReverseDomainName), synthesizes<ParentName>Basein$defswith all base properties,additionalProperties: true, and<disc>: { "type": "string", ..., "not": { "enum": [<sorted_known_tags>] } }(invariantOU-2), and strips"default"from the base discriminator property (invariantOU-4).<ParentName>to an orderedanyOfcontaining sorted$refs to the known variants followed by{"$ref": "#/$defs/<ParentName>Base"}as the final fallback branch (invariantOU-1), without emitting the OpenAPI"discriminator"keyword (invariantOU-3).enumorconst), emitsanyOfwith only the sorted variant$refs and no<ParentName>Basedefinition.Stacked PR Series Roadmap
$refrewriting (src/codegen/normalizer.rs)$refalignment (src/codegen/normalizer.rs)generate_types()library API (src/codegen/{mod,reachability,hoist,compose}.rs)generate-typessubcommand (src/bin/ucp-schema.rs,tests/generate_types_test.rs).well-known/ucp--profilescoping (src/codegen/profile.rs)src/codegen/normalizer.rs,src/codegen/reify.rs)<Union>Base) (src/codegen/reify.rs)src/codegen/reify.rs)generate_models.sh,postprocess_models.py,src/ucp_sdk/models/)generate_models.sh,scripts/generate-zod-from-types.mjs,src/spec_generated.ts)ucp-schema generate-openapi,src/openapi/mod.rs)Test Plan
cargo fmt --checkcargo test --all-targetscargo clippy --all-targets -- -D warnings