Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions packages/typespec-client-generator-core/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ model ClientOptions {
* The name of the client. If not specified, the default name will be `<Name of the target>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;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions packages/typespec-client-generator-core/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a bug from TypeSpec? This seems a hack to get back the operations that raw type graph does not have. If it only happens when having versioning, I doubt if it is a versioning mutator issue.

// 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
Expand Down
63 changes: 61 additions & 2 deletions packages/typespec-client-generator-core/src/clients.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -17,6 +24,7 @@ import {
SdkEndpointParameter,
SdkEndpointType,
SdkHttpOperation,
SdkMethodParameter,
SdkOperationGroup,
SdkPathParameter,
SdkServiceOperation,
Expand Down Expand Up @@ -190,6 +198,14 @@ export function createSdkClientType<TServiceOperation extends SdkServiceOperatio
name = override;
}
}
if (!parent && client.kind === "SdkClient" && client.parent) {
const parentRaw = context.__rawClientsOperationGroupsCache?.get(client.parent) as
| SdkClient
| undefined;
parent = context.__clientTypesCache?.find((c) => c.__raw === parentRaw) as
| SdkClientType<TServiceOperation>
| undefined;
}
Comment on lines +201 to +208
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't prefer to mix the client hierarchy build into the SDK type creation section. It's better to do it in the cache process.

const sdkClientType: SdkClientType<TServiceOperation> = {
__raw: client,
kind: "client",
Expand All @@ -213,7 +229,8 @@ export function createSdkClientType<TServiceOperation extends SdkServiceOperatio
);
addDefaultClientParameters(context, sdkClientType);
// update initialization model properties

context.__clientTypesCache = context.__clientTypesCache || [];
context.__clientTypesCache.push(sdkClientType);
return diagnostics.wrap(sdkClientType);
}

Expand All @@ -239,6 +256,48 @@ function addDefaultClientParameters<
if (apiVersionParam) break;
}
}

// Check for multi-service scenario with @useDependency
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering why the API parameter no longer poped up from the operation level's parameter for multi-service cases? I believe only the top-level client should have special logic.

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);
}
Expand Down
Loading
Loading