From b20ebea9e9ea86155f14046d7288d03f343fa3b0 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 15 Sep 2026 16:24:36 -0400 Subject: [PATCH 1/2] feat(http-specs): add union extends scenario matrix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...on-extends-scenarios-2026-8-15-16-13-34.md | 16 + ...on-extends-scenarios-2026-8-15-16-13-36.md | 7 + packages/http-specs/spec-summary.md | 1586 +++++++++++++++++ .../specs/type/union/extends/README.md | 138 ++ .../specs/type/union/extends/composition.tsp | 147 ++ .../specs/type/union/extends/constraints.tsp | 256 +++ .../specs/type/union/extends/controls.tsp | 300 ++++ .../specs/type/union/extends/main.tsp | 76 + .../specs/type/union/extends/mockapi.ts | 246 +++ .../specs/type/union/extends/models.tsp | 570 ++++++ .../specs/type/union/extends/tspconfig.yaml | 3 + .../specs/type/union/extends/usage.tsp | 218 +++ .../spector/src/actions/validate-mock-apis.ts | 10 +- packages/spector/src/scenarios-resolver.ts | 19 +- .../src/spec-utils/compile-scenario.ts | 28 + packages/spector/src/spec-utils/index.ts | 1 + packages/spector/test/scenario-config.test.ts | 250 +++ 17 files changed, 3857 insertions(+), 14 deletions(-) create mode 100644 .chronus/changes/union-extends-scenarios-2026-8-15-16-13-34.md create mode 100644 .chronus/changes/union-extends-scenarios-2026-8-15-16-13-36.md create mode 100644 packages/http-specs/specs/type/union/extends/README.md create mode 100644 packages/http-specs/specs/type/union/extends/composition.tsp create mode 100644 packages/http-specs/specs/type/union/extends/constraints.tsp create mode 100644 packages/http-specs/specs/type/union/extends/controls.tsp create mode 100644 packages/http-specs/specs/type/union/extends/main.tsp create mode 100644 packages/http-specs/specs/type/union/extends/mockapi.ts create mode 100644 packages/http-specs/specs/type/union/extends/models.tsp create mode 100644 packages/http-specs/specs/type/union/extends/tspconfig.yaml create mode 100644 packages/http-specs/specs/type/union/extends/usage.tsp create mode 100644 packages/spector/src/spec-utils/compile-scenario.ts create mode 100644 packages/spector/test/scenario-config.test.ts diff --git a/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-34.md b/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-34.md new file mode 100644 index 00000000000..a08b9d21bb2 --- /dev/null +++ b/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-34.md @@ -0,0 +1,16 @@ +--- +changeKind: feature +packages: + - "@typespec/http-specs" +--- + +Add union extends Spector scenarios covering structural constraints, model reuse, serialization formats, and exact request/response fixtures. + +```typespec +model Named { name: string; } +model Cat extends Named { meow: boolean; } +model Dog { name: string; bark: boolean; } +union Pet extends Named { cat: Cat, dog: Dog } +``` + +The matrix includes representative unions without `extends` for language API comparisons. \ No newline at end of file diff --git a/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-36.md b/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-36.md new file mode 100644 index 00000000000..03bb8b129fb --- /dev/null +++ b/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-36.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/spector" +--- + +Resolve nearest and inherited TypeSpec configuration when validating scenarios and mock APIs, while keeping emitters disabled. \ No newline at end of file diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 9c7f6485dc6..e2c68dedbf2 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -9422,6 +9422,1592 @@ Expected request to send body: } ``` +### Type_Union_Extends_Ancestry_Envelope_get + +- Endpoint: `get /type/union/extends/ancestry/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Kitten","age":1,"meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","origin":"shelter","bark":false}} + +### Type_Union_Extends_Ancestry_Envelope_put + +- Endpoint: `put /type/union/extends/ancestry/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Kitten","age":1,"meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","origin":"shelter","bark":false}} + +### Type_Union_Extends_Ancestry_Inline_get + +- Endpoint: `get /type/union/extends/ancestry/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Kitten","age":1,"meow":true}; dog: {"kind":"dog","name":"Rex","origin":"shelter","bark":false} + +### Type_Union_Extends_Ancestry_Inline_put + +- Endpoint: `put /type/union/extends/ancestry/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Kitten","age":1,"meow":true}; dog: {"kind":"dog","name":"Rex","origin":"shelter","bark":false} + +### Type_Union_Extends_Ancestry_Untagged_get + +- Endpoint: `get /type/union/extends/ancestry/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Kitten","age":1,"meow":true}; dog: {"name":"Rex","origin":"shelter","bark":false} + +### Type_Union_Extends_Ancestry_Untagged_put + +- Endpoint: `put /type/union/extends/ancestry/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Kitten","age":1,"meow":true}; dog: {"name":"Rex","origin":"shelter","bark":false} + +### Type_Union_Extends_ArrayConstraint_Envelope_get + +- Endpoint: `get /type/union/extends/array/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cats: {"kind":"cats","value":[{"name":"Whiskers","meow":true}]}; dogs: {"kind":"dogs","value":[{"name":"Rex","bark":false}]} + +### Type_Union_Extends_ArrayConstraint_Envelope_put + +- Endpoint: `put /type/union/extends/array/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cats: {"kind":"cats","value":[{"name":"Whiskers","meow":true}]}; dogs: {"kind":"dogs","value":[{"name":"Rex","bark":false}]} + +### Type_Union_Extends_ArrayConstraint_Untagged_get + +- Endpoint: `get /type/union/extends/array/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cats: [{"name":"Whiskers","meow":true}]; dogs: [{"name":"Rex","bark":false}] + +### Type_Union_Extends_ArrayConstraint_Untagged_put + +- Endpoint: `put /type/union/extends/array/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cats: [{"name":"Whiskers","meow":true}]; dogs: [{"name":"Rex","bark":false}] + +### Type_Union_Extends_ClosedScalar_Envelope_get + +- Endpoint: `get /type/union/extends/closed-scalar/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} + +### Type_Union_Extends_ClosedScalar_Envelope_put + +- Endpoint: `put /type/union/extends/closed-scalar/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} + +### Type_Union_Extends_ClosedScalar_Untagged_get + +- Endpoint: `get /type/union/extends/closed-scalar/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +start: "start"; stop: "stop" + +### Type_Union_Extends_ClosedScalar_Untagged_put + +- Endpoint: `put /type/union/extends/closed-scalar/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +start: "start"; stop: "stop" + +### Type_Union_Extends_Controls_ClosedScalar_Envelope_get + +- Endpoint: `get /type/union/extends/controls/closed-scalar/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} + +### Type_Union_Extends_Controls_ClosedScalar_Envelope_put + +- Endpoint: `put /type/union/extends/controls/closed-scalar/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} + +### Type_Union_Extends_Controls_ClosedScalar_Untagged_get + +- Endpoint: `get /type/union/extends/controls/closed-scalar/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +start: "start"; stop: "stop" + +### Type_Union_Extends_Controls_ClosedScalar_Untagged_put + +- Endpoint: `put /type/union/extends/controls/closed-scalar/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +start: "start"; stop: "stop" + +### Type_Union_Extends_Controls_DirectInheritance_Envelope_get + +- Endpoint: `get /type/union/extends/controls/direct-inheritance/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_Controls_DirectInheritance_Envelope_put + +- Endpoint: `put /type/union/extends/controls/direct-inheritance/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_Controls_DirectInheritance_Inline_get + +- Endpoint: `get /type/union/extends/controls/direct-inheritance/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_Controls_DirectInheritance_Inline_put + +- Endpoint: `put /type/union/extends/controls/direct-inheritance/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_Controls_DirectInheritance_Untagged_get + +- Endpoint: `get /type/union/extends/controls/direct-inheritance/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_Controls_DirectInheritance_Untagged_put + +- Endpoint: `put /type/union/extends/controls/direct-inheritance/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_Controls_ExplicitDefault_Inline_get + +- Endpoint: `get /type/union/extends/controls/explicit-default/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} + +### Type_Union_Extends_Controls_ExplicitDefault_Inline_put + +- Endpoint: `put /type/union/extends/controls/explicit-default/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} + +### Type_Union_Extends_Controls_NestedVariant_Envelope_get + +- Endpoint: `get /type/union/extends/controls/nested-variant/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} + +### Type_Union_Extends_Controls_NestedVariant_Envelope_put + +- Endpoint: `put /type/union/extends/controls/nested-variant/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} + +### Type_Union_Extends_Controls_NestedVariant_Untagged_get + +- Endpoint: `get /type/union/extends/controls/nested-variant/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} + +### Type_Union_Extends_Controls_NestedVariant_Untagged_put + +- Endpoint: `put /type/union/extends/controls/nested-variant/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} + +### Type_Union_Extends_Controls_OpenScalar_Envelope_get + +- Endpoint: `get /type/union/extends/controls/open-scalar/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} + +### Type_Union_Extends_Controls_OpenScalar_Envelope_put + +- Endpoint: `put /type/union/extends/controls/open-scalar/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} + +### Type_Union_Extends_Controls_OpenScalar_Untagged_get + +- Endpoint: `get /type/union/extends/controls/open-scalar/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +known: "known"; custom: "future" + +### Type_Union_Extends_Controls_OpenScalar_Untagged_put + +- Endpoint: `put /type/union/extends/controls/open-scalar/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +known: "known"; custom: "future" + +### Type_Union_Extends_Controls_SharedSameBase_First_Envelope_get + +- Endpoint: `get /type/union/extends/controls/shared-same-base/first/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_Controls_SharedSameBase_First_Envelope_put + +- Endpoint: `put /type/union/extends/controls/shared-same-base/first/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_Controls_SharedSameBase_First_Inline_get + +- Endpoint: `get /type/union/extends/controls/shared-same-base/first/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_Controls_SharedSameBase_First_Inline_put + +- Endpoint: `put /type/union/extends/controls/shared-same-base/first/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_Controls_SharedSameBase_First_Untagged_get + +- Endpoint: `get /type/union/extends/controls/shared-same-base/first/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_Controls_SharedSameBase_First_Untagged_put + +- Endpoint: `put /type/union/extends/controls/shared-same-base/first/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_Controls_SharedSameBase_Second_Envelope_get + +- Endpoint: `get /type/union/extends/controls/shared-same-base/second/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} + +### Type_Union_Extends_Controls_SharedSameBase_Second_Envelope_put + +- Endpoint: `put /type/union/extends/controls/shared-same-base/second/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} + +### Type_Union_Extends_Controls_SharedSameBase_Second_Inline_get + +- Endpoint: `get /type/union/extends/controls/shared-same-base/second/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} + +### Type_Union_Extends_Controls_SharedSameBase_Second_Inline_put + +- Endpoint: `put /type/union/extends/controls/shared-same-base/second/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} + +### Type_Union_Extends_Controls_SharedSameBase_Second_Untagged_get + +- Endpoint: `get /type/union/extends/controls/shared-same-base/second/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} + +### Type_Union_Extends_Controls_SharedSameBase_Second_Untagged_put + +- Endpoint: `put /type/union/extends/controls/shared-same-base/second/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} + +### Type_Union_Extends_Controls_Structural_Envelope_get + +- Endpoint: `get /type/union/extends/controls/structural/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} + +### Type_Union_Extends_Controls_Structural_Envelope_put + +- Endpoint: `put /type/union/extends/controls/structural/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} + +### Type_Union_Extends_Controls_Structural_Inline_get + +- Endpoint: `get /type/union/extends/controls/structural/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} + +### Type_Union_Extends_Controls_Structural_Inline_put + +- Endpoint: `put /type/union/extends/controls/structural/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} + +### Type_Union_Extends_Controls_Structural_Untagged_get + +- Endpoint: `get /type/union/extends/controls/structural/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} + +### Type_Union_Extends_Controls_Structural_Untagged_put + +- Endpoint: `put /type/union/extends/controls/structural/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} + +### Type_Union_Extends_Conversion_Direct_get + +- Endpoint: `get /type/union/extends/conversion/direct/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"} + +### Type_Union_Extends_Conversion_Direct_put + +- Endpoint: `put /type/union/extends/conversion/direct/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"} + +### Type_Union_Extends_Conversion_Envelope_get + +- Endpoint: `get /type/union/extends/conversion/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +data: {"dataType":"data","payload":{"display_name":"Data","createdAt":1704067200,"content":"-_8"}}; count: {"dataType":"count","payload":{"display_name":"Count","createdAt":1704067200,"count":3}} + +### Type_Union_Extends_Conversion_Envelope_put + +- Endpoint: `put /type/union/extends/conversion/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +data: {"dataType":"data","payload":{"display_name":"Data","createdAt":1704067200,"content":"-_8"}}; count: {"dataType":"count","payload":{"display_name":"Count","createdAt":1704067200,"count":3}} + +### Type_Union_Extends_Conversion_Inline_get + +- Endpoint: `get /type/union/extends/conversion/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +data: {"dataType":"data","display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"dataType":"count","display_name":"Count","createdAt":1704067200,"count":3} + +### Type_Union_Extends_Conversion_Inline_put + +- Endpoint: `put /type/union/extends/conversion/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +data: {"dataType":"data","display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"dataType":"count","display_name":"Count","createdAt":1704067200,"count":3} + +### Type_Union_Extends_Conversion_Untagged_get + +- Endpoint: `get /type/union/extends/conversion/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"display_name":"Count","createdAt":1704067200,"count":3} + +### Type_Union_Extends_Conversion_Untagged_put + +- Endpoint: `put /type/union/extends/conversion/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"display_name":"Count","createdAt":1704067200,"count":3} + +### Type_Union_Extends_DirectInheritance_Envelope_get + +- Endpoint: `get /type/union/extends/direct-inheritance/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_DirectInheritance_Envelope_put + +- Endpoint: `put /type/union/extends/direct-inheritance/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_DirectInheritance_Inline_get + +- Endpoint: `get /type/union/extends/direct-inheritance/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_DirectInheritance_Inline_put + +- Endpoint: `put /type/union/extends/direct-inheritance/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_DirectInheritance_Untagged_get + +- Endpoint: `get /type/union/extends/direct-inheritance/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_DirectInheritance_Untagged_put + +- Endpoint: `put /type/union/extends/direct-inheritance/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_DisjointSameBase_First_Envelope_get + +- Endpoint: `get /type/union/extends/disjoint-same-base/first/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_DisjointSameBase_First_Envelope_put + +- Endpoint: `put /type/union/extends/disjoint-same-base/first/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_DisjointSameBase_First_Inline_get + +- Endpoint: `get /type/union/extends/disjoint-same-base/first/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_DisjointSameBase_First_Inline_put + +- Endpoint: `put /type/union/extends/disjoint-same-base/first/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_DisjointSameBase_First_Untagged_get + +- Endpoint: `get /type/union/extends/disjoint-same-base/first/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_DisjointSameBase_First_Untagged_put + +- Endpoint: `put /type/union/extends/disjoint-same-base/first/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_DisjointSameBase_Second_Envelope_get + +- Endpoint: `get /type/union/extends/disjoint-same-base/second/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +bird: {"kind":"bird","value":{"name":"Sky","wings":2}}; fish: {"kind":"fish","value":{"name":"Gold","fins":3}} + +### Type_Union_Extends_DisjointSameBase_Second_Envelope_put + +- Endpoint: `put /type/union/extends/disjoint-same-base/second/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +bird: {"kind":"bird","value":{"name":"Sky","wings":2}}; fish: {"kind":"fish","value":{"name":"Gold","fins":3}} + +### Type_Union_Extends_DisjointSameBase_Second_Inline_get + +- Endpoint: `get /type/union/extends/disjoint-same-base/second/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +bird: {"kind":"bird","name":"Sky","wings":2}; fish: {"kind":"fish","name":"Gold","fins":3} + +### Type_Union_Extends_DisjointSameBase_Second_Inline_put + +- Endpoint: `put /type/union/extends/disjoint-same-base/second/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +bird: {"kind":"bird","name":"Sky","wings":2}; fish: {"kind":"fish","name":"Gold","fins":3} + +### Type_Union_Extends_DisjointSameBase_Second_Untagged_get + +- Endpoint: `get /type/union/extends/disjoint-same-base/second/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +bird: {"name":"Sky","wings":2}; fish: {"name":"Gold","fins":3} + +### Type_Union_Extends_DisjointSameBase_Second_Untagged_put + +- Endpoint: `put /type/union/extends/disjoint-same-base/second/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +bird: {"name":"Sky","wings":2}; fish: {"name":"Gold","fins":3} + +### Type_Union_Extends_EnumConstraint_Envelope_get + +- Endpoint: `get /type/union/extends/enum/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +left: {"kind":"left","value":"left"}; right: {"kind":"right","value":"right"} + +### Type_Union_Extends_EnumConstraint_Envelope_put + +- Endpoint: `put /type/union/extends/enum/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +left: {"kind":"left","value":"left"}; right: {"kind":"right","value":"right"} + +### Type_Union_Extends_EnumConstraint_Untagged_get + +- Endpoint: `get /type/union/extends/enum/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +left: "left"; right: "right" + +### Type_Union_Extends_EnumConstraint_Untagged_put + +- Endpoint: `put /type/union/extends/enum/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +left: "left"; right: "right" + +### Type_Union_Extends_ExplicitBase_Envelope_get + +- Endpoint: `get /type/union/extends/explicit-base/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; base: {"kind":"base","value":{"name":"Base"}} + +### Type_Union_Extends_ExplicitBase_Envelope_put + +- Endpoint: `put /type/union/extends/explicit-base/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; base: {"kind":"base","value":{"name":"Base"}} + +### Type_Union_Extends_ExplicitBase_Inline_get + +- Endpoint: `get /type/union/extends/explicit-base/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; base: {"kind":"base","name":"Base"} + +### Type_Union_Extends_ExplicitBase_Inline_put + +- Endpoint: `put /type/union/extends/explicit-base/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; base: {"kind":"base","name":"Base"} + +### Type_Union_Extends_ExplicitBase_Untagged_get + +- Endpoint: `get /type/union/extends/explicit-base/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; base: {"name":"Base"} + +### Type_Union_Extends_ExplicitBase_Untagged_put + +- Endpoint: `put /type/union/extends/explicit-base/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; base: {"name":"Base"} + +### Type_Union_Extends_ExplicitDefault_Inline_get + +- Endpoint: `get /type/union/extends/explicit-default/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} + +### Type_Union_Extends_ExplicitDefault_Inline_put + +- Endpoint: `put /type/union/extends/explicit-default/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} + +### Type_Union_Extends_IntersectionConstraint_Envelope_get + +- Endpoint: `get /type/union/extends/intersection/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +first: {"kind":"first","value":{"name":"First","id":1,"text":"hello"}}; second: {"kind":"second","value":{"name":"Second","id":2,"count":3}} + +### Type_Union_Extends_IntersectionConstraint_Envelope_put + +- Endpoint: `put /type/union/extends/intersection/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +first: {"kind":"first","value":{"name":"First","id":1,"text":"hello"}}; second: {"kind":"second","value":{"name":"Second","id":2,"count":3}} + +### Type_Union_Extends_IntersectionConstraint_Inline_get + +- Endpoint: `get /type/union/extends/intersection/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +first: {"kind":"first","name":"First","id":1,"text":"hello"}; second: {"kind":"second","name":"Second","id":2,"count":3} + +### Type_Union_Extends_IntersectionConstraint_Inline_put + +- Endpoint: `put /type/union/extends/intersection/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +first: {"kind":"first","name":"First","id":1,"text":"hello"}; second: {"kind":"second","name":"Second","id":2,"count":3} + +### Type_Union_Extends_IntersectionConstraint_Untagged_get + +- Endpoint: `get /type/union/extends/intersection/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +first: {"name":"First","id":1,"text":"hello"}; second: {"name":"Second","id":2,"count":3} + +### Type_Union_Extends_IntersectionConstraint_Untagged_put + +- Endpoint: `put /type/union/extends/intersection/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +first: {"name":"First","id":1,"text":"hello"}; second: {"name":"Second","id":2,"count":3} + +### Type_Union_Extends_NestedVariant_Envelope_get + +- Endpoint: `get /type/union/extends/nested-variant/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} + +### Type_Union_Extends_NestedVariant_Envelope_put + +- Endpoint: `put /type/union/extends/nested-variant/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} + +### Type_Union_Extends_NestedVariant_Untagged_get + +- Endpoint: `get /type/union/extends/nested-variant/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} + +### Type_Union_Extends_NestedVariant_Untagged_put + +- Endpoint: `put /type/union/extends/nested-variant/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} + +### Type_Union_Extends_Nullable_Envelope_get + +- Endpoint: `get /type/union/extends/nullable/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}}; null: null + +### Type_Union_Extends_Nullable_Envelope_put + +- Endpoint: `put /type/union/extends/nullable/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}}; null: null + +### Type_Union_Extends_Nullable_Inline_get + +- Endpoint: `get /type/union/extends/nullable/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; null: null + +### Type_Union_Extends_Nullable_Inline_put + +- Endpoint: `put /type/union/extends/nullable/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; null: null + +### Type_Union_Extends_Nullable_Untagged_get + +- Endpoint: `get /type/union/extends/nullable/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; null: null + +### Type_Union_Extends_Nullable_Untagged_put + +- Endpoint: `put /type/union/extends/nullable/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; null: null + +### Type_Union_Extends_OpenScalar_Envelope_get + +- Endpoint: `get /type/union/extends/open-scalar/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} + +### Type_Union_Extends_OpenScalar_Envelope_put + +- Endpoint: `put /type/union/extends/open-scalar/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} + +### Type_Union_Extends_OpenScalar_Untagged_get + +- Endpoint: `get /type/union/extends/open-scalar/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +known: "known"; custom: "future" + +### Type_Union_Extends_OpenScalar_Untagged_put + +- Endpoint: `put /type/union/extends/open-scalar/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +known: "known"; custom: "future" + +### Type_Union_Extends_Recursive_Envelope_get + +- Endpoint: `get /type/union/extends/recursive/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +leaf: {"kind":"leaf","value":{"name":"Leaf","value":1}}; branch: {"kind":"branch","value":{"name":"Branch","children":[{"kind":"leaf","value":{"name":"Leaf","value":1}}]}} + +### Type_Union_Extends_Recursive_Envelope_put + +- Endpoint: `put /type/union/extends/recursive/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +leaf: {"kind":"leaf","value":{"name":"Leaf","value":1}}; branch: {"kind":"branch","value":{"name":"Branch","children":[{"kind":"leaf","value":{"name":"Leaf","value":1}}]}} + +### Type_Union_Extends_Recursive_Untagged_get + +- Endpoint: `get /type/union/extends/recursive/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +leaf: {"name":"Leaf","value":1}; branch: {"name":"Branch","children":[{"name":"Leaf","value":1}]} + +### Type_Union_Extends_Recursive_Untagged_put + +- Endpoint: `put /type/union/extends/recursive/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +leaf: {"name":"Leaf","value":1}; branch: {"name":"Branch","children":[{"name":"Leaf","value":1}]} + +### Type_Union_Extends_SharedDifferentBases_ById_Envelope_get + +- Endpoint: `get /type/union/extends/shared-different-bases/identified/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +entity: {"entityType":"entity","entityData":{"name":"Shared","id":7,"active":true}}; other: {"entityType":"other","entityData":{"id":8,"code":"other"}} + +### Type_Union_Extends_SharedDifferentBases_ById_Envelope_put + +- Endpoint: `put /type/union/extends/shared-different-bases/identified/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +entity: {"entityType":"entity","entityData":{"name":"Shared","id":7,"active":true}}; other: {"entityType":"other","entityData":{"id":8,"code":"other"}} + +### Type_Union_Extends_SharedDifferentBases_ById_Inline_get + +- Endpoint: `get /type/union/extends/shared-different-bases/identified/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +entity: {"entityType":"entity","name":"Shared","id":7,"active":true}; other: {"entityType":"other","id":8,"code":"other"} + +### Type_Union_Extends_SharedDifferentBases_ById_Inline_put + +- Endpoint: `put /type/union/extends/shared-different-bases/identified/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +entity: {"entityType":"entity","name":"Shared","id":7,"active":true}; other: {"entityType":"other","id":8,"code":"other"} + +### Type_Union_Extends_SharedDifferentBases_ById_Untagged_get + +- Endpoint: `get /type/union/extends/shared-different-bases/identified/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +entity: {"name":"Shared","id":7,"active":true}; other: {"id":8,"code":"other"} + +### Type_Union_Extends_SharedDifferentBases_ById_Untagged_put + +- Endpoint: `put /type/union/extends/shared-different-bases/identified/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +entity: {"name":"Shared","id":7,"active":true}; other: {"id":8,"code":"other"} + +### Type_Union_Extends_SharedDifferentBases_ByName_Envelope_get + +- Endpoint: `get /type/union/extends/shared-different-bases/named/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +shared: {"kind":"shared","value":{"name":"Shared","id":7,"active":true}}; cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}} + +### Type_Union_Extends_SharedDifferentBases_ByName_Envelope_put + +- Endpoint: `put /type/union/extends/shared-different-bases/named/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +shared: {"kind":"shared","value":{"name":"Shared","id":7,"active":true}}; cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}} + +### Type_Union_Extends_SharedDifferentBases_ByName_Inline_get + +- Endpoint: `get /type/union/extends/shared-different-bases/named/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +shared: {"kind":"shared","name":"Shared","id":7,"active":true}; cat: {"kind":"cat","name":"Whiskers","meow":true} + +### Type_Union_Extends_SharedDifferentBases_ByName_Inline_put + +- Endpoint: `put /type/union/extends/shared-different-bases/named/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +shared: {"kind":"shared","name":"Shared","id":7,"active":true}; cat: {"kind":"cat","name":"Whiskers","meow":true} + +### Type_Union_Extends_SharedDifferentBases_ByName_Untagged_get + +- Endpoint: `get /type/union/extends/shared-different-bases/named/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +shared: {"name":"Shared","id":7,"active":true}; cat: {"name":"Whiskers","meow":true} + +### Type_Union_Extends_SharedDifferentBases_ByName_Untagged_put + +- Endpoint: `put /type/union/extends/shared-different-bases/named/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +shared: {"name":"Shared","id":7,"active":true}; cat: {"name":"Whiskers","meow":true} + +### Type_Union_Extends_SharedSameBase_First_Envelope_get + +- Endpoint: `get /type/union/extends/shared-same-base/first/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_SharedSameBase_First_Envelope_put + +- Endpoint: `put /type/union/extends/shared-same-base/first/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_SharedSameBase_First_Inline_get + +- Endpoint: `get /type/union/extends/shared-same-base/first/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_SharedSameBase_First_Inline_put + +- Endpoint: `put /type/union/extends/shared-same-base/first/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_SharedSameBase_First_Untagged_get + +- Endpoint: `get /type/union/extends/shared-same-base/first/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_SharedSameBase_First_Untagged_put + +- Endpoint: `put /type/union/extends/shared-same-base/first/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_SharedSameBase_Second_Envelope_get + +- Endpoint: `get /type/union/extends/shared-same-base/second/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} + +### Type_Union_Extends_SharedSameBase_Second_Envelope_put + +- Endpoint: `put /type/union/extends/shared-same-base/second/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} + +### Type_Union_Extends_SharedSameBase_Second_Inline_get + +- Endpoint: `get /type/union/extends/shared-same-base/second/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} + +### Type_Union_Extends_SharedSameBase_Second_Inline_put + +- Endpoint: `put /type/union/extends/shared-same-base/second/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} + +### Type_Union_Extends_SharedSameBase_Second_Untagged_get + +- Endpoint: `get /type/union/extends/shared-same-base/second/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} + +### Type_Union_Extends_SharedSameBase_Second_Untagged_put + +- Endpoint: `put /type/union/extends/shared-same-base/second/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} + +### Type_Union_Extends_SpreadAndIs_Envelope_get + +- Endpoint: `get /type/union/extends/spread-and-is/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_SpreadAndIs_Envelope_put + +- Endpoint: `put /type/union/extends/spread-and-is/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_SpreadAndIs_Inline_get + +- Endpoint: `get /type/union/extends/spread-and-is/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_SpreadAndIs_Inline_put + +- Endpoint: `put /type/union/extends/spread-and-is/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_SpreadAndIs_Untagged_get + +- Endpoint: `get /type/union/extends/spread-and-is/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_SpreadAndIs_Untagged_put + +- Endpoint: `put /type/union/extends/spread-and-is/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_Structural_Envelope_get + +- Endpoint: `get /type/union/extends/structural/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} + +### Type_Union_Extends_Structural_Envelope_put + +- Endpoint: `put /type/union/extends/structural/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} + +### Type_Union_Extends_Structural_Inline_get + +- Endpoint: `get /type/union/extends/structural/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} + +### Type_Union_Extends_Structural_Inline_put + +- Endpoint: `put /type/union/extends/structural/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} + +### Type_Union_Extends_Structural_Untagged_get + +- Endpoint: `get /type/union/extends/structural/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} + +### Type_Union_Extends_Structural_Untagged_put + +- Endpoint: `put /type/union/extends/structural/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} + +### Type_Union_Extends_TemplateInstance_Envelope_get + +- Endpoint: `get /type/union/extends/template-instance/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +first: {"kind":"first","value":{"item":"one","label":"First"}}; second: {"kind":"second","value":{"item":"two","active":true}} + +### Type_Union_Extends_TemplateInstance_Envelope_put + +- Endpoint: `put /type/union/extends/template-instance/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +first: {"kind":"first","value":{"item":"one","label":"First"}}; second: {"kind":"second","value":{"item":"two","active":true}} + +### Type_Union_Extends_TemplateInstance_Inline_get + +- Endpoint: `get /type/union/extends/template-instance/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +first: {"kind":"first","item":"one","label":"First"}; second: {"kind":"second","item":"two","active":true} + +### Type_Union_Extends_TemplateInstance_Inline_put + +- Endpoint: `put /type/union/extends/template-instance/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +first: {"kind":"first","item":"one","label":"First"}; second: {"kind":"second","item":"two","active":true} + +### Type_Union_Extends_TemplateInstance_Untagged_get + +- Endpoint: `get /type/union/extends/template-instance/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +first: {"item":"one","label":"First"}; second: {"item":"two","active":true} + +### Type_Union_Extends_TemplateInstance_Untagged_put + +- Endpoint: `put /type/union/extends/template-instance/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +first: {"item":"one","label":"First"}; second: {"item":"two","active":true} + +### Type_Union_Extends_UnionAndDirect_Direct_get + +- Endpoint: `get /type/union/extends/union-and-direct/direct/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true} + +### Type_Union_Extends_UnionAndDirect_Direct_put + +- Endpoint: `put /type/union/extends/union-and-direct/direct/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true} + +### Type_Union_Extends_UnionAndDirect_Envelope_get + +- Endpoint: `get /type/union/extends/union-and-direct/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_UnionAndDirect_Envelope_put + +- Endpoint: `put /type/union/extends/union-and-direct/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_UnionAndDirect_Inline_get + +- Endpoint: `get /type/union/extends/union-and-direct/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_UnionAndDirect_Inline_put + +- Endpoint: `put /type/union/extends/union-and-direct/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_UnionAndDirect_Properties_get + +- Endpoint: `get /type/union/extends/union-and-direct/properties/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Whiskers","meow":true},"inline":{"kind":"cat","name":"Whiskers","meow":true},"envelope":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; +dog: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Rex","bark":false},"inline":{"kind":"dog","name":"Rex","bark":false},"envelope":{"kind":"dog","value":{"name":"Rex","bark":false}}} + +### Type_Union_Extends_UnionAndDirect_Properties_put + +- Endpoint: `put /type/union/extends/union-and-direct/properties/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Whiskers","meow":true},"inline":{"kind":"cat","name":"Whiskers","meow":true},"envelope":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; +dog: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Rex","bark":false},"inline":{"kind":"dog","name":"Rex","bark":false},"envelope":{"kind":"dog","value":{"name":"Rex","bark":false}}} + +### Type_Union_Extends_UnionAndDirect_Untagged_get + +- Endpoint: `get /type/union/extends/union-and-direct/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_UnionAndDirect_Untagged_put + +- Endpoint: `put /type/union/extends/union-and-direct/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_UnionConstraint_Envelope_get + +- Endpoint: `get /type/union/extends/union-constraint/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_UnionConstraint_Envelope_put + +- Endpoint: `put /type/union/extends/union-constraint/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + +### Type_Union_Extends_UnionConstraint_Inline_get + +- Endpoint: `get /type/union/extends/union-constraint/inline/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_UnionConstraint_Inline_put + +- Endpoint: `put /type/union/extends/union-constraint/inline/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + +### Type_Union_Extends_UnionConstraint_Untagged_get + +- Endpoint: `get /type/union/extends/union-constraint/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_UnionConstraint_Untagged_put + +- Endpoint: `put /type/union/extends/union-constraint/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + +### Type_Union_Extends_UnionExpressionConstraint_Envelope_get + +- Endpoint: `get /type/union/extends/union-expression-constraint/envelope/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +text: {"kind":"text","value":"hello"}; number: {"kind":"number","value":42} + +### Type_Union_Extends_UnionExpressionConstraint_Envelope_put + +- Endpoint: `put /type/union/extends/union-expression-constraint/envelope/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +text: {"kind":"text","value":"hello"}; number: {"kind":"number","value":42} + +### Type_Union_Extends_UnionExpressionConstraint_Untagged_get + +- Endpoint: `get /type/union/extends/union-expression-constraint/untagged/{choice}` + +Call once for EVERY choice below. Each choice returns the corresponding JSON +body with status 200 and Content-Type application/json. Access the +variant-specific data; ambiguous untagged shapes do not require a unique class. +text: "hello"; number: 42 + +### Type_Union_Extends_UnionExpressionConstraint_Untagged_put + +- Endpoint: `put /type/union/extends/union-expression-constraint/untagged/{choice}` + +Call once for EVERY choice below. Send the corresponding exact JSON body +with Content-Type application/json. Expect the same JSON body with status 200 +and Content-Type application/json. Retain all inherited and specific fields. +text: "hello"; number: 42 + ### Type_Union_FloatsOnly_get - Endpoint: `get /type/union/floats-only` diff --git a/packages/http-specs/specs/type/union/extends/README.md b/packages/http-specs/specs/type/union/extends/README.md new file mode 100644 index 00000000000..65a521e6843 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/README.md @@ -0,0 +1,138 @@ +# Union extends matrix + +These scenarios ask how a language represents a constrained union, its base and +variants, and reuse of those variants. `union U extends Base` is a **structural +assignability constraint**. It does not require model inheritance, automatically +include the base, include all descendants, open the union, or change the wire +format. The fixtures intentionally do not enforce proposed nominal restrictions. + +## Running a case + +Every format interface has `get` and `put` operations with a finite `choice` +path parameter. Call **every** documented choice. GET returns its exact fixture; +PUT requires that exact decoded JSON and returns the same fixture. All successful +responses are 200, with `Content-Type: application/json; charset=utf-8`. +`@scenarioDoc` in the `.tsp` files specifies each selector and JSON body; +[`mockapi.ts`](./mockapi.ts) implements it. + +For example, UE01/U maps to +`Type_Union_Extends_DirectInheritance_Untagged_get` and `_put`, with concrete +paths `/type/union/extends/direct-inheritance/untagged/cat` and `/dog`. +The union remains the operation's body/response type; the selector does not narrow +that public type. A `passOnSuccess` scenario requires every concrete endpoint, +not just the first successful variant. + +There are **88 format/context groups, 176 scenarios and 370 concrete method/path +endpoints** (185 GET and 185 PUT). Of these, 19 groups / 38 scenarios are matched +without-extends controls. Mock validation proves the wire contract, **not** +generated class identity, type narrowing, union-membership APIs, or restrictions +inherent in a particular language representation. + +## Primary coverage + +U = untagged JSON; I = inline `@discriminated(#{ envelope: "none" })`; +E = object-envelope `@discriminated`. Checkmarks cover every alternative, in both +directions. Namespace names below follow `Type.Union.Extends`; each format adds +`Untagged`, `Inline`, or `Envelope`, then `get` / `put`. + +| ID | Namespace and purpose | U | I | E | +| ----- | ------------------------------------------------------------------------------------ | ----------------------------- | ---------------------------------------- | ------------------------------------ | +| UE01 | `DirectInheritance`: Cat and Dog directly inherit Named | Yes | Yes | Yes | +| UE02 | `Structural`: neither variant inherits Named; U also has an overlapping shape | Yes | Yes | Yes | +| UE03 | `SpreadAndIs`: spread and `model is DogShape` variants | Yes | Yes | Yes | +| UE04 | `Ancestry`: transitive base/intermediate fields and different compatible ancestry | Yes | Yes | Yes | +| UE05 | `SharedSameBase.First/Second`: Cat reused in two unions, same base | Both | Both, different tags/property names | Both, different tags/property names | +| UE06 | `DisjointSameBase.First/Second`: same base, disjoint variant sets | Both | Both | Both | +| UE07 | `SharedDifferentBases.ByName/ById`: shared model satisfies two different named bases | Both | Both, different tags/property names | Both, different tags/property names | +| UE08 | `UnionAndDirect`: model reused in union and directly | Yes | Yes | Yes | +| UE09 | `ExplicitBase`: base is an explicitly named alternative | Yes | Yes | Yes | +| UE10 | `ExplicitDefault`: one unnamed default retains unknown tag and modeled data | N/A, no tag/default semantics | Yes | Deferred, wire semantics unspecified | +| UE11a | `NestedVariant`: nested union alternative and outer sibling | Yes | N/A, named Union-valued variant rejected | Yes, nested envelopes | +| UE11b | `UnionConstraint`: named union is the constraint, not an alternative | Yes | Yes | Yes | +| UE11c | `UnionExpressionConstraint`: `string \| int32` constraint | Yes | N/A, non-Model variants | Yes | +| UE12a | `ClosedScalar`: closed string literals | Yes, JSON strings | N/A, non-Model variants | Yes | +| UE12b | `OpenScalar`: literal plus explicit string branch | Yes, JSON strings | N/A, non-Model variants | Yes | +| UE12c | `EnumConstraint`: enum constraint and both members | Yes, JSON strings | N/A, non-Model variants | Yes | +| UE12d | `ArrayConstraint`: arrays of both model alternatives | Yes, JSON arrays | Outside inline-object profile; see below | Yes, array payloads | +| UE12e | `IntersectionConstraint`: intersection of two named models | Yes | Yes | Yes | +| UE12f | `TemplateInstance`: concrete named model template instance constraint | Yes | Yes | Yes | + +[Model cases](./models.tsp) contain UE01-07/09; +[usage cases](./usage.tsp) contain UE08; +[composition cases](./composition.tsp) contain UE10-11; +[constraint cases](./constraints.tsp) contain UE12. + +UE06 declares an additional `Lizard` descendant but does not include it in any +union. Selectors in each context include only that context's fixtures. UE05 uses +the **same Cat model** with `kind: "cat"` versus `petType: "feline"` inline, +and `kind/value` versus `animal/data` envelopes. UE07 independently varies +`kind/value` versus `entityType/entityData` while retaining both `name` and `id`. +UE08 also has `Direct` and `Properties` groups: a direct Cat must have neither +the union's injected discriminator nor its envelope. + +### Without-extends controls + +[`controls.tsp`](./controls.tsp) retains the matched variant models, tags, +operation shapes and fixture values, but omits union extends clauses. Names add +`Controls` after `Type.Union.Extends`, and routes add `/controls` after +`/type/union/extends`. + +| Control | Pairing | Formats / contexts | +| -------------------------------------- | ------- | ---------------------------------------------------- | +| C01 `Controls.DirectInheritance` | UE01 | U/I/E | +| C02 `Controls.Structural` | UE02 | U/I/E, including U overlap | +| C05 `Controls.SharedSameBase` | UE05 | U/I/E, First and Second | +| C10 `Controls.ExplicitDefault` | UE10 | I only; E deferred with UE10 | +| C11 `Controls.NestedVariant` | UE11a | U/E, nested controls use the controlled inner unions | +| C12 `Controls.ClosedScalar/OpenScalar` | UE12a/b | U/E | + +These are representative controls, not a full second Cartesian product. Missing +controls for other primary rows mean reduced duplication, not compiler N/A. + +### Secondary axes + +| Namespace | Coverage | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | +| `Nullable` | U/I/E, outer `U \| null`, both non-null alternatives and actual JSON null; omitted bodies are not null | +| `Recursive` | U/E, finite branch containing leaf, plus standalone leaf | +| `Conversion` | U/I/E plus direct Data; inherited `display_name`, Unix timestamp `1704067200` (2024-01-01T00:00:00Z), base64url `-_8` (bytes FB FF) | + +Versioning and malformed/missing-tag resilience are deferred. Wrong closed-union +tags are not defined as valid service responses. Request rejection checks enforce +the authored fixtures; they do not introduce new resilience scenarios. + +## Excluded or unresolved combinations + +- Direct/aliased anonymous model-expression bases are compiler-invalid. Use + declared named bases instead; intersections and named template instances have + separate valid coverage. See the compiler's + [union constraint tests](../../../../../compiler/test/checker/union.test.ts). +- Named non-Model variants in inline unions are rejected by the + [discriminator helper](../../../../../compiler/src/core/helpers/discriminator-utils.ts). + Arrays are compiler Models, so **inline arrays are not labeled + compiler-invalid**. They are outside this inline-object wire profile and need + a separate wire design. +- Object-envelope defaults, including their no-extends control, are **deferred + because their wire semantics are unspecified**, not a normative prohibition. + [OpenAPI 3.2's envelope branch](../../../../../openapi3/src/schema-emitter-3-2.ts) + explicitly says default handling with envelopes is not yet specified; the + [parent schema emitter](../../../../../openapi3/src/schema-emitter.ts) currently + wraps named variants only. That omission alone does not establish whether a + default should represent the payload or the complete envelope. + +**Open design question:** For an object-envelope default, does the default type +describe the complete unknown envelope or only its payload, and how should its +union constraint apply? No guessed wire fixture is included. + +UE10 inline has `UnknownInline extends Named { kind: string; extra: +Record }` as its single unnamed default. It retains `kind: "dragon"`, +the required base `name`, and all nested `extra` data. This is different from +UE09's named `base` alternative and from opening a closed union by implication. + +## Scoped feature configuration + +Only this folder opts into `union-extends` via its project `tspconfig.yaml`. +Spector resolves nearest/inherited config for both scenario and mock validation; +configured imports and linter settings are honored. Validation still forces +no-emit and warnings-as-errors, with configured emitters disabled. The existing +empty http-specs package config stops lookup for ordinary sibling scenarios. diff --git a/packages/http-specs/specs/type/union/extends/composition.tsp b/packages/http-specs/specs/type/union/extends/composition.tsp new file mode 100644 index 00000000000..7ad77511d31 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/composition.tsp @@ -0,0 +1,147 @@ +using Http; +using Spector; + +namespace Type.Union.Extends; + +/** UE10: An explicit unnamed default, not an implicitly inserted base alternative. */ +@route("/explicit-default") +namespace ExplicitDefault { + model UnknownInline extends Named { + kind: string; + extra: Record; + } + + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + cat: Cat, + dog: Dog, + UnknownInline, + } + + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog" | "unknown", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} + """ + > {} +} + +/** UE11a: A union-valued alternative, including every nested leaf. */ +@route("/nested-variant") +namespace NestedVariant { + union UntaggedUnion extends Named { + pet: DirectInheritance.UntaggedUnion, + bird: Bird, + } + @discriminated + union EnvelopeUnion extends Named { + pet: DirectInheritance.EnvelopeUnion, + bird: Bird, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog" | "bird", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog" | "bird", + """ + cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} + """ + > {} +} + +/** UE11b: A union constraint is distinct from a union-valued alternative. */ +@route("/union-constraint") +namespace UnionConstraint { + union Allowed { + cat: Cat, + dog: Dog, + } + union UntaggedUnion extends Allowed { + cat: Cat, + dog: Dog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Allowed { + cat: Cat, + dog: Dog, + } + @discriminated + union EnvelopeUnion extends Allowed { + cat: Cat, + dog: Dog, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + """ + > {} +} + +/** UE11c: A union expression constrains two different scalar kinds. */ +@route("/union-expression-constraint") +namespace UnionExpressionConstraint { + union UntaggedUnion extends string | int32 { + text: string, + number: int32, + } + @discriminated + union EnvelopeUnion extends string | int32 { + text: string, + number: int32, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "text" | "number", + """ + text: "hello"; number: 42 + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "text" | "number", + """ + text: {"kind":"text","value":"hello"}; number: {"kind":"number","value":42} + """ + > {} +} diff --git a/packages/http-specs/specs/type/union/extends/constraints.tsp b/packages/http-specs/specs/type/union/extends/constraints.tsp new file mode 100644 index 00000000000..e122e3f0ae3 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/constraints.tsp @@ -0,0 +1,256 @@ +using Http; +using Spector; + +namespace Type.Union.Extends; + +/** UE12a: The scalar constraint does not make literal alternatives open. */ +@route("/closed-scalar") +namespace ClosedScalar { + union UntaggedUnion extends string { + start: "start", + stop: "stop", + } + @discriminated + union EnvelopeUnion extends string { + start: "start", + stop: "stop", + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "start" | "stop", + """ + start: "start"; stop: "stop" + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "start" | "stop", + """ + start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} + """ + > {} +} + +/** UE12b: Openness comes from the explicitly listed string variant. */ +@route("/open-scalar") +namespace OpenScalar { + union UntaggedUnion extends string { + known: "known", + custom: string, + } + @discriminated + union EnvelopeUnion extends string { + known: "known", + custom: string, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "known" | "custom", + """ + known: "known"; custom: "future" + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "known" | "custom", + """ + known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} + """ + > {} +} + +/** UE12c: Enum members satisfy their declared enum constraint. */ +@route("/enum") +namespace EnumConstraint { + enum Direction { + left, + right, + } + union UntaggedUnion extends Direction { + left: Direction.left, + right: Direction.right, + } + @discriminated + union EnvelopeUnion extends Direction { + left: Direction.left, + right: Direction.right, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "left" | "right", + """ + left: "left"; right: "right" + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "left" | "right", + """ + left: {"kind":"left","value":"left"}; right: {"kind":"right","value":"right"} + """ + > {} +} + +/** UE12d: Array bodies are not synthetic object models or inline discriminators. */ +@route("/array") +namespace ArrayConstraint { + union UntaggedUnion extends Named[] { + cats: Cat[], + dogs: Dog[], + } + @discriminated + union EnvelopeUnion extends Named[] { + cats: Cat[], + dogs: Dog[], + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cats" | "dogs", + """ + cats: [{"name":"Whiskers","meow":true}]; dogs: [{"name":"Rex","bark":false}] + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cats" | "dogs", + """ + cats: {"kind":"cats","value":[{"name":"Whiskers","meow":true}]}; dogs: {"kind":"dogs","value":[{"name":"Rex","bark":false}]} + """ + > {} +} + +/** UE12e: An intersection of named models, not an anonymous model-expression base. */ +@route("/intersection") +namespace IntersectionConstraint { + model Identified { + id: int32; + } + model First { + name: string; + id: int32; + text: string; + } + model Second { + name: string; + id: int32; + count: int32; + } + union UntaggedUnion extends Named & Identified { + first: First, + second: Second, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named & Identified { + first: First, + second: Second, + } + @discriminated + union EnvelopeUnion extends Named & Identified { + first: First, + second: Second, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "first" | "second", + """ + first: {"name":"First","id":1,"text":"hello"}; second: {"name":"Second","id":2,"count":3} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "first" | "second", + """ + first: {"kind":"first","name":"First","id":1,"text":"hello"}; second: {"kind":"second","name":"Second","id":2,"count":3} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "first" | "second", + """ + first: {"kind":"first","value":{"name":"First","id":1,"text":"hello"}}; second: {"kind":"second","value":{"name":"Second","id":2,"count":3}} + """ + > {} +} + +/** UE12f: A concrete named model template instance supplies the constraint. */ +@route("/template-instance") +namespace TemplateInstance { + model Wrapper { + item: T; + } + model First extends Wrapper { + label: string; + } + model Second extends Wrapper { + active: boolean; + } + union UntaggedUnion extends Wrapper { + first: First, + second: Second, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Wrapper { + first: First, + second: Second, + } + @discriminated + union EnvelopeUnion extends Wrapper { + first: First, + second: Second, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "first" | "second", + """ + first: {"item":"one","label":"First"}; second: {"item":"two","active":true} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "first" | "second", + """ + first: {"kind":"first","item":"one","label":"First"}; second: {"kind":"second","item":"two","active":true} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "first" | "second", + """ + first: {"kind":"first","value":{"item":"one","label":"First"}}; second: {"kind":"second","value":{"item":"two","active":true}} + """ + > {} +} diff --git a/packages/http-specs/specs/type/union/extends/controls.tsp b/packages/http-specs/specs/type/union/extends/controls.tsp new file mode 100644 index 00000000000..82cd273be98 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/controls.tsp @@ -0,0 +1,300 @@ +using Http; +using Spector; + +/** Matched controls omit only the corresponding union extends clauses. */ +@route("/controls") +namespace Type.Union.Extends.Controls; + +@route("/direct-inheritance") +namespace DirectInheritance { + union UntaggedUnion { + cat: Cat, + dog: Dog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion { + cat: Cat, + dog: Dog, + } + @discriminated + union EnvelopeUnion { + cat: Cat, + dog: Dog, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + """ + > {} +} + +@route("/structural") +namespace Structural { + union UntaggedUnion { + quiet: Extends.Structural.Quiet, + calm: Extends.Structural.Calm, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion { + quiet: Extends.Structural.Quiet, + calm: Extends.Structural.Calm, + } + @discriminated + union EnvelopeUnion { + quiet: Extends.Structural.Quiet, + calm: Extends.Structural.Calm, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "quiet" | "calm" | "overlap", + """ + quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "quiet" | "calm", + """ + quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "quiet" | "calm", + """ + quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} + """ + > {} +} + +@route("/shared-same-base") +namespace SharedSameBase { + @route("/first") + namespace First { + union UntaggedUnion { + cat: Cat, + dog: Dog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion { + cat: Cat, + dog: Dog, + } + @discriminated + union EnvelopeUnion { + cat: Cat, + dog: Dog, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + """ + > {} + } + @route("/second") + namespace Second { + union UntaggedUnion { + feline: Cat, + bird: Bird, + } + @discriminated(#{ envelope: "none", discriminatorPropertyName: "petType" }) + union InlineUnion { + feline: Cat, + bird: Bird, + } + @discriminated(#{ discriminatorPropertyName: "animal", envelopePropertyName: "data" }) + union EnvelopeUnion { + feline: Cat, + bird: Bird, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "feline" | "bird", + """ + feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "feline" | "bird", + """ + feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "feline" | "bird", + """ + feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} + """ + > {} + } +} + +@route("/explicit-default") +namespace ExplicitDefault { + @discriminated(#{ envelope: "none" }) + union InlineUnion { + cat: Cat, + dog: Dog, + Extends.ExplicitDefault.UnknownInline, + } + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog" | "unknown", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} + """ + > {} +} + +@route("/nested-variant") +namespace NestedVariant { + union UntaggedUnion { + pet: DirectInheritance.UntaggedUnion, + bird: Bird, + } + @discriminated + union EnvelopeUnion { + pet: DirectInheritance.EnvelopeUnion, + bird: Bird, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog" | "bird", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog" | "bird", + """ + cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} + """ + > {} +} + +@route("/closed-scalar") +namespace ClosedScalar { + union UntaggedUnion { + start: "start", + stop: "stop", + } + @discriminated + union EnvelopeUnion { + start: "start", + stop: "stop", + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "start" | "stop", + """ + start: "start"; stop: "stop" + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "start" | "stop", + """ + start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} + """ + > {} +} + +@route("/open-scalar") +namespace OpenScalar { + union UntaggedUnion { + known: "known", + custom: string, + } + @discriminated + union EnvelopeUnion { + known: "known", + custom: string, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "known" | "custom", + """ + known: "known"; custom: "future" + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "known" | "custom", + """ + known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} + """ + > {} +} diff --git a/packages/http-specs/specs/type/union/extends/main.tsp b/packages/http-specs/specs/type/union/extends/main.tsp new file mode 100644 index 00000000000..ef4481ca56f --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/main.tsp @@ -0,0 +1,76 @@ +import "@typespec/http"; +import "@typespec/spector"; +import "./models.tsp"; +import "./composition.tsp"; +import "./constraints.tsp"; +import "./controls.tsp"; +import "./usage.tsp"; + +using Http; +using Spector; + +/** + * Union extends constrains variant assignability, not inheritance or serialization. + * UE case IDs map to the coverage table in README.md. + */ +@scenarioService("/type/union/extends") +namespace Type.Union.Extends; + +model JsonResponse { + @statusCode statusCode: 200; + @header contentType: "application/json"; + @body body: T; +} + +interface GetAndPut { + @scenario + @scenarioDoc(""" + Call once for EVERY choice below. Each choice returns the corresponding JSON + body with status 200 and Content-Type application/json. Access the + variant-specific data; ambiguous untagged shapes do not require a unique class. + ${Fixtures} + """) + @get + @route("/{choice}") + get(@path choice: Choice): JsonResponse; + + @scenario + @scenarioDoc(""" + Call once for EVERY choice below. Send the corresponding exact JSON body + with Content-Type application/json. Expect the same JSON body with status 200 + and Content-Type application/json. Retain all inherited and specific fields. + ${Fixtures} + """) + @put + @route("/{choice}") + put( + @path choice: Choice, + @body body: T, + @header contentType: "application/json", + ): JsonResponse; +} + +model Named { + name: string; +} + +model Cat extends Named { + meow: boolean; +} + +model Dog extends Named { + bark: boolean; +} + +model Bird extends Named { + wings: int32; +} + +model Fish extends Named { + fins: int32; +} + +// Deliberately not a variant of any of the unions constrained by Named. +model Lizard extends Named { + scales: boolean; +} diff --git a/packages/http-specs/specs/type/union/extends/mockapi.ts b/packages/http-specs/specs/type/union/extends/mockapi.ts new file mode 100644 index 00000000000..010b551e714 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/mockapi.ts @@ -0,0 +1,246 @@ +import type { MockApiDefinition, ScenarioMockApi } from "@typespec/spec-api"; +import { json, passOnSuccess } from "@typespec/spec-api"; + +export const Scenarios: Record = {}; + +type Fixtures = Record; +type ModelFixtures = Record>; + +function define(name: string, route: string, fixtures: Fixtures) { + for (const method of ["get", "put"] as const) { + const apis: MockApiDefinition[] = Object.entries(fixtures).map(([choice, value]) => { + const response = { + status: 200, + headers: { "Content-Type": "application/json; charset=utf-8" }, + body: json(value), + }; + return { + kind: "MockApiDefinition", + uri: `/type/union/extends/${route}/${choice}`, + method, + request: + method === "put" + ? { headers: { "Content-Type": "application/json" }, body: json(value) } + : {}, + response, + handler: + method === "put" + ? (req) => { + req.expect.containsHeader("content-type", "application/json"); + // Compare decoded JSON without coercion; null is a body, not an omitted body. + req.expect.deepEqual(req.body, value, "Unexpected union payload"); + return response; + } + : undefined, + }; + }); + // Distinct concrete paths require every choice, not just one successful response. + Scenarios[`Type_Union_Extends_${name}_${method}`] = passOnSuccess(apis); + } +} + +function inline(fixtures: ModelFixtures, discriminator = "kind"): ModelFixtures { + return Object.fromEntries( + Object.entries(fixtures).map(([tag, value]) => [tag, { [discriminator]: tag, ...value }]), + ); +} + +function envelope(fixtures: Fixtures, discriminator = "kind", property = "value"): ModelFixtures { + return Object.fromEntries( + Object.entries(fixtures).map(([tag, value]) => [ + tag, + { [discriminator]: tag, [property]: value }, + ]), + ); +} + +function models( + name: string, + route: string, + fixtures: ModelFixtures, + inlineDiscriminator = "kind", + envelopeDiscriminator = "kind", + envelopeProperty = "value", +) { + define(`${name}_Untagged`, `${route}/untagged`, fixtures); + define(`${name}_Inline`, `${route}/inline`, inline(fixtures, inlineDiscriminator)); + define( + `${name}_Envelope`, + `${route}/envelope`, + envelope(fixtures, envelopeDiscriminator, envelopeProperty), + ); +} + +function values(name: string, route: string, fixtures: Fixtures) { + define(`${name}_Untagged`, `${route}/untagged`, fixtures); + define(`${name}_Envelope`, `${route}/envelope`, envelope(fixtures)); +} + +const cat = { name: "Whiskers", meow: true }; +const dog = { name: "Rex", bark: false }; +const bird = { name: "Sky", wings: 2 }; +const fish = { name: "Gold", fins: 3 }; +const pets = { cat, dog }; + +// UE01, UE03, UE05 (first context), UE06 (first context), UE08 and matched controls. +for (const [name, route] of [ + ["DirectInheritance", "direct-inheritance"], + ["SpreadAndIs", "spread-and-is"], + ["SharedSameBase_First", "shared-same-base/first"], + ["DisjointSameBase_First", "disjoint-same-base/first"], + ["UnionAndDirect", "union-and-direct"], + ["Controls_DirectInheritance", "controls/direct-inheritance"], + ["Controls_SharedSameBase_First", "controls/shared-same-base/first"], +]) { + models(name, route, pets); +} + +// UE02: Optional specific properties create one deliberately ambiguous untagged value. +const structural = { + quiet: { name: "Quiet", volume: 2 }, + calm: { name: "Calm", asleep: true }, +}; +for (const [name, route] of [ + ["Structural", "structural"], + ["Controls_Structural", "controls/structural"], +]) { + define(`${name}_Untagged`, `${route}/untagged`, { + ...structural, + overlap: { name: "Both" }, + }); + define(`${name}_Inline`, `${route}/inline`, inline(structural)); + define(`${name}_Envelope`, `${route}/envelope`, envelope(structural)); +} + +// UE04: Neither intermediate nor alternate-ancestor fields may disappear. +models("Ancestry", "ancestry", { + cat: { name: "Kitten", age: 1, meow: true }, + dog: { name: "Rex", origin: "shelter", bark: false }, +}); + +// UE05: The exact same Cat has different tags and property names in the second union. +for (const [name, route] of [ + ["SharedSameBase_Second", "shared-same-base/second"], + ["Controls_SharedSameBase_Second", "controls/shared-same-base/second"], +]) { + models(name, route, { feline: cat, bird }, "petType", "animal", "data"); +} + +// UE06: No cat/dog endpoints in the second union, nor bird/fish in the first. +models("DisjointSameBase_Second", "disjoint-same-base/second", { bird, fish }); + +// UE07: Both name and id must survive in both structurally constrained contexts. +const shared = { name: "Shared", id: 7, active: true }; +models("SharedDifferentBases_ByName", "shared-different-bases/named", { shared, cat }); +models( + "SharedDifferentBases_ById", + "shared-different-bases/identified", + { entity: shared, other: { id: 8, code: "other" } }, + "entityType", + "entityType", + "entityData", +); + +// UE08: Direct model uses have neither a union tag nor an envelope. +define("UnionAndDirect_Direct", "union-and-direct/direct", { cat }); +define( + "UnionAndDirect_Properties", + "union-and-direct/properties", + Object.fromEntries( + Object.entries(pets).map(([tag, pet]) => [ + tag, + { + direct: cat, + untagged: pet, + inline: { kind: tag, ...pet }, + envelope: { kind: tag, value: pet }, + }, + ]), + ), +); + +// UE09: Base is an explicit named variant, not a default or an implicit alternative. +models("ExplicitBase", "explicit-base", { cat, base: { name: "Base" } }); + +// UE10: Only the explicitly modeled inline default admits this unknown tag and data. +const defaults = { + ...inline(pets), + unknown: { + kind: "dragon", + name: "Future", + extra: { color: "gold", size: 3, nested: { active: true } }, + }, +}; +define("ExplicitDefault_Inline", "explicit-default/inline", defaults); +define("Controls_ExplicitDefault_Inline", "controls/explicit-default/inline", defaults); + +// UE11a: Every nested leaf and the outer sibling have their own concrete choice. +for (const [name, route] of [ + ["NestedVariant", "nested-variant"], + ["Controls_NestedVariant", "controls/nested-variant"], +]) { + define(`${name}_Untagged`, `${route}/untagged`, { ...pets, bird }); + define(`${name}_Envelope`, `${route}/envelope`, { + cat: { kind: "pet", value: { kind: "cat", value: cat } }, + dog: { kind: "pet", value: { kind: "dog", value: dog } }, + bird: { kind: "bird", value: bird }, + }); +} + +// UE11b/c: Named union and union-expression constraints. +models("UnionConstraint", "union-constraint", pets); +values("UnionExpressionConstraint", "union-expression-constraint", { text: "hello", number: 42 }); + +// UE12a/b: Open string branch is explicit; the closed union has no unknown fixture. +for (const [name, route] of [ + ["ClosedScalar", "closed-scalar"], + ["Controls_ClosedScalar", "controls/closed-scalar"], +]) { + values(name, route, { start: "start", stop: "stop" }); +} +for (const [name, route] of [ + ["OpenScalar", "open-scalar"], + ["Controls_OpenScalar", "controls/open-scalar"], +]) { + values(name, route, { known: "known", custom: "future" }); +} + +// UE12c/d/e/f: Preserve primitive/array bodies, intersection fields and template data. +values("EnumConstraint", "enum", { left: "left", right: "right" }); +values("ArrayConstraint", "array", { cats: [cat], dogs: [dog] }); +models("IntersectionConstraint", "intersection", { + first: { name: "First", id: 1, text: "hello" }, + second: { name: "Second", id: 2, count: 3 }, +}); +models("TemplateInstance", "template-instance", { + first: { item: "one", label: "First" }, + second: { item: "two", active: true }, +}); + +define("Nullable_Untagged", "nullable/untagged", { ...pets, null: null }); +define("Nullable_Inline", "nullable/inline", { ...inline(pets), null: null }); +define("Nullable_Envelope", "nullable/envelope", { ...envelope(pets), null: null }); + +const leaf = { name: "Leaf", value: 1 }; +define("Recursive_Untagged", "recursive/untagged", { + leaf, + branch: { name: "Branch", children: [leaf] }, +}); +define("Recursive_Envelope", "recursive/envelope", { + leaf: { kind: "leaf", value: leaf }, + branch: { + kind: "branch", + value: { name: "Branch", children: [{ kind: "leaf", value: leaf }] }, + }, +}); + +const data = { display_name: "Data", createdAt: 1704067200, content: "-_8" }; +models( + "Conversion", + "conversion", + { data, count: { display_name: "Count", createdAt: 1704067200, count: 3 } }, + "dataType", + "dataType", + "payload", +); +define("Conversion_Direct", "conversion/direct", { data }); diff --git a/packages/http-specs/specs/type/union/extends/models.tsp b/packages/http-specs/specs/type/union/extends/models.tsp new file mode 100644 index 00000000000..4c6092c232a --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/models.tsp @@ -0,0 +1,570 @@ +using Http; +using Spector; + +namespace Type.Union.Extends; + +/** UE01: Both alternatives directly inherit the declared base. */ +@route("/direct-inheritance") +namespace DirectInheritance { + union UntaggedUnion extends Named { + cat: Cat, + dog: Dog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + cat: Cat, + dog: Dog, + } + @discriminated + union EnvelopeUnion extends Named { + cat: Cat, + dog: Dog, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + """ + > {} +} + +/** UE02: Neither alternative inherits the base; "overlap" has no unique shape. */ +@route("/structural") +namespace Structural { + model Quiet { + name: string; + volume?: int32; + } + model Calm { + name: string; + asleep?: boolean; + } + union UntaggedUnion extends Named { + quiet: Quiet, + calm: Calm, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + quiet: Quiet, + calm: Calm, + } + @discriminated + union EnvelopeUnion extends Named { + quiet: Quiet, + calm: Calm, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "quiet" | "calm" | "overlap", + """ + quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "quiet" | "calm", + """ + quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "quiet" | "calm", + """ + quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} + """ + > {} +} + +/** UE03: Property composition with spread and model is, not nominal inheritance. */ +@route("/spread-and-is") +namespace SpreadAndIs { + model SpreadCat { + ...Named; + meow: boolean; + } + model DogShape { + ...Named; + bark: boolean; + } + model IsDog is DogShape; + union UntaggedUnion extends Named { + cat: SpreadCat, + dog: IsDog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + cat: SpreadCat, + dog: IsDog, + } + @discriminated + union EnvelopeUnion extends Named { + cat: SpreadCat, + dog: IsDog, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + """ + > {} +} + +/** UE04: Required intermediate properties and a separate compatible ancestry. */ +@route("/ancestry") +namespace Ancestry { + model Mammal extends Named { + age: int32; + } + model YoungCat extends Mammal { + meow: boolean; + } + model OtherAncestor { + name: string; + origin: string; + } + model OtherDog extends OtherAncestor { + bark: boolean; + } + union UntaggedUnion extends Named { + cat: YoungCat, + dog: OtherDog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + cat: YoungCat, + dog: OtherDog, + } + @discriminated + union EnvelopeUnion extends Named { + cat: YoungCat, + dog: OtherDog, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Kitten","age":1,"meow":true}; dog: {"name":"Rex","origin":"shelter","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Kitten","age":1,"meow":true}; dog: {"kind":"dog","name":"Rex","origin":"shelter","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Kitten","age":1,"meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","origin":"shelter","bark":false}} + """ + > {} +} + +/** UE05: Cat is the same model in two unions with the same constraint. */ +@route("/shared-same-base") +namespace SharedSameBase { + @route("/first") + namespace First { + union UntaggedUnion extends Named { + cat: Cat, + dog: Dog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + cat: Cat, + dog: Dog, + } + @discriminated + union EnvelopeUnion extends Named { + cat: Cat, + dog: Dog, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + """ + > {} + } + + @route("/second") + namespace Second { + union UntaggedUnion extends Named { + feline: Cat, + bird: Bird, + } + @discriminated(#{ envelope: "none", discriminatorPropertyName: "petType" }) + union InlineUnion extends Named { + feline: Cat, + bird: Bird, + } + @discriminated(#{ discriminatorPropertyName: "animal", envelopePropertyName: "data" }) + union EnvelopeUnion extends Named { + feline: Cat, + bird: Bird, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "feline" | "bird", + """ + feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "feline" | "bird", + """ + feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "feline" | "bird", + """ + feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} + """ + > {} + } +} + +/** UE06: Union membership is not all descendants of the shared named base. */ +@route("/disjoint-same-base") +namespace DisjointSameBase { + @route("/first") + namespace First { + union UntaggedUnion extends Named { + cat: Cat, + dog: Dog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + cat: Cat, + dog: Dog, + } + @discriminated + union EnvelopeUnion extends Named { + cat: Cat, + dog: Dog, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + """ + > {} + } + + @route("/second") + namespace Second { + union UntaggedUnion extends Named { + bird: Bird, + fish: Fish, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + bird: Bird, + fish: Fish, + } + @discriminated + union EnvelopeUnion extends Named { + bird: Bird, + fish: Fish, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "bird" | "fish", + """ + bird: {"name":"Sky","wings":2}; fish: {"name":"Gold","fins":3} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "bird" | "fish", + """ + bird: {"kind":"bird","name":"Sky","wings":2}; fish: {"kind":"fish","name":"Gold","fins":3} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "bird" | "fish", + """ + bird: {"kind":"bird","value":{"name":"Sky","wings":2}}; fish: {"kind":"fish","value":{"name":"Gold","fins":3}} + """ + > {} + } +} + +/** UE07: A structural variant satisfies two different named base shapes. */ +@route("/shared-different-bases") +namespace SharedDifferentBases { + model Identified { + id: int32; + } + model Shared { + name: string; + id: int32; + active: boolean; + } + model Other { + id: int32; + code: string; + } + + @route("/named") + namespace ByName { + union UntaggedUnion extends Named { + shared: Shared, + cat: Cat, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + shared: Shared, + cat: Cat, + } + @discriminated + union EnvelopeUnion extends Named { + shared: Shared, + cat: Cat, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "shared" | "cat", + """ + shared: {"name":"Shared","id":7,"active":true}; cat: {"name":"Whiskers","meow":true} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "shared" | "cat", + """ + shared: {"kind":"shared","name":"Shared","id":7,"active":true}; cat: {"kind":"cat","name":"Whiskers","meow":true} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "shared" | "cat", + """ + shared: {"kind":"shared","value":{"name":"Shared","id":7,"active":true}}; cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}} + """ + > {} + } + + @route("/identified") + namespace ById { + union UntaggedUnion extends Identified { + entity: Shared, + other: Other, + } + @discriminated(#{ envelope: "none", discriminatorPropertyName: "entityType" }) + union InlineUnion extends Identified { + entity: Shared, + other: Other, + } + @discriminated(#{ discriminatorPropertyName: "entityType", envelopePropertyName: "entityData" }) + union EnvelopeUnion extends Identified { + entity: Shared, + other: Other, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "entity" | "other", + """ + entity: {"name":"Shared","id":7,"active":true}; other: {"id":8,"code":"other"} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "entity" | "other", + """ + entity: {"entityType":"entity","name":"Shared","id":7,"active":true}; other: {"entityType":"other","id":8,"code":"other"} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "entity" | "other", + """ + entity: {"entityType":"entity","entityData":{"name":"Shared","id":7,"active":true}}; other: {"entityType":"other","entityData":{"id":8,"code":"other"}} + """ + > {} + } +} + +/** UE09: The base alternative is explicitly listed, never automatically inserted. */ +@route("/explicit-base") +namespace ExplicitBase { + union UntaggedUnion extends Named { + cat: Cat, + base: Named, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + cat: Cat, + base: Named, + } + @discriminated + union EnvelopeUnion extends Named { + cat: Cat, + base: Named, + } + + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "base", + """ + cat: {"name":"Whiskers","meow":true}; base: {"name":"Base"} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "base", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; base: {"kind":"base","name":"Base"} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "base", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; base: {"kind":"base","value":{"name":"Base"}} + """ + > {} +} diff --git a/packages/http-specs/specs/type/union/extends/tspconfig.yaml b/packages/http-specs/specs/type/union/extends/tspconfig.yaml new file mode 100644 index 00000000000..c4a50e9d0f1 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/tspconfig.yaml @@ -0,0 +1,3 @@ +kind: project +features: + - union-extends diff --git a/packages/http-specs/specs/type/union/extends/usage.tsp b/packages/http-specs/specs/type/union/extends/usage.tsp new file mode 100644 index 00000000000..b77cb5b7763 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/usage.tsp @@ -0,0 +1,218 @@ +using Http; +using Spector; + +namespace Type.Union.Extends; + +/** UE08: Reuse the exact variant model in unions, direct bodies, and properties. */ +@route("/union-and-direct") +namespace UnionAndDirect { + union UntaggedUnion extends Named { + cat: Cat, + dog: Dog, + } + @discriminated(#{ envelope: "none" }) + union InlineUnion extends Named { + cat: Cat, + dog: Dog, + } + @discriminated + union EnvelopeUnion extends Named { + cat: Cat, + dog: Dog, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "cat" | "dog", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "cat" | "dog", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} + """ + > {} + @route("/direct") + interface Direct + extends GetAndPut< + Cat, + "cat", + """ + cat: {"name":"Whiskers","meow":true} + """ + > {} + + model MixedUse { + direct: Cat; + untagged: UntaggedUnion; + inline: InlineUnion; + envelope: EnvelopeUnion; + } + @route("/properties") + interface Properties + extends GetAndPut< + MixedUse, + "cat" | "dog", + """ + cat: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Whiskers","meow":true},"inline":{"kind":"cat","name":"Whiskers","meow":true},"envelope":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; + dog: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Rex","bark":false},"inline":{"kind":"dog","name":"Rex","bark":false},"envelope":{"kind":"dog","value":{"name":"Rex","bark":false}}} + """ + > {} +} + +/** Outer nullability does not add null to the model-constrained union. */ +@route("/nullable") +namespace Nullable { + @route("/untagged") + interface Untagged + extends GetAndPut< + DirectInheritance.UntaggedUnion | null, + "cat" | "dog" | "null", + """ + cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; null: null + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + DirectInheritance.InlineUnion | null, + "cat" | "dog" | "null", + """ + cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; null: null + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + DirectInheritance.EnvelopeUnion | null, + "cat" | "dog" | "null", + """ + cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}}; null: null + """ + > {} +} + +/** A finite recursive fixture includes both the recursive and terminal alternative. */ +@route("/recursive") +namespace Recursive { + model Leaf extends Named { + value: int32; + } + model UntaggedBranch extends Named { + children: UntaggedUnion[]; + } + model EnvelopeBranch extends Named { + children: EnvelopeUnion[]; + } + union UntaggedUnion extends Named { + leaf: Leaf, + branch: UntaggedBranch, + } + @discriminated + union EnvelopeUnion extends Named { + leaf: Leaf, + branch: EnvelopeBranch, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "leaf" | "branch", + """ + leaf: {"name":"Leaf","value":1}; branch: {"name":"Branch","children":[{"name":"Leaf","value":1}]} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "leaf" | "branch", + """ + leaf: {"kind":"leaf","value":{"name":"Leaf","value":1}}; branch: {"kind":"branch","value":{"name":"Branch","children":[{"kind":"leaf","value":{"name":"Leaf","value":1}}]}} + """ + > {} +} + +/** Conversions must survive union/direct reuse without leaking tags or envelopes. */ +@route("/conversion") +namespace Conversion { + model EncodedBase { + @encodedName("application/json", "display_name") + displayName: string; + + @encode(DateTimeKnownEncoding.unixTimestamp, int64) + createdAt: utcDateTime; + } + model Data extends EncodedBase { + @encode(BytesKnownEncoding.base64url) + content: bytes; + } + model Count extends EncodedBase { + count: int32; + } + union UntaggedUnion extends EncodedBase { + data: Data, + count: Count, + } + @discriminated(#{ envelope: "none", discriminatorPropertyName: "dataType" }) + union InlineUnion extends EncodedBase { + data: Data, + count: Count, + } + @discriminated(#{ discriminatorPropertyName: "dataType", envelopePropertyName: "payload" }) + union EnvelopeUnion extends EncodedBase { + data: Data, + count: Count, + } + @route("/untagged") + interface Untagged + extends GetAndPut< + UntaggedUnion, + "data" | "count", + """ + data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"display_name":"Count","createdAt":1704067200,"count":3} + """ + > {} + @route("/inline") + interface Inline + extends GetAndPut< + InlineUnion, + "data" | "count", + """ + data: {"dataType":"data","display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"dataType":"count","display_name":"Count","createdAt":1704067200,"count":3} + """ + > {} + @route("/envelope") + interface Envelope + extends GetAndPut< + EnvelopeUnion, + "data" | "count", + """ + data: {"dataType":"data","payload":{"display_name":"Data","createdAt":1704067200,"content":"-_8"}}; count: {"dataType":"count","payload":{"display_name":"Count","createdAt":1704067200,"count":3}} + """ + > {} + @route("/direct") + interface Direct + extends GetAndPut< + Data, + "data", + """ + data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"} + """ + > {} +} diff --git a/packages/spector/src/actions/validate-mock-apis.ts b/packages/spector/src/actions/validate-mock-apis.ts index b0ded1d0952..b884f983f09 100644 --- a/packages/spector/src/actions/validate-mock-apis.ts +++ b/packages/spector/src/actions/validate-mock-apis.ts @@ -2,6 +2,7 @@ import type { Operation } from "@typespec/compiler"; import pc from "picocolors"; import { logger } from "../logger.js"; import { findScenarioSpecFiles, loadScenarioMockApiFiles } from "../scenarios-resolver.js"; +import { compileScenario } from "../spec-utils/compile-scenario.js"; import { importSpecExpect, importTypeSpec, importTypeSpecHttp } from "../spec-utils/import-spec.js"; import { createDiagnosticReporter } from "../utils/diagnostic-reporter.js"; import { @@ -35,13 +36,10 @@ export async function validateMockApis({ const diagnostics = createDiagnosticReporter(); for (const { name, specFilePath } of scenarioFiles) { logger.debug(`Found scenario "${specFilePath}"`); - const program = await specCompiler.compile(specCompiler.NodeHost, specFilePath, { - noEmit: true, - warningAsError: true, - }); + const [program, compilerDiagnostics] = await compileScenario(specCompiler, specFilePath); // Workaround https://github.com/Azure/cadl-azure/issues/2458 - const programDiagnostics = program.diagnostics.filter( + const programDiagnostics = compilerDiagnostics.filter( (d) => !( d.code === "@azure-tools/typespec-azure-core/casing-style" && @@ -52,7 +50,7 @@ export async function validateMockApis({ ), ); - if (programDiagnostics.length > 0) { + if (programDiagnostics.length > 0 || program === undefined) { specCompiler.logDiagnostics(programDiagnostics, specCompiler.NodeHost.logSink); diagnostics.reportDiagnostic({ message: `Scenario ${name} is invalid.`, diff --git a/packages/spector/src/scenarios-resolver.ts b/packages/spector/src/scenarios-resolver.ts index 8662808f84c..8a6eb97d2fc 100644 --- a/packages/spector/src/scenarios-resolver.ts +++ b/packages/spector/src/scenarios-resolver.ts @@ -6,7 +6,12 @@ import pc from "picocolors"; import { pathToFileURL } from "url"; import type { Scenario } from "./lib/decorators.js"; import { logger } from "./logger.js"; -import { importSpecExpect, importTypeSpec, importTypeSpecHttp } from "./spec-utils/index.js"; +import { + compileScenario, + importSpecExpect, + importTypeSpec, + importTypeSpecHttp, +} from "./spec-utils/index.js"; import { findFilesFromPattern } from "./utils/file-utils.js"; import type { Diagnostic } from "./utils/index.js"; import { @@ -69,14 +74,12 @@ export async function loadScenarios( for (const { name, specFilePath } of scenarioFiles) { logger.debug(`Found scenario "${specFilePath}"`); - const program = await typespecCompiler.compile(typespecCompiler.NodeHost, specFilePath, { - additionalImports: ["@typespec/spector"], - noEmit: true, - warningAsError: true, - }); + const [program, compilerDiagnostics] = await compileScenario(typespecCompiler, specFilePath, [ + "@typespec/spector", + ]); // Workaround https://github.com/Azure/cadl-azure/issues/2458 - const programDiagnostics = program.diagnostics.filter( + const programDiagnostics = compilerDiagnostics.filter( (d) => !( d.code === "@azure-tools/typespec-azure-core/casing-style" && @@ -87,7 +90,7 @@ export async function loadScenarios( ), ); - if (programDiagnostics.length > 0) { + if (programDiagnostics.length > 0 || program === undefined) { for (const item of programDiagnostics) { const sourceLocation = typespecCompiler.getSourceLocation(item.target); diagnostics.reportDiagnostic({ diff --git a/packages/spector/src/spec-utils/compile-scenario.ts b/packages/spector/src/spec-utils/compile-scenario.ts new file mode 100644 index 00000000000..a0767567729 --- /dev/null +++ b/packages/spector/src/spec-utils/compile-scenario.ts @@ -0,0 +1,28 @@ +import type { Diagnostic, Program } from "@typespec/compiler"; +import { dirname, resolve } from "path"; + +export async function compileScenario( + compiler: typeof import("@typespec/compiler"), + specFilePath: string, + additionalImports: string[] = [], +): Promise<[Program | undefined, readonly Diagnostic[]]> { + const entrypoint = resolve(specFilePath); + const [options, diagnostics] = await compiler.resolveCompilerOptions(compiler.NodeHost, { + entrypoint, + cwd: dirname(entrypoint), + env: process.env, + }); + if (diagnostics.length > 0) { + return [undefined, diagnostics]; + } + + const program = await compiler.compile(compiler.NodeHost, entrypoint, { + ...options, + additionalImports: [...(options.additionalImports ?? []), ...additionalImports], + noEmit: true, + warningAsError: true, + // noEmit alone still loads configured emitters. + emit: [], + }); + return [program, program.diagnostics]; +} diff --git a/packages/spector/src/spec-utils/index.ts b/packages/spector/src/spec-utils/index.ts index 63bfc668d1e..480c548c43d 100644 --- a/packages/spector/src/spec-utils/index.ts +++ b/packages/spector/src/spec-utils/index.ts @@ -1 +1,2 @@ +export * from "./compile-scenario.js"; export * from "./import-spec.js"; diff --git a/packages/spector/test/scenario-config.test.ts b/packages/spector/test/scenario-config.test.ts new file mode 100644 index 00000000000..4e755bc8052 --- /dev/null +++ b/packages/spector/test/scenario-config.test.ts @@ -0,0 +1,250 @@ +import * as compiler from "@typespec/compiler"; +import { randomUUID } from "crypto"; +import { mkdir, rm, writeFile } from "fs/promises"; +import { dirname, join, relative } from "path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { validateMockApis } from "../src/actions/validate-mock-apis.js"; +import { logger } from "../src/logger.js"; +import { loadScenarios } from "../src/scenarios-resolver.js"; +import { compileScenario } from "../src/spec-utils/compile-scenario.js"; + +const enabledConfig = "kind: project\nfeatures: [union-extends]\n"; +const source = ` +import "@typespec/http"; +import "@typespec/spector"; +using TypeSpec.Http; +using TypeSpec.Spector; + +@scenarioService("/config") +namespace Config; + +model Base { name: string; } +model Item extends Base { value: int32; } +union Result extends Base { item: Item } + +@scenario +@scenarioDoc("Read a configured union.") +@get +op read(): Result; +`; + +let root: string; +let scenariosPath: string; +let specFilePath: string; +let messages: string[]; + +async function writeFixture(path: string, content: string) { + const fullPath = join(root, path); + await mkdir(dirname(fullPath), { recursive: true }); + await writeFile(fullPath, content); +} + +beforeEach(async () => { + root = join(import.meta.dirname, `.scenario-config-${randomUUID()}`); + scenariosPath = join(root, "specs"); + specFilePath = join(scenariosPath, "case/main.tsp"); + messages = []; + vi.spyOn(logger, "info").mockImplementation(() => {}); + vi.spyOn(logger, "error").mockImplementation((message) => { + messages.push(message); + }); + vi.spyOn(compiler.NodeHost.logSink, "log").mockImplementation((log) => { + messages.push(log.message); + }); + await writeFixture("specs/case/main.tsp", source); + await writeFixture( + "dist/case/mockapi.js", + `export const Scenarios = { + Config_read: { + passCondition: "response-success", + apis: [{ + kind: "MockApiDefinition", + method: "get", + uri: "/config", + response: { status: 200, body: { name: "item", value: 1 } }, + }], + }, + };`, + ); +}); + +afterEach(async () => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + await rm(root, { recursive: true, force: true }); +}); + +describe.each(["loadScenarios", "validateMockApis"] as const)("%s configuration", (entrypoint) => { + async function validate() { + if (entrypoint === "loadScenarios") { + const [scenarios, diagnostics] = await loadScenarios(scenariosPath); + if (diagnostics.length === 0) { + expect(scenarios.map((scenario) => scenario.name)).toEqual(["Config_read"]); + } else { + expect(scenarios).toEqual([]); + } + return diagnostics.length === 0; + } + return !(await validateMockApis({ scenariosPath, hasMoreScenarios: true })); + } + + it("enables union extends using the scenario's project config", async () => { + await writeFixture("specs/case/tspconfig.yaml", enabledConfig); + expect(await validate()).toBe(true); + }); + + it.each([undefined, "", "kind: project\nfeatures: []\n"])( + "rejects union extends without the feature (%j)", + async (config) => { + if (config !== undefined) { + await writeFixture("specs/case/tspconfig.yaml", config); + } + expect(await validate()).toBe(false); + expect(messages.join("\n")).toContain("Union `extends` clauses require the 'union-extends'"); + }, + ); + + it.each([undefined, ""])( + "accepts ordinary scenarios with no/empty config (%j)", + async (config) => { + await writeFixture( + "specs/case/main.tsp", + source.replace("union Result extends Base", "union Result"), + ); + if (config !== undefined) { + await writeFixture("specs/case/tspconfig.yaml", config); + } + expect(await validate()).toBe(true); + }, + ); + + it("discovers config in the nearest parent directory", async () => { + await writeFixture("specs/tspconfig.yaml", enabledConfig); + expect(await validate()).toBe(true); + }); + + it("uses the nearest config rather than an invalid ancestor", async () => { + await writeFixture("specs/tspconfig.yaml", "features: ["); + await writeFixture("specs/case/tspconfig.yaml", enabledConfig); + expect(await validate()).toBe(true); + }); + + it("stops upward discovery at an empty config", async () => { + await writeFixture("specs/tspconfig.yaml", enabledConfig); + await writeFixture("specs/case/tspconfig.yaml", ""); + expect(await validate()).toBe(false); + expect(messages.join("\n")).toContain("Union `extends` clauses require the 'union-extends'"); + }); + + it("inherits enabled features through an explicit extends", async () => { + await writeFixture("shared/tspconfig.yaml", enabledConfig); + await writeFixture( + "specs/case/tspconfig.yaml", + "kind: project\nextends: ../../shared/tspconfig.yaml\n", + ); + expect(await validate()).toBe(true); + }); + + it("resolves declared environment variables even when emission is disabled", async () => { + vi.stubEnv("SPECTOR_SCENARIO_OUTPUT_DIR", join(root, "output")); + await writeFixture( + "specs/case/tspconfig.yaml", + `${enabledConfig}environment-variables: + SPECTOR_SCENARIO_OUTPUT_DIR: + default: "" +output-dir: "{env.SPECTOR_SCENARIO_OUTPUT_DIR}" +`, + ); + expect(await validate()).toBe(true); + }); + + it.each(["features: [", "kind: project\nfeatures: true\n"])( + "reports malformed config instead of compiling with defaults (%j)", + async (config) => { + await writeFixture( + "specs/case/main.tsp", + source.replace("union Result extends Base", "union Result"), + ); + await writeFixture("specs/case/tspconfig.yaml", config); + expect(await validate()).toBe(false); + expect(messages.join("\n")).toMatch(/Flow sequence|array/); + }, + ); + + it("reports missing inherited config", async () => { + await writeFixture("specs/case/tspconfig.yaml", `${enabledConfig}extends: ./missing.yaml\n`); + expect(await validate()).toBe(false); + expect(messages.join("\n")).toContain("missing.yaml"); + }); + + it("honors configured imports relative to the scenario", async () => { + await writeFixture( + "specs/case/main.tsp", + source.replace('import "@typespec/spector";', "").replace("model Base { name: string; }", ""), + ); + await writeFixture("specs/case/base.tsp", "namespace Config; model Base { name: string; }"); + await writeFixture( + "specs/case/tspconfig.yaml", + `${enabledConfig}imports:\n - "@typespec/spector"\n - "./base.tsp"\n`, + ); + expect(await validate()).toBe(true); + }); + + it("does not load or execute configured emitters", async () => { + await writeFixture( + "specs/case/emitter.mjs", + `throw new Error("Configured emitter must not be loaded"); + export function $onEmit() { throw new Error("Configured emitter must not run"); }`, + ); + await writeFixture( + "specs/case/tspconfig.yaml", + `${enabledConfig}emit:\n - "./emitter.mjs"\n - "nonexistent-spector-test-emitter"\n`, + ); + expect(await validate()).toBe(true); + }); +}); + +it("keeps loadScenarios' implicit Spector import alongside configured imports", async () => { + await writeFixture( + "specs/case/main.tsp", + source.replace('import "@typespec/spector";', "").replace("model Base { name: string; }", ""), + ); + await writeFixture("specs/case/base.tsp", "namespace Config; model Base { name: string; }"); + await writeFixture("specs/case/tspconfig.yaml", `${enabledConfig}imports: ["./base.tsp"]\n`); + const [scenarios, diagnostics] = await loadScenarios(scenariosPath); + expect(diagnostics).toEqual([]); + expect(scenarios.map((scenario) => scenario.name)).toEqual(["Config_read"]); +}); + +it("enforces validation options after resolving config with the supplied compiler", async () => { + await writeFixture( + "specs/case/tspconfig.yaml", + `${enabledConfig}warn-as-error: false\nemit: ["not-an-emitter"]\n`, + ); + const resolveCompilerOptions = vi.fn(compiler.resolveCompilerOptions); + const [program, diagnostics] = await compileScenario( + { ...compiler, resolveCompilerOptions }, + relative(process.cwd(), specFilePath), + ); + expect(diagnostics).toEqual([]); + expect(resolveCompilerOptions).toHaveBeenCalledWith(compiler.NodeHost, { + entrypoint: specFilePath, + cwd: dirname(specFilePath), + env: process.env, + }); + expect(program?.compilerOptions).toMatchObject({ + noEmit: true, + warningAsError: true, + emit: [], + configFile: { features: ["union-extends"] }, + }); +}); + +it("does not compile when configuration resolution reports diagnostics", async () => { + await writeFixture("specs/case/tspconfig.yaml", "features: ["); + const compile = vi.fn(compiler.compile); + const [program, diagnostics] = await compileScenario({ ...compiler, compile }, specFilePath); + expect(program).toBeUndefined(); + expect(diagnostics.length).toBeGreaterThan(0); + expect(compile).not.toHaveBeenCalled(); +}); From d1c090445969de14ec7d4596d50e6eeb5633231f Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 15 Sep 2026 16:33:41 -0400 Subject: [PATCH 2/2] docs(http-specs): simplify union extends language review Replace runtime fixtures and Spector changes with a compact12-case review document. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...on-extends-scenarios-2026-8-15-16-13-34.md | 16 - ...on-extends-scenarios-2026-8-15-16-13-36.md | 7 - packages/http-specs/spec-summary.md | 1586 ----------------- .../specs/type/union/extends/README.md | 260 ++- .../specs/type/union/extends/composition.tsp | 147 -- .../specs/type/union/extends/constraints.tsp | 256 --- .../specs/type/union/extends/controls.tsp | 300 ---- .../specs/type/union/extends/main.tsp | 76 - .../specs/type/union/extends/mockapi.ts | 246 --- .../specs/type/union/extends/models.tsp | 570 ------ .../specs/type/union/extends/tspconfig.yaml | 3 - .../specs/type/union/extends/usage.tsp | 218 --- .../spector/src/actions/validate-mock-apis.ts | 10 +- packages/spector/src/scenarios-resolver.ts | 19 +- .../src/spec-utils/compile-scenario.ts | 28 - packages/spector/src/spec-utils/index.ts | 1 - packages/spector/test/scenario-config.test.ts | 250 --- 17 files changed, 136 insertions(+), 3857 deletions(-) delete mode 100644 .chronus/changes/union-extends-scenarios-2026-8-15-16-13-34.md delete mode 100644 .chronus/changes/union-extends-scenarios-2026-8-15-16-13-36.md delete mode 100644 packages/http-specs/specs/type/union/extends/composition.tsp delete mode 100644 packages/http-specs/specs/type/union/extends/constraints.tsp delete mode 100644 packages/http-specs/specs/type/union/extends/controls.tsp delete mode 100644 packages/http-specs/specs/type/union/extends/main.tsp delete mode 100644 packages/http-specs/specs/type/union/extends/mockapi.ts delete mode 100644 packages/http-specs/specs/type/union/extends/models.tsp delete mode 100644 packages/http-specs/specs/type/union/extends/tspconfig.yaml delete mode 100644 packages/http-specs/specs/type/union/extends/usage.tsp delete mode 100644 packages/spector/src/spec-utils/compile-scenario.ts delete mode 100644 packages/spector/test/scenario-config.test.ts diff --git a/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-34.md b/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-34.md deleted file mode 100644 index a08b9d21bb2..00000000000 --- a/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-34.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -changeKind: feature -packages: - - "@typespec/http-specs" ---- - -Add union extends Spector scenarios covering structural constraints, model reuse, serialization formats, and exact request/response fixtures. - -```typespec -model Named { name: string; } -model Cat extends Named { meow: boolean; } -model Dog { name: string; bark: boolean; } -union Pet extends Named { cat: Cat, dog: Dog } -``` - -The matrix includes representative unions without `extends` for language API comparisons. \ No newline at end of file diff --git a/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-36.md b/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-36.md deleted file mode 100644 index 03bb8b129fb..00000000000 --- a/.chronus/changes/union-extends-scenarios-2026-8-15-16-13-36.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/spector" ---- - -Resolve nearest and inherited TypeSpec configuration when validating scenarios and mock APIs, while keeping emitters disabled. \ No newline at end of file diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index e2c68dedbf2..9c7f6485dc6 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -9422,1592 +9422,6 @@ Expected request to send body: } ``` -### Type_Union_Extends_Ancestry_Envelope_get - -- Endpoint: `get /type/union/extends/ancestry/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Kitten","age":1,"meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","origin":"shelter","bark":false}} - -### Type_Union_Extends_Ancestry_Envelope_put - -- Endpoint: `put /type/union/extends/ancestry/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Kitten","age":1,"meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","origin":"shelter","bark":false}} - -### Type_Union_Extends_Ancestry_Inline_get - -- Endpoint: `get /type/union/extends/ancestry/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Kitten","age":1,"meow":true}; dog: {"kind":"dog","name":"Rex","origin":"shelter","bark":false} - -### Type_Union_Extends_Ancestry_Inline_put - -- Endpoint: `put /type/union/extends/ancestry/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Kitten","age":1,"meow":true}; dog: {"kind":"dog","name":"Rex","origin":"shelter","bark":false} - -### Type_Union_Extends_Ancestry_Untagged_get - -- Endpoint: `get /type/union/extends/ancestry/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Kitten","age":1,"meow":true}; dog: {"name":"Rex","origin":"shelter","bark":false} - -### Type_Union_Extends_Ancestry_Untagged_put - -- Endpoint: `put /type/union/extends/ancestry/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Kitten","age":1,"meow":true}; dog: {"name":"Rex","origin":"shelter","bark":false} - -### Type_Union_Extends_ArrayConstraint_Envelope_get - -- Endpoint: `get /type/union/extends/array/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cats: {"kind":"cats","value":[{"name":"Whiskers","meow":true}]}; dogs: {"kind":"dogs","value":[{"name":"Rex","bark":false}]} - -### Type_Union_Extends_ArrayConstraint_Envelope_put - -- Endpoint: `put /type/union/extends/array/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cats: {"kind":"cats","value":[{"name":"Whiskers","meow":true}]}; dogs: {"kind":"dogs","value":[{"name":"Rex","bark":false}]} - -### Type_Union_Extends_ArrayConstraint_Untagged_get - -- Endpoint: `get /type/union/extends/array/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cats: [{"name":"Whiskers","meow":true}]; dogs: [{"name":"Rex","bark":false}] - -### Type_Union_Extends_ArrayConstraint_Untagged_put - -- Endpoint: `put /type/union/extends/array/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cats: [{"name":"Whiskers","meow":true}]; dogs: [{"name":"Rex","bark":false}] - -### Type_Union_Extends_ClosedScalar_Envelope_get - -- Endpoint: `get /type/union/extends/closed-scalar/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} - -### Type_Union_Extends_ClosedScalar_Envelope_put - -- Endpoint: `put /type/union/extends/closed-scalar/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} - -### Type_Union_Extends_ClosedScalar_Untagged_get - -- Endpoint: `get /type/union/extends/closed-scalar/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -start: "start"; stop: "stop" - -### Type_Union_Extends_ClosedScalar_Untagged_put - -- Endpoint: `put /type/union/extends/closed-scalar/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -start: "start"; stop: "stop" - -### Type_Union_Extends_Controls_ClosedScalar_Envelope_get - -- Endpoint: `get /type/union/extends/controls/closed-scalar/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} - -### Type_Union_Extends_Controls_ClosedScalar_Envelope_put - -- Endpoint: `put /type/union/extends/controls/closed-scalar/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} - -### Type_Union_Extends_Controls_ClosedScalar_Untagged_get - -- Endpoint: `get /type/union/extends/controls/closed-scalar/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -start: "start"; stop: "stop" - -### Type_Union_Extends_Controls_ClosedScalar_Untagged_put - -- Endpoint: `put /type/union/extends/controls/closed-scalar/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -start: "start"; stop: "stop" - -### Type_Union_Extends_Controls_DirectInheritance_Envelope_get - -- Endpoint: `get /type/union/extends/controls/direct-inheritance/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_Controls_DirectInheritance_Envelope_put - -- Endpoint: `put /type/union/extends/controls/direct-inheritance/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_Controls_DirectInheritance_Inline_get - -- Endpoint: `get /type/union/extends/controls/direct-inheritance/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_Controls_DirectInheritance_Inline_put - -- Endpoint: `put /type/union/extends/controls/direct-inheritance/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_Controls_DirectInheritance_Untagged_get - -- Endpoint: `get /type/union/extends/controls/direct-inheritance/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_Controls_DirectInheritance_Untagged_put - -- Endpoint: `put /type/union/extends/controls/direct-inheritance/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_Controls_ExplicitDefault_Inline_get - -- Endpoint: `get /type/union/extends/controls/explicit-default/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} - -### Type_Union_Extends_Controls_ExplicitDefault_Inline_put - -- Endpoint: `put /type/union/extends/controls/explicit-default/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} - -### Type_Union_Extends_Controls_NestedVariant_Envelope_get - -- Endpoint: `get /type/union/extends/controls/nested-variant/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} - -### Type_Union_Extends_Controls_NestedVariant_Envelope_put - -- Endpoint: `put /type/union/extends/controls/nested-variant/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} - -### Type_Union_Extends_Controls_NestedVariant_Untagged_get - -- Endpoint: `get /type/union/extends/controls/nested-variant/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} - -### Type_Union_Extends_Controls_NestedVariant_Untagged_put - -- Endpoint: `put /type/union/extends/controls/nested-variant/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} - -### Type_Union_Extends_Controls_OpenScalar_Envelope_get - -- Endpoint: `get /type/union/extends/controls/open-scalar/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} - -### Type_Union_Extends_Controls_OpenScalar_Envelope_put - -- Endpoint: `put /type/union/extends/controls/open-scalar/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} - -### Type_Union_Extends_Controls_OpenScalar_Untagged_get - -- Endpoint: `get /type/union/extends/controls/open-scalar/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -known: "known"; custom: "future" - -### Type_Union_Extends_Controls_OpenScalar_Untagged_put - -- Endpoint: `put /type/union/extends/controls/open-scalar/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -known: "known"; custom: "future" - -### Type_Union_Extends_Controls_SharedSameBase_First_Envelope_get - -- Endpoint: `get /type/union/extends/controls/shared-same-base/first/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_Controls_SharedSameBase_First_Envelope_put - -- Endpoint: `put /type/union/extends/controls/shared-same-base/first/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_Controls_SharedSameBase_First_Inline_get - -- Endpoint: `get /type/union/extends/controls/shared-same-base/first/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_Controls_SharedSameBase_First_Inline_put - -- Endpoint: `put /type/union/extends/controls/shared-same-base/first/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_Controls_SharedSameBase_First_Untagged_get - -- Endpoint: `get /type/union/extends/controls/shared-same-base/first/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_Controls_SharedSameBase_First_Untagged_put - -- Endpoint: `put /type/union/extends/controls/shared-same-base/first/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_Controls_SharedSameBase_Second_Envelope_get - -- Endpoint: `get /type/union/extends/controls/shared-same-base/second/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} - -### Type_Union_Extends_Controls_SharedSameBase_Second_Envelope_put - -- Endpoint: `put /type/union/extends/controls/shared-same-base/second/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} - -### Type_Union_Extends_Controls_SharedSameBase_Second_Inline_get - -- Endpoint: `get /type/union/extends/controls/shared-same-base/second/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} - -### Type_Union_Extends_Controls_SharedSameBase_Second_Inline_put - -- Endpoint: `put /type/union/extends/controls/shared-same-base/second/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} - -### Type_Union_Extends_Controls_SharedSameBase_Second_Untagged_get - -- Endpoint: `get /type/union/extends/controls/shared-same-base/second/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} - -### Type_Union_Extends_Controls_SharedSameBase_Second_Untagged_put - -- Endpoint: `put /type/union/extends/controls/shared-same-base/second/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} - -### Type_Union_Extends_Controls_Structural_Envelope_get - -- Endpoint: `get /type/union/extends/controls/structural/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} - -### Type_Union_Extends_Controls_Structural_Envelope_put - -- Endpoint: `put /type/union/extends/controls/structural/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} - -### Type_Union_Extends_Controls_Structural_Inline_get - -- Endpoint: `get /type/union/extends/controls/structural/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} - -### Type_Union_Extends_Controls_Structural_Inline_put - -- Endpoint: `put /type/union/extends/controls/structural/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} - -### Type_Union_Extends_Controls_Structural_Untagged_get - -- Endpoint: `get /type/union/extends/controls/structural/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} - -### Type_Union_Extends_Controls_Structural_Untagged_put - -- Endpoint: `put /type/union/extends/controls/structural/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} - -### Type_Union_Extends_Conversion_Direct_get - -- Endpoint: `get /type/union/extends/conversion/direct/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"} - -### Type_Union_Extends_Conversion_Direct_put - -- Endpoint: `put /type/union/extends/conversion/direct/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"} - -### Type_Union_Extends_Conversion_Envelope_get - -- Endpoint: `get /type/union/extends/conversion/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -data: {"dataType":"data","payload":{"display_name":"Data","createdAt":1704067200,"content":"-_8"}}; count: {"dataType":"count","payload":{"display_name":"Count","createdAt":1704067200,"count":3}} - -### Type_Union_Extends_Conversion_Envelope_put - -- Endpoint: `put /type/union/extends/conversion/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -data: {"dataType":"data","payload":{"display_name":"Data","createdAt":1704067200,"content":"-_8"}}; count: {"dataType":"count","payload":{"display_name":"Count","createdAt":1704067200,"count":3}} - -### Type_Union_Extends_Conversion_Inline_get - -- Endpoint: `get /type/union/extends/conversion/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -data: {"dataType":"data","display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"dataType":"count","display_name":"Count","createdAt":1704067200,"count":3} - -### Type_Union_Extends_Conversion_Inline_put - -- Endpoint: `put /type/union/extends/conversion/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -data: {"dataType":"data","display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"dataType":"count","display_name":"Count","createdAt":1704067200,"count":3} - -### Type_Union_Extends_Conversion_Untagged_get - -- Endpoint: `get /type/union/extends/conversion/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"display_name":"Count","createdAt":1704067200,"count":3} - -### Type_Union_Extends_Conversion_Untagged_put - -- Endpoint: `put /type/union/extends/conversion/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"display_name":"Count","createdAt":1704067200,"count":3} - -### Type_Union_Extends_DirectInheritance_Envelope_get - -- Endpoint: `get /type/union/extends/direct-inheritance/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_DirectInheritance_Envelope_put - -- Endpoint: `put /type/union/extends/direct-inheritance/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_DirectInheritance_Inline_get - -- Endpoint: `get /type/union/extends/direct-inheritance/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_DirectInheritance_Inline_put - -- Endpoint: `put /type/union/extends/direct-inheritance/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_DirectInheritance_Untagged_get - -- Endpoint: `get /type/union/extends/direct-inheritance/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_DirectInheritance_Untagged_put - -- Endpoint: `put /type/union/extends/direct-inheritance/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_DisjointSameBase_First_Envelope_get - -- Endpoint: `get /type/union/extends/disjoint-same-base/first/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_DisjointSameBase_First_Envelope_put - -- Endpoint: `put /type/union/extends/disjoint-same-base/first/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_DisjointSameBase_First_Inline_get - -- Endpoint: `get /type/union/extends/disjoint-same-base/first/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_DisjointSameBase_First_Inline_put - -- Endpoint: `put /type/union/extends/disjoint-same-base/first/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_DisjointSameBase_First_Untagged_get - -- Endpoint: `get /type/union/extends/disjoint-same-base/first/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_DisjointSameBase_First_Untagged_put - -- Endpoint: `put /type/union/extends/disjoint-same-base/first/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_DisjointSameBase_Second_Envelope_get - -- Endpoint: `get /type/union/extends/disjoint-same-base/second/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -bird: {"kind":"bird","value":{"name":"Sky","wings":2}}; fish: {"kind":"fish","value":{"name":"Gold","fins":3}} - -### Type_Union_Extends_DisjointSameBase_Second_Envelope_put - -- Endpoint: `put /type/union/extends/disjoint-same-base/second/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -bird: {"kind":"bird","value":{"name":"Sky","wings":2}}; fish: {"kind":"fish","value":{"name":"Gold","fins":3}} - -### Type_Union_Extends_DisjointSameBase_Second_Inline_get - -- Endpoint: `get /type/union/extends/disjoint-same-base/second/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -bird: {"kind":"bird","name":"Sky","wings":2}; fish: {"kind":"fish","name":"Gold","fins":3} - -### Type_Union_Extends_DisjointSameBase_Second_Inline_put - -- Endpoint: `put /type/union/extends/disjoint-same-base/second/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -bird: {"kind":"bird","name":"Sky","wings":2}; fish: {"kind":"fish","name":"Gold","fins":3} - -### Type_Union_Extends_DisjointSameBase_Second_Untagged_get - -- Endpoint: `get /type/union/extends/disjoint-same-base/second/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -bird: {"name":"Sky","wings":2}; fish: {"name":"Gold","fins":3} - -### Type_Union_Extends_DisjointSameBase_Second_Untagged_put - -- Endpoint: `put /type/union/extends/disjoint-same-base/second/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -bird: {"name":"Sky","wings":2}; fish: {"name":"Gold","fins":3} - -### Type_Union_Extends_EnumConstraint_Envelope_get - -- Endpoint: `get /type/union/extends/enum/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -left: {"kind":"left","value":"left"}; right: {"kind":"right","value":"right"} - -### Type_Union_Extends_EnumConstraint_Envelope_put - -- Endpoint: `put /type/union/extends/enum/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -left: {"kind":"left","value":"left"}; right: {"kind":"right","value":"right"} - -### Type_Union_Extends_EnumConstraint_Untagged_get - -- Endpoint: `get /type/union/extends/enum/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -left: "left"; right: "right" - -### Type_Union_Extends_EnumConstraint_Untagged_put - -- Endpoint: `put /type/union/extends/enum/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -left: "left"; right: "right" - -### Type_Union_Extends_ExplicitBase_Envelope_get - -- Endpoint: `get /type/union/extends/explicit-base/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; base: {"kind":"base","value":{"name":"Base"}} - -### Type_Union_Extends_ExplicitBase_Envelope_put - -- Endpoint: `put /type/union/extends/explicit-base/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; base: {"kind":"base","value":{"name":"Base"}} - -### Type_Union_Extends_ExplicitBase_Inline_get - -- Endpoint: `get /type/union/extends/explicit-base/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; base: {"kind":"base","name":"Base"} - -### Type_Union_Extends_ExplicitBase_Inline_put - -- Endpoint: `put /type/union/extends/explicit-base/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; base: {"kind":"base","name":"Base"} - -### Type_Union_Extends_ExplicitBase_Untagged_get - -- Endpoint: `get /type/union/extends/explicit-base/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; base: {"name":"Base"} - -### Type_Union_Extends_ExplicitBase_Untagged_put - -- Endpoint: `put /type/union/extends/explicit-base/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; base: {"name":"Base"} - -### Type_Union_Extends_ExplicitDefault_Inline_get - -- Endpoint: `get /type/union/extends/explicit-default/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} - -### Type_Union_Extends_ExplicitDefault_Inline_put - -- Endpoint: `put /type/union/extends/explicit-default/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} - -### Type_Union_Extends_IntersectionConstraint_Envelope_get - -- Endpoint: `get /type/union/extends/intersection/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -first: {"kind":"first","value":{"name":"First","id":1,"text":"hello"}}; second: {"kind":"second","value":{"name":"Second","id":2,"count":3}} - -### Type_Union_Extends_IntersectionConstraint_Envelope_put - -- Endpoint: `put /type/union/extends/intersection/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -first: {"kind":"first","value":{"name":"First","id":1,"text":"hello"}}; second: {"kind":"second","value":{"name":"Second","id":2,"count":3}} - -### Type_Union_Extends_IntersectionConstraint_Inline_get - -- Endpoint: `get /type/union/extends/intersection/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -first: {"kind":"first","name":"First","id":1,"text":"hello"}; second: {"kind":"second","name":"Second","id":2,"count":3} - -### Type_Union_Extends_IntersectionConstraint_Inline_put - -- Endpoint: `put /type/union/extends/intersection/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -first: {"kind":"first","name":"First","id":1,"text":"hello"}; second: {"kind":"second","name":"Second","id":2,"count":3} - -### Type_Union_Extends_IntersectionConstraint_Untagged_get - -- Endpoint: `get /type/union/extends/intersection/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -first: {"name":"First","id":1,"text":"hello"}; second: {"name":"Second","id":2,"count":3} - -### Type_Union_Extends_IntersectionConstraint_Untagged_put - -- Endpoint: `put /type/union/extends/intersection/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -first: {"name":"First","id":1,"text":"hello"}; second: {"name":"Second","id":2,"count":3} - -### Type_Union_Extends_NestedVariant_Envelope_get - -- Endpoint: `get /type/union/extends/nested-variant/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} - -### Type_Union_Extends_NestedVariant_Envelope_put - -- Endpoint: `put /type/union/extends/nested-variant/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} - -### Type_Union_Extends_NestedVariant_Untagged_get - -- Endpoint: `get /type/union/extends/nested-variant/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} - -### Type_Union_Extends_NestedVariant_Untagged_put - -- Endpoint: `put /type/union/extends/nested-variant/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} - -### Type_Union_Extends_Nullable_Envelope_get - -- Endpoint: `get /type/union/extends/nullable/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}}; null: null - -### Type_Union_Extends_Nullable_Envelope_put - -- Endpoint: `put /type/union/extends/nullable/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}}; null: null - -### Type_Union_Extends_Nullable_Inline_get - -- Endpoint: `get /type/union/extends/nullable/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; null: null - -### Type_Union_Extends_Nullable_Inline_put - -- Endpoint: `put /type/union/extends/nullable/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; null: null - -### Type_Union_Extends_Nullable_Untagged_get - -- Endpoint: `get /type/union/extends/nullable/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; null: null - -### Type_Union_Extends_Nullable_Untagged_put - -- Endpoint: `put /type/union/extends/nullable/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; null: null - -### Type_Union_Extends_OpenScalar_Envelope_get - -- Endpoint: `get /type/union/extends/open-scalar/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} - -### Type_Union_Extends_OpenScalar_Envelope_put - -- Endpoint: `put /type/union/extends/open-scalar/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} - -### Type_Union_Extends_OpenScalar_Untagged_get - -- Endpoint: `get /type/union/extends/open-scalar/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -known: "known"; custom: "future" - -### Type_Union_Extends_OpenScalar_Untagged_put - -- Endpoint: `put /type/union/extends/open-scalar/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -known: "known"; custom: "future" - -### Type_Union_Extends_Recursive_Envelope_get - -- Endpoint: `get /type/union/extends/recursive/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -leaf: {"kind":"leaf","value":{"name":"Leaf","value":1}}; branch: {"kind":"branch","value":{"name":"Branch","children":[{"kind":"leaf","value":{"name":"Leaf","value":1}}]}} - -### Type_Union_Extends_Recursive_Envelope_put - -- Endpoint: `put /type/union/extends/recursive/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -leaf: {"kind":"leaf","value":{"name":"Leaf","value":1}}; branch: {"kind":"branch","value":{"name":"Branch","children":[{"kind":"leaf","value":{"name":"Leaf","value":1}}]}} - -### Type_Union_Extends_Recursive_Untagged_get - -- Endpoint: `get /type/union/extends/recursive/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -leaf: {"name":"Leaf","value":1}; branch: {"name":"Branch","children":[{"name":"Leaf","value":1}]} - -### Type_Union_Extends_Recursive_Untagged_put - -- Endpoint: `put /type/union/extends/recursive/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -leaf: {"name":"Leaf","value":1}; branch: {"name":"Branch","children":[{"name":"Leaf","value":1}]} - -### Type_Union_Extends_SharedDifferentBases_ById_Envelope_get - -- Endpoint: `get /type/union/extends/shared-different-bases/identified/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -entity: {"entityType":"entity","entityData":{"name":"Shared","id":7,"active":true}}; other: {"entityType":"other","entityData":{"id":8,"code":"other"}} - -### Type_Union_Extends_SharedDifferentBases_ById_Envelope_put - -- Endpoint: `put /type/union/extends/shared-different-bases/identified/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -entity: {"entityType":"entity","entityData":{"name":"Shared","id":7,"active":true}}; other: {"entityType":"other","entityData":{"id":8,"code":"other"}} - -### Type_Union_Extends_SharedDifferentBases_ById_Inline_get - -- Endpoint: `get /type/union/extends/shared-different-bases/identified/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -entity: {"entityType":"entity","name":"Shared","id":7,"active":true}; other: {"entityType":"other","id":8,"code":"other"} - -### Type_Union_Extends_SharedDifferentBases_ById_Inline_put - -- Endpoint: `put /type/union/extends/shared-different-bases/identified/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -entity: {"entityType":"entity","name":"Shared","id":7,"active":true}; other: {"entityType":"other","id":8,"code":"other"} - -### Type_Union_Extends_SharedDifferentBases_ById_Untagged_get - -- Endpoint: `get /type/union/extends/shared-different-bases/identified/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -entity: {"name":"Shared","id":7,"active":true}; other: {"id":8,"code":"other"} - -### Type_Union_Extends_SharedDifferentBases_ById_Untagged_put - -- Endpoint: `put /type/union/extends/shared-different-bases/identified/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -entity: {"name":"Shared","id":7,"active":true}; other: {"id":8,"code":"other"} - -### Type_Union_Extends_SharedDifferentBases_ByName_Envelope_get - -- Endpoint: `get /type/union/extends/shared-different-bases/named/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -shared: {"kind":"shared","value":{"name":"Shared","id":7,"active":true}}; cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}} - -### Type_Union_Extends_SharedDifferentBases_ByName_Envelope_put - -- Endpoint: `put /type/union/extends/shared-different-bases/named/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -shared: {"kind":"shared","value":{"name":"Shared","id":7,"active":true}}; cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}} - -### Type_Union_Extends_SharedDifferentBases_ByName_Inline_get - -- Endpoint: `get /type/union/extends/shared-different-bases/named/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -shared: {"kind":"shared","name":"Shared","id":7,"active":true}; cat: {"kind":"cat","name":"Whiskers","meow":true} - -### Type_Union_Extends_SharedDifferentBases_ByName_Inline_put - -- Endpoint: `put /type/union/extends/shared-different-bases/named/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -shared: {"kind":"shared","name":"Shared","id":7,"active":true}; cat: {"kind":"cat","name":"Whiskers","meow":true} - -### Type_Union_Extends_SharedDifferentBases_ByName_Untagged_get - -- Endpoint: `get /type/union/extends/shared-different-bases/named/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -shared: {"name":"Shared","id":7,"active":true}; cat: {"name":"Whiskers","meow":true} - -### Type_Union_Extends_SharedDifferentBases_ByName_Untagged_put - -- Endpoint: `put /type/union/extends/shared-different-bases/named/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -shared: {"name":"Shared","id":7,"active":true}; cat: {"name":"Whiskers","meow":true} - -### Type_Union_Extends_SharedSameBase_First_Envelope_get - -- Endpoint: `get /type/union/extends/shared-same-base/first/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_SharedSameBase_First_Envelope_put - -- Endpoint: `put /type/union/extends/shared-same-base/first/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_SharedSameBase_First_Inline_get - -- Endpoint: `get /type/union/extends/shared-same-base/first/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_SharedSameBase_First_Inline_put - -- Endpoint: `put /type/union/extends/shared-same-base/first/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_SharedSameBase_First_Untagged_get - -- Endpoint: `get /type/union/extends/shared-same-base/first/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_SharedSameBase_First_Untagged_put - -- Endpoint: `put /type/union/extends/shared-same-base/first/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_SharedSameBase_Second_Envelope_get - -- Endpoint: `get /type/union/extends/shared-same-base/second/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} - -### Type_Union_Extends_SharedSameBase_Second_Envelope_put - -- Endpoint: `put /type/union/extends/shared-same-base/second/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} - -### Type_Union_Extends_SharedSameBase_Second_Inline_get - -- Endpoint: `get /type/union/extends/shared-same-base/second/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} - -### Type_Union_Extends_SharedSameBase_Second_Inline_put - -- Endpoint: `put /type/union/extends/shared-same-base/second/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} - -### Type_Union_Extends_SharedSameBase_Second_Untagged_get - -- Endpoint: `get /type/union/extends/shared-same-base/second/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} - -### Type_Union_Extends_SharedSameBase_Second_Untagged_put - -- Endpoint: `put /type/union/extends/shared-same-base/second/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} - -### Type_Union_Extends_SpreadAndIs_Envelope_get - -- Endpoint: `get /type/union/extends/spread-and-is/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_SpreadAndIs_Envelope_put - -- Endpoint: `put /type/union/extends/spread-and-is/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_SpreadAndIs_Inline_get - -- Endpoint: `get /type/union/extends/spread-and-is/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_SpreadAndIs_Inline_put - -- Endpoint: `put /type/union/extends/spread-and-is/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_SpreadAndIs_Untagged_get - -- Endpoint: `get /type/union/extends/spread-and-is/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_SpreadAndIs_Untagged_put - -- Endpoint: `put /type/union/extends/spread-and-is/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_Structural_Envelope_get - -- Endpoint: `get /type/union/extends/structural/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} - -### Type_Union_Extends_Structural_Envelope_put - -- Endpoint: `put /type/union/extends/structural/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} - -### Type_Union_Extends_Structural_Inline_get - -- Endpoint: `get /type/union/extends/structural/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} - -### Type_Union_Extends_Structural_Inline_put - -- Endpoint: `put /type/union/extends/structural/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} - -### Type_Union_Extends_Structural_Untagged_get - -- Endpoint: `get /type/union/extends/structural/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} - -### Type_Union_Extends_Structural_Untagged_put - -- Endpoint: `put /type/union/extends/structural/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} - -### Type_Union_Extends_TemplateInstance_Envelope_get - -- Endpoint: `get /type/union/extends/template-instance/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -first: {"kind":"first","value":{"item":"one","label":"First"}}; second: {"kind":"second","value":{"item":"two","active":true}} - -### Type_Union_Extends_TemplateInstance_Envelope_put - -- Endpoint: `put /type/union/extends/template-instance/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -first: {"kind":"first","value":{"item":"one","label":"First"}}; second: {"kind":"second","value":{"item":"two","active":true}} - -### Type_Union_Extends_TemplateInstance_Inline_get - -- Endpoint: `get /type/union/extends/template-instance/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -first: {"kind":"first","item":"one","label":"First"}; second: {"kind":"second","item":"two","active":true} - -### Type_Union_Extends_TemplateInstance_Inline_put - -- Endpoint: `put /type/union/extends/template-instance/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -first: {"kind":"first","item":"one","label":"First"}; second: {"kind":"second","item":"two","active":true} - -### Type_Union_Extends_TemplateInstance_Untagged_get - -- Endpoint: `get /type/union/extends/template-instance/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -first: {"item":"one","label":"First"}; second: {"item":"two","active":true} - -### Type_Union_Extends_TemplateInstance_Untagged_put - -- Endpoint: `put /type/union/extends/template-instance/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -first: {"item":"one","label":"First"}; second: {"item":"two","active":true} - -### Type_Union_Extends_UnionAndDirect_Direct_get - -- Endpoint: `get /type/union/extends/union-and-direct/direct/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true} - -### Type_Union_Extends_UnionAndDirect_Direct_put - -- Endpoint: `put /type/union/extends/union-and-direct/direct/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true} - -### Type_Union_Extends_UnionAndDirect_Envelope_get - -- Endpoint: `get /type/union/extends/union-and-direct/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_UnionAndDirect_Envelope_put - -- Endpoint: `put /type/union/extends/union-and-direct/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_UnionAndDirect_Inline_get - -- Endpoint: `get /type/union/extends/union-and-direct/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_UnionAndDirect_Inline_put - -- Endpoint: `put /type/union/extends/union-and-direct/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_UnionAndDirect_Properties_get - -- Endpoint: `get /type/union/extends/union-and-direct/properties/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Whiskers","meow":true},"inline":{"kind":"cat","name":"Whiskers","meow":true},"envelope":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; -dog: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Rex","bark":false},"inline":{"kind":"dog","name":"Rex","bark":false},"envelope":{"kind":"dog","value":{"name":"Rex","bark":false}}} - -### Type_Union_Extends_UnionAndDirect_Properties_put - -- Endpoint: `put /type/union/extends/union-and-direct/properties/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Whiskers","meow":true},"inline":{"kind":"cat","name":"Whiskers","meow":true},"envelope":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; -dog: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Rex","bark":false},"inline":{"kind":"dog","name":"Rex","bark":false},"envelope":{"kind":"dog","value":{"name":"Rex","bark":false}}} - -### Type_Union_Extends_UnionAndDirect_Untagged_get - -- Endpoint: `get /type/union/extends/union-and-direct/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_UnionAndDirect_Untagged_put - -- Endpoint: `put /type/union/extends/union-and-direct/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_UnionConstraint_Envelope_get - -- Endpoint: `get /type/union/extends/union-constraint/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_UnionConstraint_Envelope_put - -- Endpoint: `put /type/union/extends/union-constraint/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - -### Type_Union_Extends_UnionConstraint_Inline_get - -- Endpoint: `get /type/union/extends/union-constraint/inline/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_UnionConstraint_Inline_put - -- Endpoint: `put /type/union/extends/union-constraint/inline/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - -### Type_Union_Extends_UnionConstraint_Untagged_get - -- Endpoint: `get /type/union/extends/union-constraint/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_UnionConstraint_Untagged_put - -- Endpoint: `put /type/union/extends/union-constraint/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - -### Type_Union_Extends_UnionExpressionConstraint_Envelope_get - -- Endpoint: `get /type/union/extends/union-expression-constraint/envelope/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -text: {"kind":"text","value":"hello"}; number: {"kind":"number","value":42} - -### Type_Union_Extends_UnionExpressionConstraint_Envelope_put - -- Endpoint: `put /type/union/extends/union-expression-constraint/envelope/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -text: {"kind":"text","value":"hello"}; number: {"kind":"number","value":42} - -### Type_Union_Extends_UnionExpressionConstraint_Untagged_get - -- Endpoint: `get /type/union/extends/union-expression-constraint/untagged/{choice}` - -Call once for EVERY choice below. Each choice returns the corresponding JSON -body with status 200 and Content-Type application/json. Access the -variant-specific data; ambiguous untagged shapes do not require a unique class. -text: "hello"; number: 42 - -### Type_Union_Extends_UnionExpressionConstraint_Untagged_put - -- Endpoint: `put /type/union/extends/union-expression-constraint/untagged/{choice}` - -Call once for EVERY choice below. Send the corresponding exact JSON body -with Content-Type application/json. Expect the same JSON body with status 200 -and Content-Type application/json. Retain all inherited and specific fields. -text: "hello"; number: 42 - ### Type_Union_FloatsOnly_get - Endpoint: `get /type/union/floats-only` diff --git a/packages/http-specs/specs/type/union/extends/README.md b/packages/http-specs/specs/type/union/extends/README.md index 65a521e6843..00b14c035bd 100644 --- a/packages/http-specs/specs/type/union/extends/README.md +++ b/packages/http-specs/specs/type/union/extends/README.md @@ -1,138 +1,122 @@ -# Union extends matrix - -These scenarios ask how a language represents a constrained union, its base and -variants, and reuse of those variants. `union U extends Base` is a **structural -assignability constraint**. It does not require model inheritance, automatically -include the base, include all descendants, open the union, or change the wire -format. The fixtures intentionally do not enforce proposed nominal restrictions. - -## Running a case - -Every format interface has `get` and `put` operations with a finite `choice` -path parameter. Call **every** documented choice. GET returns its exact fixture; -PUT requires that exact decoded JSON and returns the same fixture. All successful -responses are 200, with `Content-Type: application/json; charset=utf-8`. -`@scenarioDoc` in the `.tsp` files specifies each selector and JSON body; -[`mockapi.ts`](./mockapi.ts) implements it. - -For example, UE01/U maps to -`Type_Union_Extends_DirectInheritance_Untagged_get` and `_put`, with concrete -paths `/type/union/extends/direct-inheritance/untagged/cat` and `/dog`. -The union remains the operation's body/response type; the selector does not narrow -that public type. A `passOnSuccess` scenario requires every concrete endpoint, -not just the first successful variant. - -There are **88 format/context groups, 176 scenarios and 370 concrete method/path -endpoints** (185 GET and 185 PUT). Of these, 19 groups / 38 scenarios are matched -without-extends controls. Mock validation proves the wire contract, **not** -generated class identity, type narrowing, union-membership APIs, or restrictions -inherent in a particular language representation. - -## Primary coverage - -U = untagged JSON; I = inline `@discriminated(#{ envelope: "none" })`; -E = object-envelope `@discriminated`. Checkmarks cover every alternative, in both -directions. Namespace names below follow `Type.Union.Extends`; each format adds -`Untagged`, `Inline`, or `Envelope`, then `get` / `put`. - -| ID | Namespace and purpose | U | I | E | -| ----- | ------------------------------------------------------------------------------------ | ----------------------------- | ---------------------------------------- | ------------------------------------ | -| UE01 | `DirectInheritance`: Cat and Dog directly inherit Named | Yes | Yes | Yes | -| UE02 | `Structural`: neither variant inherits Named; U also has an overlapping shape | Yes | Yes | Yes | -| UE03 | `SpreadAndIs`: spread and `model is DogShape` variants | Yes | Yes | Yes | -| UE04 | `Ancestry`: transitive base/intermediate fields and different compatible ancestry | Yes | Yes | Yes | -| UE05 | `SharedSameBase.First/Second`: Cat reused in two unions, same base | Both | Both, different tags/property names | Both, different tags/property names | -| UE06 | `DisjointSameBase.First/Second`: same base, disjoint variant sets | Both | Both | Both | -| UE07 | `SharedDifferentBases.ByName/ById`: shared model satisfies two different named bases | Both | Both, different tags/property names | Both, different tags/property names | -| UE08 | `UnionAndDirect`: model reused in union and directly | Yes | Yes | Yes | -| UE09 | `ExplicitBase`: base is an explicitly named alternative | Yes | Yes | Yes | -| UE10 | `ExplicitDefault`: one unnamed default retains unknown tag and modeled data | N/A, no tag/default semantics | Yes | Deferred, wire semantics unspecified | -| UE11a | `NestedVariant`: nested union alternative and outer sibling | Yes | N/A, named Union-valued variant rejected | Yes, nested envelopes | -| UE11b | `UnionConstraint`: named union is the constraint, not an alternative | Yes | Yes | Yes | -| UE11c | `UnionExpressionConstraint`: `string \| int32` constraint | Yes | N/A, non-Model variants | Yes | -| UE12a | `ClosedScalar`: closed string literals | Yes, JSON strings | N/A, non-Model variants | Yes | -| UE12b | `OpenScalar`: literal plus explicit string branch | Yes, JSON strings | N/A, non-Model variants | Yes | -| UE12c | `EnumConstraint`: enum constraint and both members | Yes, JSON strings | N/A, non-Model variants | Yes | -| UE12d | `ArrayConstraint`: arrays of both model alternatives | Yes, JSON arrays | Outside inline-object profile; see below | Yes, array payloads | -| UE12e | `IntersectionConstraint`: intersection of two named models | Yes | Yes | Yes | -| UE12f | `TemplateInstance`: concrete named model template instance constraint | Yes | Yes | Yes | - -[Model cases](./models.tsp) contain UE01-07/09; -[usage cases](./usage.tsp) contain UE08; -[composition cases](./composition.tsp) contain UE10-11; -[constraint cases](./constraints.tsp) contain UE12. - -UE06 declares an additional `Lizard` descendant but does not include it in any -union. Selectors in each context include only that context's fixtures. UE05 uses -the **same Cat model** with `kind: "cat"` versus `petType: "feline"` inline, -and `kind/value` versus `animal/data` envelopes. UE07 independently varies -`kind/value` versus `entityType/entityData` while retaining both `name` and `id`. -UE08 also has `Direct` and `Properties` groups: a direct Cat must have neither -the union's injected discriminator nor its envelope. - -### Without-extends controls - -[`controls.tsp`](./controls.tsp) retains the matched variant models, tags, -operation shapes and fixture values, but omits union extends clauses. Names add -`Controls` after `Type.Union.Extends`, and routes add `/controls` after -`/type/union/extends`. - -| Control | Pairing | Formats / contexts | -| -------------------------------------- | ------- | ---------------------------------------------------- | -| C01 `Controls.DirectInheritance` | UE01 | U/I/E | -| C02 `Controls.Structural` | UE02 | U/I/E, including U overlap | -| C05 `Controls.SharedSameBase` | UE05 | U/I/E, First and Second | -| C10 `Controls.ExplicitDefault` | UE10 | I only; E deferred with UE10 | -| C11 `Controls.NestedVariant` | UE11a | U/E, nested controls use the controlled inner unions | -| C12 `Controls.ClosedScalar/OpenScalar` | UE12a/b | U/E | - -These are representative controls, not a full second Cartesian product. Missing -controls for other primary rows mean reduced duplication, not compiler N/A. - -### Secondary axes - -| Namespace | Coverage | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -| `Nullable` | U/I/E, outer `U \| null`, both non-null alternatives and actual JSON null; omitted bodies are not null | -| `Recursive` | U/E, finite branch containing leaf, plus standalone leaf | -| `Conversion` | U/I/E plus direct Data; inherited `display_name`, Unix timestamp `1704067200` (2024-01-01T00:00:00Z), base64url `-_8` (bytes FB FF) | - -Versioning and malformed/missing-tag resilience are deferred. Wrong closed-union -tags are not defined as valid service responses. Request rejection checks enforce -the authored fixtures; they do not introduce new resilience scenarios. - -## Excluded or unresolved combinations - -- Direct/aliased anonymous model-expression bases are compiler-invalid. Use - declared named bases instead; intersections and named template instances have - separate valid coverage. See the compiler's - [union constraint tests](../../../../../compiler/test/checker/union.test.ts). -- Named non-Model variants in inline unions are rejected by the - [discriminator helper](../../../../../compiler/src/core/helpers/discriminator-utils.ts). - Arrays are compiler Models, so **inline arrays are not labeled - compiler-invalid**. They are outside this inline-object wire profile and need - a separate wire design. -- Object-envelope defaults, including their no-extends control, are **deferred - because their wire semantics are unspecified**, not a normative prohibition. - [OpenAPI 3.2's envelope branch](../../../../../openapi3/src/schema-emitter-3-2.ts) - explicitly says default handling with envelopes is not yet specified; the - [parent schema emitter](../../../../../openapi3/src/schema-emitter.ts) currently - wraps named variants only. That omission alone does not establish whether a - default should represent the payload or the complete envelope. - -**Open design question:** For an object-envelope default, does the default type -describe the complete unknown envelope or only its payload, and how should its -union constraint apply? No guessed wire fixture is included. - -UE10 inline has `UnknownInline extends Named { kind: string; extra: -Record }` as its single unnamed default. It retains `kind: "dragon"`, -the required base `name`, and all nested `extra` data. This is different from -UE09's named `base` alternative and from opening a closed union by implication. - -## Scoped feature configuration - -Only this folder opts into `union-extends` via its project `tspconfig.yaml`. -Spector resolves nearest/inherited config for both scenario and mock validation; -configured imports and linter settings are honored. Validation still forces -no-emit and warnings-as-errors, with configured emitters disabled. The existing -empty http-specs package config stops lookup for ordinary sibling scenarios. +# Union extends: language review scenarios + +Use the template below to describe your language's API for these cases. Group IDs +when the answer is the same. These are discussion examples, not executable tests. + +`union extends` is a structural constraint. It does not require inheritance, add +the base as a variant, make the union open, or determine serialization. + + +```tsp +// Shared definitions +model Named { name: string; } +model Cat extends Named { meow: boolean; } +model Dog extends Named { bark: boolean; } +model Bird extends Named { wings: int32; } +model Fish extends Named { fins: int32; } + +// UE01: Variants directly inherit the named base. +union Pets extends Named { cat: Cat, dog: Dog } + +// UE02: Structural compatibility without inheritance. +model StructuralCat { name: string; meow: boolean; } +model StructuralDog { name: string; bark: boolean; } +union StructuralPets extends Named { cat: StructuralCat, dog: StructuralDog } + +// UE03: Spread and model is. +model SpreadCat { ...Named; meow: boolean; } +model DogShape { name: string; bark: boolean; } +model CopiedDog is DogShape; +union ComposedPets extends Named { cat: SpreadCat, dog: CopiedDog } + +// UE04: Transitive inheritance and a different compatible ancestry. +model Mammal extends Named { age: int32; } +model YoungCat extends Mammal { meow: boolean; } +model OtherBase { name: string; origin: string; } +model OtherDog extends OtherBase { bark: boolean; } +union RelatedPets extends Named { cat: YoungCat, dog: OtherDog } + +// UE05: One variant in two unions with the same base. +union IndoorPets extends Named { cat: Cat, dog: Dog } +union OutdoorPets extends Named { cat: Cat, bird: Bird } + +// UE06: Disjoint variants with the same base, not all descendants. +union WalkingPets extends Named { cat: Cat, dog: Dog } +union OtherPets extends Named { bird: Bird, fish: Fish } + +// UE07: One variant satisfies two different named bases. +model Identified { id: int32; } +model SharedPet { name: string; id: int32; active: boolean; } +model Device extends Identified { serial: string; } +union NamedThings extends Named { shared: SharedPet, cat: Cat } +union IdentifiedThings extends Identified { shared: SharedPet, device: Device } + +// UE08: The same variant used through a union AND directly. +op sendPet(pet: Pets): Pets; +op sendCat(cat: Cat): Cat; +model Holder { pet: Pets; cat: Cat; } + +// UE09: Explicitly include the base as a named alternative. +union PetOrBase extends Named { cat: Cat, base: Named } + +// UE10: Explicit unnamed default retains an unknown tag and modeled data. +model UnknownPet extends Named { kind: string; extra: Record; } +@discriminated(#{ envelope: "none" }) +union OpenPets extends Named { cat: Cat, dog: Dog, UnknownPet } + +// UE11: Nested variant versus named/anonymous union constraint. +union NestedPets extends Named { pets: Pets, bird: Bird } +union ConstrainedPets extends Pets { cat: Cat, dog: Dog } +union MixedValues extends string | int32 { text: string, number: int32 } + +// UE12: Scalar (closed/open), enum, array, intersection, and template bases. +union ClosedStatus extends string { start: "start", stop: "stop" } +union OpenStatus extends string { known: "known", custom: string } +enum Direction { left, right } +union Directions extends Direction { left: Direction.left, right: Direction.right } +union PetArrays extends Named[] { cats: Cat[], dogs: Dog[] } +union Combined extends Named & Identified { shared: SharedPet } +model Wrapper { item: T; } +union Wrapped extends Wrapper { value: Wrapper } +``` + +## Serialization variations + +For applicable cases, consider these formats without repeating identical answers: + +| Format | Decorator on the union | Example Cat payload | +| -------- | --------------------------------------- | -------------------------------------------------------- | +| Untagged | None | `{"name":"Whiskers","meow":true}` | +| Inline | `@discriminated(#{ envelope: "none" })` | `{"kind":"cat","name":"Whiskers","meow":true}` | +| Envelope | `@discriminated` | `{"kind":"cat","value":{"name":"Whiskers","meow":true}}` | + +For UE05/UE07, also vary tags and discriminator/envelope property names while +reusing the same model. Compare removing the union's `extends` clause. Mention +nullable use, recursion, or property conversions only if they change your answer. + +UE10's default is distinct from UE09's named base alternative. It can preserve +`{"kind":"dragon","name":"Future","extra":{"color":"gold"}}`. +**Open question:** Does an object-envelope default describe the payload or the +entire envelope, and how does its constraint apply? This is [not yet specified][default-semantics]. + +Inline named non-model variants and anonymous model-expression bases are +compiler-invalid. Inline arrays need a separate wire design, not a claimed +compiler prohibition. Do not infer unknown-tag fallback for closed unions. + +## Language response template + +```text +Language: +Case ID(s): +Proposed public API sketch (union/base/variants/operation signature): +Request construction: +Response handling + accessing variant-specific data: +Serialization (tags/envelopes/inheritance/conversions): +Reuse impact: +Required restrictions with concrete language/runtime reason: +``` + +Distinguish limitations of the chosen representation from implementation work. +Show variant-specific access and both contexts when a model is reused. + +[default-semantics]: https://github.com/microsoft/typespec/blob/13043f01722ced03ca70e73175273f1f3b99267f/packages/openapi3/src/schema-emitter-3-2.ts#L83-L86 diff --git a/packages/http-specs/specs/type/union/extends/composition.tsp b/packages/http-specs/specs/type/union/extends/composition.tsp deleted file mode 100644 index 7ad77511d31..00000000000 --- a/packages/http-specs/specs/type/union/extends/composition.tsp +++ /dev/null @@ -1,147 +0,0 @@ -using Http; -using Spector; - -namespace Type.Union.Extends; - -/** UE10: An explicit unnamed default, not an implicitly inserted base alternative. */ -@route("/explicit-default") -namespace ExplicitDefault { - model UnknownInline extends Named { - kind: string; - extra: Record; - } - - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - cat: Cat, - dog: Dog, - UnknownInline, - } - - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog" | "unknown", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} - """ - > {} -} - -/** UE11a: A union-valued alternative, including every nested leaf. */ -@route("/nested-variant") -namespace NestedVariant { - union UntaggedUnion extends Named { - pet: DirectInheritance.UntaggedUnion, - bird: Bird, - } - @discriminated - union EnvelopeUnion extends Named { - pet: DirectInheritance.EnvelopeUnion, - bird: Bird, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog" | "bird", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog" | "bird", - """ - cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} - """ - > {} -} - -/** UE11b: A union constraint is distinct from a union-valued alternative. */ -@route("/union-constraint") -namespace UnionConstraint { - union Allowed { - cat: Cat, - dog: Dog, - } - union UntaggedUnion extends Allowed { - cat: Cat, - dog: Dog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Allowed { - cat: Cat, - dog: Dog, - } - @discriminated - union EnvelopeUnion extends Allowed { - cat: Cat, - dog: Dog, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - """ - > {} -} - -/** UE11c: A union expression constrains two different scalar kinds. */ -@route("/union-expression-constraint") -namespace UnionExpressionConstraint { - union UntaggedUnion extends string | int32 { - text: string, - number: int32, - } - @discriminated - union EnvelopeUnion extends string | int32 { - text: string, - number: int32, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "text" | "number", - """ - text: "hello"; number: 42 - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "text" | "number", - """ - text: {"kind":"text","value":"hello"}; number: {"kind":"number","value":42} - """ - > {} -} diff --git a/packages/http-specs/specs/type/union/extends/constraints.tsp b/packages/http-specs/specs/type/union/extends/constraints.tsp deleted file mode 100644 index e122e3f0ae3..00000000000 --- a/packages/http-specs/specs/type/union/extends/constraints.tsp +++ /dev/null @@ -1,256 +0,0 @@ -using Http; -using Spector; - -namespace Type.Union.Extends; - -/** UE12a: The scalar constraint does not make literal alternatives open. */ -@route("/closed-scalar") -namespace ClosedScalar { - union UntaggedUnion extends string { - start: "start", - stop: "stop", - } - @discriminated - union EnvelopeUnion extends string { - start: "start", - stop: "stop", - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "start" | "stop", - """ - start: "start"; stop: "stop" - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "start" | "stop", - """ - start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} - """ - > {} -} - -/** UE12b: Openness comes from the explicitly listed string variant. */ -@route("/open-scalar") -namespace OpenScalar { - union UntaggedUnion extends string { - known: "known", - custom: string, - } - @discriminated - union EnvelopeUnion extends string { - known: "known", - custom: string, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "known" | "custom", - """ - known: "known"; custom: "future" - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "known" | "custom", - """ - known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} - """ - > {} -} - -/** UE12c: Enum members satisfy their declared enum constraint. */ -@route("/enum") -namespace EnumConstraint { - enum Direction { - left, - right, - } - union UntaggedUnion extends Direction { - left: Direction.left, - right: Direction.right, - } - @discriminated - union EnvelopeUnion extends Direction { - left: Direction.left, - right: Direction.right, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "left" | "right", - """ - left: "left"; right: "right" - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "left" | "right", - """ - left: {"kind":"left","value":"left"}; right: {"kind":"right","value":"right"} - """ - > {} -} - -/** UE12d: Array bodies are not synthetic object models or inline discriminators. */ -@route("/array") -namespace ArrayConstraint { - union UntaggedUnion extends Named[] { - cats: Cat[], - dogs: Dog[], - } - @discriminated - union EnvelopeUnion extends Named[] { - cats: Cat[], - dogs: Dog[], - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cats" | "dogs", - """ - cats: [{"name":"Whiskers","meow":true}]; dogs: [{"name":"Rex","bark":false}] - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cats" | "dogs", - """ - cats: {"kind":"cats","value":[{"name":"Whiskers","meow":true}]}; dogs: {"kind":"dogs","value":[{"name":"Rex","bark":false}]} - """ - > {} -} - -/** UE12e: An intersection of named models, not an anonymous model-expression base. */ -@route("/intersection") -namespace IntersectionConstraint { - model Identified { - id: int32; - } - model First { - name: string; - id: int32; - text: string; - } - model Second { - name: string; - id: int32; - count: int32; - } - union UntaggedUnion extends Named & Identified { - first: First, - second: Second, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named & Identified { - first: First, - second: Second, - } - @discriminated - union EnvelopeUnion extends Named & Identified { - first: First, - second: Second, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "first" | "second", - """ - first: {"name":"First","id":1,"text":"hello"}; second: {"name":"Second","id":2,"count":3} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "first" | "second", - """ - first: {"kind":"first","name":"First","id":1,"text":"hello"}; second: {"kind":"second","name":"Second","id":2,"count":3} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "first" | "second", - """ - first: {"kind":"first","value":{"name":"First","id":1,"text":"hello"}}; second: {"kind":"second","value":{"name":"Second","id":2,"count":3}} - """ - > {} -} - -/** UE12f: A concrete named model template instance supplies the constraint. */ -@route("/template-instance") -namespace TemplateInstance { - model Wrapper { - item: T; - } - model First extends Wrapper { - label: string; - } - model Second extends Wrapper { - active: boolean; - } - union UntaggedUnion extends Wrapper { - first: First, - second: Second, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Wrapper { - first: First, - second: Second, - } - @discriminated - union EnvelopeUnion extends Wrapper { - first: First, - second: Second, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "first" | "second", - """ - first: {"item":"one","label":"First"}; second: {"item":"two","active":true} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "first" | "second", - """ - first: {"kind":"first","item":"one","label":"First"}; second: {"kind":"second","item":"two","active":true} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "first" | "second", - """ - first: {"kind":"first","value":{"item":"one","label":"First"}}; second: {"kind":"second","value":{"item":"two","active":true}} - """ - > {} -} diff --git a/packages/http-specs/specs/type/union/extends/controls.tsp b/packages/http-specs/specs/type/union/extends/controls.tsp deleted file mode 100644 index 82cd273be98..00000000000 --- a/packages/http-specs/specs/type/union/extends/controls.tsp +++ /dev/null @@ -1,300 +0,0 @@ -using Http; -using Spector; - -/** Matched controls omit only the corresponding union extends clauses. */ -@route("/controls") -namespace Type.Union.Extends.Controls; - -@route("/direct-inheritance") -namespace DirectInheritance { - union UntaggedUnion { - cat: Cat, - dog: Dog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion { - cat: Cat, - dog: Dog, - } - @discriminated - union EnvelopeUnion { - cat: Cat, - dog: Dog, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - """ - > {} -} - -@route("/structural") -namespace Structural { - union UntaggedUnion { - quiet: Extends.Structural.Quiet, - calm: Extends.Structural.Calm, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion { - quiet: Extends.Structural.Quiet, - calm: Extends.Structural.Calm, - } - @discriminated - union EnvelopeUnion { - quiet: Extends.Structural.Quiet, - calm: Extends.Structural.Calm, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "quiet" | "calm" | "overlap", - """ - quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "quiet" | "calm", - """ - quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "quiet" | "calm", - """ - quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} - """ - > {} -} - -@route("/shared-same-base") -namespace SharedSameBase { - @route("/first") - namespace First { - union UntaggedUnion { - cat: Cat, - dog: Dog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion { - cat: Cat, - dog: Dog, - } - @discriminated - union EnvelopeUnion { - cat: Cat, - dog: Dog, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - """ - > {} - } - @route("/second") - namespace Second { - union UntaggedUnion { - feline: Cat, - bird: Bird, - } - @discriminated(#{ envelope: "none", discriminatorPropertyName: "petType" }) - union InlineUnion { - feline: Cat, - bird: Bird, - } - @discriminated(#{ discriminatorPropertyName: "animal", envelopePropertyName: "data" }) - union EnvelopeUnion { - feline: Cat, - bird: Bird, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "feline" | "bird", - """ - feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "feline" | "bird", - """ - feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "feline" | "bird", - """ - feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} - """ - > {} - } -} - -@route("/explicit-default") -namespace ExplicitDefault { - @discriminated(#{ envelope: "none" }) - union InlineUnion { - cat: Cat, - dog: Dog, - Extends.ExplicitDefault.UnknownInline, - } - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog" | "unknown", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; unknown: {"kind":"dragon","name":"Future","extra":{"color":"gold","size":3,"nested":{"active":true}}} - """ - > {} -} - -@route("/nested-variant") -namespace NestedVariant { - union UntaggedUnion { - pet: DirectInheritance.UntaggedUnion, - bird: Bird, - } - @discriminated - union EnvelopeUnion { - pet: DirectInheritance.EnvelopeUnion, - bird: Bird, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog" | "bird", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; bird: {"name":"Sky","wings":2} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog" | "bird", - """ - cat: {"kind":"pet","value":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; dog: {"kind":"pet","value":{"kind":"dog","value":{"name":"Rex","bark":false}}}; bird: {"kind":"bird","value":{"name":"Sky","wings":2}} - """ - > {} -} - -@route("/closed-scalar") -namespace ClosedScalar { - union UntaggedUnion { - start: "start", - stop: "stop", - } - @discriminated - union EnvelopeUnion { - start: "start", - stop: "stop", - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "start" | "stop", - """ - start: "start"; stop: "stop" - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "start" | "stop", - """ - start: {"kind":"start","value":"start"}; stop: {"kind":"stop","value":"stop"} - """ - > {} -} - -@route("/open-scalar") -namespace OpenScalar { - union UntaggedUnion { - known: "known", - custom: string, - } - @discriminated - union EnvelopeUnion { - known: "known", - custom: string, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "known" | "custom", - """ - known: "known"; custom: "future" - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "known" | "custom", - """ - known: {"kind":"known","value":"known"}; custom: {"kind":"custom","value":"future"} - """ - > {} -} diff --git a/packages/http-specs/specs/type/union/extends/main.tsp b/packages/http-specs/specs/type/union/extends/main.tsp deleted file mode 100644 index ef4481ca56f..00000000000 --- a/packages/http-specs/specs/type/union/extends/main.tsp +++ /dev/null @@ -1,76 +0,0 @@ -import "@typespec/http"; -import "@typespec/spector"; -import "./models.tsp"; -import "./composition.tsp"; -import "./constraints.tsp"; -import "./controls.tsp"; -import "./usage.tsp"; - -using Http; -using Spector; - -/** - * Union extends constrains variant assignability, not inheritance or serialization. - * UE case IDs map to the coverage table in README.md. - */ -@scenarioService("/type/union/extends") -namespace Type.Union.Extends; - -model JsonResponse { - @statusCode statusCode: 200; - @header contentType: "application/json"; - @body body: T; -} - -interface GetAndPut { - @scenario - @scenarioDoc(""" - Call once for EVERY choice below. Each choice returns the corresponding JSON - body with status 200 and Content-Type application/json. Access the - variant-specific data; ambiguous untagged shapes do not require a unique class. - ${Fixtures} - """) - @get - @route("/{choice}") - get(@path choice: Choice): JsonResponse; - - @scenario - @scenarioDoc(""" - Call once for EVERY choice below. Send the corresponding exact JSON body - with Content-Type application/json. Expect the same JSON body with status 200 - and Content-Type application/json. Retain all inherited and specific fields. - ${Fixtures} - """) - @put - @route("/{choice}") - put( - @path choice: Choice, - @body body: T, - @header contentType: "application/json", - ): JsonResponse; -} - -model Named { - name: string; -} - -model Cat extends Named { - meow: boolean; -} - -model Dog extends Named { - bark: boolean; -} - -model Bird extends Named { - wings: int32; -} - -model Fish extends Named { - fins: int32; -} - -// Deliberately not a variant of any of the unions constrained by Named. -model Lizard extends Named { - scales: boolean; -} diff --git a/packages/http-specs/specs/type/union/extends/mockapi.ts b/packages/http-specs/specs/type/union/extends/mockapi.ts deleted file mode 100644 index 010b551e714..00000000000 --- a/packages/http-specs/specs/type/union/extends/mockapi.ts +++ /dev/null @@ -1,246 +0,0 @@ -import type { MockApiDefinition, ScenarioMockApi } from "@typespec/spec-api"; -import { json, passOnSuccess } from "@typespec/spec-api"; - -export const Scenarios: Record = {}; - -type Fixtures = Record; -type ModelFixtures = Record>; - -function define(name: string, route: string, fixtures: Fixtures) { - for (const method of ["get", "put"] as const) { - const apis: MockApiDefinition[] = Object.entries(fixtures).map(([choice, value]) => { - const response = { - status: 200, - headers: { "Content-Type": "application/json; charset=utf-8" }, - body: json(value), - }; - return { - kind: "MockApiDefinition", - uri: `/type/union/extends/${route}/${choice}`, - method, - request: - method === "put" - ? { headers: { "Content-Type": "application/json" }, body: json(value) } - : {}, - response, - handler: - method === "put" - ? (req) => { - req.expect.containsHeader("content-type", "application/json"); - // Compare decoded JSON without coercion; null is a body, not an omitted body. - req.expect.deepEqual(req.body, value, "Unexpected union payload"); - return response; - } - : undefined, - }; - }); - // Distinct concrete paths require every choice, not just one successful response. - Scenarios[`Type_Union_Extends_${name}_${method}`] = passOnSuccess(apis); - } -} - -function inline(fixtures: ModelFixtures, discriminator = "kind"): ModelFixtures { - return Object.fromEntries( - Object.entries(fixtures).map(([tag, value]) => [tag, { [discriminator]: tag, ...value }]), - ); -} - -function envelope(fixtures: Fixtures, discriminator = "kind", property = "value"): ModelFixtures { - return Object.fromEntries( - Object.entries(fixtures).map(([tag, value]) => [ - tag, - { [discriminator]: tag, [property]: value }, - ]), - ); -} - -function models( - name: string, - route: string, - fixtures: ModelFixtures, - inlineDiscriminator = "kind", - envelopeDiscriminator = "kind", - envelopeProperty = "value", -) { - define(`${name}_Untagged`, `${route}/untagged`, fixtures); - define(`${name}_Inline`, `${route}/inline`, inline(fixtures, inlineDiscriminator)); - define( - `${name}_Envelope`, - `${route}/envelope`, - envelope(fixtures, envelopeDiscriminator, envelopeProperty), - ); -} - -function values(name: string, route: string, fixtures: Fixtures) { - define(`${name}_Untagged`, `${route}/untagged`, fixtures); - define(`${name}_Envelope`, `${route}/envelope`, envelope(fixtures)); -} - -const cat = { name: "Whiskers", meow: true }; -const dog = { name: "Rex", bark: false }; -const bird = { name: "Sky", wings: 2 }; -const fish = { name: "Gold", fins: 3 }; -const pets = { cat, dog }; - -// UE01, UE03, UE05 (first context), UE06 (first context), UE08 and matched controls. -for (const [name, route] of [ - ["DirectInheritance", "direct-inheritance"], - ["SpreadAndIs", "spread-and-is"], - ["SharedSameBase_First", "shared-same-base/first"], - ["DisjointSameBase_First", "disjoint-same-base/first"], - ["UnionAndDirect", "union-and-direct"], - ["Controls_DirectInheritance", "controls/direct-inheritance"], - ["Controls_SharedSameBase_First", "controls/shared-same-base/first"], -]) { - models(name, route, pets); -} - -// UE02: Optional specific properties create one deliberately ambiguous untagged value. -const structural = { - quiet: { name: "Quiet", volume: 2 }, - calm: { name: "Calm", asleep: true }, -}; -for (const [name, route] of [ - ["Structural", "structural"], - ["Controls_Structural", "controls/structural"], -]) { - define(`${name}_Untagged`, `${route}/untagged`, { - ...structural, - overlap: { name: "Both" }, - }); - define(`${name}_Inline`, `${route}/inline`, inline(structural)); - define(`${name}_Envelope`, `${route}/envelope`, envelope(structural)); -} - -// UE04: Neither intermediate nor alternate-ancestor fields may disappear. -models("Ancestry", "ancestry", { - cat: { name: "Kitten", age: 1, meow: true }, - dog: { name: "Rex", origin: "shelter", bark: false }, -}); - -// UE05: The exact same Cat has different tags and property names in the second union. -for (const [name, route] of [ - ["SharedSameBase_Second", "shared-same-base/second"], - ["Controls_SharedSameBase_Second", "controls/shared-same-base/second"], -]) { - models(name, route, { feline: cat, bird }, "petType", "animal", "data"); -} - -// UE06: No cat/dog endpoints in the second union, nor bird/fish in the first. -models("DisjointSameBase_Second", "disjoint-same-base/second", { bird, fish }); - -// UE07: Both name and id must survive in both structurally constrained contexts. -const shared = { name: "Shared", id: 7, active: true }; -models("SharedDifferentBases_ByName", "shared-different-bases/named", { shared, cat }); -models( - "SharedDifferentBases_ById", - "shared-different-bases/identified", - { entity: shared, other: { id: 8, code: "other" } }, - "entityType", - "entityType", - "entityData", -); - -// UE08: Direct model uses have neither a union tag nor an envelope. -define("UnionAndDirect_Direct", "union-and-direct/direct", { cat }); -define( - "UnionAndDirect_Properties", - "union-and-direct/properties", - Object.fromEntries( - Object.entries(pets).map(([tag, pet]) => [ - tag, - { - direct: cat, - untagged: pet, - inline: { kind: tag, ...pet }, - envelope: { kind: tag, value: pet }, - }, - ]), - ), -); - -// UE09: Base is an explicit named variant, not a default or an implicit alternative. -models("ExplicitBase", "explicit-base", { cat, base: { name: "Base" } }); - -// UE10: Only the explicitly modeled inline default admits this unknown tag and data. -const defaults = { - ...inline(pets), - unknown: { - kind: "dragon", - name: "Future", - extra: { color: "gold", size: 3, nested: { active: true } }, - }, -}; -define("ExplicitDefault_Inline", "explicit-default/inline", defaults); -define("Controls_ExplicitDefault_Inline", "controls/explicit-default/inline", defaults); - -// UE11a: Every nested leaf and the outer sibling have their own concrete choice. -for (const [name, route] of [ - ["NestedVariant", "nested-variant"], - ["Controls_NestedVariant", "controls/nested-variant"], -]) { - define(`${name}_Untagged`, `${route}/untagged`, { ...pets, bird }); - define(`${name}_Envelope`, `${route}/envelope`, { - cat: { kind: "pet", value: { kind: "cat", value: cat } }, - dog: { kind: "pet", value: { kind: "dog", value: dog } }, - bird: { kind: "bird", value: bird }, - }); -} - -// UE11b/c: Named union and union-expression constraints. -models("UnionConstraint", "union-constraint", pets); -values("UnionExpressionConstraint", "union-expression-constraint", { text: "hello", number: 42 }); - -// UE12a/b: Open string branch is explicit; the closed union has no unknown fixture. -for (const [name, route] of [ - ["ClosedScalar", "closed-scalar"], - ["Controls_ClosedScalar", "controls/closed-scalar"], -]) { - values(name, route, { start: "start", stop: "stop" }); -} -for (const [name, route] of [ - ["OpenScalar", "open-scalar"], - ["Controls_OpenScalar", "controls/open-scalar"], -]) { - values(name, route, { known: "known", custom: "future" }); -} - -// UE12c/d/e/f: Preserve primitive/array bodies, intersection fields and template data. -values("EnumConstraint", "enum", { left: "left", right: "right" }); -values("ArrayConstraint", "array", { cats: [cat], dogs: [dog] }); -models("IntersectionConstraint", "intersection", { - first: { name: "First", id: 1, text: "hello" }, - second: { name: "Second", id: 2, count: 3 }, -}); -models("TemplateInstance", "template-instance", { - first: { item: "one", label: "First" }, - second: { item: "two", active: true }, -}); - -define("Nullable_Untagged", "nullable/untagged", { ...pets, null: null }); -define("Nullable_Inline", "nullable/inline", { ...inline(pets), null: null }); -define("Nullable_Envelope", "nullable/envelope", { ...envelope(pets), null: null }); - -const leaf = { name: "Leaf", value: 1 }; -define("Recursive_Untagged", "recursive/untagged", { - leaf, - branch: { name: "Branch", children: [leaf] }, -}); -define("Recursive_Envelope", "recursive/envelope", { - leaf: { kind: "leaf", value: leaf }, - branch: { - kind: "branch", - value: { name: "Branch", children: [{ kind: "leaf", value: leaf }] }, - }, -}); - -const data = { display_name: "Data", createdAt: 1704067200, content: "-_8" }; -models( - "Conversion", - "conversion", - { data, count: { display_name: "Count", createdAt: 1704067200, count: 3 } }, - "dataType", - "dataType", - "payload", -); -define("Conversion_Direct", "conversion/direct", { data }); diff --git a/packages/http-specs/specs/type/union/extends/models.tsp b/packages/http-specs/specs/type/union/extends/models.tsp deleted file mode 100644 index 4c6092c232a..00000000000 --- a/packages/http-specs/specs/type/union/extends/models.tsp +++ /dev/null @@ -1,570 +0,0 @@ -using Http; -using Spector; - -namespace Type.Union.Extends; - -/** UE01: Both alternatives directly inherit the declared base. */ -@route("/direct-inheritance") -namespace DirectInheritance { - union UntaggedUnion extends Named { - cat: Cat, - dog: Dog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - cat: Cat, - dog: Dog, - } - @discriminated - union EnvelopeUnion extends Named { - cat: Cat, - dog: Dog, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - """ - > {} -} - -/** UE02: Neither alternative inherits the base; "overlap" has no unique shape. */ -@route("/structural") -namespace Structural { - model Quiet { - name: string; - volume?: int32; - } - model Calm { - name: string; - asleep?: boolean; - } - union UntaggedUnion extends Named { - quiet: Quiet, - calm: Calm, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - quiet: Quiet, - calm: Calm, - } - @discriminated - union EnvelopeUnion extends Named { - quiet: Quiet, - calm: Calm, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "quiet" | "calm" | "overlap", - """ - quiet: {"name":"Quiet","volume":2}; calm: {"name":"Calm","asleep":true}; overlap: {"name":"Both"} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "quiet" | "calm", - """ - quiet: {"kind":"quiet","name":"Quiet","volume":2}; calm: {"kind":"calm","name":"Calm","asleep":true} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "quiet" | "calm", - """ - quiet: {"kind":"quiet","value":{"name":"Quiet","volume":2}}; calm: {"kind":"calm","value":{"name":"Calm","asleep":true}} - """ - > {} -} - -/** UE03: Property composition with spread and model is, not nominal inheritance. */ -@route("/spread-and-is") -namespace SpreadAndIs { - model SpreadCat { - ...Named; - meow: boolean; - } - model DogShape { - ...Named; - bark: boolean; - } - model IsDog is DogShape; - union UntaggedUnion extends Named { - cat: SpreadCat, - dog: IsDog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - cat: SpreadCat, - dog: IsDog, - } - @discriminated - union EnvelopeUnion extends Named { - cat: SpreadCat, - dog: IsDog, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - """ - > {} -} - -/** UE04: Required intermediate properties and a separate compatible ancestry. */ -@route("/ancestry") -namespace Ancestry { - model Mammal extends Named { - age: int32; - } - model YoungCat extends Mammal { - meow: boolean; - } - model OtherAncestor { - name: string; - origin: string; - } - model OtherDog extends OtherAncestor { - bark: boolean; - } - union UntaggedUnion extends Named { - cat: YoungCat, - dog: OtherDog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - cat: YoungCat, - dog: OtherDog, - } - @discriminated - union EnvelopeUnion extends Named { - cat: YoungCat, - dog: OtherDog, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Kitten","age":1,"meow":true}; dog: {"name":"Rex","origin":"shelter","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Kitten","age":1,"meow":true}; dog: {"kind":"dog","name":"Rex","origin":"shelter","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Kitten","age":1,"meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","origin":"shelter","bark":false}} - """ - > {} -} - -/** UE05: Cat is the same model in two unions with the same constraint. */ -@route("/shared-same-base") -namespace SharedSameBase { - @route("/first") - namespace First { - union UntaggedUnion extends Named { - cat: Cat, - dog: Dog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - cat: Cat, - dog: Dog, - } - @discriminated - union EnvelopeUnion extends Named { - cat: Cat, - dog: Dog, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - """ - > {} - } - - @route("/second") - namespace Second { - union UntaggedUnion extends Named { - feline: Cat, - bird: Bird, - } - @discriminated(#{ envelope: "none", discriminatorPropertyName: "petType" }) - union InlineUnion extends Named { - feline: Cat, - bird: Bird, - } - @discriminated(#{ discriminatorPropertyName: "animal", envelopePropertyName: "data" }) - union EnvelopeUnion extends Named { - feline: Cat, - bird: Bird, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "feline" | "bird", - """ - feline: {"name":"Whiskers","meow":true}; bird: {"name":"Sky","wings":2} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "feline" | "bird", - """ - feline: {"petType":"feline","name":"Whiskers","meow":true}; bird: {"petType":"bird","name":"Sky","wings":2} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "feline" | "bird", - """ - feline: {"animal":"feline","data":{"name":"Whiskers","meow":true}}; bird: {"animal":"bird","data":{"name":"Sky","wings":2}} - """ - > {} - } -} - -/** UE06: Union membership is not all descendants of the shared named base. */ -@route("/disjoint-same-base") -namespace DisjointSameBase { - @route("/first") - namespace First { - union UntaggedUnion extends Named { - cat: Cat, - dog: Dog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - cat: Cat, - dog: Dog, - } - @discriminated - union EnvelopeUnion extends Named { - cat: Cat, - dog: Dog, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - """ - > {} - } - - @route("/second") - namespace Second { - union UntaggedUnion extends Named { - bird: Bird, - fish: Fish, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - bird: Bird, - fish: Fish, - } - @discriminated - union EnvelopeUnion extends Named { - bird: Bird, - fish: Fish, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "bird" | "fish", - """ - bird: {"name":"Sky","wings":2}; fish: {"name":"Gold","fins":3} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "bird" | "fish", - """ - bird: {"kind":"bird","name":"Sky","wings":2}; fish: {"kind":"fish","name":"Gold","fins":3} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "bird" | "fish", - """ - bird: {"kind":"bird","value":{"name":"Sky","wings":2}}; fish: {"kind":"fish","value":{"name":"Gold","fins":3}} - """ - > {} - } -} - -/** UE07: A structural variant satisfies two different named base shapes. */ -@route("/shared-different-bases") -namespace SharedDifferentBases { - model Identified { - id: int32; - } - model Shared { - name: string; - id: int32; - active: boolean; - } - model Other { - id: int32; - code: string; - } - - @route("/named") - namespace ByName { - union UntaggedUnion extends Named { - shared: Shared, - cat: Cat, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - shared: Shared, - cat: Cat, - } - @discriminated - union EnvelopeUnion extends Named { - shared: Shared, - cat: Cat, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "shared" | "cat", - """ - shared: {"name":"Shared","id":7,"active":true}; cat: {"name":"Whiskers","meow":true} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "shared" | "cat", - """ - shared: {"kind":"shared","name":"Shared","id":7,"active":true}; cat: {"kind":"cat","name":"Whiskers","meow":true} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "shared" | "cat", - """ - shared: {"kind":"shared","value":{"name":"Shared","id":7,"active":true}}; cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}} - """ - > {} - } - - @route("/identified") - namespace ById { - union UntaggedUnion extends Identified { - entity: Shared, - other: Other, - } - @discriminated(#{ envelope: "none", discriminatorPropertyName: "entityType" }) - union InlineUnion extends Identified { - entity: Shared, - other: Other, - } - @discriminated(#{ discriminatorPropertyName: "entityType", envelopePropertyName: "entityData" }) - union EnvelopeUnion extends Identified { - entity: Shared, - other: Other, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "entity" | "other", - """ - entity: {"name":"Shared","id":7,"active":true}; other: {"id":8,"code":"other"} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "entity" | "other", - """ - entity: {"entityType":"entity","name":"Shared","id":7,"active":true}; other: {"entityType":"other","id":8,"code":"other"} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "entity" | "other", - """ - entity: {"entityType":"entity","entityData":{"name":"Shared","id":7,"active":true}}; other: {"entityType":"other","entityData":{"id":8,"code":"other"}} - """ - > {} - } -} - -/** UE09: The base alternative is explicitly listed, never automatically inserted. */ -@route("/explicit-base") -namespace ExplicitBase { - union UntaggedUnion extends Named { - cat: Cat, - base: Named, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - cat: Cat, - base: Named, - } - @discriminated - union EnvelopeUnion extends Named { - cat: Cat, - base: Named, - } - - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "base", - """ - cat: {"name":"Whiskers","meow":true}; base: {"name":"Base"} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "base", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; base: {"kind":"base","name":"Base"} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "base", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; base: {"kind":"base","value":{"name":"Base"}} - """ - > {} -} diff --git a/packages/http-specs/specs/type/union/extends/tspconfig.yaml b/packages/http-specs/specs/type/union/extends/tspconfig.yaml deleted file mode 100644 index c4a50e9d0f1..00000000000 --- a/packages/http-specs/specs/type/union/extends/tspconfig.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: project -features: - - union-extends diff --git a/packages/http-specs/specs/type/union/extends/usage.tsp b/packages/http-specs/specs/type/union/extends/usage.tsp deleted file mode 100644 index b77cb5b7763..00000000000 --- a/packages/http-specs/specs/type/union/extends/usage.tsp +++ /dev/null @@ -1,218 +0,0 @@ -using Http; -using Spector; - -namespace Type.Union.Extends; - -/** UE08: Reuse the exact variant model in unions, direct bodies, and properties. */ -@route("/union-and-direct") -namespace UnionAndDirect { - union UntaggedUnion extends Named { - cat: Cat, - dog: Dog, - } - @discriminated(#{ envelope: "none" }) - union InlineUnion extends Named { - cat: Cat, - dog: Dog, - } - @discriminated - union EnvelopeUnion extends Named { - cat: Cat, - dog: Dog, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "cat" | "dog", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "cat" | "dog", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}} - """ - > {} - @route("/direct") - interface Direct - extends GetAndPut< - Cat, - "cat", - """ - cat: {"name":"Whiskers","meow":true} - """ - > {} - - model MixedUse { - direct: Cat; - untagged: UntaggedUnion; - inline: InlineUnion; - envelope: EnvelopeUnion; - } - @route("/properties") - interface Properties - extends GetAndPut< - MixedUse, - "cat" | "dog", - """ - cat: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Whiskers","meow":true},"inline":{"kind":"cat","name":"Whiskers","meow":true},"envelope":{"kind":"cat","value":{"name":"Whiskers","meow":true}}}; - dog: {"direct":{"name":"Whiskers","meow":true},"untagged":{"name":"Rex","bark":false},"inline":{"kind":"dog","name":"Rex","bark":false},"envelope":{"kind":"dog","value":{"name":"Rex","bark":false}}} - """ - > {} -} - -/** Outer nullability does not add null to the model-constrained union. */ -@route("/nullable") -namespace Nullable { - @route("/untagged") - interface Untagged - extends GetAndPut< - DirectInheritance.UntaggedUnion | null, - "cat" | "dog" | "null", - """ - cat: {"name":"Whiskers","meow":true}; dog: {"name":"Rex","bark":false}; null: null - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - DirectInheritance.InlineUnion | null, - "cat" | "dog" | "null", - """ - cat: {"kind":"cat","name":"Whiskers","meow":true}; dog: {"kind":"dog","name":"Rex","bark":false}; null: null - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - DirectInheritance.EnvelopeUnion | null, - "cat" | "dog" | "null", - """ - cat: {"kind":"cat","value":{"name":"Whiskers","meow":true}}; dog: {"kind":"dog","value":{"name":"Rex","bark":false}}; null: null - """ - > {} -} - -/** A finite recursive fixture includes both the recursive and terminal alternative. */ -@route("/recursive") -namespace Recursive { - model Leaf extends Named { - value: int32; - } - model UntaggedBranch extends Named { - children: UntaggedUnion[]; - } - model EnvelopeBranch extends Named { - children: EnvelopeUnion[]; - } - union UntaggedUnion extends Named { - leaf: Leaf, - branch: UntaggedBranch, - } - @discriminated - union EnvelopeUnion extends Named { - leaf: Leaf, - branch: EnvelopeBranch, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "leaf" | "branch", - """ - leaf: {"name":"Leaf","value":1}; branch: {"name":"Branch","children":[{"name":"Leaf","value":1}]} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "leaf" | "branch", - """ - leaf: {"kind":"leaf","value":{"name":"Leaf","value":1}}; branch: {"kind":"branch","value":{"name":"Branch","children":[{"kind":"leaf","value":{"name":"Leaf","value":1}}]}} - """ - > {} -} - -/** Conversions must survive union/direct reuse without leaking tags or envelopes. */ -@route("/conversion") -namespace Conversion { - model EncodedBase { - @encodedName("application/json", "display_name") - displayName: string; - - @encode(DateTimeKnownEncoding.unixTimestamp, int64) - createdAt: utcDateTime; - } - model Data extends EncodedBase { - @encode(BytesKnownEncoding.base64url) - content: bytes; - } - model Count extends EncodedBase { - count: int32; - } - union UntaggedUnion extends EncodedBase { - data: Data, - count: Count, - } - @discriminated(#{ envelope: "none", discriminatorPropertyName: "dataType" }) - union InlineUnion extends EncodedBase { - data: Data, - count: Count, - } - @discriminated(#{ discriminatorPropertyName: "dataType", envelopePropertyName: "payload" }) - union EnvelopeUnion extends EncodedBase { - data: Data, - count: Count, - } - @route("/untagged") - interface Untagged - extends GetAndPut< - UntaggedUnion, - "data" | "count", - """ - data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"display_name":"Count","createdAt":1704067200,"count":3} - """ - > {} - @route("/inline") - interface Inline - extends GetAndPut< - InlineUnion, - "data" | "count", - """ - data: {"dataType":"data","display_name":"Data","createdAt":1704067200,"content":"-_8"}; count: {"dataType":"count","display_name":"Count","createdAt":1704067200,"count":3} - """ - > {} - @route("/envelope") - interface Envelope - extends GetAndPut< - EnvelopeUnion, - "data" | "count", - """ - data: {"dataType":"data","payload":{"display_name":"Data","createdAt":1704067200,"content":"-_8"}}; count: {"dataType":"count","payload":{"display_name":"Count","createdAt":1704067200,"count":3}} - """ - > {} - @route("/direct") - interface Direct - extends GetAndPut< - Data, - "data", - """ - data: {"display_name":"Data","createdAt":1704067200,"content":"-_8"} - """ - > {} -} diff --git a/packages/spector/src/actions/validate-mock-apis.ts b/packages/spector/src/actions/validate-mock-apis.ts index b884f983f09..b0ded1d0952 100644 --- a/packages/spector/src/actions/validate-mock-apis.ts +++ b/packages/spector/src/actions/validate-mock-apis.ts @@ -2,7 +2,6 @@ import type { Operation } from "@typespec/compiler"; import pc from "picocolors"; import { logger } from "../logger.js"; import { findScenarioSpecFiles, loadScenarioMockApiFiles } from "../scenarios-resolver.js"; -import { compileScenario } from "../spec-utils/compile-scenario.js"; import { importSpecExpect, importTypeSpec, importTypeSpecHttp } from "../spec-utils/import-spec.js"; import { createDiagnosticReporter } from "../utils/diagnostic-reporter.js"; import { @@ -36,10 +35,13 @@ export async function validateMockApis({ const diagnostics = createDiagnosticReporter(); for (const { name, specFilePath } of scenarioFiles) { logger.debug(`Found scenario "${specFilePath}"`); - const [program, compilerDiagnostics] = await compileScenario(specCompiler, specFilePath); + const program = await specCompiler.compile(specCompiler.NodeHost, specFilePath, { + noEmit: true, + warningAsError: true, + }); // Workaround https://github.com/Azure/cadl-azure/issues/2458 - const programDiagnostics = compilerDiagnostics.filter( + const programDiagnostics = program.diagnostics.filter( (d) => !( d.code === "@azure-tools/typespec-azure-core/casing-style" && @@ -50,7 +52,7 @@ export async function validateMockApis({ ), ); - if (programDiagnostics.length > 0 || program === undefined) { + if (programDiagnostics.length > 0) { specCompiler.logDiagnostics(programDiagnostics, specCompiler.NodeHost.logSink); diagnostics.reportDiagnostic({ message: `Scenario ${name} is invalid.`, diff --git a/packages/spector/src/scenarios-resolver.ts b/packages/spector/src/scenarios-resolver.ts index 8a6eb97d2fc..8662808f84c 100644 --- a/packages/spector/src/scenarios-resolver.ts +++ b/packages/spector/src/scenarios-resolver.ts @@ -6,12 +6,7 @@ import pc from "picocolors"; import { pathToFileURL } from "url"; import type { Scenario } from "./lib/decorators.js"; import { logger } from "./logger.js"; -import { - compileScenario, - importSpecExpect, - importTypeSpec, - importTypeSpecHttp, -} from "./spec-utils/index.js"; +import { importSpecExpect, importTypeSpec, importTypeSpecHttp } from "./spec-utils/index.js"; import { findFilesFromPattern } from "./utils/file-utils.js"; import type { Diagnostic } from "./utils/index.js"; import { @@ -74,12 +69,14 @@ export async function loadScenarios( for (const { name, specFilePath } of scenarioFiles) { logger.debug(`Found scenario "${specFilePath}"`); - const [program, compilerDiagnostics] = await compileScenario(typespecCompiler, specFilePath, [ - "@typespec/spector", - ]); + const program = await typespecCompiler.compile(typespecCompiler.NodeHost, specFilePath, { + additionalImports: ["@typespec/spector"], + noEmit: true, + warningAsError: true, + }); // Workaround https://github.com/Azure/cadl-azure/issues/2458 - const programDiagnostics = compilerDiagnostics.filter( + const programDiagnostics = program.diagnostics.filter( (d) => !( d.code === "@azure-tools/typespec-azure-core/casing-style" && @@ -90,7 +87,7 @@ export async function loadScenarios( ), ); - if (programDiagnostics.length > 0 || program === undefined) { + if (programDiagnostics.length > 0) { for (const item of programDiagnostics) { const sourceLocation = typespecCompiler.getSourceLocation(item.target); diagnostics.reportDiagnostic({ diff --git a/packages/spector/src/spec-utils/compile-scenario.ts b/packages/spector/src/spec-utils/compile-scenario.ts deleted file mode 100644 index a0767567729..00000000000 --- a/packages/spector/src/spec-utils/compile-scenario.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Diagnostic, Program } from "@typespec/compiler"; -import { dirname, resolve } from "path"; - -export async function compileScenario( - compiler: typeof import("@typespec/compiler"), - specFilePath: string, - additionalImports: string[] = [], -): Promise<[Program | undefined, readonly Diagnostic[]]> { - const entrypoint = resolve(specFilePath); - const [options, diagnostics] = await compiler.resolveCompilerOptions(compiler.NodeHost, { - entrypoint, - cwd: dirname(entrypoint), - env: process.env, - }); - if (diagnostics.length > 0) { - return [undefined, diagnostics]; - } - - const program = await compiler.compile(compiler.NodeHost, entrypoint, { - ...options, - additionalImports: [...(options.additionalImports ?? []), ...additionalImports], - noEmit: true, - warningAsError: true, - // noEmit alone still loads configured emitters. - emit: [], - }); - return [program, program.diagnostics]; -} diff --git a/packages/spector/src/spec-utils/index.ts b/packages/spector/src/spec-utils/index.ts index 480c548c43d..63bfc668d1e 100644 --- a/packages/spector/src/spec-utils/index.ts +++ b/packages/spector/src/spec-utils/index.ts @@ -1,2 +1 @@ -export * from "./compile-scenario.js"; export * from "./import-spec.js"; diff --git a/packages/spector/test/scenario-config.test.ts b/packages/spector/test/scenario-config.test.ts deleted file mode 100644 index 4e755bc8052..00000000000 --- a/packages/spector/test/scenario-config.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -import * as compiler from "@typespec/compiler"; -import { randomUUID } from "crypto"; -import { mkdir, rm, writeFile } from "fs/promises"; -import { dirname, join, relative } from "path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { validateMockApis } from "../src/actions/validate-mock-apis.js"; -import { logger } from "../src/logger.js"; -import { loadScenarios } from "../src/scenarios-resolver.js"; -import { compileScenario } from "../src/spec-utils/compile-scenario.js"; - -const enabledConfig = "kind: project\nfeatures: [union-extends]\n"; -const source = ` -import "@typespec/http"; -import "@typespec/spector"; -using TypeSpec.Http; -using TypeSpec.Spector; - -@scenarioService("/config") -namespace Config; - -model Base { name: string; } -model Item extends Base { value: int32; } -union Result extends Base { item: Item } - -@scenario -@scenarioDoc("Read a configured union.") -@get -op read(): Result; -`; - -let root: string; -let scenariosPath: string; -let specFilePath: string; -let messages: string[]; - -async function writeFixture(path: string, content: string) { - const fullPath = join(root, path); - await mkdir(dirname(fullPath), { recursive: true }); - await writeFile(fullPath, content); -} - -beforeEach(async () => { - root = join(import.meta.dirname, `.scenario-config-${randomUUID()}`); - scenariosPath = join(root, "specs"); - specFilePath = join(scenariosPath, "case/main.tsp"); - messages = []; - vi.spyOn(logger, "info").mockImplementation(() => {}); - vi.spyOn(logger, "error").mockImplementation((message) => { - messages.push(message); - }); - vi.spyOn(compiler.NodeHost.logSink, "log").mockImplementation((log) => { - messages.push(log.message); - }); - await writeFixture("specs/case/main.tsp", source); - await writeFixture( - "dist/case/mockapi.js", - `export const Scenarios = { - Config_read: { - passCondition: "response-success", - apis: [{ - kind: "MockApiDefinition", - method: "get", - uri: "/config", - response: { status: 200, body: { name: "item", value: 1 } }, - }], - }, - };`, - ); -}); - -afterEach(async () => { - vi.restoreAllMocks(); - vi.unstubAllEnvs(); - await rm(root, { recursive: true, force: true }); -}); - -describe.each(["loadScenarios", "validateMockApis"] as const)("%s configuration", (entrypoint) => { - async function validate() { - if (entrypoint === "loadScenarios") { - const [scenarios, diagnostics] = await loadScenarios(scenariosPath); - if (diagnostics.length === 0) { - expect(scenarios.map((scenario) => scenario.name)).toEqual(["Config_read"]); - } else { - expect(scenarios).toEqual([]); - } - return diagnostics.length === 0; - } - return !(await validateMockApis({ scenariosPath, hasMoreScenarios: true })); - } - - it("enables union extends using the scenario's project config", async () => { - await writeFixture("specs/case/tspconfig.yaml", enabledConfig); - expect(await validate()).toBe(true); - }); - - it.each([undefined, "", "kind: project\nfeatures: []\n"])( - "rejects union extends without the feature (%j)", - async (config) => { - if (config !== undefined) { - await writeFixture("specs/case/tspconfig.yaml", config); - } - expect(await validate()).toBe(false); - expect(messages.join("\n")).toContain("Union `extends` clauses require the 'union-extends'"); - }, - ); - - it.each([undefined, ""])( - "accepts ordinary scenarios with no/empty config (%j)", - async (config) => { - await writeFixture( - "specs/case/main.tsp", - source.replace("union Result extends Base", "union Result"), - ); - if (config !== undefined) { - await writeFixture("specs/case/tspconfig.yaml", config); - } - expect(await validate()).toBe(true); - }, - ); - - it("discovers config in the nearest parent directory", async () => { - await writeFixture("specs/tspconfig.yaml", enabledConfig); - expect(await validate()).toBe(true); - }); - - it("uses the nearest config rather than an invalid ancestor", async () => { - await writeFixture("specs/tspconfig.yaml", "features: ["); - await writeFixture("specs/case/tspconfig.yaml", enabledConfig); - expect(await validate()).toBe(true); - }); - - it("stops upward discovery at an empty config", async () => { - await writeFixture("specs/tspconfig.yaml", enabledConfig); - await writeFixture("specs/case/tspconfig.yaml", ""); - expect(await validate()).toBe(false); - expect(messages.join("\n")).toContain("Union `extends` clauses require the 'union-extends'"); - }); - - it("inherits enabled features through an explicit extends", async () => { - await writeFixture("shared/tspconfig.yaml", enabledConfig); - await writeFixture( - "specs/case/tspconfig.yaml", - "kind: project\nextends: ../../shared/tspconfig.yaml\n", - ); - expect(await validate()).toBe(true); - }); - - it("resolves declared environment variables even when emission is disabled", async () => { - vi.stubEnv("SPECTOR_SCENARIO_OUTPUT_DIR", join(root, "output")); - await writeFixture( - "specs/case/tspconfig.yaml", - `${enabledConfig}environment-variables: - SPECTOR_SCENARIO_OUTPUT_DIR: - default: "" -output-dir: "{env.SPECTOR_SCENARIO_OUTPUT_DIR}" -`, - ); - expect(await validate()).toBe(true); - }); - - it.each(["features: [", "kind: project\nfeatures: true\n"])( - "reports malformed config instead of compiling with defaults (%j)", - async (config) => { - await writeFixture( - "specs/case/main.tsp", - source.replace("union Result extends Base", "union Result"), - ); - await writeFixture("specs/case/tspconfig.yaml", config); - expect(await validate()).toBe(false); - expect(messages.join("\n")).toMatch(/Flow sequence|array/); - }, - ); - - it("reports missing inherited config", async () => { - await writeFixture("specs/case/tspconfig.yaml", `${enabledConfig}extends: ./missing.yaml\n`); - expect(await validate()).toBe(false); - expect(messages.join("\n")).toContain("missing.yaml"); - }); - - it("honors configured imports relative to the scenario", async () => { - await writeFixture( - "specs/case/main.tsp", - source.replace('import "@typespec/spector";', "").replace("model Base { name: string; }", ""), - ); - await writeFixture("specs/case/base.tsp", "namespace Config; model Base { name: string; }"); - await writeFixture( - "specs/case/tspconfig.yaml", - `${enabledConfig}imports:\n - "@typespec/spector"\n - "./base.tsp"\n`, - ); - expect(await validate()).toBe(true); - }); - - it("does not load or execute configured emitters", async () => { - await writeFixture( - "specs/case/emitter.mjs", - `throw new Error("Configured emitter must not be loaded"); - export function $onEmit() { throw new Error("Configured emitter must not run"); }`, - ); - await writeFixture( - "specs/case/tspconfig.yaml", - `${enabledConfig}emit:\n - "./emitter.mjs"\n - "nonexistent-spector-test-emitter"\n`, - ); - expect(await validate()).toBe(true); - }); -}); - -it("keeps loadScenarios' implicit Spector import alongside configured imports", async () => { - await writeFixture( - "specs/case/main.tsp", - source.replace('import "@typespec/spector";', "").replace("model Base { name: string; }", ""), - ); - await writeFixture("specs/case/base.tsp", "namespace Config; model Base { name: string; }"); - await writeFixture("specs/case/tspconfig.yaml", `${enabledConfig}imports: ["./base.tsp"]\n`); - const [scenarios, diagnostics] = await loadScenarios(scenariosPath); - expect(diagnostics).toEqual([]); - expect(scenarios.map((scenario) => scenario.name)).toEqual(["Config_read"]); -}); - -it("enforces validation options after resolving config with the supplied compiler", async () => { - await writeFixture( - "specs/case/tspconfig.yaml", - `${enabledConfig}warn-as-error: false\nemit: ["not-an-emitter"]\n`, - ); - const resolveCompilerOptions = vi.fn(compiler.resolveCompilerOptions); - const [program, diagnostics] = await compileScenario( - { ...compiler, resolveCompilerOptions }, - relative(process.cwd(), specFilePath), - ); - expect(diagnostics).toEqual([]); - expect(resolveCompilerOptions).toHaveBeenCalledWith(compiler.NodeHost, { - entrypoint: specFilePath, - cwd: dirname(specFilePath), - env: process.env, - }); - expect(program?.compilerOptions).toMatchObject({ - noEmit: true, - warningAsError: true, - emit: [], - configFile: { features: ["union-extends"] }, - }); -}); - -it("does not compile when configuration resolution reports diagnostics", async () => { - await writeFixture("specs/case/tspconfig.yaml", "features: ["); - const compile = vi.fn(compiler.compile); - const [program, diagnostics] = await compileScenario({ ...compiler, compile }, specFilePath); - expect(program).toBeUndefined(); - expect(diagnostics.length).toBeGreaterThan(0); - expect(compile).not.toHaveBeenCalled(); -});