From 35d7068202b41d1ac87bcb3fce56466e5b531163 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 16 Aug 2024 13:37:41 -0400 Subject: [PATCH 01/24] add clientInitialization decorator --- .../src/decorators.ts | 19 ++++++ .../src/interfaces.ts | 6 +- .../src/package.ts | 59 +++++++++++-------- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 1e6485c0ae..3a96f4d1a0 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -1159,3 +1159,22 @@ export const $useSystemTextJsonConverter: DecoratorFunction = ( entity: Model, scope?: LanguageScopes ) => {}; + + +const clientInitializationKey = createStateSymbol("clientInitialization"); + +export const $clientInitialization: DecoratorFunction = ( + context: DecoratorContext, + target: Namespace | Interface, + options: Model, + scope?: LanguageScopes +) => { + setScopedDecoratorData(context, $override, clientInitializationKey, target, options, scope); +}; + +export function getClientInitialization( + context: TCGCContext, + entity: Namespace | Interface +): Model | undefined { + return getScopedDecoratorData(context, clientInitializationKey, entity); +} diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index f0cfe9e14b..8d4d7f9b12 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -90,17 +90,13 @@ export interface SdkClient { crossLanguageDefinitionId: string; } -export interface SdkInitializationType extends SdkModelType { - properties: SdkParameter[]; -} - export interface SdkClientType extends DecoratedType { kind: "client"; name: string; description?: string; details?: string; - initialization: SdkInitializationType; + initialization: SdkModelType; methods: SdkMethod[]; apiVersions: string[]; nameSpace: string; // fully qualified diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 25c7785163..6e38d756a3 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -13,6 +13,7 @@ import { resolveVersions } from "@typespec/versioning"; import { camelCase } from "change-case"; import { getAccess, + getClientInitialization, getClientNameOverride, getOverriddenClientMethod, listClients, @@ -28,7 +29,6 @@ import { SdkEndpointParameter, SdkEndpointType, SdkEnumType, - SdkInitializationType, SdkLroPagingServiceMethod, SdkLroServiceMethod, SdkMethod, @@ -39,7 +39,6 @@ import { SdkOperationGroup, SdkPackage, SdkPagingServiceMethod, - SdkParameter, SdkPathParameter, SdkServiceMethod, SdkServiceOperation, @@ -75,6 +74,7 @@ import { getAllModelsWithDiagnostics, getClientTypeWithDiagnostics, getSdkCredentialParameter, + getSdkModel, getSdkModelPropertyType, getTypeSpecBuiltInType, } from "./types.js"; @@ -309,14 +309,37 @@ function getClientDefaultApiVersion( function getSdkInitializationType( context: TCGCContext, client: SdkClient | SdkOperationGroup -): [SdkInitializationType, readonly Diagnostic[]] { +): [SdkModelType, 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 - ]; + let initializationModel: SdkModelType | undefined; + const initializationDecorator = getClientInitialization(context, client.type); + if (initializationDecorator) { + initializationModel = getSdkModel(context, initializationDecorator); + } else { + const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath; + const name = `${namePrefix.split(".").at(-1)}Options`; + initializationModel = { + __raw: client.service, + description: "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, + decorators: [], + } + } + + // there will always be an endpoint property + initializationModel.properties.push(diagnostics.pipe(getSdkEndpointParameter(context, client))); + if (credentialParam) { - properties.push(credentialParam); + initializationModel.properties.push(credentialParam); } let apiVersionParam = context.__namespaceToApiVersionParameter.get(client.type); if (!apiVersionParam) { @@ -328,28 +351,12 @@ function getSdkInitializationType( } } if (apiVersionParam) { - properties.push(apiVersionParam); + initializationModel.properties.push(apiVersionParam); } if (context.__subscriptionIdParameter) { - properties.push(context.__subscriptionIdParameter); + initializationModel.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", - 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( From 70c1e00a774dfecd16c1ce8c32833bf23a125180 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Aug 2024 10:51:23 -0400 Subject: [PATCH 02/24] remove onClient --- .../src/decorators.ts | 5 ++-- .../src/http.ts | 26 ++++++++++++------- .../src/interfaces.ts | 5 ++-- .../src/internal-utils.ts | 8 +----- .../src/package.ts | 26 ++++++++++++++----- .../src/types.ts | 10 +++++++ 6 files changed, 51 insertions(+), 29 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 3a96f4d1a0..d706ee5231 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -614,9 +614,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$/, }; } @@ -1160,7 +1160,6 @@ export const $useSystemTextJsonConverter: DecoratorFunction = ( scope?: LanguageScopes ) => {}; - const clientInitializationKey = createStateSymbol("clientInitialization"); export const $clientInitialization: DecoratorFunction = ( diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index dd3309a1a0..2dd623f409 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -53,7 +53,11 @@ import { isSubscriptionId, } 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, @@ -473,8 +477,13 @@ export function getCorrespondingMethodParams( const diagnostics = createDiagnosticCollector(); const operationLocation = getLocationOfOperation(operation); + let clientParams = context.__clientToParameters.get(operationLocation); + if (!clientParams) { + clientParams = []; + context.__clientToParameters.set(operationLocation, clientParams); + } if (serviceParam.isApiVersionParam) { - const existingApiVersion = context.__namespaceToApiVersionParameter.get(operationLocation); + const existingApiVersion = clientParams?.find((x) => isApiVersion(context, x)); if (!existingApiVersion) { const apiVersionParam = methodParameters.find((x) => x.name.includes("apiVersion")); if (!apiVersionParam) { @@ -495,15 +504,14 @@ export function getCorrespondingMethodParams( name: "apiVersion", isGeneratedName: apiVersionParam.name !== "apiVersion", optional: false, - clientDefaultValue: - context.__namespaceToApiVersionClientDefaultValue.get(operationLocation), + clientDefaultValue: context.__clientToApiVersionClientDefaultValue.get(operationLocation), }; - context.__namespaceToApiVersionParameter.set(operationLocation, apiVersionParamUpdated); + clientParams.push(apiVersionParamUpdated); // TODO: } - return diagnostics.wrap([context.__namespaceToApiVersionParameter.get(operationLocation)!]); + return diagnostics.wrap(clientParams); } if (isSubscriptionId(context, serviceParam)) { - if (!context.__subscriptionIdParameter) { + if (!clientParams.find((x) => isSubscriptionId(context, x))) { const subscriptionIdParam = methodParameters.find((x) => isSubscriptionId(context, x)); if (!subscriptionIdParam) { diagnostics.add( @@ -518,9 +526,9 @@ export function getCorrespondingMethodParams( ); return diagnostics.wrap([]); } - context.__subscriptionIdParameter = subscriptionIdParam; + clientParams.push(subscriptionIdParam); } - return diagnostics.wrap([context.__subscriptionIdParameter]); + return diagnostics.wrap(clientParams); } // 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 8d4d7f9b12..9ada39cdf3 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -40,12 +40,11 @@ export interface TCGCContext { spreadModels?: 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; diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 156c1fcc5a..74f9ab2fb9 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -111,16 +111,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), }; } @@ -457,10 +455,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)!; diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 6e38d756a3..d12df1e59c 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -59,6 +59,7 @@ import { getLocationOfOperation, getTypeDecorators, isNeverOrVoidType, + isSubscriptionId, updateWithApiVersionInformation, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; @@ -314,8 +315,16 @@ function getSdkInitializationType( const credentialParam = getSdkCredentialParameter(context, client); let initializationModel: SdkModelType | undefined; const initializationDecorator = getClientInitialization(context, client.type); + let clientParams = context.__clientToParameters.get(client.type); + if (!clientParams) { + clientParams = []; + context.__clientToParameters.set(client.type, clientParams); + } if (initializationDecorator) { initializationModel = getSdkModel(context, initializationDecorator); + for (const property of initializationModel.properties) { + property.onClient = true; + } } else { const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath; const name = `${namePrefix.split(".").at(-1)}Options`; @@ -332,7 +341,7 @@ function getSdkInitializationType( apiVersions: context.__tspTypeToApiVersions.get(client.type)!, isFormDataType: false, decorators: [], - } + }; } // there will always be an endpoint property @@ -341,20 +350,23 @@ function getSdkInitializationType( if (credentialParam) { initializationModel.properties.push(credentialParam); } - let apiVersionParam = context.__namespaceToApiVersionParameter.get(client.type); + let apiVersionParam = clientParams.find((x) => x.isApiVersionParam); 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); + apiVersionParam = context.__clientToParameters + .get(operationGroup.type) + ?.find((x) => x.isApiVersionParam); if (apiVersionParam) break; } } if (apiVersionParam) { initializationModel.properties.push(apiVersionParam); } - if (context.__subscriptionIdParameter) { - initializationModel.properties.push(context.__subscriptionIdParameter); + const subId = clientParams.find((x) => isSubscriptionId(context, x)); + if (subId) { + initializationModel.properties.push(subId); } return diagnostics.wrap(initializationModel); } @@ -615,7 +627,7 @@ function populateApiVersionInformation(context: TCGCContext): void { filterApiVersionsWithDecorators(context, client.type, clientApiVersions) ); - context.__namespaceToApiVersionClientDefaultValue.set( + context.__clientToApiVersionClientDefaultValue.set( client.type, getClientDefaultApiVersion(context, client) ); @@ -628,7 +640,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/types.ts b/packages/typespec-client-generator-core/src/types.ts index 772955c1fa..95617bb228 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -97,6 +97,7 @@ import { isMultipartFormData, isMultipartOperation, isNeverOrVoidType, + isSubscriptionId, isXmlContentType, updateWithApiVersionInformation, } from "./internal-utils.js"; @@ -108,6 +109,7 @@ import { getHttpOperationWithCache, getLibraryName, getPropertyNames, + isApiVersion, } from "./public-utils.js"; import { getVersions } from "@typespec/versioning"; @@ -1178,6 +1180,13 @@ export function getSdkModelPropertyTypeBase( } const docWrapper = getDocHelper(context, type); const name = getPropertyNames(context, type)[0]; + const namespace = type.model?.namespace; + const onClient = + isSubscriptionId(context, type) || + isApiVersion(context, type) || + Boolean( + namespace && context.__clientToParameters.get(namespace)?.find((x) => x.name === type.name) + ); return diagnostics.wrap({ __raw: type, description: docWrapper.description, @@ -1192,6 +1201,7 @@ export function getSdkModelPropertyTypeBase( type, operation ? getLocationOfOperation(operation) : undefined ), + onClient, crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, type, operation), decorators: diagnostics.pipe(getTypeDecorators(context, type)), }); From d0b9fee115a8d01b86d5a22e7c4d8226ddf0a6f2 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Aug 2024 11:38:11 -0400 Subject: [PATCH 03/24] add decorator and initial test --- .../reference/decorators.md | 39 ++++++++++++++++ .../reference/index.mdx | 1 + .../typespec-client-generator-core/README.md | 40 +++++++++++++++++ .../Azure.ClientGenerator.Core.ts | 31 +++++++++++++ .../lib/decorators.tsp | 27 ++++++++++++ .../src/decorators.ts | 3 +- .../src/tsp-index.ts | 2 + .../test/decorators.test.ts | 44 +++++++++++++++++++ 8 files changed, 186 insertions(+), 1 deletion(-) diff --git a/docs/libraries/typespec-client-generator-core/reference/decorators.md b/docs/libraries/typespec-client-generator-core/reference/decorators.md index d77e60c518..a1569db0d9 100644 --- a/docs/libraries/typespec-client-generator-core/reference/decorators.md +++ b/docs/libraries/typespec-client-generator-core/reference/decorators.md @@ -235,6 +235,45 @@ model MyModel { } ``` +### `@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 diff --git a/docs/libraries/typespec-client-generator-core/reference/index.mdx b/docs/libraries/typespec-client-generator-core/reference/index.mdx index d504e45c31..7705c5fbd8 100644 --- a/docs/libraries/typespec-client-generator-core/reference/index.mdx +++ b/docs/libraries/typespec-client-generator-core/reference/index.mdx @@ -42,6 +42,7 @@ npm install --save-peer @azure-tools/typespec-client-generator-core - [`@access`](./decorators.md#@Azure.ClientGenerator.Core.access) - [`@client`](./decorators.md#@Azure.ClientGenerator.Core.client) - [`@clientFormat`](./decorators.md#@Azure.ClientGenerator.Core.clientFormat) +- [`@clientInitialization`](./decorators.md#@Azure.ClientGenerator.Core.clientInitialization) - [`@clientName`](./decorators.md#@Azure.ClientGenerator.Core.clientName) - [`@convenientAPI`](./decorators.md#@Azure.ClientGenerator.Core.convenientAPI) - [`@exclude`](./decorators.md#@Azure.ClientGenerator.Core.exclude) diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index e214eaa2a4..cac297c8ba 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -15,6 +15,7 @@ npm install @azure-tools/typespec-client-generator-core - [`@access`](#@access) - [`@client`](#@client) - [`@clientFormat`](#@clientformat) +- [`@clientInitialization`](#@clientinitialization) - [`@clientName`](#@clientname) - [`@convenientAPI`](#@convenientapi) - [`@exclude`](#@exclude) @@ -252,6 +253,45 @@ model MyModel { } ``` +#### `@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 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 d8788affc7..3ad76ea2f8 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 @@ -507,6 +507,36 @@ 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; + export type AzureClientGeneratorCoreDecorators = { clientName: ClientNameDecorator; convenientAPI: ConvenientAPIDecorator; @@ -522,4 +552,5 @@ export type AzureClientGeneratorCoreDecorators = { flattenProperty: FlattenPropertyDecorator; override: OverrideDecorator; useSystemTextJsonConverter: UseSystemTextJsonConverterDecorator; + clientInitialization: ClientInitializationDecorator; }; diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index c45f1a1293..dfe26ad6fb 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -490,3 +490,30 @@ 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); diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index d706ee5231..5a3a789784 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -36,6 +36,7 @@ import { AccessDecorator, ClientDecorator, ClientFormatDecorator, + ClientInitializationDecorator, ClientNameDecorator, ConvenientAPIDecorator, ExcludeDecorator, @@ -1162,7 +1163,7 @@ export const $useSystemTextJsonConverter: DecoratorFunction = ( const clientInitializationKey = createStateSymbol("clientInitialization"); -export const $clientInitialization: DecoratorFunction = ( +export const $clientInitialization: ClientInitializationDecorator = ( context: DecoratorContext, target: Namespace | Interface, options: Model, diff --git a/packages/typespec-client-generator-core/src/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index e7e07f3790..74ad1b0493 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -14,6 +14,7 @@ import { $protocolAPI, $usage, $useSystemTextJsonConverter, + $clientInitialization, } from "./decorators.js"; export { $lib } from "./lib.js"; @@ -41,5 +42,6 @@ export const $decorators = { flattenProperty: $flattenProperty, override: $override, useSystemTextJsonConverter: $useSystemTextJsonConverter, + clientInitialization: $clientInitialization, } as AzureClientGeneratorCoreDecorators, }; diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 8ef8609e50..ad51e412ce 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4743,4 +4743,48 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(method.operation.bodyParam.correspondingMethodParams[0], inputParam); }); }); + describe("@clientInitialization", () => { + it("main client", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(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); + }); + }); }); From 48a726b783aa4838b7d12d1316396e7c596bd2e8 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Aug 2024 12:58:46 -0400 Subject: [PATCH 04/24] base test passing --- .../lib/decorators.tsp | 12 +- .../src/interfaces.ts | 3 +- .../src/package.ts | 105 +++++++++--------- .../src/tsp-index.ts | 2 +- .../test/decorators.test.ts | 15 ++- 5 files changed, 74 insertions(+), 63 deletions(-) diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index dfe26ad6fb..c98dba4bdd 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -499,21 +499,23 @@ extern dec useSystemTextJsonConverter(target: Model, scope?: valueof string); * ```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, +extern dec clientInitialization( + target: Namespace | Interface, options: Model, - scope?: valueof string); + scope?: valueof string +); diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 9ada39cdf3..51a0677075 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -40,7 +40,7 @@ export interface TCGCContext { spreadModels?: Map; httpOperationCache?: Map; unionsMap?: Map; - __clientToParameters: Map; + __clientToParameters: Map; __tspTypeToApiVersions: Map; __clientToApiVersionClientDefaultValue: Map; knownScalars?: Record; @@ -91,6 +91,7 @@ export interface SdkClient { export interface SdkClientType extends DecoratedType { + __raw: SdkClient | SdkOperationGroup; kind: "client"; name: string; description?: string; diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index d12df1e59c..abe79564b9 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -29,6 +29,7 @@ import { SdkEndpointParameter, SdkEndpointType, SdkEnumType, + SdkHttpOperation, SdkLroPagingServiceMethod, SdkLroServiceMethod, SdkMethod, @@ -312,7 +313,6 @@ function getSdkInitializationType( client: SdkClient | SdkOperationGroup ): [SdkModelType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const credentialParam = getSdkCredentialParameter(context, client); let initializationModel: SdkModelType | undefined; const initializationDecorator = getClientInitialization(context, client.type); let clientParams = context.__clientToParameters.get(client.type); @@ -324,6 +324,7 @@ function getSdkInitializationType( initializationModel = getSdkModel(context, initializationDecorator); for (const property of initializationModel.properties) { property.onClient = true; + clientParams.push(property); } } else { const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath; @@ -344,30 +345,6 @@ function getSdkInitializationType( }; } - // there will always be an endpoint property - initializationModel.properties.push(diagnostics.pipe(getSdkEndpointParameter(context, client))); - - if (credentialParam) { - initializationModel.properties.push(credentialParam); - } - let apiVersionParam = clientParams.find((x) => x.isApiVersionParam); - 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.__clientToParameters - .get(operationGroup.type) - ?.find((x) => x.isApiVersionParam); - if (apiVersionParam) break; - } - } - if (apiVersionParam) { - initializationModel.properties.push(apiVersionParam); - } - const subId = clientParams.find((x) => isSubscriptionId(context, x)); - if (subId) { - initializationModel.properties.push(subId); - } return diagnostics.wrap(initializationModel); } @@ -438,9 +415,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(); @@ -464,8 +443,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: [], }, ], @@ -483,12 +462,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({ @@ -522,12 +501,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) { @@ -543,9 +523,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 { @@ -559,10 +539,10 @@ function getSdkEndpointParameter( description: "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: [], }); } @@ -582,6 +562,7 @@ function createSdkClientType( } const docWrapper = getDocHelper(context, client.type); const sdkClientType: SdkClientType = { + __raw: client, kind: "client", name, description: docWrapper.description, @@ -589,18 +570,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: [], - isFormDataType: false, - decorators: [], - }, + initialization: diagnostics.pipe(getSdkInitializationType(context, client)), // eslint-disable-next-line deprecation/deprecation arm: client.kind === "SdkClient" ? client.arm : false, decorators: diagnostics.pipe(getTypeDecorators(context, client.type)), @@ -612,11 +582,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) diff --git a/packages/typespec-client-generator-core/src/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index 74ad1b0493..3ba50a77d6 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -3,6 +3,7 @@ import { $access, $client, $clientFormat, + $clientInitialization, $clientName, $convenientAPI, $exclude, @@ -14,7 +15,6 @@ import { $protocolAPI, $usage, $useSystemTextJsonConverter, - $clientInitialization, } from "./decorators.js"; export { $lib } from "./lib.js"; diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index ad51e412ce..78d989e861 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4750,7 +4750,7 @@ describe("typespec-client-generator-core: decorators", () => { @service namespace MyService; - op download(blobName: string): void; + op download(@path blobName: string): void; `, ` namespace MyCustomizations; @@ -4772,19 +4772,26 @@ describe("typespec-client-generator-core: decorators", () => { 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); + strictEqual(download.parameters.length, 1); + strictEqual(download.parameters[0].name, "blobName"); + strictEqual(download.parameters[0].onClient, true); + 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); + + const blobNameCorresponding = blobNameOpParam.correspondingMethodParams[0]; + strictEqual(blobNameCorresponding.name, "blobName"); + strictEqual(blobNameCorresponding.onClient, true); + strictEqual(blobName.type.kind, "string"); }); }); }); From 420ca6122d76d879ce9252895e0b6280755c387e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Aug 2024 14:22:04 -0400 Subject: [PATCH 05/24] start working on subclients --- .../src/package.ts | 11 ++++++- .../test/decorators.test.ts | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index abe79564b9..abf256ff6c 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -398,10 +398,19 @@ function getSdkMethods( const operationGroupClient = diagnostics.pipe( createSdkClientType(context, operationGroup, sdkClientType) ); + const clientInitialization = getClientInitialization(context, operationGroup.type); + const parameters: SdkMethodParameter[] = []; + if (clientInitialization) { + for (const property of getSdkModel(context, 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, diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 78d989e861..256f263674 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4793,5 +4793,38 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(blobNameCorresponding.onClient, true); strictEqual(blobName.type.kind, "string"); }); + it("subclient", async() => { + await runner.compileWithCustomization( + ` + @service + namespace StorageClient { + interface BlobClient { + op download(@path blobName: string): void; + } + } + `, + ` + model BlobClientInitialization { + blobName: string + }; + + @@clientInitialization(StorageClient.BlobClient, BlobClientInitialization); + ` + ); + const sdkPackage = runner.context.sdkPackage; + const clients = sdkPackage.clients; + strictEqual(clients.length, 1); + const client = clients[0]; + strictEqual(client.name, "StorageClient"); + strictEqual(client.initialization.properties.length, 1); + strictEqual(client.initialization.properties[0].kind, "endpoint"); + + const methods = client.methods; + strictEqual(methods.length, 1); + const getBlobClient = methods[0]; + strictEqual(getBlobClient.kind, "clientaccessor"); + strictEqual(getBlobClient.name, "getBlobClient"); + strictEqual(getBlobClient.parameters.length, 2); + }) }); }); From 78c701b9db87644f98714b38e9190c6bd0be4f80 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Aug 2024 14:44:18 -0400 Subject: [PATCH 06/24] format --- .../src/interfaces.ts | 6 ++++- .../src/package.ts | 24 ++++++++++++------- .../src/types.ts | 13 ++++++++++ .../test/decorators.test.ts | 7 +++--- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 51a0677075..3e89f87d79 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -96,7 +96,7 @@ export interface SdkClientType name: string; description?: string; details?: string; - initialization: SdkModelType; + initialization: SdkInitializationType; methods: SdkMethod[]; apiVersions: string[]; nameSpace: string; // fully qualified @@ -390,6 +390,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/package.ts b/packages/typespec-client-generator-core/src/package.ts index abf256ff6c..3a8eaf62ea 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -30,6 +30,7 @@ import { SdkEndpointType, SdkEnumType, SdkHttpOperation, + SdkInitializationType, SdkLroPagingServiceMethod, SdkLroServiceMethod, SdkMethod, @@ -311,9 +312,9 @@ function getClientDefaultApiVersion( function getSdkInitializationType( context: TCGCContext, client: SdkClient | SdkOperationGroup -): [SdkModelType, readonly Diagnostic[]] { +): [SdkInitializationType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let initializationModel: SdkModelType | undefined; + let initializationModel: SdkInitializationType | undefined; const initializationDecorator = getClientInitialization(context, client.type); let clientParams = context.__clientToParameters.get(client.type); if (!clientParams) { @@ -321,11 +322,19 @@ function getSdkInitializationType( context.__clientToParameters.set(client.type, clientParams); } if (initializationDecorator) { - initializationModel = getSdkModel(context, initializationDecorator); - for (const property of initializationModel.properties) { - property.onClient = true; - clientParams.push(property); - } + const sdkModel = getSdkModel(context, initializationDecorator); + const initializationProps = sdkModel.properties.map( + (property: SdkModelPropertyType): SdkMethodParameter => { + property.onClient = true; + clientParams.push(property); + property.kind = "method"; + return property as SdkMethodParameter; + } + ); + initializationModel = { + ...sdkModel, + properties: initializationProps, + }; } else { const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath; const name = `${namePrefix.split(".").at(-1)}Options`; @@ -405,7 +414,6 @@ function getSdkMethods( parameters.push(property); } } else { - } const name = `get${operationGroup.type.name}`; retval.push({ diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 95617bb228..1282fc5298 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, @@ -714,6 +715,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; +} + export function getSdkModelWithDiagnostics( context: TCGCContext, type: Model, diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 256f263674..90789b95ab 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4793,7 +4793,7 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(blobNameCorresponding.onClient, true); strictEqual(blobName.type.kind, "string"); }); - it("subclient", async() => { + it("subclient", async () => { await runner.compileWithCustomization( ` @service @@ -4824,7 +4824,8 @@ describe("typespec-client-generator-core: decorators", () => { const getBlobClient = methods[0]; strictEqual(getBlobClient.kind, "clientaccessor"); strictEqual(getBlobClient.name, "getBlobClient"); - strictEqual(getBlobClient.parameters.length, 2); - }) + strictEqual(getBlobClient.parameters.length, 1); + strictEqual(getBlobClient.parameters[0].name, "blobName"); + }); }); }); From 11668acbb8bad99663df2e8f5251d7fefc858729 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Aug 2024 14:44:34 -0400 Subject: [PATCH 07/24] add changeset --- .../add_client_initialization-2024-7-19-14-44-30.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/add_client_initialization-2024-7-19-14-44-30.md 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 From c40d9a3fedd58f988bc679854952e386ce0b7e17 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Aug 2024 16:17:57 -0400 Subject: [PATCH 08/24] fix build --- .../src/decorators.ts | 21 +++++++++++++-- .../src/interfaces.ts | 2 +- .../src/package.ts | 26 ++++++------------- .../src/types.ts | 2 +- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 5a3a789784..2a75746f53 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -56,6 +56,9 @@ import { SdkContext, SdkEmitterOptions, SdkHttpOperation, + SdkInitializationType, + SdkMethodParameter, + SdkModelPropertyType, SdkOperationGroup, SdkServiceOperation, TCGCContext, @@ -1175,6 +1178,20 @@ export const $clientInitialization: ClientInitializationDecorator = ( export function getClientInitialization( context: TCGCContext, entity: Namespace | Interface -): Model | undefined { - return getScopedDecoratorData(context, clientInitializationKey, entity); +): 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, + } + } diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 3e89f87d79..52a9990892 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -40,7 +40,7 @@ export interface TCGCContext { spreadModels?: Map; httpOperationCache?: Map; unionsMap?: Map; - __clientToParameters: Map; + __clientToParameters: Map; __tspTypeToApiVersions: Map; __clientToApiVersionClientDefaultValue: Map; knownScalars?: Record; diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 3a8eaf62ea..4166cf6148 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -41,6 +41,7 @@ import { SdkOperationGroup, SdkPackage, SdkPagingServiceMethod, + SdkParameter, SdkPathParameter, SdkServiceMethod, SdkServiceOperation, @@ -314,27 +315,16 @@ function getSdkInitializationType( client: SdkClient | SdkOperationGroup ): [SdkInitializationType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let initializationModel: SdkInitializationType | undefined; - const initializationDecorator = getClientInitialization(context, client.type); + let initializationModel = getClientInitialization(context, client.type); let clientParams = context.__clientToParameters.get(client.type); if (!clientParams) { clientParams = []; context.__clientToParameters.set(client.type, clientParams); } - if (initializationDecorator) { - const sdkModel = getSdkModel(context, initializationDecorator); - const initializationProps = sdkModel.properties.map( - (property: SdkModelPropertyType): SdkMethodParameter => { - property.onClient = true; - clientParams.push(property); - property.kind = "method"; - return property as SdkMethodParameter; - } - ); - initializationModel = { - ...sdkModel, - properties: initializationProps, - }; + if (initializationModel) { + for (const prop of initializationModel.properties) { + clientParams.push(prop); + } } else { const namePrefix = client.kind === "SdkClient" ? client.name : client.groupPath; const name = `${namePrefix.split(".").at(-1)}Options`; @@ -408,9 +398,9 @@ function getSdkMethods( createSdkClientType(context, operationGroup, sdkClientType) ); const clientInitialization = getClientInitialization(context, operationGroup.type); - const parameters: SdkMethodParameter[] = []; + const parameters: SdkParameter[] = []; if (clientInitialization) { - for (const property of getSdkModel(context, clientInitialization).properties) { + for (const property of clientInitialization.properties) { parameters.push(property); } } else { diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 1282fc5298..322943db6d 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -724,7 +724,7 @@ export function getInitializationType( for (const property of model.properties) { property.kind = "method"; } - return model; + return model as SdkInitializationType; } export function getSdkModelWithDiagnostics( From c1b0574736689676c1b9cab977f631fe4389de7b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 19 Aug 2024 16:19:06 -0400 Subject: [PATCH 09/24] pnpm format --- packages/typespec-client-generator-core/src/decorators.ts | 3 +-- packages/typespec-client-generator-core/src/package.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 2a75746f53..63a45271b3 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -1192,6 +1192,5 @@ export function getClientInitialization( return { ...sdkModel, properties: initializationProps, - } - + }; } diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 4166cf6148..5bb70f51dc 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -78,7 +78,6 @@ import { getAllModelsWithDiagnostics, getClientTypeWithDiagnostics, getSdkCredentialParameter, - getSdkModel, getSdkModelPropertyType, getTypeSpecBuiltInType, } from "./types.js"; From 54a72b7a07f3670f81c4e6a23599e55ba6637c07 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Aug 2024 11:41:01 -0400 Subject: [PATCH 10/24] directly return client param in cases of client initialization --- .../src/http.ts | 13 +++- .../src/internal-utils.ts | 4 ++ .../src/types.ts | 7 +++ .../test/decorators.test.ts | 59 ++++++++++++++++--- 4 files changed, 72 insertions(+), 11 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 2dd623f409..2dc3cfc178 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -51,6 +51,7 @@ import { isContentTypeHeader, isNeverOrVoidType, isSubscriptionId, + twoParamsEquivalent, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; import { @@ -476,12 +477,18 @@ 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(x, serviceParam) + ); + if (correspondingClientParams.length > 0) return diagnostics.wrap(correspondingClientParams); + if (serviceParam.isApiVersionParam) { const existingApiVersion = clientParams?.find((x) => isApiVersion(context, x)); if (!existingApiVersion) { @@ -508,7 +515,7 @@ export function getCorrespondingMethodParams( }; clientParams.push(apiVersionParamUpdated); // TODO: } - return diagnostics.wrap(clientParams); + return diagnostics.wrap(clientParams.filter((x) => isApiVersion(context, x))); } if (isSubscriptionId(context, serviceParam)) { if (!clientParams.find((x) => isSubscriptionId(context, x))) { @@ -528,7 +535,7 @@ export function getCorrespondingMethodParams( } clientParams.push(subscriptionIdParam); } - return diagnostics.wrap(clientParams); + return diagnostics.wrap(clientParams.filter((x) => isSubscriptionId(context, x))); } // 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/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 74f9ab2fb9..bc8f7bbcf8 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -540,3 +540,7 @@ export function isXmlContentType(contentType: string): boolean { const regex = new RegExp(/^(application|text)\/(.+\+)?xml$/); return regex.test(contentType); } + +export function twoParamsEquivalent(param1: { name: string }, param2: { name: string }): boolean { + return param1.name === param2.name; +} diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 322943db6d..6b034b871c 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -100,6 +100,7 @@ import { isNeverOrVoidType, isSubscriptionId, isXmlContentType, + twoParamsEquivalent, updateWithApiVersionInformation, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; @@ -1353,6 +1354,12 @@ 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(x, 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 90789b95ab..6363ac9bf7 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4787,18 +4787,19 @@ describe("typespec-client-generator-core: decorators", () => { const blobNameOpParam = downloadOp.parameters[0]; strictEqual(blobNameOpParam.name, "blobName"); strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); - - const blobNameCorresponding = blobNameOpParam.correspondingMethodParams[0]; - strictEqual(blobNameCorresponding.name, "blobName"); - strictEqual(blobNameCorresponding.onClient, true); - strictEqual(blobName.type.kind, "string"); + 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; } } @@ -4820,12 +4821,54 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(client.initialization.properties[0].kind, "endpoint"); const methods = client.methods; - strictEqual(methods.length, 1); - const getBlobClient = methods[0]; + 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, 1); + strictEqual(mainClientDownload.parameters[0].name, "blobName"); + strictEqual(mainClientDownload.parameters[0].onClient, false); + + const getBlobClient = methods.find((x) => x.kind === "clientaccessor"); + ok(getBlobClient); strictEqual(getBlobClient.kind, "clientaccessor"); strictEqual(getBlobClient.name, "getBlobClient"); strictEqual(getBlobClient.parameters.length, 1); - strictEqual(getBlobClient.parameters[0].name, "blobName"); + 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.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, 1); + strictEqual(download.parameters[0].name, "blobName"); + strictEqual(download.parameters[0].onClient, true); + + 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); }); }); }); From 6a4667bc9e6ce7c2dae94d0e31203c8e55086e70 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Aug 2024 13:26:44 -0400 Subject: [PATCH 11/24] add more tests --- .../test/decorators.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 6363ac9bf7..82ee5b7f06 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4870,5 +4870,64 @@ describe("typespec-client-generator-core: decorators", () => { 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, 2); + + const blobNameParam = download.parameters.find((x) => x.name === "blobName"); + ok(blobNameParam); + strictEqual(blobNameParam.onClient, true); + + 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); + }); }); }); From b8dca8f187c5470bb926f045a3c78555de18926a Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Aug 2024 14:10:23 -0400 Subject: [PATCH 12/24] format --- .../test/decorators.test.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 82ee5b7f06..e6f848e053 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4929,5 +4929,70 @@ describe("typespec-client-generator-core: decorators", () => { 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, 2); + + const blobNameParam = download.parameters.find((x) => x.name === "blobName"); + ok(blobNameParam); + strictEqual(blobNameParam.onClient, true); + + const containerNameParam = download.parameters.find((x) => x.name === "containerName"); + ok(containerNameParam); + strictEqual(containerNameParam.onClient, true); + + 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); + }); }); }); From 0f5fe392cfb8a191cf3bfebcaea7ff96c5dce0e1 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 27 Aug 2024 17:32:44 -0400 Subject: [PATCH 13/24] add tests for og groups with parent clients --- .../src/decorators.ts | 53 +++++++++++++------ .../typespec-client-generator-core/src/lib.ts | 6 +++ .../test/decorators.test.ts | 24 +++++++++ 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index e2f1ece5c6..a0c404bee7 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -90,13 +90,14 @@ function getScopedDecoratorData( return retval[AllScopes]; // in this case it applies to all languages } -function listScopedDecoratorData(context: TCGCContext, key: symbol): any[] { +function listScopedDecoratorData(context: {program: Program}, key: symbol, emitterName?: string): any[] { + const scope = emitterName || AllScopes; const retval = [...context.program.stateMap(key).values()]; return retval .filter((targetEntry) => { - return targetEntry[context.emitterName] || targetEntry[AllScopes]; + return targetEntry[scope] || targetEntry[AllScopes]; }) - .flatMap((targetEntry) => targetEntry[context.emitterName] ?? targetEntry[AllScopes]); + .flatMap((targetEntry) => targetEntry[scope] ?? targetEntry[AllScopes]); } function setScopedDecoratorData( @@ -231,10 +232,10 @@ export function getClient( return undefined; } -function hasExplicitClientOrOperationGroup(context: TCGCContext): boolean { +function hasExplicitClientOrOperationGroup(context: {program: Program, emitterName?: string}): boolean { return ( - listScopedDecoratorData(context, clientKey).length > 0 || - listScopedDecoratorData(context, operationGroupKey).length > 0 + listScopedDecoratorData(context, clientKey, context.emitterName).length > 0 || + listScopedDecoratorData(context, operationGroupKey, context.emitterName).length > 0 ); } @@ -308,7 +309,7 @@ function getClientsWithVersioning(context: TCGCContext, clients: SdkClient[]): S export function listClients(context: TCGCContext): SdkClient[] { if (context.__rawClients) return context.__rawClients; - const explicitClients = [...listScopedDecoratorData(context, clientKey)]; + const explicitClients = [...listScopedDecoratorData(context, clientKey, context.emitterName)]; if (explicitClients.length > 0) { context.__rawClients = getClientsWithVersioning(context, explicitClients); if (context.__rawClients.some((client) => isArm(client.service))) { @@ -380,6 +381,17 @@ export const $operationGroup: OperationGroupDecorator = ( ); }; +function isOperationGroupWithNoExplicitDecorator(type: Namespace | Interface): boolean { + // if there is no explicit client, we will treat non-client namespaces and all interfaces as operation group + if (type.kind === "Interface" && !isTemplateDeclaration(type)) { + return true; + } + if (type.kind === "Namespace" && !type.decorators.some((t) => t.decorator.name === "$service")) { + return true; + } + return false; +} + /** * Check a namespace or interface is an operation group. * @param context TCGCContext @@ -390,14 +402,7 @@ export function isOperationGroup(context: TCGCContext, type: Namespace | Interfa if (hasExplicitClientOrOperationGroup(context)) { return getScopedDecoratorData(context, operationGroupKey, type) !== undefined; } - // if there is no explicit client, we will treat non-client namespaces and all interfaces as operation group - if (type.kind === "Interface" && !isTemplateDeclaration(type)) { - return true; - } - if (type.kind === "Namespace" && !type.decorators.some((t) => t.decorator.name === "$service")) { - return true; - } - return false; + return isOperationGroupWithNoExplicitDecorator(type); } /** * Check an operation is in an operation group. @@ -982,12 +987,30 @@ export const $useSystemTextJsonConverter: DecoratorFunction = ( const clientInitializationKey = createStateSymbol("clientInitialization"); +function isOperationGroupNoScopeCheck(context: {program: Program}, target: Namespace | Interface): boolean { + if (hasExplicitClientOrOperationGroup(context)) { + return Boolean(context.program.stateMap(operationGroupKey).get(target)); + } + return isOperationGroupWithNoExplicitDecorator(target); +} + export const $clientInitialization: ClientInitializationDecorator = ( context: DecoratorContext, target: Namespace | Interface, options: Model, scope?: LanguageScopes ) => { + if (isOperationGroupNoScopeCheck(context, target)) { + if (target.namespace && context.program.stateMap(clientInitializationKey).get(target.namespace) === options) { + setScopedDecoratorData(context, $override, clientInitializationKey, target, options, scope); + } else { + reportDiagnostic(context.program, { + code: "assigning-public-params-to-internal-client", + format: { name: target.name }, + target: context.decoratorTarget, + }); + } + } setScopedDecoratorData(context, $override, clientInitializationKey, target, options, scope); }; diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index 552ad38d26..2495281a4a 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -210,6 +210,12 @@ export const $lib = createTypeSpecLibrary({ default: `@usage override conflicts with the usage calculated from operation or other @usage override.`, }, }, + "assigning-public-params-to-internal-client": { + severity: "error", + messages: { + default: `Cannot assign client parameters to an internal client if they're not on the parent client`, + }, + }, }, }); diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 472d9942b7..e671531b1b 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4036,5 +4036,29 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(containerNameOpParam.correspondingMethodParams.length, 1); strictEqual(containerNameOpParam.correspondingMethodParams[0], containerName); }); + + it("@operationGroup without same model on parent client", async () => { + const diagnostics = await runner.diagnose( + ` + @service + namespace MyService; + + @operationGroup + interface MyInterface { + op download(@path blobName: string, @path containerName: string): void; + } + + model MyClientInitialization { + blobName: string; + containerName: string; + } + + @@clientInitialization(MyService.MyInterface, MyClientInitialization); + ` + ); + expectDiagnostics(diagnostics, { + code: "@azure-tools/typespec-client-generator-core/assigning-public-params-to-internal-client", + }); + }); }); }); From 61ab4f1c8be31a4bc7b46743143ca26c3ba6c5a2 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Aug 2024 14:51:57 -0400 Subject: [PATCH 14/24] format --- .../src/decorators.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index a0c404bee7..e9580becdc 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -90,7 +90,11 @@ function getScopedDecoratorData( return retval[AllScopes]; // in this case it applies to all languages } -function listScopedDecoratorData(context: {program: Program}, key: symbol, emitterName?: string): any[] { +function listScopedDecoratorData( + context: { program: Program }, + key: symbol, + emitterName?: string +): any[] { const scope = emitterName || AllScopes; const retval = [...context.program.stateMap(key).values()]; return retval @@ -232,7 +236,10 @@ export function getClient( return undefined; } -function hasExplicitClientOrOperationGroup(context: {program: Program, emitterName?: string}): boolean { +function hasExplicitClientOrOperationGroup(context: { + program: Program; + emitterName?: string; +}): boolean { return ( listScopedDecoratorData(context, clientKey, context.emitterName).length > 0 || listScopedDecoratorData(context, operationGroupKey, context.emitterName).length > 0 @@ -987,7 +994,10 @@ export const $useSystemTextJsonConverter: DecoratorFunction = ( const clientInitializationKey = createStateSymbol("clientInitialization"); -function isOperationGroupNoScopeCheck(context: {program: Program}, target: Namespace | Interface): boolean { +function isOperationGroupNoScopeCheck( + context: { program: Program }, + target: Namespace | Interface +): boolean { if (hasExplicitClientOrOperationGroup(context)) { return Boolean(context.program.stateMap(operationGroupKey).get(target)); } @@ -1001,7 +1011,10 @@ export const $clientInitialization: ClientInitializationDecorator = ( scope?: LanguageScopes ) => { if (isOperationGroupNoScopeCheck(context, target)) { - if (target.namespace && context.program.stateMap(clientInitializationKey).get(target.namespace) === options) { + if ( + target.namespace && + context.program.stateMap(clientInitializationKey).get(target.namespace) === options + ) { setScopedDecoratorData(context, $override, clientInitializationKey, target, options, scope); } else { reportDiagnostic(context.program, { From 29eb08d0dcc0cad7cb1973db4952c9f658eff0ff Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Aug 2024 15:02:40 -0400 Subject: [PATCH 15/24] only throw error if explicitly decorated with @operationGroup --- .../typespec-client-generator-core/src/decorators.ts | 12 +----------- .../test/decorators.test.ts | 2 ++ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index e9580becdc..2d5f313a0c 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -994,23 +994,13 @@ export const $useSystemTextJsonConverter: DecoratorFunction = ( const clientInitializationKey = createStateSymbol("clientInitialization"); -function isOperationGroupNoScopeCheck( - context: { program: Program }, - target: Namespace | Interface -): boolean { - if (hasExplicitClientOrOperationGroup(context)) { - return Boolean(context.program.stateMap(operationGroupKey).get(target)); - } - return isOperationGroupWithNoExplicitDecorator(target); -} - export const $clientInitialization: ClientInitializationDecorator = ( context: DecoratorContext, target: Namespace | Interface, options: Model, scope?: LanguageScopes ) => { - if (isOperationGroupNoScopeCheck(context, target)) { + if (context.program.stateMap(operationGroupKey).get(target)) { if ( target.namespace && context.program.stateMap(clientInitializationKey).get(target.namespace) === options diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index e671531b1b..00ea9c2474 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -3859,6 +3859,7 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(clients.length, 1); const client = clients[0]; strictEqual(client.name, "StorageClient"); + strictEqual(client.initialization.access, "public"); strictEqual(client.initialization.properties.length, 1); strictEqual(client.initialization.properties[0].kind, "endpoint"); @@ -3887,6 +3888,7 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(blobClient.kind, "client"); strictEqual(blobClient.name, "BlobClient"); + strictEqual(blobClient.initialization.access, "public"); strictEqual(blobClient.initialization.properties.length, 2); ok(blobClient.initialization.properties.find((x) => x.kind === "endpoint")); From 9137d21ad4bf9f3eb11ee7cbbdd31e27767242e4 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Aug 2024 16:04:40 -0400 Subject: [PATCH 16/24] add alias decorator --- .../src/decorators.ts | 15 +++++++++++++++ .../typespec-client-generator-core/src/http.ts | 7 +++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 2d5f313a0c..b6946c9b3b 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -1036,3 +1036,18 @@ export function getClientInitialization( properties: initializationProps, }; } + +const aliasKey = createStateSymbol("alias"); + +export const $alias: DecoratorFunction = ( + context: DecoratorContext, + original: ModelProperty, + alias: string, + scope?: LanguageScopes +) => { + setScopedDecoratorData(context, $alias, aliasKey, original, alias, scope); +}; + +export function getAlias(context: TCGCContext, original: ModelProperty): string | undefined { + return getScopedDecoratorData(context, aliasKey, original); +} diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 576b96b20d..2ef512328f 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 { getAlias } from "./decorators.js"; import { CollectionFormat, SdkBodyParameter, @@ -506,8 +507,10 @@ export function getCorrespondingMethodParams( context.__clientToParameters.set(operationLocation, clientParams); } - const correspondingClientParams = clientParams.filter((x) => - twoParamsEquivalent(x, serviceParam) + const correspondingClientParams = clientParams.filter( + (x) => + twoParamsEquivalent(x, serviceParam) || + (x.__raw?.kind === "ModelProperty" && getAlias(context, x.__raw) === serviceParam.name) ); if (correspondingClientParams.length > 0) return diagnostics.wrap(correspondingClientParams); From a04c95bd7f49974c1c900bc3c4fb66a2c3873125 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 29 Aug 2024 15:06:44 -0400 Subject: [PATCH 17/24] introduce @paramAlias --- .../reference/decorators.md | 40 ++++++++++++++ .../reference/index.mdx | 1 + .../typespec-client-generator-core/README.md | 41 +++++++++++++++ .../Azure.ClientGenerator.Core.ts | 32 ++++++++++++ .../lib/decorators.tsp | 31 +++++++++++ .../src/decorators.ts | 13 ++--- .../src/http.ts | 6 +-- .../src/internal-utils.ts | 17 +++++- .../src/tsp-index.ts | 2 + .../src/types.ts | 11 ++-- .../test/decorators.test.ts | 52 +++++++++++++++++++ 11 files changed, 227 insertions(+), 19 deletions(-) diff --git a/docs/libraries/typespec-client-generator-core/reference/decorators.md b/docs/libraries/typespec-client-generator-core/reference/decorators.md index 12692f627c..43dc89d70f 100644 --- a/docs/libraries/typespec-client-generator-core/reference/decorators.md +++ b/docs/libraries/typespec-client-generator-core/reference/decorators.md @@ -417,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 da9791c82b..7d32ab66b3 100644 --- a/docs/libraries/typespec-client-generator-core/reference/index.mdx +++ b/docs/libraries/typespec-client-generator-core/reference/index.mdx @@ -47,6 +47,7 @@ npm install --save-peer @azure-tools/typespec-client-generator-core - [`@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 e0428c6bea..d3d6ea98ea 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -20,6 +20,7 @@ npm install @azure-tools/typespec-client-generator-core - [`@flattenProperty`](#@flattenproperty) - [`@operationGroup`](#@operationgroup) - [`@override`](#@override) +- [`@paramAlias`](#@paramalias) - [`@protocolAPI`](#@protocolapi) - [`@usage`](#@usage) - [`@useSystemTextJsonConverter`](#@usesystemtextjsonconverter) @@ -431,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 eeac5599e0..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 @@ -463,6 +463,37 @@ export type ClientInitializationDecorator = ( 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; @@ -475,4 +506,5 @@ export type AzureClientGeneratorCoreDecorators = { 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 9a4216db27..6c4fdbfde6 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -448,3 +448,34 @@ extern dec clientInitialization( 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 b6946c9b3b..f72da8552f 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -39,6 +39,7 @@ import { ConvenientAPIDecorator, FlattenPropertyDecorator, OperationGroupDecorator, + ParamAliasDecorator, ProtocolAPIDecorator, UsageDecorator, } from "../generated-defs/Azure.ClientGenerator.Core.js"; @@ -1037,17 +1038,17 @@ export function getClientInitialization( }; } -const aliasKey = createStateSymbol("alias"); +const paramAliasKey = createStateSymbol("paramAlias"); -export const $alias: DecoratorFunction = ( +export const paramAliasDecorator: ParamAliasDecorator = ( context: DecoratorContext, original: ModelProperty, - alias: string, + paramAlias: string, scope?: LanguageScopes ) => { - setScopedDecoratorData(context, $alias, aliasKey, original, alias, scope); + setScopedDecoratorData(context, paramAliasDecorator, paramAliasKey, original, paramAlias, scope); }; -export function getAlias(context: TCGCContext, original: ModelProperty): string | undefined { - return getScopedDecoratorData(context, aliasKey, original); +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 2ef512328f..2ea1195630 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -27,7 +27,7 @@ import { isQueryParam, } from "@typespec/http"; import { camelCase } from "change-case"; -import { getAlias } from "./decorators.js"; +import { getParamAlias } from "./decorators.js"; import { CollectionFormat, SdkBodyParameter, @@ -509,8 +509,8 @@ export function getCorrespondingMethodParams( const correspondingClientParams = clientParams.filter( (x) => - twoParamsEquivalent(x, serviceParam) || - (x.__raw?.kind === "ModelProperty" && getAlias(context, x.__raw) === serviceParam.name) + twoParamsEquivalent(context, x.__raw, serviceParam.__raw) || + (x.__raw?.kind === "ModelProperty" && getParamAlias(context, x.__raw) === serviceParam.name) ); if (correspondingClientParams.length > 0) return diagnostics.wrap(correspondingClientParams); diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 609573ef8f..1a8987a49e 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -49,6 +49,7 @@ import { isApiVersion, } from "./public-utils.js"; import { getClientTypeWithDiagnostics } from "./types.js"; +import { getParamAlias } from "./decorators.js"; export const AllScopes = Symbol.for("@azure-core/typespec-client-generator-core/all-scopes"); @@ -527,8 +528,11 @@ export function isXmlContentType(contentType: string): boolean { return regex.test(contentType); } -export function twoParamsEquivalent(param1: { name: string }, param2: { name: string }): boolean { - return param1.name === param2.name; +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. @@ -569,3 +573,12 @@ 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/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index 3732c1ee15..f54a26a046 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -8,6 +8,7 @@ import { $flattenProperty, $operationGroup, $override, + paramAliasDecorator, $protocolAPI, $usage, $useSystemTextJsonConverter, @@ -31,5 +32,6 @@ export const $decorators = { 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 049eb29a6c..f9148f63fd 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -99,6 +99,7 @@ import { isJsonContentType, isMultipartOperation, isNeverOrVoidType, + isOnClient, isSubscriptionId, isXmlContentType, twoParamsEquivalent, @@ -1225,13 +1226,7 @@ export function getSdkModelPropertyTypeBase( } const docWrapper = getDocHelper(context, type); const name = getPropertyNames(context, type)[0]; - const namespace = type.model?.namespace; - const onClient = - isSubscriptionId(context, type) || - isApiVersion(context, type) || - Boolean( - namespace && context.__clientToParameters.get(namespace)?.find((x) => x.name === type.name) - ); + const onClient = isOnClient(context, type); return diagnostics.wrap({ __raw: type, description: docWrapper.description, @@ -1391,7 +1386,7 @@ export function getSdkModelPropertyType( const clientParams = operation ? context.__clientToParameters.get(getLocationOfOperation(operation)) : undefined; - const correspondingClientParams = clientParams?.find((x) => twoParamsEquivalent(x, type)); + const correspondingClientParams = clientParams?.find((x) => twoParamsEquivalent(context, x.__raw, type)); if (correspondingClientParams) return diagnostics.wrap(correspondingClientParams); const base = diagnostics.pipe(getSdkModelPropertyTypeBase(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 00ea9c2474..03bf7e73bc 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4062,5 +4062,57 @@ describe("typespec-client-generator-core: decorators", () => { code: "@azure-tools/typespec-client-generator-core/assigning-public-params-to-internal-client", }); }); + + 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, 1); + + const downloadBlobNameParam = download.parameters.find((x) => x.name === "blobName"); + ok(downloadBlobNameParam); + strictEqual(downloadBlobNameParam.onClient, true); + + const upload = methods[1]; + strictEqual(upload.name, "upload"); + strictEqual(upload.kind, "basic"); + strictEqual(upload.parameters.length, 1); + + const uploadBlobNameParam = upload.parameters.find((x) => x.name === "blobName"); + ok(uploadBlobNameParam); + strictEqual(uploadBlobNameParam.onClient, true); + }); }); }); From 12dc1d17ec886ce993ed75930ff3ef7afcb1aecb Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 29 Aug 2024 15:07:47 -0400 Subject: [PATCH 18/24] format --- .../lib/decorators.tsp | 9 ++----- .../src/internal-utils.ts | 25 ++++++++++++++----- .../src/tsp-index.ts | 2 +- .../src/types.ts | 6 ++--- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 6c4fdbfde6..45435b5469 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -468,14 +468,9 @@ extern dec clientInitialization( * * @@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 -); - +extern dec paramAlias(original: ModelProperty, paramAlias: valueof string, scope?: valueof string); diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 1a8987a49e..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, @@ -49,7 +50,6 @@ import { isApiVersion, } from "./public-utils.js"; import { getClientTypeWithDiagnostics } from "./types.js"; -import { getParamAlias } from "./decorators.js"; export const AllScopes = Symbol.for("@azure-core/typespec-client-generator-core/all-scopes"); @@ -528,11 +528,19 @@ export function isXmlContentType(contentType: string): boolean { return regex.test(contentType); } -export function twoParamsEquivalent(context: TCGCContext, param1?: ModelProperty, param2?: ModelProperty): boolean { +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); + 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. @@ -576,9 +584,14 @@ export function getHttpBodySpreadModel(context: TCGCContext, type: Model): Model export function isOnClient(context: TCGCContext, type: ModelProperty): boolean { const namespace = type.model?.namespace; - return isSubscriptionId(context, type) || + return ( + isSubscriptionId(context, type) || isApiVersion(context, type) || Boolean( - namespace && context.__clientToParameters.get(namespace)?.find((x) => twoParamsEquivalent(context, x.__raw, type)) - ); + namespace && + context.__clientToParameters + .get(namespace) + ?.find((x) => twoParamsEquivalent(context, x.__raw, type)) + ) + ); } diff --git a/packages/typespec-client-generator-core/src/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index f54a26a046..488e2de60f 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -8,10 +8,10 @@ import { $flattenProperty, $operationGroup, $override, - paramAliasDecorator, $protocolAPI, $usage, $useSystemTextJsonConverter, + paramAliasDecorator, } from "./decorators.js"; export { $lib } from "./lib.js"; diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index f9148f63fd..82632b8cdf 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -100,7 +100,6 @@ import { isMultipartOperation, isNeverOrVoidType, isOnClient, - isSubscriptionId, isXmlContentType, twoParamsEquivalent, updateWithApiVersionInformation, @@ -113,7 +112,6 @@ import { getHttpOperationWithCache, getLibraryName, getPropertyNames, - isApiVersion, } from "./public-utils.js"; import { getVersions } from "@typespec/versioning"; @@ -1386,7 +1384,9 @@ export function getSdkModelPropertyType( const clientParams = operation ? context.__clientToParameters.get(getLocationOfOperation(operation)) : undefined; - const correspondingClientParams = clientParams?.find((x) => twoParamsEquivalent(context, x.__raw, type)); + const correspondingClientParams = clientParams?.find((x) => + twoParamsEquivalent(context, x.__raw, type) + ); if (correspondingClientParams) return diagnostics.wrap(correspondingClientParams); const base = diagnostics.pipe(getSdkModelPropertyTypeBase(context, type, operation)); From 1b3da09b7748e1ef3f38003fc88ce1633b30c400 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 30 Aug 2024 14:40:45 -0400 Subject: [PATCH 19/24] fix initialization access for inferred ogs --- packages/typespec-client-generator-core/src/package.ts | 4 +++- .../typespec-client-generator-core/test/decorators.test.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index b95e991345..5cbaec1f1c 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -332,10 +332,12 @@ function getSdkInitializationType( clientParams = []; context.__clientToParameters.set(client.type, clientParams); } + 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`; @@ -346,7 +348,7 @@ function getSdkInitializationType( properties: [], name, isGeneratedName: true, - access: client.kind === "SdkClient" ? "public" : "internal", + access, usage: UsageFlags.Input, crossLanguageDefinitionId: `${getNamespaceFullName(client.service.namespace!)}.${name}`, apiVersions: context.__tspTypeToApiVersions.get(client.type)!, diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 03bf7e73bc..4d403438c6 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -3888,7 +3888,7 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(blobClient.kind, "client"); strictEqual(blobClient.name, "BlobClient"); - strictEqual(blobClient.initialization.access, "public"); + strictEqual(blobClient.initialization.access, "internal"); strictEqual(blobClient.initialization.properties.length, 2); ok(blobClient.initialization.properties.find((x) => x.kind === "endpoint")); From c6041e6bb8abad1072404e42cb9c776dcb127925 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 30 Aug 2024 15:06:11 -0400 Subject: [PATCH 20/24] fix initialization access for inferred ogs --- .../src/decorators.ts | 9 +++++++-- .../test/decorators.test.ts | 14 +++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index f72da8552f..6c8a7db6b4 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -1001,10 +1001,15 @@ export const $clientInitialization: ClientInitializationDecorator = ( options: Model, scope?: LanguageScopes ) => { - if (context.program.stateMap(operationGroupKey).get(target)) { + let isOg = isOperationGroupWithNoExplicitDecorator(target); + if (hasExplicitClientOrOperationGroup(context)) { + isOg = context.program.stateMap(operationGroupKey).get(target) !== undefined; + } + if (isOg) { if ( target.namespace && - context.program.stateMap(clientInitializationKey).get(target.namespace) === options + context.program.stateMap(clientInitializationKey).get(target.namespace) && + context.program.stateMap(clientInitializationKey).get(target.namespace)[AllScopes] === options ) { setScopedDecoratorData(context, $override, clientInitializationKey, target, options, scope); } else { diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 4d403438c6..982edbd244 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -3847,11 +3847,12 @@ describe("typespec-client-generator-core: decorators", () => { } `, ` - model BlobClientInitialization { + model ClientInitialization { blobName: string }; - @@clientInitialization(StorageClient.BlobClient, BlobClientInitialization); + @@clientInitialization(StorageClient, ClientInitialization); + @@clientInitialization(StorageClient.BlobClient, ClientInitialization); ` ); const sdkPackage = runner.context.sdkPackage; @@ -3860,8 +3861,11 @@ describe("typespec-client-generator-core: decorators", () => { const client = clients[0]; strictEqual(client.name, "StorageClient"); strictEqual(client.initialization.access, "public"); - strictEqual(client.initialization.properties.length, 1); - strictEqual(client.initialization.properties[0].kind, "endpoint"); + 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); @@ -3871,7 +3875,7 @@ describe("typespec-client-generator-core: decorators", () => { ok(mainClientDownload); strictEqual(mainClientDownload.parameters.length, 1); strictEqual(mainClientDownload.parameters[0].name, "blobName"); - strictEqual(mainClientDownload.parameters[0].onClient, false); + strictEqual(mainClientDownload.parameters[0].onClient, true); const getBlobClient = methods.find((x) => x.kind === "clientaccessor"); ok(getBlobClient); From 657b17df6f258a172ad913198b6fc99d3c6052b5 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 30 Aug 2024 16:56:28 -0400 Subject: [PATCH 21/24] format --- .../src/package.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index f76cfb3aa8..e6038d0c82 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -229,11 +229,7 @@ function getSdkBasicServiceMethod // we have to calculate apiVersions first, so that the information is put // in __tspTypeToApiVersions before we call parameters since method wraps parameter const operationLocation = getLocationOfOperation(operation); - const apiVersions = getAvailableApiVersions( - context, - operation, - operationLocation, - ); + const apiVersions = getAvailableApiVersions(context, operation, operationLocation); let clientParams = context.__clientToParameters.get(operationLocation); if (!clientParams) { @@ -250,11 +246,17 @@ function getSdkBasicServiceMethod if (sdkMethodParam.onClient) { const operationLocation = getLocationOfOperation(operation); if (sdkMethodParam.isApiVersionParam) { - if (!context.__clientToParameters.get(operationLocation)?.find((x) => x.isApiVersionParam)) { + if ( + !context.__clientToParameters.get(operationLocation)?.find((x) => x.isApiVersionParam) + ) { clientParams.push(sdkMethodParam); } } else if (isSubscriptionId(context, param)) { - if (!context.__clientToParameters.get(operationLocation)?.find((x) => isSubscriptionId(context, x))) { + if ( + !context.__clientToParameters + .get(operationLocation) + ?.find((x) => isSubscriptionId(context, x)) + ) { clientParams.push(sdkMethodParam); } } From ad2d00184d89d3f36fba57ab5b1253b06bafbb76 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 3 Sep 2024 13:11:06 -0400 Subject: [PATCH 22/24] remove check for og --- .../typespec-client-generator-core/README.md | 4 +- .../src/decorators.ts | 70 ++++++------------- .../typespec-client-generator-core/src/lib.ts | 6 -- .../test/decorators.test.ts | 49 +++++++++++-- 4 files changed, 69 insertions(+), 60 deletions(-) diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index d3d6ea98ea..da6f4c5f97 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -468,8 +468,8 @@ model MyServiceClientOptions { @@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. +// The generated client will have `blob` on it. We will also +// elevate the existing `blobName` parameter to the client level. ``` #### `@protocolAPI` diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 08ca77f5f6..9c523755db 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -92,18 +92,13 @@ function getScopedDecoratorData( return retval[AllScopes]; // in this case it applies to all languages } -function listScopedDecoratorData( - context: { program: Program }, - key: symbol, - emitterName?: string -): any[] { - const scope = emitterName || AllScopes; +function listScopedDecoratorData(context: TCGCContext, key: symbol): any[] { const retval = [...context.program.stateMap(key).values()]; return retval .filter((targetEntry) => { - return targetEntry[scope] || targetEntry[AllScopes]; + return targetEntry[context.emitterName] || targetEntry[AllScopes]; }) - .flatMap((targetEntry) => targetEntry[scope] ?? targetEntry[AllScopes]); + .flatMap((targetEntry) => targetEntry[context.emitterName] ?? targetEntry[AllScopes]); } function setScopedDecoratorData( @@ -238,13 +233,10 @@ export function getClient( return undefined; } -function hasExplicitClientOrOperationGroup(context: { - program: Program; - emitterName?: string; -}): boolean { +function hasExplicitClientOrOperationGroup(context: TCGCContext): boolean { return ( - listScopedDecoratorData(context, clientKey, context.emitterName).length > 0 || - listScopedDecoratorData(context, operationGroupKey, context.emitterName).length > 0 + listScopedDecoratorData(context, clientKey).length > 0 || + listScopedDecoratorData(context, operationGroupKey).length > 0 ); } @@ -318,7 +310,7 @@ function getClientsWithVersioning(context: TCGCContext, clients: SdkClient[]): S export function listClients(context: TCGCContext): SdkClient[] { if (context.__rawClients) return context.__rawClients; - const explicitClients = [...listScopedDecoratorData(context, clientKey, context.emitterName)]; + const explicitClients = [...listScopedDecoratorData(context, clientKey)]; if (explicitClients.length > 0) { context.__rawClients = getClientsWithVersioning(context, explicitClients); if (context.__rawClients.some((client) => isArm(client.service))) { @@ -390,17 +382,6 @@ export const $operationGroup: OperationGroupDecorator = ( ); }; -function isOperationGroupWithNoExplicitDecorator(type: Namespace | Interface): boolean { - // if there is no explicit client, we will treat non-client namespaces and all interfaces as operation group - if (type.kind === "Interface" && !isTemplateDeclaration(type)) { - return true; - } - if (type.kind === "Namespace" && !type.decorators.some((t) => t.decorator.name === "$service")) { - return true; - } - return false; -} - /** * Check a namespace or interface is an operation group. * @param context TCGCContext @@ -411,7 +392,14 @@ export function isOperationGroup(context: TCGCContext, type: Namespace | Interfa if (hasExplicitClientOrOperationGroup(context)) { return getScopedDecoratorData(context, operationGroupKey, type) !== undefined; } - return isOperationGroupWithNoExplicitDecorator(type); + // if there is no explicit client, we will treat non-client namespaces and all interfaces as operation group + if (type.kind === "Interface" && !isTemplateDeclaration(type)) { + return true; + } + if (type.kind === "Namespace" && !type.decorators.some((t) => t.decorator.name === "$service")) { + return true; + } + return false; } /** * Check an operation is in an operation group. @@ -1015,26 +1003,14 @@ export const $clientInitialization: ClientInitializationDecorator = ( options: Model, scope?: LanguageScopes ) => { - let isOg = isOperationGroupWithNoExplicitDecorator(target); - if (hasExplicitClientOrOperationGroup(context)) { - isOg = context.program.stateMap(operationGroupKey).get(target) !== undefined; - } - if (isOg) { - if ( - target.namespace && - context.program.stateMap(clientInitializationKey).get(target.namespace) && - context.program.stateMap(clientInitializationKey).get(target.namespace)[AllScopes] === options - ) { - setScopedDecoratorData(context, $override, clientInitializationKey, target, options, scope); - } else { - reportDiagnostic(context.program, { - code: "assigning-public-params-to-internal-client", - format: { name: target.name }, - target: context.decoratorTarget, - }); - } - } - setScopedDecoratorData(context, $override, clientInitializationKey, target, options, scope); + setScopedDecoratorData( + context, + $clientInitialization, + clientInitializationKey, + target, + options, + scope + ); }; export function getClientInitialization( diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index 2495281a4a..552ad38d26 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -210,12 +210,6 @@ export const $lib = createTypeSpecLibrary({ default: `@usage override conflicts with the usage calculated from operation or other @usage override.`, }, }, - "assigning-public-params-to-internal-client": { - severity: "error", - messages: { - default: `Cannot assign client parameters to an internal client if they're not on the parent client`, - }, - }, }, }); diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 931403796c..7ef450b35c 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4070,8 +4070,8 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(containerNameOpParam.correspondingMethodParams[0], containerName); }); - it("@operationGroup without same model on parent client", async () => { - const diagnostics = await runner.diagnose( + it("@operationGroup with same model on parent client", async () => { + await runner.compile( ` @service namespace MyService; @@ -4086,12 +4086,51 @@ describe("typespec-client-generator-core: decorators", () => { containerName: string; } + @@clientInitialization(MyService, MyClientInitialization); @@clientInitialization(MyService.MyInterface, MyClientInitialization); ` ); - expectDiagnostics(diagnostics, { - code: "@azure-tools/typespec-client-generator-core/assigning-public-params-to-internal-client", - }); + 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("@paramAlias", async () => { From e456785e314cb66e45a35b08e95768a7ad9eaa92 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 3 Sep 2024 13:25:27 -0400 Subject: [PATCH 23/24] add test for client reorganization --- .../test/decorators.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 7ef450b35c..c04c7c36af 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4133,6 +4133,90 @@ describe("typespec-client-generator-core: decorators", () => { 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( ` From 86296421ca35a62eb1868cb0d42eeb38b342532d Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 3 Sep 2024 16:21:44 -0400 Subject: [PATCH 24/24] rebuild --- packages/typespec-client-generator-core/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index da6f4c5f97..d3d6ea98ea 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -468,8 +468,8 @@ model MyServiceClientOptions { @@clientInitialization(MyService, MyServiceClientOptions) @@paramAlias(MyServiceClientOptions.blob, "blobName") -// The generated client will have `blob` on it. We will also -// elevate the existing `blobName` parameter to the client level. +// The generated client will have `blobName` on it. We will also +// elevate the existing `blob` parameter to the client level. ``` #### `@protocolAPI`