From cb7a145a5fe21145b72e8850f6292a3bda3a7c58 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 9 Dec 2025 13:32:46 -0500 Subject: [PATCH 1/2] Revert "[tcgc] one client multiple services (#3460)" This reverts commit a872ce22151e4cc95e79f796c7e6b5237c59a236. --- ...ultiServiceOneClient-2025-9-30-17-39-36.md | 7 - .../lib/decorators.tsp | 5 - .../src/cache.ts | 16 - .../src/clients.ts | 63 +--- .../src/context.ts | 211 ++---------- .../src/decorators.ts | 2 - .../src/example.ts | 2 +- .../src/interfaces.ts | 7 +- .../src/internal-utils.ts | 85 +---- .../typespec-client-generator-core/src/lib.ts | 6 - .../src/package.ts | 101 +----- .../src/public-utils.ts | 45 +-- .../test/clients/structure.test.ts | 315 ------------------ .../test/decorators/client.test.ts | 2 - .../test/package/versioning.test.ts | 14 +- .../test/utils.ts | 9 - .../test/validations/package.test.ts | 3 - .../Generate client libraries/03client.mdx | 277 --------------- .../reference/data-types.md | 9 +- 19 files changed, 52 insertions(+), 1127 deletions(-) delete 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 deleted file mode 100644 index f714d038ab..0000000000 --- a/.chronus/changes/tcgc-multiServiceOneClient-2025-9-30-17-39-36.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -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 diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index ba145e3c6c..fb6c0752ef 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -172,11 +172,6 @@ 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/cache.ts b/packages/typespec-client-generator-core/src/cache.ts index 74567628e0..30e32d8876 100644 --- a/packages/typespec-client-generator-core/src/cache.ts +++ b/packages/typespec-client-generator-core/src/cache.ts @@ -111,22 +111,6 @@ 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 2dc2dcbbf7..7ff059339a 100644 --- a/packages/typespec-client-generator-core/src/clients.ts +++ b/packages/typespec-client-generator-core/src/clients.ts @@ -1,13 +1,6 @@ -import { - createDiagnosticCollector, - Diagnostic, - getDoc, - getSummary, - listServices, -} from "@typespec/compiler"; +import { createDiagnosticCollector, Diagnostic, getDoc, getSummary } from "@typespec/compiler"; import { $ } from "@typespec/compiler/typekit"; import { getServers, HttpServer } from "@typespec/http"; -import { getVersionDependencies } from "@typespec/versioning"; import { getClientInitializationOptions, getClientNameOverride, @@ -24,7 +17,6 @@ import { SdkEndpointParameter, SdkEndpointType, SdkHttpOperation, - SdkMethodParameter, SdkOperationGroup, SdkPathParameter, SdkServiceOperation, @@ -199,14 +191,6 @@ export function createSdkClientType c.__raw === parentRaw) as - | SdkClientType - | undefined; - } const sdkClientType: SdkClientType = { __raw: client, kind: "client", @@ -230,8 +214,7 @@ export function createSdkClientType 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 23226b82a2..5eaf70af12 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -2,6 +2,7 @@ import { createDiagnosticCollector, EmitContext, emitFile, + Enum, Interface, listServices, Model, @@ -14,7 +15,7 @@ import { Union, } from "@typespec/compiler"; import { HttpOperation } from "@typespec/http"; -import { getVersionDependencies, getVersions } from "@typespec/versioning"; +import { getVersions } from "@typespec/versioning"; import { stringify } from "yaml"; import { prepareClientAndOperationCache } from "./cache.js"; import { defaultDecoratorsAllowList } from "./configs.js"; @@ -47,51 +48,12 @@ 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 } -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, @@ -139,59 +101,6 @@ 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 { @@ -204,116 +113,50 @@ export function createTCGCContext( } this.__tspTypeToApiVersions.set(type, mergedApiVersions); }, - getApiVersions(service?: Namespace): string[] { - if (!this.__serviceToVersions) { - this.__serviceToVersions = new Map(); + getPackageVersions(): string[] { + if (this.__packageVersions) { + return this.__packageVersions; } - - // If no service specified, try to get from undefined key (global) or the first service + const service = listServices(program)[0]; if (!service) { - // Check if we have global versions cached (undefined key) - const globalVersions = this.__serviceToVersions.get(undefined); - 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) { - service = firstService; - } else { - return []; - } + this.__packageVersions = []; + return this.__packageVersions; } - // Check cache for this specific service - const cachedVersions = this.__serviceToVersions.get(service); - if (cachedVersions?.length) { - return cachedVersions; - } - - const versions = getVersions(program, service)[1]?.getVersions(); + const versions = getVersions(program, service.type)[1]?.getVersions(); if (!versions) { - return []; + this.__packageVersions = []; + return this.__packageVersions; } removeVersionsLargerThanExplicitlySpecified(this, versions); - 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); - } + this.__packageVersions = versions.map((version) => version.value); if ( this.apiVersion !== undefined && this.apiVersion !== "latest" && this.apiVersion !== "all" && - !serviceVersions.includes(this.apiVersion) + !this.__packageVersions.includes(this.apiVersion) ) { reportDiagnostic(this.program, { code: "api-version-undefined", format: { version: this.apiVersion }, - target: service, + target: service.type, }); - this.apiVersion = serviceVersions[serviceVersions.length - 1]; + this.apiVersion = this.__packageVersions[this.__packageVersions.length - 1]; + } + return this.__packageVersions; + }, + getPackageVersionEnum(): Enum | undefined { + if (this.__packageVersionEnum) { + return this.__packageVersionEnum; + } + const namespaces = listAllServiceNamespaces(this); + if (namespaces.length === 0) { + return undefined; } - return serviceVersions; + return getVersions(this.program, namespaces[0])[1]?.getVersions()?.[0].enumMember.enum; }, getClients(): SdkClient[] { if (!this.__rawClientsOperationGroupsCache) { diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index a95c1af1cb..873f93b8b7 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -177,7 +177,6 @@ export const $client: ClientDecorator = ( options?.kind === "Model" ? options?.properties.get("name")?.type : undefined; const name: string = explicitName?.kind === "String" ? explicitName.value : target.name; let service = options?.kind === "Model" ? options?.properties.get("service")?.type : undefined; - const parent = options?.kind === "Model" ? options?.properties.get("parent")?.type : undefined; if (service?.kind !== "Namespace") { service = findClientService(context.program, target); @@ -203,7 +202,6 @@ export const $client: ClientDecorator = ( type: target, crossLanguageDefinitionId: `${getNamespaceFullName(service)}.${name}`, subOperationGroups: [], - parent: parent as Namespace | Interface | undefined, }; setScopedDecoratorData(context, $client, clientKey, target, client, scope); }; diff --git a/packages/typespec-client-generator-core/src/example.ts b/packages/typespec-client-generator-core/src/example.ts index bfc6d41dfb..ed357fe73b 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.getApiVersions(); + const packageVersions = context.getPackageVersions(); 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 178e83d929..b18fa9134d 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -67,21 +67,21 @@ export interface TCGCContext { SdkClient | SdkOperationGroup >; __clientToOperationsCache?: Map; - __clientTypesCache?: SdkClientType[]; __operationToClientCache?: Map; __clientParametersCache: Map; __clientApiVersionDefaultValueCache: Map; __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 - __serviceToVersions?: Map; // the package versions from the service versioning config and api version setting in tspconfig. + __packageVersions?: string[]; // 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; getMutatedGlobalNamespace(): Namespace; getApiVersionsForType(type: Type): string[]; setApiVersionsForType(type: Type, apiVersions: string[]): void; - getApiVersions(service?: Namespace): string[]; + getPackageVersions(): string[]; + getPackageVersionEnum(): Enum | undefined; getClients(): SdkClient[]; getClientOrOperationGroup(type: Namespace | Interface): SdkClient | SdkOperationGroup | undefined; getOperationsForClient(client: SdkClient | SdkOperationGroup): Operation[]; @@ -105,7 +105,6 @@ 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/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 8bf9606627..e57a8d1e6e 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -41,7 +41,6 @@ import { HttpOperation, HttpOperationResponseContent, HttpPayloadBody } from "@t import { getAddedOnVersions, getRemovedOnVersions, - getVersionDependencies, getVersioningMutators, getVersions, } from "@typespec/versioning"; @@ -289,8 +288,7 @@ export function getAvailableApiVersions( context.setApiVersionsForType(type, explicitlyDecorated); return explicitlyDecorated; } - // If no explicit decorators, use the calculated apiVersions instead of just wrapperApiVersions - context.setApiVersionsForType(type, apiVersions); + context.setApiVersionsForType(type, wrapperApiVersions); return context.getApiVersionsForType(type); } @@ -761,89 +759,12 @@ function getVersioningMutator( export function handleVersioningMutationForGlobalNamespace(context: TCGCContext): Namespace { const globalNamespace = context.program.getGlobalNamespaceType(); - const allApiVersions = context.getApiVersions(); + const allApiVersions = context.getPackageVersions(); if (allApiVersions.length === 0 || context.apiVersion === "all") return 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, - targetService, + listServices(context.program)[0].type, 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 35c024c51a..1adec5fe61 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -452,12 +452,6 @@ 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.`, }, }, - "multiple-services-require-use-dependency": { - severity: "error", - messages: { - 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"}`, - }, - }, }, emitter: { options: TCGCEmitterOptionsSchema, diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index f4d98926c4..60af13ed56 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -1,10 +1,4 @@ -import { - createDiagnosticCollector, - Diagnostic, - ignoreDiagnostics, - listServices, -} from "@typespec/compiler"; -import { getVersionDependencies } from "@typespec/versioning"; +import { createDiagnosticCollector, Diagnostic, ignoreDiagnostics } from "@typespec/compiler"; import { prepareClientAndOperationCache } from "./cache.js"; import { createSdkClientType } from "./clients.js"; import { listClients } from "./decorators.js"; @@ -33,9 +27,9 @@ export function createSdkPackage( diagnostics.pipe(handleAllTypes(context)); const crossLanguagePackageId = diagnostics.pipe(getCrossLanguagePackageId(context)); const allReferencedTypes = getAllReferencedTypes(context); - const versions = context.getApiVersions(); + const versions = context.getPackageVersions(); const sdkPackage: SdkPackage = { - clients: diagnostics.pipe(createClients(context)), + clients: listClients(context).map((c) => diagnostics.pipe(createSdkClientType(context, c))), models: allReferencedTypes.filter((x): x is SdkModelType => x.kind === "model"), enums: allReferencedTypes.filter((x): x is SdkEnumType => x.kind === "enum"), unions: allReferencedTypes.filter( @@ -52,47 +46,6 @@ 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, @@ -161,58 +114,12 @@ 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, - apiVersions, + context.getPackageVersions(), ), ); diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index b43892beb1..845cd6af63 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -16,7 +16,6 @@ import { ignoreDiagnostics, isGlobalNamespace, isService, - listServices, resolveEncodedName, } from "@typespec/compiler"; import { @@ -90,48 +89,6 @@ export function getDefaultApiVersion( } } -function getVersionEnumForService(context: TCGCContext, type: ModelProperty): Enum | undefined { - if (context.__packageVersionEnum) { - return context.__packageVersionEnum; - } - - // Try to find the service from the model property's namespace hierarchy - // 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 namespace (could be service or library) - while (namespace) { - const versions = getVersions(context.program, namespace)[1]?.getVersions(); - if (versions?.length) { - const versionEnum = versions[0].enumMember.enum; - context.__packageVersionEnum = versionEnum; - return versionEnum; - } - 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; -} - /** * Return whether a parameter is the Api Version parameter of a client * @param program @@ -145,7 +102,7 @@ export function isApiVersion(context: TCGCContext, type: ModelProperty): boolean return override; } // if the service is not versioning, then no api version parameter - const versionEnum = getVersionEnumForService(context, type); + const versionEnum = context.getPackageVersionEnum(); 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 21eb00dd09..1d0965ff7f 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -762,318 +762,3 @@ 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(@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.av2, ServiceB.VersionsB.bv2) - 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 = 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, 2); - deepStrictEqual(client.apiVersions, ["av2", "bv2"]); - strictEqual(client.children!.length, 2); - 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); - - // 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"); - - // 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.parameters.length, 0); - const aiOperation = aiMethod.operation; - strictEqual(aiOperation.parameters.length, 1); - const aiOperationApiVersionParam = aiOperation.parameters.find((p) => p.isApiVersionParam); - ok(aiOperationApiVersionParam); - 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"); - - // 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.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 - @useDependency( - ServiceA.VersionsA.av3, - ServiceB.VersionsB.bv2 - ) - 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"); - - // 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); - - 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 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/decorators/client.test.ts b/packages/typespec-client-generator-core/test/decorators/client.test.ts index bff9650d44..092805dbf5 100644 --- a/packages/typespec-client-generator-core/test/decorators/client.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/client.test.ts @@ -44,7 +44,6 @@ describe("@client", () => { deepStrictEqual(clients, [ { kind: "SdkClient", - parent: undefined, name: "MyClient", service: MyClient, type: MyClient, @@ -67,7 +66,6 @@ describe("@client", () => { { kind: "SdkClient", name: "MyClient", - parent: undefined, service: MyService, type: MyClient, crossLanguageDefinitionId: "MyService.MyClient", 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 c7940e1606..dd749132cd 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.getApiVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["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.getApiVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["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.getApiVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["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.getApiVersions(), ["v1", "v2"]); + deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["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.getApiVersions(), ["v1"]); + deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["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.getApiVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["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.getApiVersions(), ["v1", "v2", "v3"]); + deepStrictEqual(runnerWithVersion.context.getPackageVersions(), ["v1", "v2", "v3"]); strictEqual(sdkPackage.clients.length, 1); const apiVersionParam = sdkPackage.clients[0].clientInitialization.parameters.find( diff --git a/packages/typespec-client-generator-core/test/utils.ts b/packages/typespec-client-generator-core/test/utils.ts index b99375f3d9..c1bac7afc9 100644 --- a/packages/typespec-client-generator-core/test/utils.ts +++ b/packages/typespec-client-generator-core/test/utils.ts @@ -1,4 +1,3 @@ -import { isService } from "@typespec/compiler"; import { strictEqual } from "assert"; import { SdkClientType, @@ -59,13 +58,5 @@ export function getServiceMethodOfClient( } export function getServiceNamespace(runner: SdkTestRunner) { - // 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)) { - 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 5ae903db7b..3c94778f87 100644 --- a/packages/typespec-client-generator-core/test/validations/package.test.ts +++ b/packages/typespec-client-generator-core/test/validations/package.test.ts @@ -28,9 +28,6 @@ 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); 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 af4f12cf93..f8c3c40927 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,283 +590,6 @@ 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`. 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 1fee06f2c9..49c8521699 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,11 +31,10 @@ 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`. | -| parent? | `Namespace \| Interface` | The parent client of this client. If specified, this client will be generated as a sub client of the parent 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`. | ### `ExternalType` {#Azure.ClientGenerator.Core.ExternalType} From 32024d0a7eb6f53cf585d1ecd7349c2c38c04577 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 9 Dec 2025 13:39:17 -0500 Subject: [PATCH 2/2] add changeset --- .../tcgc-revertMultiServiceCurrent-2025-11-9-13-38-59.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/tcgc-revertMultiServiceCurrent-2025-11-9-13-38-59.md diff --git a/.chronus/changes/tcgc-revertMultiServiceCurrent-2025-11-9-13-38-59.md b/.chronus/changes/tcgc-revertMultiServiceCurrent-2025-11-9-13-38-59.md new file mode 100644 index 0000000000..a5d6097b48 --- /dev/null +++ b/.chronus/changes/tcgc-revertMultiServiceCurrent-2025-11-9-13-38-59.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Roll back multi-service change \ No newline at end of file