From 70cf314666995105877a4868eda880505c2b083e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 13:24:58 -0400 Subject: [PATCH 01/20] add rough test --- .../test/clients/structure.test.ts | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index 1d0965ff7f..bf49afbd71 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -60,7 +60,7 @@ it("arm client with operation groups", async () => { enum Versions { /** 2024-04-01-preview api version */ V2024_04_01_PREVIEW: "2024-04-01-preview", - } + }F model TestTrackedResource is TrackedResource { ...ResourceNameParameter; @@ -762,3 +762,54 @@ it("optional params propagated", async () => { `, ); }); + +it("one client from multiple services", async () => { + await runner.compileWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { + av1, + av2, + } + + interface AI { + @route("/atest") + atest(): void; + } + } + + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { + bv1, + bv2, + } + + interface BI { + @route("/btest") + btest(): void; + } + }`, + ` + @client + @service + namespace CombineClient { + @clientInitialization({initializedBy: InitializedBy.individually}) + @client({service: ServiceA, parent: CombineClient}) + interface AI extends ServiceA.AI {} + @clientInitialization({initializedBy: InitializedBy.individually}) + @client({service: ServiceB, parent: CombineClient}) + interface BI extends ServiceB.BI {} + } +`, + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "CombineClient"); + strictEqual(client.apiVersions.length, 2); + deepStrictEqual(client.apiVersions, ["av2", "bv2"]); +}); From a76ed5ee15d3995fa3e04be6ca0d427168ee1783 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 13:29:56 -0400 Subject: [PATCH 02/20] switch to parent initialization --- .../test/clients/structure.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index bf49afbd71..0365ff27c6 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -797,11 +797,11 @@ it("one client from multiple services", async () => { @client @service namespace CombineClient { - @clientInitialization({initializedBy: InitializedBy.individually}) - @client({service: ServiceA, parent: CombineClient}) + @clientInitialization({initializedBy: InitializedBy.individually | InitializedBy.parent}) + @client({service: ServiceA}) interface AI extends ServiceA.AI {} - @clientInitialization({initializedBy: InitializedBy.individually}) - @client({service: ServiceB, parent: CombineClient}) + @clientInitialization({initializedBy: InitializedBy.individually | InitializedBy.parent}) + @client({service: ServiceB}) interface BI extends ServiceB.BI {} } `, From c7a60a600b88fd149f14382b84efb1bd300a3c13 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 13:30:52 -0400 Subject: [PATCH 03/20] get rid of erroneous commit --- .../test/clients/structure.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index 0365ff27c6..ba13493658 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -60,7 +60,7 @@ it("arm client with operation groups", async () => { enum Versions { /** 2024-04-01-preview api version */ V2024_04_01_PREVIEW: "2024-04-01-preview", - }F + } model TestTrackedResource is TrackedResource { ...ResourceNameParameter; From e078c040b68b87681fca976ef8ae50db6e4fbbda Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 14:57:26 -0400 Subject: [PATCH 04/20] add parent arg to client decorator --- .../lib/decorators.tsp | 5 +++ .../src/clients.ts | 11 ++++- .../src/decorators.ts | 2 + .../src/interfaces.ts | 2 + .../src/package.ts | 43 ++++++++++++++++++- .../test/clients/structure.test.ts | 16 +++++-- .../test/decorators/client.test.ts | 2 + 7 files changed, 75 insertions(+), 6 deletions(-) diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 1a63a16679..fa7eea3e57 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -126,6 +126,11 @@ model ClientOptions { * The name of the client. If not specified, the default name will be `Client`. */ name?: string; + + /** + * The parent client of this client. If specified, this client will be generated as a sub client of the parent client. + */ + parent?: Namespace | Interface; } /** diff --git a/packages/typespec-client-generator-core/src/clients.ts b/packages/typespec-client-generator-core/src/clients.ts index 3159406c12..eae6b86c35 100644 --- a/packages/typespec-client-generator-core/src/clients.ts +++ b/packages/typespec-client-generator-core/src/clients.ts @@ -187,6 +187,14 @@ export function createSdkClientType c.__raw === parentRaw) as + | SdkClientType + | undefined; + } const sdkClientType: SdkClientType = { __raw: client, kind: "client", @@ -208,7 +216,8 @@ export function createSdkClientType; __clientToOperationsCache?: Map; + __clientTypesCache?: SdkClientType[]; __operationToClientCache?: Map; __clientParametersCache: Map; __clientApiVersionDefaultValueCache: Map; @@ -105,6 +106,7 @@ export interface SdkClient { type: Namespace | Interface; /** Unique ID for the current type. */ crossLanguageDefinitionId: string; + parent?: Namespace | Interface; subOperationGroups: SdkOperationGroup[]; } diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 60af13ed56..19e058a263 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -29,7 +29,7 @@ export function createSdkPackage( const allReferencedTypes = getAllReferencedTypes(context); const versions = context.getPackageVersions(); const sdkPackage: SdkPackage = { - clients: listClients(context).map((c) => diagnostics.pipe(createSdkClientType(context, c))), + clients: diagnostics.pipe(createClients(context)), models: allReferencedTypes.filter((x): x is SdkModelType => x.kind === "model"), enums: allReferencedTypes.filter((x): x is SdkEnumType => x.kind === "enum"), unions: allReferencedTypes.filter( @@ -46,6 +46,47 @@ export function createSdkPackage( return diagnostics.wrap(sdkPackage); } +function createClients( + context: TCGCContext, +): [SdkClientType[], readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + if (context.__clientTypesCache) { + return diagnostics.wrap(context.__clientTypesCache as SdkClientType[]); + } + + const allClients = listClients(context).map((c) => + diagnostics.pipe(createSdkClientType(context, c)), + ); + + // Build parent-child relationships + // Create a map for quick lookup + const clientMap = new Map, SdkClientType>(); + for (const client of allClients) { + clientMap.set(client, client); + } + + // Populate children arrays for each client based on parent relationships + for (const client of allClients) { + if (client.parent) { + // Find the parent client in our map + const parentClient = clientMap.get(client.parent); + if (parentClient) { + if (!parentClient.children) { + parentClient.children = []; + } + parentClient.children.push(client); + } + } + } + + // Filter to only include root-level clients (those without a parent) + // Child clients will only appear in their parent's .children property + const rootClients = allClients.filter((client) => !client.parent); + + context.__clientTypesCache = rootClients; + return diagnostics.wrap(rootClients); +} + function organizeNamespaces( context: TCGCContext, sdkPackage: SdkPackage, diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index ba13493658..7cb24cf748 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -798,10 +798,10 @@ it("one client from multiple services", async () => { @service namespace CombineClient { @clientInitialization({initializedBy: InitializedBy.individually | InitializedBy.parent}) - @client({service: ServiceA}) + @client({service: ServiceA, parent: CombineClient}) interface AI extends ServiceA.AI {} @clientInitialization({initializedBy: InitializedBy.individually | InitializedBy.parent}) - @client({service: ServiceB}) + @client({service: ServiceB, parent: CombineClient}) interface BI extends ServiceB.BI {} } `, @@ -810,6 +810,14 @@ it("one client from multiple services", async () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; strictEqual(client.name, "CombineClient"); - strictEqual(client.apiVersions.length, 2); - deepStrictEqual(client.apiVersions, ["av2", "bv2"]); + strictEqual(client.apiVersions.length, 0); + strictEqual(client.children!.length, 2); + const aiClient = client.children!.find((c) => c.name === "AI"); + ok(aiClient); + strictEqual(aiClient.methods.length, 1); + strictEqual(aiClient.methods[0].name, "atest"); + const biClient = client.children!.find((c) => c.name === "BI"); + ok(biClient); + strictEqual(biClient.methods.length, 1); + strictEqual(biClient.methods[0].name, "btest"); }); diff --git a/packages/typespec-client-generator-core/test/decorators/client.test.ts b/packages/typespec-client-generator-core/test/decorators/client.test.ts index 092805dbf5..bff9650d44 100644 --- a/packages/typespec-client-generator-core/test/decorators/client.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/client.test.ts @@ -44,6 +44,7 @@ describe("@client", () => { deepStrictEqual(clients, [ { kind: "SdkClient", + parent: undefined, name: "MyClient", service: MyClient, type: MyClient, @@ -66,6 +67,7 @@ describe("@client", () => { { kind: "SdkClient", name: "MyClient", + parent: undefined, service: MyService, type: MyClient, crossLanguageDefinitionId: "MyService.MyClient", From 6c708022e9af8f49537033ce22e5facb1d556a52 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 15:45:02 -0400 Subject: [PATCH 05/20] set up ideal structure --- .../test/clients/structure.test.ts | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index 7cb24cf748..30a7e22641 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -808,16 +808,70 @@ it("one client from multiple services", async () => { ); const sdkPackage = runner.context.sdkPackage; strictEqual(sdkPackage.clients.length, 1); + const aVersionsEnum = sdkPackage.enums.find((e) => e.name === "VersionsA"); + ok(aVersionsEnum); + const bVersionsEnum = sdkPackage.enums.find((e) => e.name === "VersionsB"); + ok(bVersionsEnum); const client = sdkPackage.clients[0]; strictEqual(client.name, "CombineClient"); strictEqual(client.apiVersions.length, 0); strictEqual(client.children!.length, 2); + strictEqual(client.clientInitialization.parameters.length, 1); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); const aiClient = client.children!.find((c) => c.name === "AI"); ok(aiClient); + + // AI client should have api versions from ServiceA + strictEqual(aiClient.apiVersions.length, 2); + deepStrictEqual(aiClient.apiVersions, ["av1", "av2"]); + strictEqual(aiClient.clientInitialization.parameters.length, 2); + strictEqual(aiClient.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(aiClient.clientInitialization.parameters[1].name, "apiVersion"); + const aiApiVersionParam = aiClient.clientInitialization.parameters[1]; + strictEqual(aiApiVersionParam.isApiVersionParam, true); + strictEqual(aiApiVersionParam.onClient, true); + strictEqual(aiApiVersionParam.clientDefaultValue, "av2"); + strictEqual(aiApiVersionParam.type, aVersionsEnum); + + // AI client should have atest method with VersionsA api version strictEqual(aiClient.methods.length, 1); - strictEqual(aiClient.methods[0].name, "atest"); + const aiMethod = aiClient.methods[0]; + strictEqual(aiMethod.name, "atest"); + strictEqual(aiMethod.parameters.length, 0); + const aiOperation = aiMethod.operation; + strictEqual(aiOperation.parameters.length, 1); + const aiOperationApiVersionParam = aiOperation.parameters.find((p) => p.isApiVersionParam); + ok(aiOperationApiVersionParam); + strictEqual(aiOperationApiVersionParam.type, aVersionsEnum); + strictEqual(aiOperationApiVersionParam.correspondingMethodParams.length, 1); + strictEqual(aiOperationApiVersionParam.correspondingMethodParams[0], aiApiVersionParam); + const biClient = client.children!.find((c) => c.name === "BI"); ok(biClient); + + strictEqual(biClient.apiVersions.length, 2); + deepStrictEqual(biClient.apiVersions, ["bv1", "bv2"]); + strictEqual(biClient.clientInitialization.parameters.length, 2); + strictEqual(biClient.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(biClient.clientInitialization.parameters[1].name, "apiVersion"); + const biApiVersionParam = biClient.clientInitialization.parameters[1]; + strictEqual(biApiVersionParam.isApiVersionParam, true); + strictEqual(biApiVersionParam.onClient, true); + strictEqual(biApiVersionParam.clientDefaultValue, "bv2"); + strictEqual(biApiVersionParam.type, bVersionsEnum); + + // BI client should have btest method with VersionsB api version + const biMethod = biClient.methods[0]; + strictEqual(biMethod.name, "btest"); + strictEqual(biMethod.parameters.length, 0); + const biOperation = biMethod.operation; + strictEqual(biOperation.parameters.length, 1); + const biOperationApiVersionParam = biOperation.parameters.find((p) => p.isApiVersionParam); + ok(biOperationApiVersionParam); + strictEqual(biOperationApiVersionParam.type, bVersionsEnum); + strictEqual(biOperationApiVersionParam.correspondingMethodParams.length, 1); + strictEqual(biOperationApiVersionParam.correspondingMethodParams[0], biApiVersionParam); + strictEqual(biClient.methods.length, 1); strictEqual(biClient.methods[0].name, "btest"); }); From 88af7802bc1223a596fff140f97da9d5c7fc3497 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 16:33:19 -0400 Subject: [PATCH 06/20] get api versions by service when applicable --- packages/typespec-client-generator-core/src/context.ts | 10 +++++----- .../typespec-client-generator-core/src/interfaces.ts | 2 +- packages/typespec-client-generator-core/src/package.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index 5eaf70af12..d0b239a309 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -113,17 +113,17 @@ export function createTCGCContext( } this.__tspTypeToApiVersions.set(type, mergedApiVersions); }, - getPackageVersions(): string[] { - if (this.__packageVersions) { + getPackageVersions(service?: Namespace): string[] { + if (this.__packageVersions?.length) { return this.__packageVersions; } - const service = listServices(program)[0]; + service = service ?? listServices(program)[0]?.type; if (!service) { this.__packageVersions = []; return this.__packageVersions; } - const versions = getVersions(program, service.type)[1]?.getVersions(); + const versions = getVersions(program, service)[1]?.getVersions(); if (!versions) { this.__packageVersions = []; return this.__packageVersions; @@ -142,7 +142,7 @@ export function createTCGCContext( reportDiagnostic(this.program, { code: "api-version-undefined", format: { version: this.apiVersion }, - target: service.type, + target: service, }); this.apiVersion = this.__packageVersions[this.__packageVersions.length - 1]; } diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index a315718371..d299f71629 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -81,7 +81,7 @@ export interface TCGCContext { getMutatedGlobalNamespace(): Namespace; getApiVersionsForType(type: Type): string[]; setApiVersionsForType(type: Type, apiVersions: string[]): void; - getPackageVersions(): string[]; + getPackageVersions(service?: Namespace): string[]; getPackageVersionEnum(): Enum | undefined; getClients(): SdkClient[]; getClientOrOperationGroup(type: Namespace | Interface): SdkClient | SdkOperationGroup | undefined; diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 19e058a263..7b9b6b8163 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -160,7 +160,7 @@ function populateApiVersionInformation(context: TCGCContext): void { filterApiVersionsWithDecorators( context, clientOperationGroup.type ?? clientOperationGroup.service, - context.getPackageVersions(), + context.getPackageVersions(clientOperationGroup.service), ), ); From fa746edef3ecc147396d7479843be440cefdbd29 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 17:16:29 -0400 Subject: [PATCH 07/20] fix getting api versions enum so it's more granular --- .../src/context.ts | 12 +--------- .../src/example.ts | 2 +- .../src/interfaces.ts | 3 +-- .../src/internal-utils.ts | 2 +- .../src/package.ts | 4 ++-- .../src/public-utils.ts | 24 ++++++++++++++++++- .../test/clients/structure.test.ts | 8 ++----- .../test/package/versioning.test.ts | 14 +++++------ 8 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index d0b239a309..cc0b77035c 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -113,7 +113,7 @@ export function createTCGCContext( } this.__tspTypeToApiVersions.set(type, mergedApiVersions); }, - getPackageVersions(service?: Namespace): string[] { + getApiVersions(service?: Namespace): string[] { if (this.__packageVersions?.length) { return this.__packageVersions; } @@ -148,16 +148,6 @@ export function createTCGCContext( } return this.__packageVersions; }, - getPackageVersionEnum(): Enum | undefined { - if (this.__packageVersionEnum) { - return this.__packageVersionEnum; - } - const namespaces = listAllServiceNamespaces(this); - if (namespaces.length === 0) { - return undefined; - } - return getVersions(this.program, namespaces[0])[1]?.getVersions()?.[0].enumMember.enum; - }, getClients(): SdkClient[] { if (!this.__rawClientsOperationGroupsCache) { prepareClientAndOperationCache(this); diff --git a/packages/typespec-client-generator-core/src/example.ts b/packages/typespec-client-generator-core/src/example.ts index ed357fe73b..bfc6d41dfb 100644 --- a/packages/typespec-client-generator-core/src/example.ts +++ b/packages/typespec-client-generator-core/src/example.ts @@ -171,7 +171,7 @@ export async function handleClientExamples( ): Promise<[void, readonly Diagnostic[]]> { const diagnostics = createDiagnosticCollector(); - const packageVersions = context.getPackageVersions(); + const packageVersions = context.getApiVersions(); const examples = diagnostics.pipe( await loadExamples(context, packageVersions[packageVersions.length - 1]), ); diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index d299f71629..74e6e30cf8 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -81,8 +81,7 @@ export interface TCGCContext { getMutatedGlobalNamespace(): Namespace; getApiVersionsForType(type: Type): string[]; setApiVersionsForType(type: Type, apiVersions: string[]): void; - getPackageVersions(service?: Namespace): string[]; - getPackageVersionEnum(): Enum | undefined; + getApiVersions(service?: Namespace): string[]; getClients(): SdkClient[]; getClientOrOperationGroup(type: Namespace | Interface): SdkClient | SdkOperationGroup | undefined; getOperationsForClient(client: SdkClient | SdkOperationGroup): Operation[]; diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 4a574a8301..20861db20b 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -780,7 +780,7 @@ function getVersioningMutator( export function handleVersioningMutationForGlobalNamespace(context: TCGCContext): Namespace { const globalNamespace = context.program.getGlobalNamespaceType(); - const allApiVersions = context.getPackageVersions(); + const allApiVersions = context.getApiVersions(); if (allApiVersions.length === 0 || context.apiVersion === "all") return globalNamespace; const mutator = getVersioningMutator( diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 7b9b6b8163..ac3bcb61b5 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -27,7 +27,7 @@ export function createSdkPackage( diagnostics.pipe(handleAllTypes(context)); const crossLanguagePackageId = diagnostics.pipe(getCrossLanguagePackageId(context)); const allReferencedTypes = getAllReferencedTypes(context); - const versions = context.getPackageVersions(); + const versions = context.getApiVersions(); const sdkPackage: SdkPackage = { clients: diagnostics.pipe(createClients(context)), models: allReferencedTypes.filter((x): x is SdkModelType => x.kind === "model"), @@ -160,7 +160,7 @@ function populateApiVersionInformation(context: TCGCContext): void { filterApiVersionsWithDecorators( context, clientOperationGroup.type ?? clientOperationGroup.service, - context.getPackageVersions(clientOperationGroup.service), + context.getApiVersions(clientOperationGroup.service), ), ); diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index eb47958ace..a2046fbdaa 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -89,6 +89,28 @@ export function getDefaultApiVersion( } } +function getVersionEnumForService(context: TCGCContext, type: ModelProperty): Enum | undefined { + if (context.__packageVersionEnum) { + return context.__packageVersionEnum; + } + let service = getNamespaceFromType(type.model); + if (!service) { + const namespaces = listAllServiceNamespaces(context); + if (namespaces.length === 0) { + return undefined; + } + service = namespaces[0]; + } + let retval = getVersions(context.program, service)[1]?.getVersions()?.[0].enumMember.enum + while(!retval && service) { + service = service.namespace; + if (service) { + retval = getVersions(context.program, service)[1]?.getVersions()?.[0].enumMember.enum + } + } + return retval; +} + /** * Return whether a parameter is the Api Version parameter of a client * @param program @@ -102,7 +124,7 @@ export function isApiVersion(context: TCGCContext, type: ModelProperty): boolean return override; } // if the service is not versioning, then no api version parameter - const versionEnum = context.getPackageVersionEnum(); + const versionEnum = getVersionEnumForService(context, type); if (!versionEnum) { return false; } diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index 30a7e22641..306f60dc6e 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -776,7 +776,7 @@ it("one client from multiple services", async () => { interface AI { @route("/atest") - atest(): void; + atest(@query("api-version") apiVersion: VersionsA): void; } } @@ -790,7 +790,7 @@ it("one client from multiple services", async () => { interface BI { @route("/btest") - btest(): void; + btest(@query("api-version") apiVersion: VersionsB): void; } }`, ` @@ -831,7 +831,6 @@ it("one client from multiple services", async () => { strictEqual(aiApiVersionParam.isApiVersionParam, true); strictEqual(aiApiVersionParam.onClient, true); strictEqual(aiApiVersionParam.clientDefaultValue, "av2"); - strictEqual(aiApiVersionParam.type, aVersionsEnum); // AI client should have atest method with VersionsA api version strictEqual(aiClient.methods.length, 1); @@ -842,7 +841,6 @@ it("one client from multiple services", async () => { strictEqual(aiOperation.parameters.length, 1); const aiOperationApiVersionParam = aiOperation.parameters.find((p) => p.isApiVersionParam); ok(aiOperationApiVersionParam); - strictEqual(aiOperationApiVersionParam.type, aVersionsEnum); strictEqual(aiOperationApiVersionParam.correspondingMethodParams.length, 1); strictEqual(aiOperationApiVersionParam.correspondingMethodParams[0], aiApiVersionParam); @@ -858,7 +856,6 @@ it("one client from multiple services", async () => { strictEqual(biApiVersionParam.isApiVersionParam, true); strictEqual(biApiVersionParam.onClient, true); strictEqual(biApiVersionParam.clientDefaultValue, "bv2"); - strictEqual(biApiVersionParam.type, bVersionsEnum); // BI client should have btest method with VersionsB api version const biMethod = biClient.methods[0]; @@ -868,7 +865,6 @@ it("one client from multiple services", async () => { strictEqual(biOperation.parameters.length, 1); const biOperationApiVersionParam = biOperation.parameters.find((p) => p.isApiVersionParam); ok(biOperationApiVersionParam); - strictEqual(biOperationApiVersionParam.type, bVersionsEnum); strictEqual(biOperationApiVersionParam.correspondingMethodParams.length, 1); strictEqual(biOperationApiVersionParam.correspondingMethodParams[0], biApiVersionParam); diff --git a/packages/typespec-client-generator-core/test/package/versioning.test.ts b/packages/typespec-client-generator-core/test/package/versioning.test.ts index dd749132cd..c7940e1606 100644 --- a/packages/typespec-client-generator-core/test/package/versioning.test.ts +++ b/packages/typespec-client-generator-core/test/package/versioning.test.ts @@ -73,7 +73,7 @@ it("basic default version", async () => { const sdkPackage = runnerWithVersion.context.sdkPackage; strictEqual(sdkPackage.metadata.apiVersion, "v3"); - deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getApiVersions(), ["v1", "v2", "v3"]); strictEqual(sdkPackage.clients.length, 1); const apiVersionParam = sdkPackage.clients[0].clientInitialization.parameters.find( @@ -174,7 +174,7 @@ it("basic latest version", async () => { const sdkPackage = runnerWithVersion.context.sdkPackage; strictEqual(sdkPackage.metadata.apiVersion, "v3"); - deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getApiVersions(), ["v1", "v2", "v3"]); strictEqual(sdkPackage.clients.length, 1); const apiVersionParam = sdkPackage.clients[0].clientInitialization.parameters.find( @@ -274,7 +274,7 @@ it("basic v3 version", async () => { const sdkPackage = runnerWithVersion.context.sdkPackage; strictEqual(sdkPackage.metadata.apiVersion, "v3"); - deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getApiVersions(), ["v1", "v2", "v3"]); strictEqual(sdkPackage.clients.length, 1); const apiVersionParam = sdkPackage.clients[0].clientInitialization.parameters.find( @@ -374,7 +374,7 @@ it("basic v2 version", async () => { const sdkPackage = runnerWithVersion.context.sdkPackage; strictEqual(sdkPackage.metadata.apiVersion, "v2"); - deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["v1", "v2"]); + deepStrictEqual(runnerWithVersion.context.getApiVersions(), ["v1", "v2"]); strictEqual(sdkPackage.clients.length, 1); const apiVersionParam = sdkPackage.clients[0].clientInitialization.parameters.find( @@ -477,7 +477,7 @@ it("basic v1 version", async () => { const sdkPackage = runnerWithVersion.context.sdkPackage; strictEqual(sdkPackage.metadata.apiVersion, "v1"); - deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["v1"]); + deepStrictEqual(runnerWithVersion.context.getApiVersions(), ["v1"]); strictEqual(sdkPackage.clients.length, 1); const apiVersionParam = sdkPackage.clients[0].clientInitialization.parameters.find( @@ -565,7 +565,7 @@ it("basic all version", async () => { const sdkPackage = runnerWithVersion.context.sdkPackage; strictEqual(sdkPackage.metadata.apiVersion, "all"); - deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getApiVersions(), ["v1", "v2", "v3"]); strictEqual(sdkPackage.clients.length, 1); const apiVersionParam = sdkPackage.clients[0].clientInitialization.parameters.find( @@ -1427,7 +1427,7 @@ it("version not exist", async () => { const sdkPackage = runnerWithVersion.context.sdkPackage; strictEqual(sdkPackage.metadata.apiVersion, "v3"); - deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getApiVersions(), ["v1", "v2", "v3"]); strictEqual(sdkPackage.clients.length, 1); const apiVersionParam = sdkPackage.clients[0].clientInitialization.parameters.find( From 5ddb972f12d640fc635fbcb89349e31801978f47 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 17:36:57 -0400 Subject: [PATCH 08/20] all tests passing --- .../src/context.ts | 47 ++++++++++++++----- .../src/interfaces.ts | 2 +- .../src/public-utils.ts | 35 +++++++++----- .../test/clients/structure.test.ts | 3 -- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index cc0b77035c..0ff3f8e2aa 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -2,7 +2,6 @@ import { createDiagnosticCollector, EmitContext, emitFile, - Enum, Interface, listServices, Model, @@ -48,7 +47,6 @@ import { } from "./internal-utils.js"; import { reportDiagnostic } from "./lib.js"; import { createSdkPackage } from "./package.js"; -import { listAllServiceNamespaces } from "./public-utils.js"; interface CreateTCGCContextOptions { mutateNamespace?: boolean; // whether to mutate global namespace for versioning @@ -114,39 +112,62 @@ export function createTCGCContext( this.__tspTypeToApiVersions.set(type, mergedApiVersions); }, getApiVersions(service?: Namespace): string[] { - if (this.__packageVersions?.length) { - return this.__packageVersions; + if (!this.__serviceToVersions) { + this.__serviceToVersions = new Map(); } - service = service ?? listServices(program)[0]?.type; + + // If no service specified, try to get from undefined key (global) or the first service if (!service) { - this.__packageVersions = []; - return this.__packageVersions; + // Check if we have global versions cached (undefined key) + const globalVersions = this.__serviceToVersions.get(undefined); + if (globalVersions?.length) { + return globalVersions; + } + + // Try to get from the first service + const firstService = listServices(program)[0]?.type; + if (firstService) { + service = firstService; + } else { + return []; + } + } + + // Check cache for this specific service + const cachedVersions = this.__serviceToVersions.get(service); + if (cachedVersions?.length) { + return cachedVersions; } const versions = getVersions(program, service)[1]?.getVersions(); if (!versions) { - this.__packageVersions = []; - return this.__packageVersions; + return []; } removeVersionsLargerThanExplicitlySpecified(this, versions); - this.__packageVersions = versions.map((version) => version.value); + const serviceVersions = versions.map((version) => version.value); + this.__serviceToVersions.set(service, serviceVersions); + + // Also cache as global versions if we don't have any global versions yet + if (!this.__serviceToVersions.has(undefined)) { + this.__serviceToVersions.set(undefined, serviceVersions); + } if ( this.apiVersion !== undefined && this.apiVersion !== "latest" && this.apiVersion !== "all" && - !this.__packageVersions.includes(this.apiVersion) + !serviceVersions.includes(this.apiVersion) ) { reportDiagnostic(this.program, { code: "api-version-undefined", format: { version: this.apiVersion }, target: service, }); - this.apiVersion = this.__packageVersions[this.__packageVersions.length - 1]; + this.apiVersion = serviceVersions[serviceVersions.length - 1]; } - return this.__packageVersions; + return serviceVersions; }, getClients(): SdkClient[] { if (!this.__rawClientsOperationGroupsCache) { diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 74e6e30cf8..1110cb22cb 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -74,7 +74,7 @@ export interface TCGCContext { __httpOperationExamples: Map; __pagedResultSet: Set; __mutatedGlobalNamespace?: Namespace; // the root of all tsp namespaces for this instance. Starting point for traversal, so we don't call mutation multiple times - __packageVersions?: string[]; // the package versions from the service versioning config and api version setting in tspconfig. + __serviceToVersions?: Map; // the package versions from the service versioning config and api version setting in tspconfig. __packageVersionEnum?: Enum; // the enum type that contains all the package versions. __externalPackageToVersions?: Map; diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index a2046fbdaa..87e1d94b3c 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -93,22 +93,33 @@ function getVersionEnumForService(context: TCGCContext, type: ModelProperty): En if (context.__packageVersionEnum) { return context.__packageVersionEnum; } - let service = getNamespaceFromType(type.model); - if (!service) { - const namespaces = listAllServiceNamespaces(context); - if (namespaces.length === 0) { - return undefined; + + // Try to find the service from the model property's namespace hierarchy + let namespace = getNamespaceFromType(type.model); + + // Walk up the namespace hierarchy to find a versioned service + while (namespace) { + const versions = getVersions(context.program, namespace)[1]?.getVersions(); + if (versions?.length) { + const versionEnum = versions[0].enumMember.enum; + context.__packageVersionEnum = versionEnum; + return versionEnum; } - service = namespaces[0]; + namespace = namespace.namespace; } - let retval = getVersions(context.program, service)[1]?.getVersions()?.[0].enumMember.enum - while(!retval && service) { - service = service.namespace; - if (service) { - retval = getVersions(context.program, service)[1]?.getVersions()?.[0].enumMember.enum + + // Fallback: try to get from any service namespace + const services = listAllServiceNamespaces(context); + if (services.length > 0) { + const versions = getVersions(context.program, services[0])[1]?.getVersions(); + if (versions?.length) { + const versionEnum = versions[0].enumMember.enum; + context.__packageVersionEnum = versionEnum; + return versionEnum; } } - return retval; + + return undefined; } /** diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index 306f60dc6e..1a5b475d4e 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -867,7 +867,4 @@ it("one client from multiple services", async () => { ok(biOperationApiVersionParam); strictEqual(biOperationApiVersionParam.correspondingMethodParams.length, 1); strictEqual(biOperationApiVersionParam.correspondingMethodParams[0], biApiVersionParam); - - strictEqual(biClient.methods.length, 1); - strictEqual(biClient.methods[0].name, "btest"); }); From 00d990c15f1ad0e510f380125df6fb1588fd72ab Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 17:38:35 -0400 Subject: [PATCH 09/20] format --- .../test/clients/structure.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index 1a5b475d4e..a1cef2c84c 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -820,7 +820,7 @@ it("one client from multiple services", async () => { strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); const aiClient = client.children!.find((c) => c.name === "AI"); ok(aiClient); - + // AI client should have api versions from ServiceA strictEqual(aiClient.apiVersions.length, 2); deepStrictEqual(aiClient.apiVersions, ["av1", "av2"]); @@ -846,7 +846,7 @@ it("one client from multiple services", async () => { const biClient = client.children!.find((c) => c.name === "BI"); ok(biClient); - + strictEqual(biClient.apiVersions.length, 2); deepStrictEqual(biClient.apiVersions, ["bv1", "bv2"]); strictEqual(biClient.clientInitialization.parameters.length, 2); @@ -856,7 +856,7 @@ it("one client from multiple services", async () => { strictEqual(biApiVersionParam.isApiVersionParam, true); strictEqual(biApiVersionParam.onClient, true); strictEqual(biApiVersionParam.clientDefaultValue, "bv2"); - + // BI client should have btest method with VersionsB api version const biMethod = biClient.methods[0]; strictEqual(biMethod.name, "btest"); From 224d610ca7cebae8c8ed0b2fd678868a20fbffd6 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 17:39:44 -0400 Subject: [PATCH 10/20] add changeset --- .../tcgc-multiServiceOneClient-2025-9-30-17-39-36.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/tcgc-multiServiceOneClient-2025-9-30-17-39-36.md diff --git a/.chronus/changes/tcgc-multiServiceOneClient-2025-9-30-17-39-36.md b/.chronus/changes/tcgc-multiServiceOneClient-2025-9-30-17-39-36.md new file mode 100644 index 0000000000..f714d038ab --- /dev/null +++ b/.chronus/changes/tcgc-multiServiceOneClient-2025-9-30-17-39-36.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Add support to define one client from multiple services. Api versions for services will rest on the subclient \ No newline at end of file From 05ed64b2f6a52e70b775b0b6667245c18248196e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 30 Oct 2025 17:40:42 -0400 Subject: [PATCH 11/20] cspell --- .../test/clients/structure.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index a1cef2c84c..5c6eba240c 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -775,8 +775,8 @@ it("one client from multiple services", async () => { } interface AI { - @route("/atest") - atest(@query("api-version") apiVersion: VersionsA): void; + @route("/aTest") + aTest(@query("api-version") apiVersion: VersionsA): void; } } @@ -789,8 +789,8 @@ it("one client from multiple services", async () => { } interface BI { - @route("/btest") - btest(@query("api-version") apiVersion: VersionsB): void; + @route("/bTest") + bTest(@query("api-version") apiVersion: VersionsB): void; } }`, ` @@ -832,10 +832,10 @@ it("one client from multiple services", async () => { strictEqual(aiApiVersionParam.onClient, true); strictEqual(aiApiVersionParam.clientDefaultValue, "av2"); - // AI client should have atest method with VersionsA api version + // AI client should have aTest method with VersionsA api version strictEqual(aiClient.methods.length, 1); const aiMethod = aiClient.methods[0]; - strictEqual(aiMethod.name, "atest"); + strictEqual(aiMethod.name, "aTest"); strictEqual(aiMethod.parameters.length, 0); const aiOperation = aiMethod.operation; strictEqual(aiOperation.parameters.length, 1); @@ -857,9 +857,9 @@ it("one client from multiple services", async () => { strictEqual(biApiVersionParam.onClient, true); strictEqual(biApiVersionParam.clientDefaultValue, "bv2"); - // BI client should have btest method with VersionsB api version + // BI client should have bTest method with VersionsB api version const biMethod = biClient.methods[0]; - strictEqual(biMethod.name, "btest"); + strictEqual(biMethod.name, "bTest"); strictEqual(biMethod.parameters.length, 0); const biOperation = biMethod.operation; strictEqual(biOperation.parameters.length, 1); From d3c20dd9553069bc3c5faf460eda99494758e020 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 3 Nov 2025 13:27:23 -0500 Subject: [PATCH 12/20] build docs and add versioning test --- .../test/clients/structure.test.ts | 128 ++++++++++++++++++ .../reference/data-types.md | 9 +- 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index 5c6eba240c..030f4b6169 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -868,3 +868,131 @@ it("one client from multiple services", async () => { strictEqual(biOperationApiVersionParam.correspondingMethodParams.length, 1); strictEqual(biOperationApiVersionParam.correspondingMethodParams[0], biApiVersionParam); }); + +it("one client from multiple services with versioning", async () => { + await runner.compileWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { + av1, + av2, + av3, + } + + @added(VersionsA.av2) + model ServiceAModel { + id: string; + @added(VersionsA.av3) + name?: string; + } + + interface AI { + @added(VersionsA.av2) + @route("/atest") + atest(@query("api-version") apiVersion: VersionsA): ServiceAModel; + + @added(VersionsA.av3) + @route("/anew") + aNewOp(@query("api-version") apiVersion: VersionsA): void; + } + } + + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { + bv1, + bv2, + } + + model ServiceBModel { + data: string; + } + + interface BI { + @route("/btest") + btest(@query("api-version") apiVersion: VersionsB): ServiceBModel; + + @added(VersionsB.bv2) + @route("/bnew") + bNewOp(@query("api-version") apiVersion: VersionsB): void; + } + }`, + ` + @client + @service + namespace CombineClient { + @client({parent: CombineClient, service: ServiceA}) + @clientInitialization({initializedBy: InitializedBy.parent}) + interface AI extends ServiceA.AI {} + + @client({parent: CombineClient, service: ServiceB}) + @clientInitialization({initializedBy: InitializedBy.parent}) + interface BI extends ServiceB.BI {} + } + `, + ); + + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "CombineClient"); + + // Should have 2 children (AI and BI) + strictEqual(client.children?.length, 2); + + const aiClient = client.children?.find((c) => c.name === "AI"); + ok(aiClient); + + // AI client should have ServiceA's api versions + deepStrictEqual(aiClient.apiVersions, ["av1", "av2", "av3"]); + + // Check ServiceAModel exists and has correct versioning + const serviceAModel = sdkPackage.models.find((m) => m.name === "ServiceAModel"); + ok(serviceAModel); + deepStrictEqual(serviceAModel.apiVersions, ["av2", "av3"]); // Added in av2 + + // Check model property versioning + const nameProperty = serviceAModel.properties.find((p) => p.name === "name"); + ok(nameProperty); + deepStrictEqual(nameProperty.apiVersions, ["av3"]); // Added in av3 + + const idProperty = serviceAModel.properties.find((p) => p.name === "id"); + ok(idProperty); + + // AI client should have 2 methods initially, but aNewOp is only available from av3 + const aiMethods = aiClient.methods; + const atestMethod = aiMethods.find((m) => m.name === "atest"); + ok(atestMethod); + deepStrictEqual(atestMethod.apiVersions, ["av2", "av3"]); + + const aNewMethod = aiMethods.find((m) => m.name === "aNewOp"); + ok(aNewMethod); + deepStrictEqual(aNewMethod.apiVersions, ["av3"]); // Added in av3 + + const biClient = client.children?.find((c) => c.name === "BI"); + ok(biClient); + + // BI client should have ServiceB's api versions + deepStrictEqual(biClient.apiVersions, ["bv1", "bv2"]); + + // Check ServiceBModel versioning + const serviceBModel = sdkPackage.models.find((m) => m.name === "ServiceBModel"); + ok(serviceBModel); + deepStrictEqual(serviceBModel.apiVersions, ["bv1", "bv2"]); // Available from start + + // BI client methods + const biMethods = biClient.methods; + const btestMethod = biMethods.find((m) => m.name === "btest"); + ok(btestMethod); + deepStrictEqual(btestMethod.apiVersions, ["bv1", "bv2"]); + + const bNewMethod = biMethods.find((m) => m.name === "bNewOp"); + ok(bNewMethod); + deepStrictEqual(bNewMethod.apiVersions, ["bv2"]); // Added in bv2 + + // Parent client should have no versioning + deepStrictEqual(client.apiVersions, []); +}); diff --git a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md index 49c8521699..1fee06f2c9 100644 --- a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md +++ b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md @@ -31,10 +31,11 @@ model Azure.ClientGenerator.Core.ClientOptions #### Properties -| Name | Type | Description | -| -------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| service? | `Namespace` | The service that this client is generated for. If not specified, TCGC will look up the first parent namespace decorated with `@service` for the target.
The namespace should be decorated with `@service`. | -| name? | `string` | The name of the client. If not specified, the default name will be `Client`. | +| Name | Type | Description | +| -------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| service? | `Namespace` | The service that this client is generated for. If not specified, TCGC will look up the first parent namespace decorated with `@service` for the target.
The namespace should be decorated with `@service`. | +| name? | `string` | The name of the client. If not specified, the default name will be `Client`. | +| parent? | `Namespace \| Interface` | The parent client of this client. If specified, this client will be generated as a sub client of the parent client. | ### `ExternalType` {#Azure.ClientGenerator.Core.ExternalType} From 9623302198d1c6d924455253b38e0c3f6ea348d8 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 11 Nov 2025 13:55:35 -0500 Subject: [PATCH 13/20] fix cspell --- .../test/clients/structure.test.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index 030f4b6169..fc96d6bd50 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -890,8 +890,8 @@ it("one client from multiple services with versioning", async () => { interface AI { @added(VersionsA.av2) - @route("/atest") - atest(@query("api-version") apiVersion: VersionsA): ServiceAModel; + @route("/aTest") + aTest(@query("api-version") apiVersion: VersionsA): ServiceAModel; @added(VersionsA.av3) @route("/anew") @@ -912,11 +912,11 @@ it("one client from multiple services with versioning", async () => { } interface BI { - @route("/btest") - btest(@query("api-version") apiVersion: VersionsB): ServiceBModel; + @route("/bTest") + bTest(@query("api-version") apiVersion: VersionsB): ServiceBModel; @added(VersionsB.bv2) - @route("/bnew") + @route("/bNew") bNewOp(@query("api-version") apiVersion: VersionsB): void; } }`, @@ -964,9 +964,9 @@ it("one client from multiple services with versioning", async () => { // AI client should have 2 methods initially, but aNewOp is only available from av3 const aiMethods = aiClient.methods; - const atestMethod = aiMethods.find((m) => m.name === "atest"); - ok(atestMethod); - deepStrictEqual(atestMethod.apiVersions, ["av2", "av3"]); + const aTestMethod = aiMethods.find((m) => m.name === "aTest"); + ok(aTestMethod); + deepStrictEqual(aTestMethod.apiVersions, ["av2", "av3"]); const aNewMethod = aiMethods.find((m) => m.name === "aNewOp"); ok(aNewMethod); @@ -985,9 +985,9 @@ it("one client from multiple services with versioning", async () => { // BI client methods const biMethods = biClient.methods; - const btestMethod = biMethods.find((m) => m.name === "btest"); - ok(btestMethod); - deepStrictEqual(btestMethod.apiVersions, ["bv1", "bv2"]); + const bTestMethod = biMethods.find((m) => m.name === "bTest"); + ok(bTestMethod); + deepStrictEqual(bTestMethod.apiVersions, ["bv1", "bv2"]); const bNewMethod = biMethods.find((m) => m.name === "bNewOp"); ok(bNewMethod); From 2a73a95e1caaa24f87e37a7db8a7d67ca7727b75 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 13 Nov 2025 16:10:04 -0500 Subject: [PATCH 14/20] remove unnecessary fallback --- .../src/public-utils.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index 87e1d94b3c..68c22e7da3 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -108,17 +108,6 @@ function getVersionEnumForService(context: TCGCContext, type: ModelProperty): En namespace = namespace.namespace; } - // Fallback: try to get from any service namespace - const services = listAllServiceNamespaces(context); - if (services.length > 0) { - const versions = getVersions(context.program, services[0])[1]?.getVersions(); - if (versions?.length) { - const versionEnum = versions[0].enumMember.enum; - context.__packageVersionEnum = versionEnum; - return versionEnum; - } - } - return undefined; } From bb9a49bdb32e6362756c0870ab32e41e20ebd281 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 13 Nov 2025 16:22:33 -0500 Subject: [PATCH 15/20] throw error for mutating multiple services --- .../src/internal-utils.ts | 10 ++++++++++ packages/typespec-client-generator-core/src/lib.ts | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 14628d4dbf..f8be1b1a7a 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -782,6 +782,16 @@ export function handleVersioningMutationForGlobalNamespace(context: TCGCContext) const globalNamespace = context.program.getGlobalNamespaceType(); const allApiVersions = context.getApiVersions(); if (allApiVersions.length === 0 || context.apiVersion === "all") return globalNamespace; + if (listServices(context.program).length > 1) { + context.program.reportDiagnostic( + createDiagnostic({ + code: "mutating-multiple-services", + format: {}, + target: globalNamespace, + }), + ); + return globalNamespace; + } const mutator = getVersioningMutator( context, diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index 1adec5fe61..b2d772b9b1 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -452,6 +452,12 @@ export const $lib = createTypeSpecLibrary({ default: paramMessage`The API version specified in the config: "${"version"}" is not defined in service versioning list. Fall back to the latest version.`, }, }, + "mutating-multiple-services": { + severity: "error", + messages: { + default: "Mutating multiple services in a single compilation is not supported.", + }, + }, }, emitter: { options: TCGCEmitterOptionsSchema, From e274d6b77654d08b12f48fb2b8ba989864754030 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 19 Nov 2025 17:12:20 -0500 Subject: [PATCH 16/20] add requirement for useDependency --- .../src/cache.ts | 16 ++ .../src/clients.ts | 52 +++++- .../src/context.ts | 148 +++++++++++++++++- .../src/internal-utils.ts | 89 +++++++++-- .../typespec-client-generator-core/src/lib.ts | 4 +- .../src/package.ts | 56 ++++++- .../src/public-utils.ts | 25 ++- .../test/clients/structure.test.ts | 95 ++++++++++- .../test/utils.ts | 9 ++ .../test/validations/package.test.ts | 3 + 10 files changed, 472 insertions(+), 25 deletions(-) diff --git a/packages/typespec-client-generator-core/src/cache.ts b/packages/typespec-client-generator-core/src/cache.ts index 30e32d8876..74567628e0 100644 --- a/packages/typespec-client-generator-core/src/cache.ts +++ b/packages/typespec-client-generator-core/src/cache.ts @@ -111,6 +111,22 @@ export function prepareClientAndOperationCache(context: TCGCContext): void { // operations directly under the group const operations = [...group.type.operations.values()]; + // For interfaces that extend other interfaces, also collect operations from the source interfaces + // TypeSpec doesn't always automatically copy operations from extended interfaces, + // especially in versioning scenarios, so we need to manually collect them + if (group.type.kind === "Interface" && group.type.sourceInterfaces.length > 0) { + const operationsByName = new Map(operations.map((op) => [op.name, op])); + for (const sourceIface of group.type.sourceInterfaces) { + for (const [name, op] of sourceIface.operations) { + // Only add if not already present (prefer operations from derived interface) + if (!operationsByName.has(name)) { + operations.push(op); + operationsByName.set(name, op); + } + } + } + } + // when there is explicitly `@operationGroup` or `@client` // operations under namespace or interface that are not decorated with `@operationGroup` or `@client` // should be placed in the first accessor client or operation group diff --git a/packages/typespec-client-generator-core/src/clients.ts b/packages/typespec-client-generator-core/src/clients.ts index 40a75177b6..af4530ff4b 100644 --- a/packages/typespec-client-generator-core/src/clients.ts +++ b/packages/typespec-client-generator-core/src/clients.ts @@ -1,6 +1,13 @@ -import { createDiagnosticCollector, Diagnostic, getDoc, getSummary } from "@typespec/compiler"; +import { + createDiagnosticCollector, + Diagnostic, + getDoc, + getSummary, + listServices, +} from "@typespec/compiler"; import { $ } from "@typespec/compiler/typekit"; import { getServers, HttpServer } from "@typespec/http"; +import { getVersionDependencies } from "@typespec/versioning"; import { getClientInitializationOptions, getClientNameOverride, @@ -17,6 +24,7 @@ import { SdkEndpointParameter, SdkEndpointType, SdkHttpOperation, + SdkMethodParameter, SdkOperationGroup, SdkPathParameter, SdkServiceOperation, @@ -248,6 +256,48 @@ function addDefaultClientParameters< if (apiVersionParam) break; } } + + // Check for multi-service scenario with @useDependency + if (!apiVersionParam && client.__raw.kind === "SdkClient") { + const services = listServices(context.program); + if (services.length > 1 && client.__raw.type?.kind === "Namespace") { + // Check if this is a multi-service client with @useDependency + const versionDependencies = getVersionDependencies(context.program, client.__raw.type); + if (versionDependencies && versionDependencies.size > 0) { + // Create an API version parameter for multi-service clients with @useDependency + const stringType = context.program.checker.getStdType("string"); + + // Create a SdkMethodParameter for the API version + const apiVersionMethodParam: SdkMethodParameter = { + __raw: undefined, + kind: "method", + name: "apiVersion", + isGeneratedName: false, + doc: "The API version to use for the operation", + type: getSdkBuiltInType(context, stringType), + optional: false, + isApiVersionParam: true, + onClient: true, + apiVersions: context.getApiVersionsForType(client.__raw.type ?? client.__raw.service), + clientDefaultValue: undefined, + decorators: [], + crossLanguageDefinitionId: `${client.crossLanguageDefinitionId}.apiVersion`, + access: "public", + flatten: false, + }; + + // Add to cache + let clientParams = context.__clientParametersCache.get(client.__raw); + if (!clientParams) { + clientParams = []; + context.__clientParametersCache.set(client.__raw, clientParams); + } + clientParams.push(apiVersionMethodParam); + apiVersionParam = apiVersionMethodParam; + } + } + } + if (apiVersionParam) { defaultClientParamters.push(apiVersionParam); } diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index 0ff3f8e2aa..23226b82a2 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -14,7 +14,7 @@ import { Union, } from "@typespec/compiler"; import { HttpOperation } from "@typespec/http"; -import { getVersions } from "@typespec/versioning"; +import { getVersionDependencies, getVersions } from "@typespec/versioning"; import { stringify } from "yaml"; import { prepareClientAndOperationCache } from "./cache.js"; import { defaultDecoratorsAllowList } from "./configs.js"; @@ -52,6 +52,46 @@ interface CreateTCGCContextOptions { mutateNamespace?: boolean; // whether to mutate global namespace for versioning } +function validateMultiServiceVersionDependencies(context: TCGCContext): boolean { + const clients = context.getClients(); + + // Find the top-level client (root client without parent) + const topLevelClient = clients.find((client) => !client.parent); + + if (!topLevelClient) { + // No top-level client found + return false; + } + + // Get all sub-clients (clients with parents) + const subClients = clients.filter((client) => client.parent); + + if (subClients.length === 0) { + // No sub-services, validation passes + return true; + } + + // Get version dependencies for the top-level client + const versionDependencies = getVersionDependencies( + context.program, + topLevelClient.type as Namespace, + ); + + // Check if @useDependency decorator is used properly + // This would be where you check if the top-level client has @useDependency + // and if each sub-service has its version specified + + for (const subClient of subClients) { + // Check if this sub-service has version dependencies specified + if (!versionDependencies || !versionDependencies.get(subClient.service)) { + // Sub-service version not specified in @useDependency + return false; + } + } + + return true; +} + export function createTCGCContext( program: Program, emitterName?: string, @@ -99,6 +139,59 @@ export function createTCGCContext( return globalNamespace; }, getApiVersionsForType(type): string[] { + const cachedVersions = this.__tspTypeToApiVersions.get(type); + if (cachedVersions && cachedVersions.length > 0) { + return cachedVersions; + } + + // Check if this is a multi-service client with @useDependency + if (type.kind === "Namespace") { + const services = listServices(this.program); + if (services.length > 1) { + const versionDependencies = getVersionDependencies(this.program, type); + if (versionDependencies && versionDependencies.size > 0) { + const allVersions: string[] = []; + for (const [_service, versions] of versionDependencies.entries()) { + // The versions might be enum members, so we need to extract the value + if (Array.isArray(versions)) { + for (const version of versions) { + if (typeof version === "string") { + allVersions.push(version); + } else if (version && typeof version === "object" && "value" in version) { + // Handle enum member case + allVersions.push(String(version.value)); + } else if (version && typeof version === "object" && "name" in version) { + // Handle enum member case with name + allVersions.push(String(version.name)); + } + } + } else if (typeof versions === "string") { + allVersions.push(versions); + } else if (versions && typeof versions === "object" && "value" in versions) { + // Handle single enum member case + allVersions.push(String(versions.value)); + } else if (versions && typeof versions === "object" && "name" in versions) { + // Handle single enum member case with name + allVersions.push(String(versions.name)); + } + } + // Cache and return the version dependencies + if (allVersions.length > 0) { + this.__tspTypeToApiVersions.set(type, allVersions); + return allVersions; + } + } + } + } + + // Fall back to normal versioning logic for single service types + const [_namespace, versionMap] = getVersions(this.program, type); + if (versionMap) { + const versions = versionMap.getVersions().map((v) => v.value); + this.__tspTypeToApiVersions.set(type, versions); + return versions; + } + return this.__tspTypeToApiVersions.get(type) ?? []; }, setApiVersionsForType(type, apiVersions: string[]): void { @@ -123,7 +216,60 @@ export function createTCGCContext( if (globalVersions?.length) { return globalVersions; } + const services = listServices(this.program); + if (services.length === 0) { + return []; + } + if (services.length === 1) { + service = services[0].type; + } else { + const clients = this.getClients(); + if (clients.length !== 0) { + // In this case, there needs to be one top-level client with a service, and that is decorated with `@useDependency` + if (!validateMultiServiceVersionDependencies(this)) { + reportDiagnostic(this.program, { + code: "multiple-services-require-use-dependency", + format: { services: services.map((s) => s.type.name).join(", ") }, + target: services[0].type, + }); + } + // Process all services and cache their versions + for (const svc of services) { + const svcNamespace = svc.type; + // Check if already cached + if (!this.__serviceToVersions.has(svcNamespace)) { + const versions = getVersions(program, svcNamespace)[1]?.getVersions(); + if (versions) { + removeVersionsLargerThanExplicitlySpecified(this, versions); + const serviceVersions = versions.map((version) => version.value); + this.__serviceToVersions.set(svcNamespace, serviceVersions); + // Also cache in __tspTypeToApiVersions so getApiVersionsForType can find it + this.__tspTypeToApiVersions.set(svcNamespace, serviceVersions); + } + } + } + + // Get version dependencies from the top-level client and extract all version strings + const versionDependencies = getVersionDependencies( + this.program, + clients[0].type as Namespace, + ); + if (versionDependencies) { + const allVersions: string[] = []; + for (const versions of versionDependencies.values()) { + if (Array.isArray(versions)) { + allVersions.push(...versions); + } else if (typeof versions === "string") { + allVersions.push(versions); + } + } + this.__serviceToVersions.set(undefined, allVersions); + return allVersions; + } + return []; + } + } // Try to get from the first service const firstService = listServices(program)[0]?.type; if (firstService) { diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index f8be1b1a7a..324587d3fe 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -41,6 +41,7 @@ import { HttpOperation, HttpOperationResponseContent, HttpPayloadBody } from "@t import { getAddedOnVersions, getRemovedOnVersions, + getVersionDependencies, getVersioningMutators, getVersions, } from "@typespec/versioning"; @@ -289,7 +290,8 @@ export function getAvailableApiVersions( context.setApiVersionsForType(type, explicitlyDecorated); return explicitlyDecorated; } - context.setApiVersionsForType(type, wrapperApiVersions); + // If no explicit decorators, use the calculated apiVersions instead of just wrapperApiVersions + context.setApiVersionsForType(type, apiVersions); return context.getApiVersionsForType(type); } @@ -782,20 +784,87 @@ export function handleVersioningMutationForGlobalNamespace(context: TCGCContext) const globalNamespace = context.program.getGlobalNamespaceType(); const allApiVersions = context.getApiVersions(); if (allApiVersions.length === 0 || context.apiVersion === "all") return globalNamespace; - if (listServices(context.program).length > 1) { - context.program.reportDiagnostic( - createDiagnostic({ - code: "mutating-multiple-services", - format: {}, - target: globalNamespace, - }), - ); + + const services = listServices(context.program); + const mutators = []; + + // Check for multi-service scenarios with @useDependency + if (services.length > 1) { + const clients = context.getClients(); + if (clients.length > 0) { + const versionDependencies = getVersionDependencies( + context.program, + clients[0].type as Namespace, + ); + if (versionDependencies && versionDependencies.size > 0) { + // Multi-service scenario with @useDependency - create mutators for each service + for (const [serviceNs, versions] of versionDependencies.entries()) { + if (serviceNs.kind === "Namespace") { + // Extract the version string from the enum member + let versionString: string; + if (Array.isArray(versions)) { + // Take the last version if multiple versions + const lastVersion = versions[versions.length - 1]; + if (typeof lastVersion === "string") { + versionString = lastVersion; + } else if (lastVersion && typeof lastVersion === "object" && "value" in lastVersion) { + versionString = String(lastVersion.value); + } else if (lastVersion && typeof lastVersion === "object" && "name" in lastVersion) { + versionString = String(lastVersion.name); + } else { + continue; + } + } else if (typeof versions === "string") { + versionString = versions; + } else if (versions && typeof versions === "object" && "value" in versions) { + versionString = String(versions.value); + } else if (versions && typeof versions === "object" && "name" in versions) { + versionString = String(versions.name); + } else { + continue; + } + + const mutator = getVersioningMutator(context, serviceNs, versionString); + mutators.push(mutator); + } + } + + if (mutators.length > 0) { + const subgraph = unsafe_mutateSubgraphWithNamespace( + context.program, + mutators, + globalNamespace, + ); + compilerAssert( + subgraph.type.kind === "Namespace", + "Should not have mutated to another type", + ); + return subgraph.type; + } + } + } + } + + // Single service scenario or no @useDependency + if (services.length === 0) { return globalNamespace; } + // Find the service that has versioning information + // In single-service scenarios, this should be the service with @versioned + // In multi-service without @useDependency, use the first service with versions + let targetService = services[0].type; + for (const service of services) { + const versions = getVersions(context.program, service.type)[1]; + if (versions) { + targetService = service.type; + break; + } + } + const mutator = getVersioningMutator( context, - listServices(context.program)[0].type, + targetService, allApiVersions[allApiVersions.length - 1], ); const subgraph = unsafe_mutateSubgraphWithNamespace(context.program, [mutator], globalNamespace); diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index b2d772b9b1..35c024c51a 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -452,10 +452,10 @@ export const $lib = createTypeSpecLibrary({ default: paramMessage`The API version specified in the config: "${"version"}" is not defined in service versioning list. Fall back to the latest version.`, }, }, - "mutating-multiple-services": { + "multiple-services-require-use-dependency": { severity: "error", messages: { - default: "Mutating multiple services in a single compilation is not supported.", + default: paramMessage`When using multiple services, you must use @useDependency to specify the versions of sub-services that the main service depends on. Found services: ${"services"}`, }, }, }, diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index ac3bcb61b5..f4d98926c4 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -1,4 +1,10 @@ -import { createDiagnosticCollector, Diagnostic, ignoreDiagnostics } from "@typespec/compiler"; +import { + createDiagnosticCollector, + Diagnostic, + ignoreDiagnostics, + listServices, +} from "@typespec/compiler"; +import { getVersionDependencies } from "@typespec/versioning"; import { prepareClientAndOperationCache } from "./cache.js"; import { createSdkClientType } from "./clients.js"; import { listClients } from "./decorators.js"; @@ -155,12 +161,58 @@ function populateApiVersionInformation(context: TCGCContext): void { prepareClientAndOperationCache(context); } for (const clientOperationGroup of context.__rawClientsOperationGroupsCache!.values()) { + let apiVersions: string[]; + + // Check if this is a multi-service client with @useDependency + if (clientOperationGroup.type?.kind === "Namespace") { + const services = listServices(context.program); + if (services.length > 1) { + const versionDependencies = getVersionDependencies( + context.program, + clientOperationGroup.type, + ); + if (versionDependencies && versionDependencies.size > 0) { + // Extract version strings from dependencies for multi-service clients + const allVersions: string[] = []; + for (const [_service, versions] of versionDependencies.entries()) { + if (Array.isArray(versions)) { + for (const version of versions) { + if (typeof version === "string") { + allVersions.push(version); + } else if (version && typeof version === "object" && "value" in version) { + allVersions.push(String(version.value)); + } else if (version && typeof version === "object" && "name" in version) { + allVersions.push(String(version.name)); + } + } + } else if (typeof versions === "string") { + allVersions.push(versions); + } else if (versions && typeof versions === "object" && "value" in versions) { + allVersions.push(String(versions.value)); + } else if (versions && typeof versions === "object" && "name" in versions) { + allVersions.push(String(versions.name)); + } + } + apiVersions = allVersions; + } else { + // No @useDependency, use normal logic + apiVersions = context.getApiVersions(clientOperationGroup.service); + } + } else { + // Single service, use normal logic + apiVersions = context.getApiVersions(clientOperationGroup.service); + } + } else { + // Not a namespace, use normal logic + apiVersions = context.getApiVersions(clientOperationGroup.service); + } + context.setApiVersionsForType( clientOperationGroup.type ?? clientOperationGroup.service, filterApiVersionsWithDecorators( context, clientOperationGroup.type ?? clientOperationGroup.service, - context.getApiVersions(clientOperationGroup.service), + apiVersions, ), ); diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index 68c22e7da3..beee12c273 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -16,6 +16,7 @@ import { ignoreDiagnostics, isGlobalNamespace, isService, + listServices, resolveEncodedName, } from "@typespec/compiler"; import { @@ -95,9 +96,16 @@ function getVersionEnumForService(context: TCGCContext, type: ModelProperty): En } // Try to find the service from the model property's namespace hierarchy - let namespace = getNamespaceFromType(type.model); + // For server parameters where type is an enum, start from the enum's namespace + // For operation parameters, start from the model's namespace + let namespace: Namespace | undefined; + if (type.type.kind === "Enum") { + namespace = type.type.namespace; + } else { + namespace = getNamespaceFromType(type.model); + } - // Walk up the namespace hierarchy to find a versioned service + // Walk up the namespace hierarchy to find a versioned namespace (could be service or library) while (namespace) { const versions = getVersions(context.program, namespace)[1]?.getVersions(); if (versions?.length) { @@ -108,6 +116,19 @@ function getVersionEnumForService(context: TCGCContext, type: ModelProperty): En namespace = namespace.namespace; } + // Fallback: check if any service in the program has versioning + // This handles cases where a parameter is defined in a non-versioned namespace + // but is used in a versioned service (e.g., interface extends scenarios) + const services = listServices(context.program); + for (const service of services) { + const versions = getVersions(context.program, service.type)[1]?.getVersions(); + if (versions?.length) { + const versionEnum = versions[0].enumMember.enum; + context.__packageVersionEnum = versionEnum; + return versionEnum; + } + } + return undefined; } diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index fc96d6bd50..f7b7d0f030 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -796,11 +796,12 @@ it("one client from multiple services", async () => { ` @client @service + @useDependency(ServiceA.VersionsA.av2, ServiceB.VersionsB.bv2) namespace CombineClient { - @clientInitialization({initializedBy: InitializedBy.individually | InitializedBy.parent}) + @clientInitialization({initializedBy: InitializedBy.parent}) @client({service: ServiceA, parent: CombineClient}) interface AI extends ServiceA.AI {} - @clientInitialization({initializedBy: InitializedBy.individually | InitializedBy.parent}) + @clientInitialization({initializedBy: InitializedBy.parent}) @client({service: ServiceB, parent: CombineClient}) interface BI extends ServiceB.BI {} } @@ -814,10 +815,17 @@ it("one client from multiple services", async () => { ok(bVersionsEnum); const client = sdkPackage.clients[0]; strictEqual(client.name, "CombineClient"); - strictEqual(client.apiVersions.length, 0); + strictEqual(client.apiVersions.length, 2); + deepStrictEqual(client.apiVersions, ["av2", "bv2"]); strictEqual(client.children!.length, 2); - strictEqual(client.clientInitialization.parameters.length, 1); - strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters.length, 2); + ok(client.clientInitialization.parameters.find((p) => p.name === "endpoint")); + const apiVersionParam = client.clientInitialization.parameters.find( + (p) => p.name === "apiVersion", + ); + ok(apiVersionParam); + strictEqual(apiVersionParam.apiVersions.length, 2); + deepStrictEqual(apiVersionParam.apiVersions, ["av2", "bv2"]); const aiClient = client.children!.find((c) => c.name === "AI"); ok(aiClient); @@ -923,6 +931,10 @@ it("one client from multiple services with versioning", async () => { ` @client @service + @useDependency( + ServiceA.VersionsA.av3, + ServiceB.VersionsB.bv2 + ) namespace CombineClient { @client({parent: CombineClient, service: ServiceA}) @clientInitialization({initializedBy: InitializedBy.parent}) @@ -940,6 +952,11 @@ it("one client from multiple services with versioning", async () => { const client = sdkPackage.clients[0]; strictEqual(client.name, "CombineClient"); + // Client should have endpoint and apiVersion parameters + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters[1].name, "apiVersion"); + // Should have 2 children (AI and BI) strictEqual(client.children?.length, 2); @@ -993,6 +1010,70 @@ it("one client from multiple services with versioning", async () => { ok(bNewMethod); deepStrictEqual(bNewMethod.apiVersions, ["bv2"]); // Added in bv2 - // Parent client should have no versioning - deepStrictEqual(client.apiVersions, []); + // Parent client should have API versions from @useDependency + deepStrictEqual(client.apiVersions, ["av3", "bv2"]); +}); + +it("multiple services without @useDependency should require it", async () => { + const runnerWithVersion = await createSdkTestRunner({ + "api-version": "latest", + emitterName: "@azure-tools/typespec-python", + }); + await runnerWithVersion.compileWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { + av1, + av2, + } + + interface AI { + @route("/atest") + atest(@query("api-version") apiVersion: VersionsA): void; + } + } + + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { + bv1, + bv2, + } + + interface BI { + @route("/btest") + btest(@query("api-version") apiVersion: VersionsB): void; + } + }`, + + `@client + @service + @useDependency(ServiceA.VersionsA.av1, ServiceB.VersionsB.bv1) + namespace CombineClient { + @clientInitialization({initializedBy: InitializedBy.parent}) + @client({service: ServiceA, parent: CombineClient}) + interface AI extends ServiceA.AI {} + @clientInitialization({initializedBy: InitializedBy.parent}) + @client({service: ServiceB, parent: CombineClient}) + interface BI extends ServiceB.BI {} + } + `, + ); + + const sdkPackage = runnerWithVersion.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + + const client = sdkPackage.clients[0]; + strictEqual(client.name, "CombineClient"); + + // Client should have endpoint and apiVersion parameters + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters[1].name, "apiVersion"); + + // Parent client should have API versions from @useDependency + deepStrictEqual(client.apiVersions, ["av1", "bv1"]); }); diff --git a/packages/typespec-client-generator-core/test/utils.ts b/packages/typespec-client-generator-core/test/utils.ts index c1bac7afc9..b2fb0bf9a7 100644 --- a/packages/typespec-client-generator-core/test/utils.ts +++ b/packages/typespec-client-generator-core/test/utils.ts @@ -1,3 +1,4 @@ +import { isService } from "@typespec/compiler"; import { strictEqual } from "assert"; import { SdkClientType, @@ -58,5 +59,13 @@ export function getServiceMethodOfClient( } export function getServiceNamespace(runner: SdkTestRunner) { + // Use the unmutated program to find service namespaces, since servers are defined there + const globalNs = runner.context.program.getGlobalNamespaceType(); + for (const [_, ns] of globalNs.namespaces) { + if (isService(runner.context.program, ns)) { + return ns; + } + } + // Fallback to mutated namespace lookup return listAllServiceNamespaces(runner.context)[0]; } diff --git a/packages/typespec-client-generator-core/test/validations/package.test.ts b/packages/typespec-client-generator-core/test/validations/package.test.ts index 3c94778f87..5ae903db7b 100644 --- a/packages/typespec-client-generator-core/test/validations/package.test.ts +++ b/packages/typespec-client-generator-core/test/validations/package.test.ts @@ -28,6 +28,9 @@ it("multiple-services", async () => { { code: "@azure-tools/typespec-client-generator-core/multiple-services", }, + { + code: "@azure-tools/typespec-client-generator-core/multiple-services", + }, ]); strictEqual(listClients(runner.context).length, 1); From 7b813980eaa848363fc27b019bcec484ae83fbae Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 20 Nov 2025 15:24:03 -0500 Subject: [PATCH 17/20] use cspell --- .../test/clients/structure.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index f7b7d0f030..21eb00dd09 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -1030,8 +1030,8 @@ it("multiple services without @useDependency should require it", async () => { } interface AI { - @route("/atest") - atest(@query("api-version") apiVersion: VersionsA): void; + @route("/aTest") + aTest(@query("api-version") apiVersion: VersionsA): void; } } @@ -1044,8 +1044,8 @@ it("multiple services without @useDependency should require it", async () => { } interface BI { - @route("/btest") - btest(@query("api-version") apiVersion: VersionsB): void; + @route("/bTest") + bTest(@query("api-version") apiVersion: VersionsB): void; } }`, From a1d766434b3ada126dc85322724111a334448719 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 20 Nov 2025 15:30:23 -0500 Subject: [PATCH 18/20] cspell again --- packages/typespec-client-generator-core/test/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typespec-client-generator-core/test/utils.ts b/packages/typespec-client-generator-core/test/utils.ts index b2fb0bf9a7..b99375f3d9 100644 --- a/packages/typespec-client-generator-core/test/utils.ts +++ b/packages/typespec-client-generator-core/test/utils.ts @@ -59,7 +59,7 @@ export function getServiceMethodOfClient( } export function getServiceNamespace(runner: SdkTestRunner) { - // Use the unmutated program to find service namespaces, since servers are defined there + // Use the non-mutated program to find service namespaces, since servers are defined there const globalNs = runner.context.program.getGlobalNamespaceType(); for (const [_, ns] of globalNs.namespaces) { if (isService(runner.context.program, ns)) { From 59b92a63765160ec9598b306de889796edcec3b6 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 20 Nov 2025 16:15:07 -0500 Subject: [PATCH 19/20] add docs --- .../Generate client libraries/03client.mdx | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) diff --git a/website/src/content/docs/docs/howtos/Generate client libraries/03client.mdx b/website/src/content/docs/docs/howtos/Generate client libraries/03client.mdx index f8c3c40927..d3a77827ad 100644 --- a/website/src/content/docs/docs/howtos/Generate client libraries/03client.mdx +++ b/website/src/content/docs/docs/howtos/Generate client libraries/03client.mdx @@ -590,6 +590,283 @@ petStorePetsActionsClient.Pet(context.Background(), &PetStorePetsActionsClientPe +### Multi-Service Clients + +When a TypeSpec specification defines multiple services that need to be accessed through a single client, you can use the `@useDependency` decorator to specify version dependencies between services. This is an edge case used when a client needs to interact with multiple related services, each potentially at different API versions. + +#### Requirements + +To define a multi-service client: + +1. **Multiple `@service` decorators**: Define each service with its own `@service` decorator in separate namespaces +2. **Versioning**: Each service should use the `@versioned` decorator to define its API versions +3. **Version dependencies**: Use `@useDependency` on the client namespace to specify which version of each dependent service to use +4. **Client definition**: Use the `@client` decorator to define the root client that will access multiple services + +#### Example + + + +```typespec +// main.tsp +@service({ title: "Widget Service" }) +@versioned(WidgetService.Versions) +namespace WidgetService { + enum Versions { + v1_0: "1.0", + v2_0: "2.0", + } + + interface Widget { + @route("/widgets") + @get op listWidgets(): Widget[]; + } + + model Widget { + id: string; + name: string; + } +} + +@service({ title: "Gadget Service" }) +@versioned(GadgetService.Versions) +namespace GadgetService { + enum Versions { + v1_0: "1.0", + v1_1: "1.1", + } + + interface Gadget { + @route("/gadgets") + @get op listGadgets(): Gadget[]; + } + + model Gadget { + id: string; + type: string; + } +} + +// client.tsp +@client({ + name: "MultiServiceClient", +}) +@service +@useDependency(WidgetService.Versions.v2_0) +@useDependency(GadgetService.Versions.v1_1) +namespace MultiServiceClient { + @clientInitialization({initializedBy: InitializedBy.parent}) + @client({service: WidgetService, parent: MultiServiceClient}) + interface WidgetClient extends WidgetService.Widget; + + @clientInitialization({initializedBy: InitializedBy.parent}) + @client({service: GadgetService, parent: MultiServiceClient}) + interface GadgetClient extends GadgetService.Gadget; +} +``` + +```python +# generated _client.py +class MultiServiceClient: + def __init__( + self, + endpoint: str, + **kwargs: Any + ) -> None: + self._endpoint = endpoint + self.widget = WidgetOperations(endpoint=endpoint, api_version="2.0", **kwargs) + self.gadget = GadgetOperations(endpoint=endpoint, api_version="1.1", **kwargs) + +# generated operations/_operations.py +class WidgetOperations: + @distributed_trace + def list_widgets(self, **kwargs: Any) -> List[Widget]: + """List widgets from Widget Service v2.0""" + +class GadgetOperations: + @distributed_trace + def list_gadgets(self, **kwargs: Any) -> List[Gadget]: + """List gadgets from Gadget Service v1.1""" + +# Usage +from multi_service import MultiServiceClient + +client = MultiServiceClient(endpoint="https://api.example.com") +widgets = client.widget.list_widgets() +gadgets = client.gadget.list_gadgets() +``` + +```csharp +namespace MultiService +{ + public partial class MultiServiceClient + { + public MultiServiceClient(Uri endpoint, MultiServiceClientOptions options) + { + Widget = new WidgetClient(endpoint, "2.0", options); + Gadget = new GadgetClient(endpoint, "1.1", options); + } + + public virtual WidgetClient Widget { get; } + public virtual GadgetClient Gadget { get; } + } + + public partial class WidgetClient + { + public virtual async Task>> ListWidgetsAsync(RequestContext context = null) {} + public virtual Response> ListWidgets(RequestContext context = null) {} + } + + public partial class GadgetClient + { + public virtual async Task>> ListGadgetsAsync(RequestContext context = null) {} + public virtual Response> ListGadgets(RequestContext context = null) {} + } +} + +// Usage +using MultiService; + +var client = new MultiServiceClient(new Uri("https://api.example.com"), new MultiServiceClientOptions()); +var widgets = client.Widget.ListWidgets(); +var gadgets = client.Gadget.ListGadgets(); +``` + +```typescript +import { MultiServiceClient } from "@azure/multi-service"; + +const client = new MultiServiceClient("https://api.example.com"); + +// Access Widget service operations (v2.0) +const widgets = await client.widget.listWidgets(); + +// Access Gadget service operations (v1.1) +const gadgets = await client.gadget.listGadgets(); +``` + +```java +@ServiceClientBuilder(serviceClients = { + MultiServiceClient.class, + WidgetClient.class, + GadgetClient.class, + MultiServiceAsyncClient.class, + WidgetAsyncClient.class, + GadgetAsyncClient.class +}) +public final class MultiServiceClientBuilder implements HttpTrait, + ConfigurationTrait, EndpointTrait { + + public MultiServiceClientBuilder(); + public MultiServiceClient buildClient(); +} + +@ServiceClient(builder = MultiServiceClientBuilder.class) +public final class MultiServiceClient { + private final WidgetClient widgetClient; + private final GadgetClient gadgetClient; + + MultiServiceClient(String endpoint) { + this.widgetClient = new WidgetClient(endpoint, "2.0"); + this.gadgetClient = new GadgetClient(endpoint, "1.1"); + } + + public WidgetClient getWidget() { + return this.widgetClient; + } + + public GadgetClient getGadget() { + return this.gadgetClient; + } +} + +@ServiceClient(builder = MultiServiceClientBuilder.class) +public final class WidgetClient { + public PagedIterable listWidgets(); +} + +@ServiceClient(builder = MultiServiceClientBuilder.class) +public final class GadgetClient { + public PagedIterable listGadgets(); +} + +// Usage +MultiServiceClient client = new MultiServiceClientBuilder() + .endpoint("https://api.example.com") + .buildClient(); + +var widgets = client.getWidget().listWidgets(); +var gadgets = client.getGadget().listGadgets(); +``` + +```go +// generated multiservice_client.go +type MultiServiceClient struct { + widgetClient *WidgetClient + gadgetClient *GadgetClient +} + +func NewMultiServiceClient(endpoint string, options *MultiServiceClientOptions) (*MultiServiceClient, error) { + widgetClient, err := NewWidgetClient(endpoint, "2.0", options) + if err != nil { + return nil, err + } + + gadgetClient, err := NewGadgetClient(endpoint, "1.1", options) + if err != nil { + return nil, err + } + + return &MultiServiceClient{ + widgetClient: widgetClient, + gadgetClient: gadgetClient, + }, nil +} + +func (client *MultiServiceClient) Widget() *WidgetClient { + return client.widgetClient +} + +func (client *MultiServiceClient) Gadget() *GadgetClient { + return client.gadgetClient +} + +// generated widget_client.go +type WidgetClient struct {} + +func NewWidgetClient(endpoint string, apiVersion string, options *MultiServiceClientOptions) (*WidgetClient, error) { + return &WidgetClient{}, nil +} + +func (client *WidgetClient) ListWidgets(ctx context.Context, options *WidgetClientListWidgetsOptions) (WidgetClientListWidgetsResponse, error) {} + +// generated gadget_client.go +type GadgetClient struct {} + +func NewGadgetClient(endpoint string, apiVersion string, options *MultiServiceClientOptions) (*GadgetClient, error) { + return &GadgetClient{}, nil +} + +func (client *GadgetClient) ListGadgets(ctx context.Context, options *GadgetClientListGadgetsOptions) (GadgetClientListGadgetsResponse, error) {} + +// Usage +client, err := NewMultiServiceClient("https://api.example.com", nil) +if err != nil { + // handle error +} + +widgets, err := client.Widget().ListWidgets(context.Background(), nil) +gadgets, err := client.Gadget().ListGadgets(context.Background(), nil) +``` + + + +#### Key Behaviors + +- **API Version Parameter**: The generated client will include an `apiVersion` parameter that combines the versions specified in the `@useDependency` decorators +- **Versioning Mutation**: Operations from each service are correctly mutated to the specified version, ensuring that only operations available at that version are included +- **Service Namespace**: The `service` property in the `@client` decorator specifies which service namespace serves as the primary service +- **Version Format**: The API versions are extracted from the `@useDependency` decorators and populated in the client's `apiVersions` array + ## Customizations Customizations SHOULD always be made in a file named `client.tsp` alongside `main.tsp`. From 9ed2b7795222d9676704e8744dc3afc68a811f3f Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 20 Nov 2025 16:19:23 -0500 Subject: [PATCH 20/20] update docs --- .../Generate client libraries/03client.mdx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/website/src/content/docs/docs/howtos/Generate client libraries/03client.mdx b/website/src/content/docs/docs/howtos/Generate client libraries/03client.mdx index d3a77827ad..af4f12cf93 100644 --- a/website/src/content/docs/docs/howtos/Generate client libraries/03client.mdx +++ b/website/src/content/docs/docs/howtos/Generate client libraries/03client.mdx @@ -706,17 +706,17 @@ namespace MultiService Widget = new WidgetClient(endpoint, "2.0", options); Gadget = new GadgetClient(endpoint, "1.1", options); } - + public virtual WidgetClient Widget { get; } public virtual GadgetClient Gadget { get; } } - + public partial class WidgetClient { public virtual async Task>> ListWidgetsAsync(RequestContext context = null) {} public virtual Response> ListWidgets(RequestContext context = null) {} } - + public partial class GadgetClient { public virtual async Task>> ListGadgetsAsync(RequestContext context = null) {} @@ -745,8 +745,8 @@ const gadgets = await client.gadget.listGadgets(); ``` ```java -@ServiceClientBuilder(serviceClients = { - MultiServiceClient.class, +@ServiceClientBuilder(serviceClients = { + MultiServiceClient.class, WidgetClient.class, GadgetClient.class, MultiServiceAsyncClient.class, @@ -755,7 +755,7 @@ const gadgets = await client.gadget.listGadgets(); }) public final class MultiServiceClientBuilder implements HttpTrait, ConfigurationTrait, EndpointTrait { - + public MultiServiceClientBuilder(); public MultiServiceClient buildClient(); } @@ -764,16 +764,16 @@ public final class MultiServiceClientBuilder implements HttpTrait