diff --git a/.chronus/changes/add_client_initialization-2024-7-19-14-44-30.md b/.chronus/changes/add_client_initialization-2024-7-19-14-44-30.md new file mode 100644 index 0000000000..0a659af40b --- /dev/null +++ b/.chronus/changes/add_client_initialization-2024-7-19-14-44-30.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +add `@clientInitialization` decorator \ No newline at end of file diff --git a/docs/libraries/typespec-client-generator-core/reference/decorators.md b/docs/libraries/typespec-client-generator-core/reference/decorators.md index 45ed5de2d2..43dc89d70f 100644 --- a/docs/libraries/typespec-client-generator-core/reference/decorators.md +++ b/docs/libraries/typespec-client-generator-core/reference/decorators.md @@ -201,6 +201,45 @@ interface MyInterface {} interface MyInterface {} ``` +### `@clientInitialization` {#@Azure.ClientGenerator.Core.clientInitialization} + +Client parameters you would like to add to the client. By default, we apply endpoint, credential, and api-version parameters. If you add clientInitialization, we will append those to the default list of parameters. + +```typespec +@Azure.ClientGenerator.Core.clientInitialization(options: Model, scope?: valueof string) +``` + +#### Target + +`Namespace | Interface` + +#### Parameters + +| Name | Type | Description | +| ------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | +| options | `Model` | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | + +#### Examples + +```typespec +// main.tsp +namespace MyService; + +op upload(blobName: string): void; +op download(blobName: string): void; + +// client.tsp +namespace MyCustomizations; +model MyServiceClientOptions { + blobName: string; +} + +@@clientInitialization(MyService, MyServiceClientOptions) +// The generated client will have `blobName` on it. We will also +// elevate the existing `blobName` parameter to the client level. +``` + ### `@clientName` {#@Azure.ClientGenerator.Core.clientName} Changes the name of a method, parameter, property, or model generated in the client SDK @@ -378,6 +417,46 @@ op myOperationCustomization(params: Params): void; // method signature is now `op myOperation(params: Params)` just for csharp ``` +### `@paramAlias` {#@Azure.ClientGenerator.Core.paramAlias} + +Alias the name of a client parameter to a different name. This permits you to have a different name for the parameter in client initialization then on individual methods and still refer to the same parameter. + +```typespec +@Azure.ClientGenerator.Core.paramAlias(paramAlias: valueof string, scope?: valueof string) +``` + +#### Target + +`ModelProperty` + +#### Parameters + +| Name | Type | Description | +| ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | +| paramAlias | `valueof string` | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | + +#### Examples + +```typespec +// main.tsp +namespace MyService; + +op upload(blobName: string): void; + +// client.tsp +namespace MyCustomizations; +model MyServiceClientOptions { + blob: string; +} + +@@clientInitialization(MyService, MyServiceClientOptions) +@@paramAlias(MyServiceClientOptions.blob, "blobName") + +// The generated client will have `blobName` on it. We will also +// elevate the existing `blob` parameter to the client level. +``` + ### `@protocolAPI` {#@Azure.ClientGenerator.Core.protocolAPI} Whether you want to generate an operation as a protocol operation. diff --git a/docs/libraries/typespec-client-generator-core/reference/index.mdx b/docs/libraries/typespec-client-generator-core/reference/index.mdx index 5e3f1ac90c..7d32ab66b3 100644 --- a/docs/libraries/typespec-client-generator-core/reference/index.mdx +++ b/docs/libraries/typespec-client-generator-core/reference/index.mdx @@ -41,11 +41,13 @@ npm install --save-peer @azure-tools/typespec-client-generator-core - [`@access`](./decorators.md#@Azure.ClientGenerator.Core.access) - [`@client`](./decorators.md#@Azure.ClientGenerator.Core.client) +- [`@clientInitialization`](./decorators.md#@Azure.ClientGenerator.Core.clientInitialization) - [`@clientName`](./decorators.md#@Azure.ClientGenerator.Core.clientName) - [`@convenientAPI`](./decorators.md#@Azure.ClientGenerator.Core.convenientAPI) - [`@flattenProperty`](./decorators.md#@Azure.ClientGenerator.Core.flattenProperty) - [`@operationGroup`](./decorators.md#@Azure.ClientGenerator.Core.operationGroup) - [`@override`](./decorators.md#@Azure.ClientGenerator.Core.override) +- [`@paramAlias`](./decorators.md#@Azure.ClientGenerator.Core.paramAlias) - [`@protocolAPI`](./decorators.md#@Azure.ClientGenerator.Core.protocolAPI) - [`@usage`](./decorators.md#@Azure.ClientGenerator.Core.usage) - [`@useSystemTextJsonConverter`](./decorators.md#@Azure.ClientGenerator.Core.useSystemTextJsonConverter) diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index df340c12db..d3d6ea98ea 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -14,11 +14,13 @@ npm install @azure-tools/typespec-client-generator-core - [`@access`](#@access) - [`@client`](#@client) +- [`@clientInitialization`](#@clientinitialization) - [`@clientName`](#@clientname) - [`@convenientAPI`](#@convenientapi) - [`@flattenProperty`](#@flattenproperty) - [`@operationGroup`](#@operationgroup) - [`@override`](#@override) +- [`@paramAlias`](#@paramalias) - [`@protocolAPI`](#@protocolapi) - [`@usage`](#@usage) - [`@useSystemTextJsonConverter`](#@usesystemtextjsonconverter) @@ -216,6 +218,45 @@ interface MyInterface {} interface MyInterface {} ``` +#### `@clientInitialization` + +Client parameters you would like to add to the client. By default, we apply endpoint, credential, and api-version parameters. If you add clientInitialization, we will append those to the default list of parameters. + +```typespec +@Azure.ClientGenerator.Core.clientInitialization(options: Model, scope?: valueof string) +``` + +##### Target + +`Namespace | Interface` + +##### Parameters + +| Name | Type | Description | +| ------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | +| options | `Model` | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | + +##### Examples + +```typespec +// main.tsp +namespace MyService; + +op upload(blobName: string): void; +op download(blobName: string): void; + +// client.tsp +namespace MyCustomizations; +model MyServiceClientOptions { + blobName: string; +} + +@@clientInitialization(MyService, MyServiceClientOptions) +// The generated client will have `blobName` on it. We will also +// elevate the existing `blobName` parameter to the client level. +``` + #### `@clientName` Changes the name of a method, parameter, property, or model generated in the client SDK @@ -391,6 +432,46 @@ op myOperationCustomization(params: Params): void; // method signature is now `op myOperation(params: Params)` just for csharp ``` +#### `@paramAlias` + +Alias the name of a client parameter to a different name. This permits you to have a different name for the parameter in client initialization then on individual methods and still refer to the same parameter. + +```typespec +@Azure.ClientGenerator.Core.paramAlias(paramAlias: valueof string, scope?: valueof string) +``` + +##### Target + +`ModelProperty` + +##### Parameters + +| Name | Type | Description | +| ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | +| paramAlias | `valueof string` | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | + +##### Examples + +```typespec +// main.tsp +namespace MyService; + +op upload(blobName: string): void; + +// client.tsp +namespace MyCustomizations; +model MyServiceClientOptions { + blob: string; +} + +@@clientInitialization(MyService, MyServiceClientOptions) +@@paramAlias(MyServiceClientOptions.blob, "blobName") + +// The generated client will have `blobName` on it. We will also +// elevate the existing `blob` parameter to the client level. +``` + #### `@protocolAPI` Whether you want to generate an operation as a protocol operation. diff --git a/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts b/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts index 67391cb3c9..f518209de8 100644 --- a/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts +++ b/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts @@ -433,6 +433,67 @@ export type UseSystemTextJsonConverterDecorator = ( scope?: string ) => void; +/** + * Client parameters you would like to add to the client. By default, we apply endpoint, credential, and api-version parameters. If you add clientInitialization, we will append those to the default list of parameters. + * + * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * @example + * ```typespec + * // main.tsp + * namespace MyService; + * + * op upload(blobName: string): void; + * op download(blobName: string): void; + * + * // client.tsp + * namespace MyCustomizations; + * model MyServiceClientOptions { + * blobName: string; + * } + * + * @@clientInitialization(MyService, MyServiceClientOptions) + * // The generated client will have `blobName` on it. We will also + * // elevate the existing `blobName` parameter to the client level. + * ``` + */ +export type ClientInitializationDecorator = ( + context: DecoratorContext, + target: Namespace | Interface, + options: Model, + scope?: string +) => void; + +/** + * Alias the name of a client parameter to a different name. This permits you to have a different name for the parameter in client initialization then on individual methods and still refer to the same parameter. + * + * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * @example + * ```typespec + * // main.tsp + * namespace MyService; + * + * op upload(blobName: string): void; + * + * // client.tsp + * namespace MyCustomizations; + * model MyServiceClientOptions { + * blob: string; + * } + * + * @@clientInitialization(MyService, MyServiceClientOptions) + * @@paramAlias(MyServiceClientOptions.blob, "blobName") + * + * // The generated client will have `blobName` on it. We will also + * // elevate the existing `blob` parameter to the client level. + * ``` + */ +export type ParamAliasDecorator = ( + context: DecoratorContext, + original: ModelProperty, + paramAlias: string, + scope?: string +) => void; + export type AzureClientGeneratorCoreDecorators = { clientName: ClientNameDecorator; convenientAPI: ConvenientAPIDecorator; @@ -444,4 +505,6 @@ export type AzureClientGeneratorCoreDecorators = { flattenProperty: FlattenPropertyDecorator; override: OverrideDecorator; useSystemTextJsonConverter: UseSystemTextJsonConverterDecorator; + clientInitialization: ClientInitializationDecorator; + paramAlias: ParamAliasDecorator; }; diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 4b80bfe611..45435b5469 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -419,3 +419,58 @@ extern dec override(original: Operation, override: Operation, scope?: valueof st * ``` */ extern dec useSystemTextJsonConverter(target: Model, scope?: valueof string); + +/** + * Client parameters you would like to add to the client. By default, we apply endpoint, credential, and api-version parameters. If you add clientInitialization, we will append those to the default list of parameters. + * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * + * @example + * ```typespec + * // main.tsp + * namespace MyService; + * + * op upload(blobName: string): void; + * op download(blobName: string): void; + * + * // client.tsp + * namespace MyCustomizations; + * model MyServiceClientOptions { + * blobName: string; + * } + * + * @@clientInitialization(MyService, MyServiceClientOptions) + * // The generated client will have `blobName` on it. We will also + * // elevate the existing `blobName` parameter to the client level. + * ``` + */ +extern dec clientInitialization( + target: Namespace | Interface, + options: Model, + scope?: valueof string +); + +/** + * Alias the name of a client parameter to a different name. This permits you to have a different name for the parameter in client initialization then on individual methods and still refer to the same parameter. + * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * + * @example + * ```typespec + * // main.tsp + * namespace MyService; + * + * op upload(blobName: string): void; + * + * // client.tsp + * namespace MyCustomizations; + * model MyServiceClientOptions { + * blob: string; + * } + * + * @@clientInitialization(MyService, MyServiceClientOptions) + * @@paramAlias(MyServiceClientOptions.blob, "blobName") + * + * // The generated client will have `blobName` on it. We will also + * // elevate the existing `blob` parameter to the client level. + * ``` + */ +extern dec paramAlias(original: ModelProperty, paramAlias: valueof string, scope?: valueof string); diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 4a4ec69e2a..9c523755db 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -34,10 +34,12 @@ import { buildVersionProjections, getVersions } from "@typespec/versioning"; import { AccessDecorator, ClientDecorator, + ClientInitializationDecorator, ClientNameDecorator, ConvenientAPIDecorator, FlattenPropertyDecorator, OperationGroupDecorator, + ParamAliasDecorator, ProtocolAPIDecorator, UsageDecorator, } from "../generated-defs/Azure.ClientGenerator.Core.js"; @@ -50,6 +52,9 @@ import { SdkContext, SdkEmitterOptions, SdkHttpOperation, + SdkInitializationType, + SdkMethodParameter, + SdkModelPropertyType, SdkOperationGroup, SdkServiceOperation, TCGCContext, @@ -608,9 +613,9 @@ export function createTCGCContext(program: Program, emitterName: string): TCGCCo emitterName: diagnostics.pipe(parseEmitterName(program, emitterName)), diagnostics: diagnostics.diagnostics, originalProgram: program, - __namespaceToApiVersionParameter: new Map(), + __clientToParameters: new Map(), __tspTypeToApiVersions: new Map(), - __namespaceToApiVersionClientDefaultValue: new Map(), + __clientToApiVersionClientDefaultValue: new Map(), previewStringRegex: /-preview$/, }; } @@ -989,3 +994,56 @@ export const $useSystemTextJsonConverter: DecoratorFunction = ( entity: Model, scope?: LanguageScopes ) => {}; + +const clientInitializationKey = createStateSymbol("clientInitialization"); + +export const $clientInitialization: ClientInitializationDecorator = ( + context: DecoratorContext, + target: Namespace | Interface, + options: Model, + scope?: LanguageScopes +) => { + setScopedDecoratorData( + context, + $clientInitialization, + clientInitializationKey, + target, + options, + scope + ); +}; + +export function getClientInitialization( + context: TCGCContext, + entity: Namespace | Interface +): SdkInitializationType | undefined { + const model = getScopedDecoratorData(context, clientInitializationKey, entity); + if (!model) return model; + const sdkModel = getSdkModel(context, model); + const initializationProps = sdkModel.properties.map( + (property: SdkModelPropertyType): SdkMethodParameter => { + property.onClient = true; + property.kind = "method"; + return property as SdkMethodParameter; + } + ); + return { + ...sdkModel, + properties: initializationProps, + }; +} + +const paramAliasKey = createStateSymbol("paramAlias"); + +export const paramAliasDecorator: ParamAliasDecorator = ( + context: DecoratorContext, + original: ModelProperty, + paramAlias: string, + scope?: LanguageScopes +) => { + setScopedDecoratorData(context, paramAliasDecorator, paramAliasKey, original, paramAlias, scope); +}; + +export function getParamAlias(context: TCGCContext, original: ModelProperty): string | undefined { + return getScopedDecoratorData(context, paramAliasKey, original); +} diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 2297b183dd..cbfb369272 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -27,6 +27,7 @@ import { isQueryParam, } from "@typespec/http"; import { camelCase } from "change-case"; +import { getParamAlias } from "./decorators.js"; import { CollectionFormat, SdkBodyParameter, @@ -56,9 +57,14 @@ import { isHttpBodySpread, isNeverOrVoidType, isSubscriptionId, + twoParamsEquivalent, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; -import { getCrossLanguageDefinitionId, getEffectivePayloadType } from "./public-utils.js"; +import { + getCrossLanguageDefinitionId, + getEffectivePayloadType, + isApiVersion, +} from "./public-utils.js"; import { addEncodeInfo, addFormatInfo, @@ -495,9 +501,22 @@ export function getCorrespondingMethodParams( ): [SdkModelPropertyType[], readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const operationLocation = getLocationOfOperation(operation); + const operationLocation = getLocationOfOperation(operation)!; + let clientParams = context.__clientToParameters.get(operationLocation); + if (!clientParams) { + clientParams = []; + context.__clientToParameters.set(operationLocation, clientParams); + } + + const correspondingClientParams = clientParams.filter( + (x) => + twoParamsEquivalent(context, x.__raw, serviceParam.__raw) || + (x.__raw?.kind === "ModelProperty" && getParamAlias(context, x.__raw) === serviceParam.name) + ); + if (correspondingClientParams.length > 0) return diagnostics.wrap(correspondingClientParams); + if (serviceParam.isApiVersionParam) { - const existingApiVersion = context.__namespaceToApiVersionParameter.get(operationLocation); + const existingApiVersion = clientParams?.find((x) => isApiVersion(context, x)); if (!existingApiVersion) { diagnostics.add( createDiagnostic({ @@ -511,10 +530,11 @@ export function getCorrespondingMethodParams( ); return diagnostics.wrap([]); } - return diagnostics.wrap([context.__namespaceToApiVersionParameter.get(operationLocation)!]); + return diagnostics.wrap(clientParams.filter((x) => isApiVersion(context, x))); } if (isSubscriptionId(context, serviceParam)) { - if (!context.__subscriptionIdParameter) { + const subId = clientParams.find((x) => isSubscriptionId(context, x)); + if (!subId) { diagnostics.add( createDiagnostic({ code: "no-corresponding-method-param", @@ -527,7 +547,7 @@ export function getCorrespondingMethodParams( ); return diagnostics.wrap([]); } - return diagnostics.wrap([context.__subscriptionIdParameter]); + return diagnostics.wrap(subId ? [subId] : []); } // to see if the service parameter is a method parameter or a property of a method parameter diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index b5174d24d9..a60b2bd3be 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -38,12 +38,11 @@ export interface TCGCContext { generatedNames?: Map; httpOperationCache?: Map; unionsMap?: Map; - __namespaceToApiVersionParameter: Map; + __clientToParameters: Map; __tspTypeToApiVersions: Map; - __namespaceToApiVersionClientDefaultValue: Map; + __clientToApiVersionClientDefaultValue: Map; knownScalars?: Record; diagnostics: readonly Diagnostic[]; - __subscriptionIdParameter?: SdkParameter; __rawClients?: SdkClient[]; apiVersion?: string; __service_projection?: Map; @@ -84,12 +83,9 @@ export interface SdkClient { crossLanguageDefinitionId: string; } -export interface SdkInitializationType extends SdkModelType { - properties: SdkParameter[]; -} - export interface SdkClientType extends DecoratedType { + __raw: SdkClient | SdkOperationGroup; kind: "client"; name: string; /** @@ -382,6 +378,10 @@ export interface SdkModelType extends SdkTypeBase { apiVersions: string[]; } +export interface SdkInitializationType extends SdkModelType { + properties: SdkParameter[]; +} + export interface SdkCredentialType extends SdkTypeBase { kind: "credential"; scheme: HttpAuth; diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 4a79e592d8..22b9d6fe40 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -31,6 +31,7 @@ import { HttpStatusCodeRange, } from "@typespec/http"; import { getAddedOnVersions, getRemovedOnVersions, getVersions } from "@typespec/versioning"; +import { getParamAlias } from "./decorators.js"; import { DecoratorInfo, SdkBuiltInType, @@ -118,16 +119,14 @@ export function updateWithApiVersionInformation( ): { isApiVersionParam: boolean; clientDefaultValue?: unknown; - onClient: boolean; } { const isApiVersionParam = isApiVersion(context, type); return { isApiVersionParam, clientDefaultValue: isApiVersionParam && namespace - ? context.__namespaceToApiVersionClientDefaultValue.get(namespace) + ? context.__clientToApiVersionClientDefaultValue.get(namespace) : undefined, - onClient: onClient(context, type), }; } @@ -443,10 +442,6 @@ export function isSubscriptionId(context: TCGCContext, parameter: { name: string return Boolean(context.arm) && parameter.name === "subscriptionId"; } -export function onClient(context: TCGCContext, parameter: { name: string }): boolean { - return isSubscriptionId(context, parameter) || isApiVersion(context, parameter); -} - export function getLocationOfOperation(operation: Operation): Namespace | Interface { // have to check interface first, because interfaces are more granular than namespaces return (operation.interface || operation.namespace)!; @@ -533,6 +528,20 @@ export function isXmlContentType(contentType: string): boolean { return regex.test(contentType); } +export function twoParamsEquivalent( + context: TCGCContext, + param1?: ModelProperty, + param2?: ModelProperty +): boolean { + if (!param1 || !param2) { + return false; + } + return ( + param1.name === param2.name || + getParamAlias(context, param1) === param2.name || + param1.name === getParamAlias(context, param2) + ); +} /** * If body is from spread, then it does not directly from a model property. * @param httpBody @@ -572,3 +581,17 @@ export function getHttpBodySpreadModel(context: TCGCContext, type: Model): Model } return type; } + +export function isOnClient(context: TCGCContext, type: ModelProperty): boolean { + const namespace = type.model?.namespace; + return ( + isSubscriptionId(context, type) || + isApiVersion(context, type) || + Boolean( + namespace && + context.__clientToParameters + .get(namespace) + ?.find((x) => twoParamsEquivalent(context, x.__raw, type)) + ) + ); +} diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 1e19bbe886..e6038d0c82 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -15,6 +15,7 @@ import { resolveVersions } from "@typespec/versioning"; import { camelCase } from "change-case"; import { getAccess, + getClientInitialization, getClientNameOverride, getOverriddenClientMethod, listClients, @@ -30,6 +31,7 @@ import { SdkEndpointParameter, SdkEndpointType, SdkEnumType, + SdkHttpOperation, SdkInitializationType, SdkLroPagingServiceMethod, SdkLroServiceMethod, @@ -226,11 +228,14 @@ function getSdkBasicServiceMethod const methodParameters: SdkMethodParameter[] = []; // we have to calculate apiVersions first, so that the information is put // in __tspTypeToApiVersions before we call parameters since method wraps parameter - const apiVersions = getAvailableApiVersions( - context, - operation, - getLocationOfOperation(operation) - ); + const operationLocation = getLocationOfOperation(operation); + const apiVersions = getAvailableApiVersions(context, operation, operationLocation); + + let clientParams = context.__clientToParameters.get(operationLocation); + if (!clientParams) { + clientParams = []; + context.__clientToParameters.set(operationLocation, clientParams); + } const override = getOverriddenClientMethod(context, operation); const params = (override ?? operation).parameters.properties.values(); @@ -241,11 +246,19 @@ function getSdkBasicServiceMethod if (sdkMethodParam.onClient) { const operationLocation = getLocationOfOperation(operation); if (sdkMethodParam.isApiVersionParam) { - if (!context.__namespaceToApiVersionParameter.has(operationLocation)) { - context.__namespaceToApiVersionParameter.set(operationLocation, sdkMethodParam); + if ( + !context.__clientToParameters.get(operationLocation)?.find((x) => x.isApiVersionParam) + ) { + clientParams.push(sdkMethodParam); } } else if (isSubscriptionId(context, param)) { - context.__subscriptionIdParameter = sdkMethodParam; + if ( + !context.__clientToParameters + .get(operationLocation) + ?.find((x) => isSubscriptionId(context, x)) + ) { + clientParams.push(sdkMethodParam); + } } } else { methodParameters.push(sdkMethodParam); @@ -336,46 +349,37 @@ function getSdkInitializationType( client: SdkClient | SdkOperationGroup ): [SdkInitializationType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const credentialParam = getSdkCredentialParameter(context, client); - const properties: SdkParameter[] = [ - diagnostics.pipe(getSdkEndpointParameter(context, client)), // there will always be an endpoint parameter - ]; - if (credentialParam) { - properties.push(credentialParam); + let initializationModel = getClientInitialization(context, client.type); + let clientParams = context.__clientToParameters.get(client.type); + if (!clientParams) { + clientParams = []; + context.__clientToParameters.set(client.type, clientParams); } - let apiVersionParam = context.__namespaceToApiVersionParameter.get(client.type); - if (!apiVersionParam) { - for (const operationGroup of listOperationGroups(context, client)) { - // if any sub operation groups have an api version param, the top level needs - // the api version param as well - apiVersionParam = context.__namespaceToApiVersionParameter.get(operationGroup.type); - if (apiVersionParam) break; + const access = client.kind === "SdkClient" ? "public" : "internal"; + if (initializationModel) { + for (const prop of initializationModel.properties) { + clientParams.push(prop); } + initializationModel.access = access; + } else { + const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath; + const name = `${namePrefix.split(".").at(-1)}Options`; + initializationModel = { + __raw: client.service, + doc: "Initialization class for the client", + kind: "model", + properties: [], + name, + isGeneratedName: true, + access, + usage: UsageFlags.Input, + crossLanguageDefinitionId: `${getNamespaceFullName(client.service.namespace!)}.${name}`, + apiVersions: context.__tspTypeToApiVersions.get(client.type)!, + decorators: [], + }; } - if (apiVersionParam) { - properties.push(apiVersionParam); - } - if (context.__subscriptionIdParameter) { - properties.push(context.__subscriptionIdParameter); - } - const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath; - const name = `${namePrefix.split(".").at(-1)}Options`; - return diagnostics.wrap({ - __raw: client.service, - description: "Initialization class for the client", - doc: "Initialization class for the client", - kind: "model", - properties, - name, - isGeneratedName: true, - access: client.kind === "SdkClient" ? "public" : "internal", - usage: UsageFlags.Input, - crossLanguageDefinitionId: `${getNamespaceFullName(client.service.namespace!)}.${name}`, - apiVersions: context.__tspTypeToApiVersions.get(client.type)!, - isFormDataType: false, - isError: false, - decorators: [], - }); + + return diagnostics.wrap(initializationModel); } function getSdkMethodParameter( @@ -430,10 +434,18 @@ function getSdkMethods( const operationGroupClient = diagnostics.pipe( createSdkClientType(context, operationGroup, sdkClientType) ); + const clientInitialization = getClientInitialization(context, operationGroup.type); + const parameters: SdkParameter[] = []; + if (clientInitialization) { + for (const property of clientInitialization.properties) { + parameters.push(property); + } + } else { + } const name = `get${operationGroup.type.name}`; retval.push({ kind: "clientaccessor", - parameters: [], + parameters, name, description: getDocHelper(context, operationGroup.type).description, details: getDocHelper(context, operationGroup.type).details, @@ -449,9 +461,11 @@ function getSdkMethods( return diagnostics.wrap(retval); } -function getEndpointTypeFromSingleServer( +function getEndpointTypeFromSingleServer< + TServiceOperation extends SdkServiceOperation = SdkHttpOperation, +>( context: TCGCContext, - client: SdkClient | SdkOperationGroup, + client: SdkClientType, server: HttpServer | undefined ): [SdkEndpointType[], readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); @@ -476,8 +490,8 @@ function getEndpointTypeFromSingleServer( correspondingMethodParams: [], type: getTypeSpecBuiltInType(context, "string"), isApiVersionParam: false, - apiVersions: context.__tspTypeToApiVersions.get(client.type)!, - crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, client.service)}.endpoint`, + apiVersions: context.__tspTypeToApiVersions.get(client.__raw.type)!, + crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, client.__raw.service)}.endpoint`, decorators: [], }, ], @@ -495,12 +509,12 @@ function getEndpointTypeFromSingleServer( if (param.defaultValue && "value" in param.defaultValue) { sdkParam.clientDefaultValue = param.defaultValue.value; } - const apiVersionInfo = updateWithApiVersionInformation(context, param, client.type); + const apiVersionInfo = updateWithApiVersionInformation(context, param, client.__raw.type); sdkParam.isApiVersionParam = apiVersionInfo.isApiVersionParam; if (sdkParam.isApiVersionParam) { sdkParam.clientDefaultValue = apiVersionInfo.clientDefaultValue; } - sdkParam.apiVersions = getAvailableApiVersions(context, param, client.type); + sdkParam.apiVersions = getAvailableApiVersions(context, param, client.__raw.type); } else { diagnostics.add( createDiagnostic({ @@ -534,12 +548,13 @@ function getEndpointTypeFromSingleServer( return diagnostics.wrap(types); } -function getSdkEndpointParameter( +function getSdkEndpointParameter( context: TCGCContext, - client: SdkClient | SdkOperationGroup + client: SdkClientType ): [SdkEndpointParameter, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const servers = getServers(context.program, client.service); + const rawClient = client.__raw; + const servers = getServers(context.program, client.__raw.service); const types: SdkEndpointType[] = []; if (servers === undefined) { @@ -555,9 +570,9 @@ function getSdkEndpointParameter( type = { kind: "union", values: types, - name: createGeneratedName(context, client.service, "Endpoint"), + name: createGeneratedName(context, rawClient.service, "Endpoint"), isGeneratedName: true, - crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, client.service), + crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, rawClient.service), decorators: [], } as SdkUnionType; } else { @@ -572,10 +587,10 @@ function getSdkEndpointParameter( doc: "Service host", onClient: true, urlEncode: false, - apiVersions: context.__tspTypeToApiVersions.get(client.type)!, + apiVersions: context.__tspTypeToApiVersions.get(rawClient.type)!, optional: false, isApiVersionParam: false, - crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, client.service)}.endpoint`, + crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, rawClient.service)}.endpoint`, decorators: [], }); } @@ -595,6 +610,7 @@ function createSdkClientType( } const docWrapper = getDocHelper(context, client.type); const sdkClientType: SdkClientType = { + __raw: client, kind: "client", name, description: docWrapper.description, @@ -604,17 +620,7 @@ function createSdkClientType( methods: [], apiVersions: context.__tspTypeToApiVersions.get(client.type)!, nameSpace: getClientNamespaceStringHelper(context, client.service)!, - initialization: { - kind: "model", - properties: [], - name: "", - isGeneratedName: true, - access: "internal", - usage: UsageFlags.None, - crossLanguageDefinitionId: "", - apiVersions: [], - decorators: [], - }, + initialization: diagnostics.pipe(getSdkInitializationType(context, client)), decorators: diagnostics.pipe(getTypeDecorators(context, client.type)), parent, // if it is client, the crossLanguageDefinitionId is the ${namespace}, if it is operation group, the crosslanguageDefinitionId is the %{namespace}.%{operationGroupName} @@ -624,11 +630,42 @@ function createSdkClientType( sdkClientType.methods = diagnostics.pipe( getSdkMethods(context, client, sdkClientType) ); - sdkClientType.initialization = diagnostics.pipe(getSdkInitializationType(context, client)); // MUST call this after getSdkMethods has been called - + addDefaultClientParameters(context, sdkClientType); return diagnostics.wrap(sdkClientType); } +function addDefaultClientParameters< + TServiceOperation extends SdkServiceOperation = SdkHttpOperation, +>(context: TCGCContext, client: SdkClientType): void { + const diagnostics = createDiagnosticCollector(); + // there will always be an endpoint property + client.initialization.properties.push(diagnostics.pipe(getSdkEndpointParameter(context, client))); + const credentialParam = getSdkCredentialParameter(context, client.__raw); + if (credentialParam) { + client.initialization.properties.push(credentialParam); + } + let apiVersionParam = context.__clientToParameters + .get(client.__raw.type) + ?.find((x) => x.isApiVersionParam); + if (!apiVersionParam) { + for (const operationGroup of listOperationGroups(context, client.__raw)) { + // if any sub operation groups have an api version param, the top level needs + // the api version param as well + apiVersionParam = context.__clientToParameters + .get(operationGroup.type) + ?.find((x) => x.isApiVersionParam); + if (apiVersionParam) break; + } + } + if (apiVersionParam) { + client.initialization.properties.push(apiVersionParam); + } + const subId = client.initialization.properties.find((x) => isSubscriptionId(context, x)); + if (subId) { + client.initialization.properties.push(subId); + } +} + function populateApiVersionInformation(context: TCGCContext): void { for (const client of listClients(context)) { let clientApiVersions = resolveVersions(context.program, client.service) @@ -639,7 +676,7 @@ function populateApiVersionInformation(context: TCGCContext): void { filterApiVersionsWithDecorators(context, client.type, clientApiVersions) ); - context.__namespaceToApiVersionClientDefaultValue.set( + context.__clientToApiVersionClientDefaultValue.set( client.type, getClientDefaultApiVersion(context, client) ); @@ -652,7 +689,7 @@ function populateApiVersionInformation(context: TCGCContext): void { filterApiVersionsWithDecorators(context, og.type, clientApiVersions) ); - context.__namespaceToApiVersionClientDefaultValue.set( + context.__clientToApiVersionClientDefaultValue.set( og.type, getClientDefaultApiVersion(context, og) ); diff --git a/packages/typespec-client-generator-core/src/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index f5c59a1155..488e2de60f 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -2,6 +2,7 @@ import { AzureClientGeneratorCoreDecorators } from "../generated-defs/Azure.Clie import { $access, $client, + $clientInitialization, $clientName, $convenientAPI, $flattenProperty, @@ -10,6 +11,7 @@ import { $protocolAPI, $usage, $useSystemTextJsonConverter, + paramAliasDecorator, } from "./decorators.js"; export { $lib } from "./lib.js"; @@ -29,5 +31,7 @@ export const $decorators = { flattenProperty: $flattenProperty, override: $override, useSystemTextJsonConverter: $useSystemTextJsonConverter, + clientInitialization: $clientInitialization, + paramAlias: paramAliasDecorator, } as AzureClientGeneratorCoreDecorators, }; diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 6213cafcc8..5d4c997561 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -67,6 +67,7 @@ import { SdkDurationType, SdkEnumType, SdkEnumValueType, + SdkInitializationType, SdkModelPropertyType, SdkModelPropertyTypeBase, SdkModelType, @@ -98,7 +99,9 @@ import { isJsonContentType, isMultipartOperation, isNeverOrVoidType, + isOnClient, isXmlContentType, + twoParamsEquivalent, updateWithApiVersionInformation, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; @@ -719,6 +722,18 @@ export function getSdkModel( return ignoreDiagnostics(getSdkModelWithDiagnostics(context, type, operation)); } +export function getInitializationType( + context: TCGCContext, + type: Model, + operation?: Operation +): SdkInitializationType { + const model = ignoreDiagnostics(getSdkModelWithDiagnostics(context, type, operation)); + for (const property of model.properties) { + property.kind = "method"; + } + return model as SdkInitializationType; +} + export function getSdkModelWithDiagnostics( context: TCGCContext, type: Model, @@ -1209,6 +1224,7 @@ export function getSdkModelPropertyTypeBase( } const docWrapper = getDocHelper(context, type); const name = getPropertyNames(context, type)[0]; + const onClient = isOnClient(context, type); return diagnostics.wrap({ __raw: type, description: docWrapper.description, @@ -1225,6 +1241,7 @@ export function getSdkModelPropertyTypeBase( type, operation ? getLocationOfOperation(operation) : undefined ), + onClient, crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, type, operation), decorators: diagnostics.pipe(getTypeDecorators(context, type)), }); @@ -1363,6 +1380,14 @@ export function getSdkModelPropertyType( operation?: Operation ): [SdkModelPropertyType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); + + const clientParams = operation + ? context.__clientToParameters.get(getLocationOfOperation(operation)) + : undefined; + const correspondingClientParams = clientParams?.find((x) => + twoParamsEquivalent(context, x.__raw, type) + ); + if (correspondingClientParams) return diagnostics.wrap(correspondingClientParams); const base = diagnostics.pipe(getSdkModelPropertyTypeBase(context, type, operation)); if (isSdkHttpParameter(context, type)) return getSdkHttpParameter(context, type, operation!); diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 2aada02f1a..c04c7c36af 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -3830,4 +3830,447 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(paramsParam.type.name, "Params"); }); }); + describe("@clientInitialization", () => { + it("main client", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blobName: string): void; + `, + ` + namespace MyCustomizations; + + model MyClientInitialization { + blobName: string; + } + + @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); + ` + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.initialization.properties.length, 2); + const endpoint = client.initialization.properties.find((x) => x.kind === "endpoint"); + ok(endpoint); + const blobName = client.initialization.properties.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + strictEqual(blobName.optional, false); + + const methods = client.methods; + strictEqual(methods.length, 1); + const download = methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 0); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 1); + const blobNameOpParam = downloadOp.parameters[0]; + strictEqual(blobNameOpParam.name, "blobName"); + strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); + strictEqual(blobNameOpParam.correspondingMethodParams[0], blobName); + }); + it("subclient", async () => { + await runner.compileWithCustomization( + ` + @service + namespace StorageClient { + + @route("/main") + op download(@path blobName: string): void; + + interface BlobClient { + @route("/blob") + op download(@path blobName: string): void; + } + } + `, + ` + model ClientInitialization { + blobName: string + }; + + @@clientInitialization(StorageClient, ClientInitialization); + @@clientInitialization(StorageClient.BlobClient, ClientInitialization); + ` + ); + const sdkPackage = runner.context.sdkPackage; + const clients = sdkPackage.clients; + strictEqual(clients.length, 1); + const client = clients[0]; + strictEqual(client.name, "StorageClient"); + strictEqual(client.initialization.access, "public"); + strictEqual(client.initialization.properties.length, 2); + ok(client.initialization.properties.find((x) => x.kind === "endpoint")); + const blobName = client.initialization.properties.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual(blobName.onClient, true); + + const methods = client.methods; + strictEqual(methods.length, 2); + + // the main client's function should not have `blobName` as a client method parameter + const mainClientDownload = methods.find((x) => x.kind === "basic" && x.name === "download"); + ok(mainClientDownload); + strictEqual(mainClientDownload.parameters.length, 0); + + const getBlobClient = methods.find((x) => x.kind === "clientaccessor"); + ok(getBlobClient); + strictEqual(getBlobClient.kind, "clientaccessor"); + strictEqual(getBlobClient.name, "getBlobClient"); + strictEqual(getBlobClient.parameters.length, 1); + const blobNameParam = getBlobClient.parameters.find((x) => x.name === "blobName"); + ok(blobNameParam); + strictEqual(blobNameParam.onClient, true); + strictEqual(blobNameParam.optional, false); + strictEqual(blobNameParam.kind, "method"); + + const blobClient = getBlobClient.response; + + strictEqual(blobClient.kind, "client"); + strictEqual(blobClient.name, "BlobClient"); + strictEqual(blobClient.initialization.access, "internal"); + strictEqual(blobClient.initialization.properties.length, 2); + + ok(blobClient.initialization.properties.find((x) => x.kind === "endpoint")); + const blobClientBlobInitializationProp = blobClient.initialization.properties.find( + (x) => x.name === "blobName" + ); + ok(blobClientBlobInitializationProp); + strictEqual(blobClientBlobInitializationProp.kind, "method"); + strictEqual(blobClientBlobInitializationProp.onClient, true); + strictEqual(blobClient.methods.length, 1); + + const download = blobClient.methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 0); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 1); + const blobNameOpParam = downloadOp.parameters[0]; + strictEqual(blobNameOpParam.name, "blobName"); + strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); + strictEqual(blobNameOpParam.correspondingMethodParams[0], blobClientBlobInitializationProp); + }); + it("some methods don't have client initialization params", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blobName: string, @header header: int32): void; + op noClientParams(@query query: int32): void; + `, + ` + namespace MyCustomizations; + + model MyClientInitialization { + blobName: string; + } + + @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); + ` + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.initialization.properties.length, 2); + const endpoint = client.initialization.properties.find((x) => x.kind === "endpoint"); + ok(endpoint); + const blobName = client.initialization.properties.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + strictEqual(blobName.optional, false); + + const methods = client.methods; + strictEqual(methods.length, 2); + const download = methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 1); + + const headerParam = download.parameters.find((x) => x.name === "header"); + ok(headerParam); + strictEqual(headerParam.onClient, false); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 2); + const blobNameOpParam = downloadOp.parameters[0]; + strictEqual(blobNameOpParam.name, "blobName"); + strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); + strictEqual(blobNameOpParam.correspondingMethodParams[0], blobName); + + const noClientParamsMethod = methods[1]; + strictEqual(noClientParamsMethod.name, "noClientParams"); + strictEqual(noClientParamsMethod.kind, "basic"); + strictEqual(noClientParamsMethod.parameters.length, 1); + strictEqual(noClientParamsMethod.parameters[0].name, "query"); + strictEqual(noClientParamsMethod.parameters[0].onClient, false); + }); + + it("multiple client params", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blobName: string, @path containerName: string): void; + `, + ` + namespace MyCustomizations; + + model MyClientInitialization { + blobName: string; + containerName: string; + } + + @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); + ` + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.initialization.properties.length, 3); + const endpoint = client.initialization.properties.find((x) => x.kind === "endpoint"); + ok(endpoint); + const blobName = client.initialization.properties.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + strictEqual(blobName.optional, false); + + const containerName = client.initialization.properties.find( + (x) => x.name === "containerName" + ); + ok(containerName); + strictEqual(containerName.clientDefaultValue, undefined); + strictEqual(containerName.onClient, true); + + const methods = client.methods; + strictEqual(methods.length, 1); + const download = methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 0); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 2); + const blobNameOpParam = downloadOp.parameters[0]; + strictEqual(blobNameOpParam.name, "blobName"); + strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); + strictEqual(blobNameOpParam.correspondingMethodParams[0], blobName); + + const containerNameOpParam = downloadOp.parameters[1]; + strictEqual(containerNameOpParam.name, "containerName"); + strictEqual(containerNameOpParam.correspondingMethodParams.length, 1); + strictEqual(containerNameOpParam.correspondingMethodParams[0], containerName); + }); + + it("@operationGroup with same model on parent client", async () => { + await runner.compile( + ` + @service + namespace MyService; + + @operationGroup + interface MyInterface { + op download(@path blobName: string, @path containerName: string): void; + } + + model MyClientInitialization { + blobName: string; + containerName: string; + } + + @@clientInitialization(MyService, MyClientInitialization); + @@clientInitialization(MyService.MyInterface, MyClientInitialization); + ` + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + + const client = sdkPackage.clients[0]; + strictEqual(client.initialization.access, "public"); + strictEqual(client.initialization.properties.length, 3); + ok(client.initialization.properties.find((x) => x.kind === "endpoint")); + const blobName = client.initialization.properties.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + + const containerName = client.initialization.properties.find( + (x) => x.name === "containerName" + ); + ok(containerName); + strictEqual(containerName.clientDefaultValue, undefined); + strictEqual(containerName.onClient, true); + + const methods = client.methods; + strictEqual(methods.length, 1); + const clientAccessor = methods[0]; + strictEqual(clientAccessor.kind, "clientaccessor"); + const og = clientAccessor.response; + strictEqual(og.kind, "client"); + + strictEqual(og.initialization.access, "internal"); + strictEqual(og.initialization.properties.length, 3); + ok(og.initialization.properties.find((x) => x.kind === "endpoint")); + ok(og.initialization.properties.find((x) => x === blobName)); + ok(og.initialization.properties.find((x) => x === containerName)); + + const download = og.methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 0); + + const op = download.operation; + strictEqual(op.parameters.length, 2); + strictEqual(op.parameters[0].correspondingMethodParams[0], blobName); + strictEqual(op.parameters[1].correspondingMethodParams[0], containerName); + }); + + it("redefine client structure", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op uploadContainer(@path containerName: string): void; + op uploadBlob(@path containerName: string, @path blobName: string): void; + `, + ` + namespace MyCustomizations { + model ContainerClientInitialization { + containerName: string; + } + @client({service: MyService}) + @clientInitialization(ContainerClientInitialization) + namespace ContainerClient { + op upload is MyService.uploadContainer; + + + model BlobClientInitialization { + containerName: string; + blobName: string; + } + + @client({service: MyService}) + @clientInitialization(BlobClientInitialization) + namespace BlobClient { + op upload is MyService.uploadBlob; + } + } + } + + ` + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 2); + + const containerClient = sdkPackage.clients.find((x) => x.name === "ContainerClient"); + ok(containerClient); + strictEqual(containerClient.initialization.access, "public"); + strictEqual(containerClient.initialization.properties.length, 2); + ok(containerClient.initialization.properties.find((x) => x.kind === "endpoint")); + + const containerName = containerClient.initialization.properties.find( + (x) => x.name === "containerName" + ); + ok(containerName); + + const methods = containerClient.methods; + strictEqual(methods.length, 1); + strictEqual(methods[0].name, "upload"); + strictEqual(methods[0].kind, "basic"); + strictEqual(methods[0].parameters.length, 0); + strictEqual(methods[0].operation.parameters.length, 1); + strictEqual(methods[0].operation.parameters[0].correspondingMethodParams[0], containerName); + + const blobClient = sdkPackage.clients.find((x) => x.name === "BlobClient"); + ok(blobClient); + strictEqual(blobClient.initialization.access, "public"); + strictEqual(blobClient.initialization.properties.length, 3); + ok(blobClient.initialization.properties.find((x) => x.kind === "endpoint")); + + const containerNameOnBlobClient = blobClient.initialization.properties.find( + (x) => x.name === "containerName" + ); + ok(containerNameOnBlobClient); + + const blobName = blobClient.initialization.properties.find((x) => x.name === "blobName"); + ok(blobName); + + const blobMethods = blobClient.methods; + strictEqual(blobMethods.length, 1); + strictEqual(blobMethods[0].name, "upload"); + strictEqual(blobMethods[0].kind, "basic"); + strictEqual(blobMethods[0].parameters.length, 0); + strictEqual(blobMethods[0].operation.parameters.length, 2); + strictEqual( + blobMethods[0].operation.parameters[0].correspondingMethodParams[0], + containerNameOnBlobClient + ); + strictEqual(blobMethods[0].operation.parameters[1].correspondingMethodParams[0], blobName); + }); + + it("@paramAlias", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blob: string): void; + op upload(@path blobName: string): void; + `, + ` + namespace MyCustomizations; + + model MyClientInitialization { + @paramAlias("blob") + blobName: string; + } + + @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); + ` + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.initialization.properties.length, 2); + const endpoint = client.initialization.properties.find((x) => x.kind === "endpoint"); + ok(endpoint); + const blobName = client.initialization.properties.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + strictEqual(blobName.optional, false); + + const methods = client.methods; + strictEqual(methods.length, 2); + const download = methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 0); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 1); + strictEqual(downloadOp.parameters[0].name, "blob"); + strictEqual(downloadOp.parameters[0].correspondingMethodParams.length, 1); + strictEqual(downloadOp.parameters[0].correspondingMethodParams[0], blobName); + + const upload = methods[1]; + strictEqual(upload.name, "upload"); + strictEqual(upload.kind, "basic"); + strictEqual(upload.parameters.length, 0); + + const uploadOp = upload.operation; + strictEqual(uploadOp.parameters.length, 1); + strictEqual(uploadOp.parameters[0].name, "blobName"); + strictEqual(uploadOp.parameters[0].correspondingMethodParams.length, 1); + strictEqual(uploadOp.parameters[0].correspondingMethodParams[0], blobName); + }); + }); });