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..e5d42c12dba 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -9422,6 +9422,85 @@ Expected request to send body: } ``` +### Type_Union_Extends_Explicit_roundTrip + +- Endpoint: `put /type/union/extends/explicit` + +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 +{ + "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 + } + ] +} +``` + +### 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 new file mode 100644 index 00000000000..f3b705ca8c6 --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/main.tsp @@ -0,0 +1,170 @@ +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; + +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: MultiplePetWithName[]; + byFood: MultiplePetWithFood[]; +} + +@route("/structural") +interface Structural { + @scenario + @scenarioDoc(""" + 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" + } + ] + ``` + """) + @put + roundTrip(@body input: StructuralPet[]): StructuralPet[]; +} + +@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" + }, + { + "name": "rex", + "food": "bones" + } + ] + ``` + """) + @put + roundTrip(@body input: ExplicitPet[]): ExplicitPet[]; +} + +@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 new file mode 100644 index 00000000000..ae35a6aaf9e --- /dev/null +++ b/packages/http-specs/specs/type/union/extends/mockapi.ts @@ -0,0 +1,81 @@ +import type { ScenarioMockApi } from "@typespec/spec-api"; +import { json, passOnSuccess } from "@typespec/spec-api"; + +export const Scenarios: Record = {}; + +const pets = [ + { + name: "mittens", + toy: "ball", + }, + { + name: "rex", + food: "bones", + }, +]; + +const multiple = { + 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, + }, + ], +}; + +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(multiple), + }, + response: { + status: 200, + body: json(multiple), + }, + 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" &&