From 89bd4216d5cabd31a53a0901a2ee763c01fded83 Mon Sep 17 00:00:00 2001 From: Weidong Xu Date: Mon, 14 Sep 2026 12:36:02 +0800 Subject: [PATCH 1/4] feat(http-specs): add union extends scenario Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac3c4495-6450-418b-b0e1-154a545aef63 --- .../http-specs-union-extends-2026-09-14.md | 7 + .../spector-scenario-tspconfig-2026-09-14.md | 7 + packages/http-specs/spec-summary.md | 57 +++++++ .../specs/type/union/extends/main.tsp | 148 ++++++++++++++++++ .../specs/type/union/extends/mockapi.ts | 64 ++++++++ packages/http-specs/tspconfig.yaml | 3 + .../spector/src/actions/validate-mock-apis.ts | 10 +- packages/spector/src/scenarios-resolver.ts | 10 +- 8 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 .chronus/changes/http-specs-union-extends-2026-09-14.md create mode 100644 .chronus/changes/spector-scenario-tspconfig-2026-09-14.md create mode 100644 packages/http-specs/specs/type/union/extends/main.tsp create mode 100644 packages/http-specs/specs/type/union/extends/mockapi.ts diff --git a/.chronus/changes/http-specs-union-extends-2026-09-14.md b/.chronus/changes/http-specs-union-extends-2026-09-14.md new file mode 100644 index 00000000000..cd27db3e8fc --- /dev/null +++ b/.chronus/changes/http-specs-union-extends-2026-09-14.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-specs" +--- + +Add a round-trip scenario for unions constrained to common base types. diff --git a/.chronus/changes/spector-scenario-tspconfig-2026-09-14.md b/.chronus/changes/spector-scenario-tspconfig-2026-09-14.md new file mode 100644 index 00000000000..11521f4492e --- /dev/null +++ b/.chronus/changes/spector-scenario-tspconfig-2026-09-14.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/spector" +--- + +Honor project `tspconfig.yaml` settings when validating scenarios and mock APIs. diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 9c7f6485dc6..6d2d4c7093b 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -9422,6 +9422,63 @@ Expected request to send body: } ``` +### Type_Union_Extends_roundTrip + +- Endpoint: `put /type/union/extends/` + +Send and receive unions constrained to common base types. + +Expected request and response body: + +```json +{ + "structural": [ + { + "name": "mittens", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones" + } + ], + "explicit": [ + { + "name": "mittens", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones" + } + ], + "multipleByName": [ + { + "name": "mittens", + "food": "fish", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones", + "bark": true + } + ], + "multipleByFood": [ + { + "name": "mittens", + "food": "fish", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones", + "bark": true + } + ] +} +``` + ### Type_Union_FloatsOnly_get - Endpoint: `get /type/union/floats-only` 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..665fe13e390 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/main.tsp @@ -0,0 +1,148 @@ +import "@typespec/http"; +import "@typespec/spector"; + +using Http; +using Spector; + +/** + * Describe unions constrained to a common base type. + */ +@scenarioService("/type/union/extends") +namespace Type.Union.Extends; + +namespace Structural { + model PetBase { + name: string; + } + + model Cat { + name: string; + toy: string; + } + + model Dog { + name: string; + food: string; + } + + union Pet extends PetBase { + cat: Cat, + dog: Dog, + } +} + +namespace Explicit { + model PetBase { + name: string; + } + + model Cat extends PetBase { + toy: string; + } + + model Dog extends PetBase { + food: string; + } + + union Pet extends PetBase { + cat: Cat, + dog: Dog, + } +} + +namespace Multiple { + model NameBase { + name: string; + } + + model FoodBase { + food: string; + } + + model Cat { + name: string; + food: string; + toy: string; + } + + model Dog { + name: string; + food: string; + bark: boolean; + } + + union PetWithName extends NameBase { + cat: Cat, + dog: Dog, + } + + union PetWithFood extends FoodBase { + cat: Cat, + dog: Dog, + } +} + +model UnionExtendsCases { + structural: Structural.Pet[]; + explicit: Explicit.Pet[]; + multipleByName: Multiple.PetWithName[]; + multipleByFood: Multiple.PetWithFood[]; +} + +@scenario +@scenarioDoc(""" + Send and receive unions constrained to common base types. + + Expected request and response body: + ```json + { + "structural": [ + { + "name": "mittens", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones" + } + ], + "explicit": [ + { + "name": "mittens", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones" + } + ], + "multipleByName": [ + { + "name": "mittens", + "food": "fish", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones", + "bark": true + } + ], + "multipleByFood": [ + { + "name": "mittens", + "food": "fish", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones", + "bark": true + } + ] + } + ``` + """) +@route("/") +@put +op roundTrip(@body input: UnionExtendsCases): UnionExtendsCases; 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..585745802cf --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/mockapi.ts @@ -0,0 +1,64 @@ +import type { ScenarioMockApi } from "@typespec/spec-api"; +import { json, passOnSuccess } from "@typespec/spec-api"; + +export const Scenarios: Record = {}; + +const body = { + structural: [ + { + name: "mittens", + toy: "ball", + }, + { + name: "rex", + food: "bones", + }, + ], + explicit: [ + { + name: "mittens", + toy: "ball", + }, + { + name: "rex", + food: "bones", + }, + ], + multipleByName: [ + { + name: "mittens", + food: "fish", + toy: "ball", + }, + { + name: "rex", + food: "bones", + bark: true, + }, + ], + multipleByFood: [ + { + name: "mittens", + food: "fish", + toy: "ball", + }, + { + name: "rex", + food: "bones", + bark: true, + }, + ], +}; + +Scenarios.Type_Union_Extends_roundTrip = passOnSuccess({ + uri: "/type/union/extends/", + method: "put", + request: { + body: json(body), + }, + response: { + status: 200, + body: json(body), + }, + kind: "MockApiDefinition", +}); diff --git a/packages/http-specs/tspconfig.yaml b/packages/http-specs/tspconfig.yaml index e69de29bb2d..c4a50e9d0f1 100644 --- a/packages/http-specs/tspconfig.yaml +++ b/packages/http-specs/tspconfig.yaml @@ -0,0 +1,3 @@ +kind: project +features: + - union-extends diff --git a/packages/spector/src/actions/validate-mock-apis.ts b/packages/spector/src/actions/validate-mock-apis.ts index b0ded1d0952..c85259a33ae 100644 --- a/packages/spector/src/actions/validate-mock-apis.ts +++ b/packages/spector/src/actions/validate-mock-apis.ts @@ -35,13 +35,21 @@ export async function validateMockApis({ const diagnostics = createDiagnosticReporter(); for (const { name, specFilePath } of scenarioFiles) { logger.debug(`Found scenario "${specFilePath}"`); + const [compilerOptions, configDiagnostics] = await specCompiler.resolveCompilerOptions( + specCompiler.NodeHost, + { + cwd: process.cwd(), + entrypoint: specFilePath, + }, + ); const program = await specCompiler.compile(specCompiler.NodeHost, specFilePath, { + ...compilerOptions, noEmit: true, warningAsError: true, }); // Workaround https://github.com/Azure/cadl-azure/issues/2458 - const programDiagnostics = program.diagnostics.filter( + const programDiagnostics = [...configDiagnostics, ...program.diagnostics].filter( (d) => !( d.code === "@azure-tools/typespec-azure-core/casing-style" && diff --git a/packages/spector/src/scenarios-resolver.ts b/packages/spector/src/scenarios-resolver.ts index 8662808f84c..a84539b4ea0 100644 --- a/packages/spector/src/scenarios-resolver.ts +++ b/packages/spector/src/scenarios-resolver.ts @@ -69,14 +69,22 @@ export async function loadScenarios( for (const { name, specFilePath } of scenarioFiles) { logger.debug(`Found scenario "${specFilePath}"`); + const [compilerOptions, configDiagnostics] = await typespecCompiler.resolveCompilerOptions( + typespecCompiler.NodeHost, + { + cwd: process.cwd(), + entrypoint: specFilePath, + }, + ); const program = await typespecCompiler.compile(typespecCompiler.NodeHost, specFilePath, { + ...compilerOptions, additionalImports: ["@typespec/spector"], noEmit: true, warningAsError: true, }); // Workaround https://github.com/Azure/cadl-azure/issues/2458 - const programDiagnostics = program.diagnostics.filter( + const programDiagnostics = [...configDiagnostics, ...program.diagnostics].filter( (d) => !( d.code === "@azure-tools/typespec-azure-core/casing-style" && From 7dbd0f7297add2f6c390d5f235ecec5dd7477ac0 Mon Sep 17 00:00:00 2001 From: Weidong Xu Date: Mon, 14 Sep 2026 12:43:41 +0800 Subject: [PATCH 2/4] test(http-specs): split union extends cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac3c4495-6450-418b-b0e1-154a545aef63 --- packages/http-specs/spec-summary.md | 72 +++++++---- .../specs/type/union/extends/main.tsp | 122 +++++++++++------- .../specs/type/union/extends/mockapi.ts | 71 ++++++---- 3 files changed, 166 insertions(+), 99 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 6d2d4c7093b..e5d42c12dba 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -9422,37 +9422,38 @@ Expected request to send body: } ``` -### Type_Union_Extends_roundTrip +### Type_Union_Extends_Explicit_roundTrip -- Endpoint: `put /type/union/extends/` +- Endpoint: `put /type/union/extends/explicit` -Send and receive unions constrained to common base types. +Send and receive a union whose variants explicitly extend its base type. + +Expected request and response body: + +```json +[ + { + "name": "mittens", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones" + } +] +``` + +### Type_Union_Extends_Multiple_roundTrip + +- Endpoint: `put /type/union/extends/multiple` + +Send and receive the same variants through unions constrained to different base types. Expected request and response body: ```json { - "structural": [ - { - "name": "mittens", - "toy": "ball" - }, - { - "name": "rex", - "food": "bones" - } - ], - "explicit": [ - { - "name": "mittens", - "toy": "ball" - }, - { - "name": "rex", - "food": "bones" - } - ], - "multipleByName": [ + "byName": [ { "name": "mittens", "food": "fish", @@ -9464,7 +9465,7 @@ Expected request and response body: "bark": true } ], - "multipleByFood": [ + "byFood": [ { "name": "mittens", "food": "fish", @@ -9479,6 +9480,27 @@ Expected request and response body: } ``` +### Type_Union_Extends_Structural_roundTrip + +- Endpoint: `put /type/union/extends/structural` + +Send and receive a union whose variants structurally satisfy its base type. + +Expected request and response body: + +```json +[ + { + "name": "mittens", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones" + } +] +``` + ### Type_Union_FloatsOnly_get - Endpoint: `get /type/union/floats-only` diff --git a/packages/http-specs/specs/type/union/extends/main.tsp b/packages/http-specs/specs/type/union/extends/main.tsp index 665fe13e390..ad978d293ee 100644 --- a/packages/http-specs/specs/type/union/extends/main.tsp +++ b/packages/http-specs/specs/type/union/extends/main.tsp @@ -10,7 +10,7 @@ using Spector; @scenarioService("/type/union/extends") namespace Type.Union.Extends; -namespace Structural { +namespace StructuralTypes { model PetBase { name: string; } @@ -31,7 +31,7 @@ namespace Structural { } } -namespace Explicit { +namespace ExplicitTypes { model PetBase { name: string; } @@ -50,7 +50,7 @@ namespace Explicit { } } -namespace Multiple { +namespace MultipleTypes { model NameBase { name: string; } @@ -82,21 +82,20 @@ namespace Multiple { } } -model UnionExtendsCases { - structural: Structural.Pet[]; - explicit: Explicit.Pet[]; - multipleByName: Multiple.PetWithName[]; - multipleByFood: Multiple.PetWithFood[]; +model MultipleCases { + byName: MultipleTypes.PetWithName[]; + byFood: MultipleTypes.PetWithFood[]; } -@scenario -@scenarioDoc(""" - Send and receive unions constrained to common base types. +@route("/structural") +interface Structural { + @scenario + @scenarioDoc(""" + Send and receive a union whose variants structurally satisfy its base type. - Expected request and response body: - ```json - { - "structural": [ + Expected request and response body: + ```json + [ { "name": "mittens", "toy": "ball" @@ -105,8 +104,22 @@ model UnionExtendsCases { "name": "rex", "food": "bones" } - ], - "explicit": [ + ] + ``` + """) + @put + roundTrip(@body input: StructuralTypes.Pet[]): StructuralTypes.Pet[]; +} + +@route("/explicit") +interface Explicit { + @scenario + @scenarioDoc(""" + Send and receive a union whose variants explicitly extend its base type. + + Expected request and response body: + ```json + [ { "name": "mittens", "toy": "ball" @@ -115,34 +128,49 @@ model UnionExtendsCases { "name": "rex", "food": "bones" } - ], - "multipleByName": [ - { - "name": "mittens", - "food": "fish", - "toy": "ball" - }, - { - "name": "rex", - "food": "bones", - "bark": true - } - ], - "multipleByFood": [ - { - "name": "mittens", - "food": "fish", - "toy": "ball" - }, - { - "name": "rex", - "food": "bones", - "bark": true - } ] - } - ``` - """) -@route("/") -@put -op roundTrip(@body input: UnionExtendsCases): UnionExtendsCases; + ``` + """) + @put + roundTrip(@body input: ExplicitTypes.Pet[]): ExplicitTypes.Pet[]; +} + +@route("/multiple") +interface Multiple { + @scenario + @scenarioDoc(""" + Send and receive the same variants through unions constrained to different base types. + + Expected request and response body: + ```json + { + "byName": [ + { + "name": "mittens", + "food": "fish", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones", + "bark": true + } + ], + "byFood": [ + { + "name": "mittens", + "food": "fish", + "toy": "ball" + }, + { + "name": "rex", + "food": "bones", + "bark": true + } + ] + } + ``` + """) + @put + roundTrip(@body input: MultipleCases): MultipleCases; +} diff --git a/packages/http-specs/specs/type/union/extends/mockapi.ts b/packages/http-specs/specs/type/union/extends/mockapi.ts index 585745802cf..ae35a6aaf9e 100644 --- a/packages/http-specs/specs/type/union/extends/mockapi.ts +++ b/packages/http-specs/specs/type/union/extends/mockapi.ts @@ -3,28 +3,19 @@ import { json, passOnSuccess } from "@typespec/spec-api"; export const Scenarios: Record = {}; -const body = { - structural: [ - { - name: "mittens", - toy: "ball", - }, - { - name: "rex", - food: "bones", - }, - ], - explicit: [ - { - name: "mittens", - toy: "ball", - }, - { - name: "rex", - food: "bones", - }, - ], - multipleByName: [ +const pets = [ + { + name: "mittens", + toy: "ball", + }, + { + name: "rex", + food: "bones", + }, +]; + +const multiple = { + byName: [ { name: "mittens", food: "fish", @@ -36,7 +27,7 @@ const body = { bark: true, }, ], - multipleByFood: [ + byFood: [ { name: "mittens", food: "fish", @@ -50,15 +41,41 @@ const body = { ], }; -Scenarios.Type_Union_Extends_roundTrip = passOnSuccess({ - uri: "/type/union/extends/", +Scenarios.Type_Union_Extends_Structural_roundTrip = passOnSuccess({ + uri: "/type/union/extends/structural", + method: "put", + request: { + body: json(pets), + }, + response: { + status: 200, + body: json(pets), + }, + kind: "MockApiDefinition", +}); + +Scenarios.Type_Union_Extends_Explicit_roundTrip = passOnSuccess({ + uri: "/type/union/extends/explicit", + method: "put", + request: { + body: json(pets), + }, + response: { + status: 200, + body: json(pets), + }, + kind: "MockApiDefinition", +}); + +Scenarios.Type_Union_Extends_Multiple_roundTrip = passOnSuccess({ + uri: "/type/union/extends/multiple", method: "put", request: { - body: json(body), + body: json(multiple), }, response: { status: 200, - body: json(body), + body: json(multiple), }, kind: "MockApiDefinition", }); From 70d0d7b19487bb053e07dc5c5d2142ed4f03aca8 Mon Sep 17 00:00:00 2001 From: Weidong Xu Date: Mon, 14 Sep 2026 12:46:43 +0800 Subject: [PATCH 3/4] test(http-specs): flatten union extends models Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac3c4495-6450-418b-b0e1-154a545aef63 --- .../specs/type/union/extends/main.tsp | 142 +++++++++--------- 1 file changed, 68 insertions(+), 74 deletions(-) diff --git a/packages/http-specs/specs/type/union/extends/main.tsp b/packages/http-specs/specs/type/union/extends/main.tsp index ad978d293ee..7b1eebbc986 100644 --- a/packages/http-specs/specs/type/union/extends/main.tsp +++ b/packages/http-specs/specs/type/union/extends/main.tsp @@ -10,81 +10,75 @@ using Spector; @scenarioService("/type/union/extends") namespace Type.Union.Extends; -namespace StructuralTypes { - model PetBase { - name: string; - } - - model Cat { - name: string; - toy: string; - } - - model Dog { - name: string; - food: string; - } - - union Pet extends PetBase { - cat: Cat, - dog: Dog, - } -} - -namespace ExplicitTypes { - model PetBase { - name: string; - } - - model Cat extends PetBase { - toy: string; - } - - model Dog extends PetBase { - food: string; - } - - union Pet extends PetBase { - cat: Cat, - dog: Dog, - } -} - -namespace MultipleTypes { - model NameBase { - name: string; - } - - model FoodBase { - food: string; - } - - model Cat { - name: string; - food: string; - toy: string; - } - - model Dog { - name: string; - food: string; - bark: boolean; - } - - union PetWithName extends NameBase { - cat: Cat, - dog: Dog, - } - - union PetWithFood extends FoodBase { - cat: Cat, - dog: Dog, - } +model StructuralPetBase { + name: string; +} + +model StructuralCat { + name: string; + toy: string; +} + +model StructuralDog { + name: string; + food: string; +} + +union StructuralPet extends StructuralPetBase { + cat: StructuralCat, + dog: StructuralDog, +} + +model ExplicitPetBase { + name: string; +} + +model ExplicitCat extends ExplicitPetBase { + toy: string; +} + +model ExplicitDog extends ExplicitPetBase { + food: string; +} + +union ExplicitPet extends ExplicitPetBase { + cat: ExplicitCat, + dog: ExplicitDog, +} + +model MultipleNameBase { + name: string; +} + +model MultipleFoodBase { + food: string; +} + +model MultipleCat { + name: string; + food: string; + toy: string; +} + +model MultipleDog { + name: string; + food: string; + bark: boolean; +} + +union MultiplePetWithName extends MultipleNameBase { + cat: MultipleCat, + dog: MultipleDog, +} + +union MultiplePetWithFood extends MultipleFoodBase { + cat: MultipleCat, + dog: MultipleDog, } model MultipleCases { - byName: MultipleTypes.PetWithName[]; - byFood: MultipleTypes.PetWithFood[]; + byName: MultiplePetWithName[]; + byFood: MultiplePetWithFood[]; } @route("/structural") @@ -108,7 +102,7 @@ interface Structural { ``` """) @put - roundTrip(@body input: StructuralTypes.Pet[]): StructuralTypes.Pet[]; + roundTrip(@body input: StructuralPet[]): StructuralPet[]; } @route("/explicit") @@ -132,7 +126,7 @@ interface Explicit { ``` """) @put - roundTrip(@body input: ExplicitTypes.Pet[]): ExplicitTypes.Pet[]; + roundTrip(@body input: ExplicitPet[]): ExplicitPet[]; } @route("/multiple") From f1a25dc3ef416d0b8adfdd60fb0c625530c3c53a Mon Sep 17 00:00:00 2001 From: Weidong Xu Date: Mon, 14 Sep 2026 12:58:05 +0800 Subject: [PATCH 4/4] style(http-specs): format union extends scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac3c4495-6450-418b-b0e1-154a545aef63 --- packages/http-specs/specs/type/union/extends/main.tsp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-specs/specs/type/union/extends/main.tsp b/packages/http-specs/specs/type/union/extends/main.tsp index 7b1eebbc986..f3b705ca8c6 100644 --- a/packages/http-specs/specs/type/union/extends/main.tsp +++ b/packages/http-specs/specs/type/union/extends/main.tsp @@ -86,7 +86,7 @@ interface Structural { @scenario @scenarioDoc(""" Send and receive a union whose variants structurally satisfy its base type. - + Expected request and response body: ```json [ @@ -110,7 +110,7 @@ interface Explicit { @scenario @scenarioDoc(""" Send and receive a union whose variants explicitly extend its base type. - + Expected request and response body: ```json [ @@ -134,7 +134,7 @@ interface Multiple { @scenario @scenarioDoc(""" Send and receive the same variants through unions constrained to different base types. - + Expected request and response body: ```json {