From dea1bf0515944847dcad9e70d84b376e1b24bfcc Mon Sep 17 00:00:00 2001 From: iscai-msft <43154838+iscai-msft@users.noreply.github.com> Date: Fri, 31 Jan 2025 17:22:13 -0500 Subject: [PATCH] Backmerge/release/january 2025 2025 01 31 (#2160) Co-authored-by: Chenjie Shi Co-authored-by: Weidong Xu Co-authored-by: iscai-msft --- .../CHANGELOG.md | 11 + .../typespec-client-generator-core/README.md | 18 +- .../design-docs/client.md | 564 +++++++++++++ .../Azure.ClientGenerator.Core.ts | 10 +- .../lib/decorators.tsp | 41 +- .../package.json | 2 +- .../src/decorators.ts | 122 ++- .../src/interfaces.ts | 153 ++-- .../typespec-client-generator-core/src/lib.ts | 12 +- .../src/package.ts | 136 +++- .../test/decorators.test.ts | 495 ------------ .../decorators/client-initialization.test.ts | 752 ++++++++++++++++++ .../test/packages/client-scenario.test.ts | 428 ++++++++++ .../test/packages/client.test.ts | 143 ++-- .../get-http-operation-parameter.test.ts | 4 +- .../reference/data-types.md | 59 ++ .../reference/decorators.md | 18 +- .../reference/index.mdx | 4 + 18 files changed, 2281 insertions(+), 691 deletions(-) create mode 100644 packages/typespec-client-generator-core/design-docs/client.md create mode 100644 packages/typespec-client-generator-core/test/decorators/client-initialization.test.ts create mode 100644 packages/typespec-client-generator-core/test/packages/client-scenario.test.ts create mode 100644 website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md diff --git a/packages/typespec-client-generator-core/CHANGELOG.md b/packages/typespec-client-generator-core/CHANGELOG.md index 352e836bec..d6c91fa717 100644 --- a/packages/typespec-client-generator-core/CHANGELOG.md +++ b/packages/typespec-client-generator-core/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log - @azure-tools/typespec-client-generator-core +## 0.50.3 + +### Bug Fixes + +- [#2027](https://github.com/Azure/typespec-azure/pull/2027) Change `@clientInitialization` decorator's `options` parameter to `ClientInitializationOptions` type. The options now could set how to initialize the client. Though the implementation could support backward compatibility, it's better to have all specs that use this decorator change from `@clientInitialization(CustomizedOption)` to `@clientInitialization({parameters: CustomizedOption})`. A new helper `getClientInitializationOptions` is added for getting the new `ClientInitializationOptions` info from the `@clientInitialization` decorator. +- [#2027](https://github.com/Azure/typespec-azure/pull/2027) Add new `children` property to `SdkClientType` to include all the sub client belong to that client. +- [#2027](https://github.com/Azure/typespec-azure/pull/2027) Add `clientInitialization` property to `SdkClientType`. Its type is `SdkClientInitializationType` which includes the initialization parameters and how to initialize the client. +- [#2027](https://github.com/Azure/typespec-azure/pull/2027) Deprecate `initialization` property of `SdkClientType`. Use `init.paramters` of `SdkClientType` instead. +- [#2027](https://github.com/Azure/typespec-azure/pull/2027) Deprecate `SdkClientAccessor` type. Use `parent` and `children` property from `SdkClientType` to find client hierarchy instead. + + ## 0.50.2 ### Bug Fixes diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index 558b5203bc..e204c482d7 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -354,10 +354,10 @@ interface MyInterface {} #### `@clientInitialization` -Client parameters you would like to add to the client. By default, we apply endpoint, credential, and api-version parameters. If you add clientInitialization, we will append those to the default list of parameters. +Customize the client initialization way. ```typespec -@Azure.ClientGenerator.Core.clientInitialization(options: Model, scope?: valueof string) +@Azure.ClientGenerator.Core.clientInitialization(options: Azure.ClientGenerator.Core.ClientInitializationOptions, scope?: valueof string) ``` ##### Target @@ -366,10 +366,10 @@ Client parameters you would like to add to the client. By default, we apply endp ##### 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.
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | +| Name | Type | Description | +| ------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| options | [`ClientInitializationOptions`](#clientinitializationoptions) | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters.
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -386,9 +386,9 @@ 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. +@@clientInitialization(MyService, {parameters: MyServiceClientOptions}) +// The generated client will have `blobName` on its initialization method. We will also +// elevate the existing `blobName` parameter from method level to client level. ``` #### `@clientName` diff --git a/packages/typespec-client-generator-core/design-docs/client.md b/packages/typespec-client-generator-core/design-docs/client.md new file mode 100644 index 0000000000..ca720b12fd --- /dev/null +++ b/packages/typespec-client-generator-core/design-docs/client.md @@ -0,0 +1,564 @@ +# TCGC client related type design + +We have many client related types, decorators and concepts in TCGC and some of them are implicit, which makes author hard to know how to generate a needed client. This doc aims to revisit all client related things in TCGC and try to have a consolidated design output. + +## User scenario + +1. Single client + +```Python +client = SingleClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client.do_something() +``` + +2. Client with sub client and sub client could be initialized by parent client + +- Sub client has same initialization parameters with parent client + +ARM services always follow this pattern. + +```python +client = TestClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client.do_something() + +sub_client = client.sub_client() +sub_client.do_something() +``` + +- Sub client has additional initialization parameter than parent client + +```python +client = TestClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client.do_something() + +sub_client = client.sub_client(sub_name="sub") +sub_client.do_something() +``` + +3. Client with sub client and sub client could be initialized both by parent and individually + +Storage, container registry, etc., could fit this scenario. + +- Sub client has same initialization parameters with parent client + +```python +client = TestClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client.do_something() + +sub_client = client.sub_client() +sub_client.do_something() + +sub_client = TestSubClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +sub_client.do_something() +``` + +- Sub client has additional initialization parameter than parent client + +```python +client = TestClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client.do_something() + +sub_client = client.sub_client(sub_name="sub") +sub_client.do_something() + +sub_client = TestSubClient(endpoint="endpoint", credential=AzureKeyCredential("key"), sub_name="sub") +sub_client.do_something() +``` + +## TCGC types and decorators for client concept + +### Client structure + +The entrance of TCGC is `SdkPackage` which represents a complete package and includes clients, models, etc. The clients depend on the combination usage of namespace, interface, `@service`, `@client`. + +If there is no explicitly defined `@client`, then every namespaces with `@service` will be a client. The nested namespaces and interfaces under that namespace will be a sub client with hierarchy. + +- Example 1: + +```typespec +@service({ + title: "Pet Store", +}) +namespace PetStore { + interface Dogs { + feed(): void; + pet(): void; + } + + namespace Cats { + op feed(): void; + op pet(): void; + } +} + +@service({ + title: "Toy Store", +}) +namespace ToyStore { + interface Dolls { + price(): void; + buy(): void; + } + + namespace Cars { + op price(): void; + op buy(): void; + } +} +``` + +The above tsp gets two root clients: `PetStoreClient` and `ToyStoreClient` (naming logic is ensuring suffix with `Client`). The former has two child clients: `Dogs` and `Cats`. The later has two child clients: `Dolls` and `Cars`. + +If there is any `@client` definition, then each top level `@client` will be a client and each nested `@client` will be a sub client with hierarchy. + +Example 2: + +```typespec +@service({ + title: "Pet Store", +}) +namespace PetStore { + interface Dogs { + feed(): void; + pet(): void; + } + + namespace Cats { + op feed(): void; + op pet(): void; + } +} + +@client({ + name: "DogsClient", + service: PetStore, +}) +namespace DogsClient { + @client + interface Feed { + feed is PetStore.Dogs.feed; + } + + @client + interface Pet { + pet is PetStore.Dogs.pet; + } +} + +@client({ + name: "CatsClient", + service: PetStore, +}) +namespace CatsClient { + @client + interface Feed { + feed is PetStore.Cats.feed; + } + + @client + interface Pet { + pet is PetStore.Cats.pet; + } +} +``` + +The above tsp gets the two root clients: `DogsClient` and `CatsClient`. All of them has two child clients: `Feed` and `Pet`. + +### TCGC client types and client initialization + +TCGC client type (`SdkClientType`) has `subClients` and `parent` property to indicate the client hierarchy. +It also has `initialization` property of `SdkInitializationType` to indicate the initialization paramters and how to initialize the client. + +TCGC always puts the following things in initialization parameters: + +1. Endpoint parameter: it is converted from `@server` definition on the service the client belong to. +2. Credential parameter: it is converted from `@useAuth` definition on the service the client belong to. +3. API version parameter: if the service is versioned, then the API version parameter on method will be elevated to client. +4. Subscription ID parameter: if the service is an ARM service, then the subscription ID parameter on method will be elevated to client. + +The `SdkInitializationType` has `initializedBy` property. +The value could be `InitializedBy.parent (1)` (the client could be initialized by parent client), +`InitializedBy.individually (2)` (the client could be initialized individually) or `InitializedBy.parent | InitializedBy.individually (3)` (both). + +Default value of `initializedBy` for client is `InitializedBy.individually`, while `InitializedBy.parent` for sub client. + +For above example 1, you will get TCGC types like this: + +```yaml +clients: + - &a1 + kind: client + name: PetStoreClient + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: PetStoreClientOptions + isGeneratedName: true + initializedBy: individually + subClients: + - kind: client + name: Cats + parent: *a1 + subClients: [] + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: CatsOptions + isGeneratedName: true + initializedBy: parent + - kind: client + name: Dogs + parent: *a1 + subClients: [] + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: DogsOptions + isGeneratedName: true + initializedBy: parent + - &a2 + kind: client + name: ToyStoreClient + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: ToyStoreClientOptions + isGeneratedName: true + initializedBy: individually + subClients: + - kind: client + name: Cars + parent: *a2 + subClients: [] + subClients: [] + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: CarsOptions + isGeneratedName: true + initializedBy: parent + - kind: client + name: Dolls + parent: *a2 + subClients: [] + subClients: [] + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: DollsOptions + isGeneratedName: true + initializedBy: parent +``` + +For above example 2, you will get TCGC types like this: + +```yaml +clients: + - &a1 + kind: client + name: DogsClient + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: DogsClientOptions + isGeneratedName: true + initializedBy: individually + subClients: + - kind: client + name: Feed + parent: *a1 + subClients: [] + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: FeedOptions + isGeneratedName: true + initializedBy: parent + - kind: client + name: Pet + parent: *a1 + subClients: [] + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: PetOptions + isGeneratedName: true + initializedBy: parent + - &a2 + kind: client + name: CatsClient + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: CatsClientOptions + isGeneratedName: true + initializedBy: individually + subClients: + - kind: client + name: Feed + parent: *a2 + subClients: [] + subClients: [] + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: FeedOptions + isGeneratedName: true + initializedBy: parent + - kind: client + name: Pet + parent: *a2 + subClients: [] + subClients: [] + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: PetOptions + isGeneratedName: true + initializedBy: parent +``` + +### Customization for client initialization + +TCGC has `@clientInitialization` to do the customization for the client initialization parameters and initialization way. +Users could elevate any method's parameter to the clients, as well as change the way of how to initialize the client. + +Example 3: + +```typespec +@service({ + title: "My Service", +}) +namespace MyService { + interface InnerGroup { + upload(@path blobName: string): void; + } +} + +namespace MyCustomizations { + model InnerGroupClientOptions { + blobName: string; + } + + @@clientInitialization(MyService.InnerGroup, + { + parameters: InnerGroupClientOptions, + initializedBy: InitializedBy.parent | InitializedBy.individually, + } + ); +} +``` + +The above tsp gets client `MyServiceClient` and sub client `InnerGroup`. +The `InnerGroup`'s `initialization` model's properties contains a property named `blob`. +The method `upload` no longer has `blobName` parameter, its corresponding operation's parameter `blobName` is mapped to the client `blob` parameter. +The `InnerGroup` client could be initialized both by parent or individually. + +You will get TCGC types like this: + +```yaml +clients: + - &a3 + kind: client + name: MyServiceClient + subClients: + - kind: client + name: InnerGroup + methods: + - kind: basic + name: upload + parameters: [] + initialization: + kind: model + name: InnerGroupClientOptions + isGeneratedName: false + properties: + - kind: method + name: blobName + isGeneratedName: false + onClient: true + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + initializedBy: parent | individually + parent: *a3 + initialization: + kind: model + properties: + - kind: endpoint + name: endpoint + isGeneratedName: true + onClient: true + name: MyServiceClientOptions + initializedBy: individually +``` + +## Usage for all scenarios + +1. Single client + +```typespec +@service({ + title: "Scenario1", +}) +namespace SingleClient { + op do_something(): void; +} +``` + +2. Client with sub client and sub client could be initialized by parent client + +- Sub client has same initialization parameters with parent client + +```typespec +@service({ + title: "Scenario2", +}) +namespace TestClient { + op do_something(): void; + + interface SubClient { + do_something(): void; + } +} +``` + +- Sub client has additional initialization parameter than parent client + +```typespec +@service({ + title: "Scenario2", +}) +namespace TestClient { + op do_something(): void; + + interface SubClient { + do_something(subName: string): void; + } +} + +model SubClientOptions { + subName: string; +} + +@@clientInitialization(TestClient.SubClient, + { + parameters: SubClientOptions, + } +); +``` + +3. Client with sub client and sub client could be initialized both by parent and individually + +- Sub client has same initialization parameters with parent client + +```typespec +@service({ + title: "Scenario4", +}) +namespace TestClient { + op do_something(): void; + + interface SubClient { + do_something(): void; + } +} + +@@clientInitialization(TestClient.SubClient, + { + intializedBy: InitializedBy.individually | InitializedBy.parent, + } +); +``` + +- Sub client has additional initialization parameter than parent client + +```typespec +@service({ + title: "Scenario4", +}) +namespace TestClient { + op do_something(): void; + + interface SubClient { + do_something(subName: string): void; + } +} + +model SubClientOptions { + subName: string; +} + +@@clientInitialization(TestClient.SubClient, + { + parameters: SubClientOptions, + intializedBy: InitializedBy.individually | InitializedBy.parent, + } +); +``` + +## Changes needed with above design + +1. Change `@clientInitialization` decorator and add `initializedBy` property to `SdkInitializationType` + +- Change `@clientInitialization` decorator's `options` parameter to `ClientInitializationOptions` type to accept `initializedBy` setting. +- Add `clientInitialization` property to `SdkInitializationType`. +- Add check for `initializedBy`, root clients could only have `individually` value. + +2. Deprecate client accessor method. Add `subClients` property to `SdkClientType` and put all sub clients in this list. + +3. Consolidate `@client` and `@operationGroup` + +- Deprecate decorator `@operationGroup` and `SdkOperationGroup` type. +- Current explicitly `@operationGroup` could be migrated to `@client`. If `@client` is nested, then it is a sub client, will follow previous operation group default logic. +- Add `subClients`, `clientPath` properties to the `SdkClient` type to keep backward compatible for metadata type. +- Add `getClientPath` helper to provide similar function for TCGC `SdkClientType` type. 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 7ca1da2521..c6a072e218 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 @@ -445,7 +445,7 @@ export type UseSystemTextJsonConverterDecorator = ( ) => 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. + * Customize the client initialization way. * * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters. * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". @@ -463,15 +463,15 @@ export type UseSystemTextJsonConverterDecorator = ( * 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. + * @@clientInitialization(MyService, {parameters: MyServiceClientOptions}) + * // The generated client will have `blobName` on its initialization method. We will also + * // elevate the existing `blobName` parameter from method level to client level. * ``` */ export type ClientInitializationDecorator = ( context: DecoratorContext, target: Namespace | Interface, - options: Model, + options: Type, scope?: string, ) => void; diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 198910496e..c6d315d011 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -431,7 +431,22 @@ 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. + * InitializedBy value. + */ +enum InitializedBy { + /** + * The client could be initialized individually. + */ + individually: 1, + + /** + * The client could be initialized by parent client. + */ + parent: 2, +} + +/** + * Customize the client initialization way. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters. * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @@ -449,17 +464,33 @@ extern dec useSystemTextJsonConverter(target: Model, scope?: valueof string); * 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. + * @@clientInitialization(MyService, {parameters: MyServiceClientOptions}) + * // The generated client will have `blobName` on its initialization method. We will also + * // elevate the existing `blobName` parameter from method level to client level. * ``` */ extern dec clientInitialization( target: Namespace | Interface, - options: Model, + options: ClientInitializationOptions, scope?: valueof string ); +/** + * Client initialization customization options. + */ +model ClientInitializationOptions { + /** + * Redefine the client initialization parameters you would like to add to the client. + * By default, we apply endpoint, credential, and api-version parameters. If you specify parameters model, we will append the properties of the model to the parameters list of the client initialization. + */ + parameters?: Model; + + /** + * Determines how the client could be initialized. Use `InitializedBy` enum to set the value. The value could be `InitializedBy.individually`, `InitializedBy.parent` or `InitializedBy.individually | InitializedBy.parent`. + */ + initializedBy?: EnumMember | Union; +} + /** * 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. diff --git a/packages/typespec-client-generator-core/package.json b/packages/typespec-client-generator-core/package.json index 5cd1a0072d..ecd301bbf3 100644 --- a/packages/typespec-client-generator-core/package.json +++ b/packages/typespec-client-generator-core/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/typespec-client-generator-core", - "version": "0.50.2", + "version": "0.50.3", "author": "Microsoft Corporation", "description": "TypeSpec Data Plane Generation library", "homepage": "https://azure.github.io/typespec-azure", diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index caecf9db66..8f451db21b 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -46,6 +46,7 @@ import { } from "../generated-defs/Azure.ClientGenerator.Core.js"; import { AccessFlags, + ClientInitializationOptions, LanguageScopes, SdkClient, SdkInitializationType, @@ -764,6 +765,7 @@ export const $access: AccessDecorator = ( format: {}, target: entity, }); + return; } setScopedDecoratorData(context, $access, accessKey, entity, value.value, scope); }; @@ -822,6 +824,7 @@ export const $flattenProperty: FlattenPropertyDecorator = ( format: {}, target: target, }); + return; } setScopedDecoratorData(context, $flattenProperty, flattenPropertyKey, target, true, scope); // eslint-disable-line @typescript-eslint/no-deprecated }; @@ -869,6 +872,7 @@ export const $clientName: ClientNameDecorator = ( format: {}, target: entity, }); + return; } setScopedDecoratorData(context, $clientName, clientNameKey, entity, value, scope); }; @@ -957,6 +961,7 @@ export const $override = ( overrideParameters: overrideParams.map((x) => x.name).join(`", "`), }, }); + return; } setScopedDecoratorData(context, $override, overrideKey, original, override, scope); }; @@ -1000,6 +1005,7 @@ export const $alternateType: AlternateTypeDecorator = ( }, target: source, }); + return; } setScopedDecoratorData(context, $alternateType, alternateTypeKey, source, alternate, scope); }; @@ -1029,26 +1035,74 @@ const clientInitializationKey = createStateSymbol("clientInitialization"); export const $clientInitialization: ClientInitializationDecorator = ( context: DecoratorContext, target: Namespace | Interface, - options: Model, + options: Type, scope?: LanguageScopes, ) => { - setScopedDecoratorData( - context, - $clientInitialization, - clientInitializationKey, - target, - options, - scope, - ); + if (options.kind === "Model") { + if (options.properties.get("initializedBy")) { + const value = options.properties.get("initializedBy")!.type; + + const isValidValue = (value: number): boolean => value === 1 || value === 2; + + if (value.kind === "EnumMember") { + if (typeof value.value !== "number" || !isValidValue(value.value)) { + reportDiagnostic(context.program, { + code: "invalid-initialized-by", + format: { message: "Please use `InitializedBy` enum to set the value." }, + target: target, + }); + return; + } + } else if (value.kind === "Union") { + for (const variant of value.variants.values()) { + if ( + variant.type.kind !== "EnumMember" || + typeof variant.type.value !== "number" || + !isValidValue(variant.type.value) + ) { + reportDiagnostic(context.program, { + code: "invalid-initialized-by", + format: { message: "Please use `InitializedBy` enum to set the value." }, + target: target, + }); + return; + } + } + } + } + + setScopedDecoratorData( + context, + $clientInitialization, + clientInitializationKey, + target, + options, + scope, + ); + } }; +/** + * Get `SdkInitializationType` for namespace or interface. The info is from `@clientInitialization` decorator. + * + * @param context + * @param entity namespace or interface which represents a client + * @returns + * @deprecated This function is deprecated. Use `getClientInitializationOptions` instead. + */ export function getClientInitialization( context: TCGCContext, entity: Namespace | Interface, ): SdkInitializationType | undefined { - const model = getScopedDecoratorData(context, clientInitializationKey, entity); - if (!model) return model; - const sdkModel = getSdkModel(context, model); + let options = getScopedDecoratorData(context, clientInitializationKey, entity); + if (options === undefined) return undefined; + // backward compatibility + if (options.properties.get("parameters")) { + options = options.properties.get("parameters").type; + } else if (options.properties.get("initializedBy")) { + return undefined; + } + const sdkModel = getSdkModel(context, options); const initializationProps = sdkModel.properties.map( (property: SdkModelPropertyType): SdkMethodParameter => { property.onClient = true; @@ -1062,6 +1116,49 @@ export function getClientInitialization( }; } +/** + * Get client initialization options for namespace or interface. The info is from `@clientInitialization` decorator. + * + * @param context + * @param entity namespace or interface which represents a client + * @returns + */ +export function getClientInitializationOptions( + context: TCGCContext, + entity: Namespace | Interface, +): ClientInitializationOptions | undefined { + const options = getScopedDecoratorData(context, clientInitializationKey, entity); + if (options === undefined) return undefined; + + // backward compatibility + if ( + options.properties.get("initializedBy") === undefined && + options.properties.get("parameters") === undefined + ) { + return { + parameters: options, + }; + } + + let initializedBy = undefined; + + if (options.properties.get("initializedBy")) { + if (options.properties.get("initializedBy").type.kind === "EnumMember") { + initializedBy = options.properties.get("initializedBy").type.value; + } else if (options.properties.get("initializedBy").type.kind === "Union") { + initializedBy = 0; + for (const variant of options.properties.get("initializedBy").type.variants.values()) { + initializedBy |= variant.type.value; + } + } + } + + return { + parameters: options.properties.get("parameters")?.type, + initializedBy: initializedBy, + }; +} + const paramAliasKey = createStateSymbol("paramAlias"); export const $paramAlias: ParamAliasDecorator = ( @@ -1104,6 +1201,7 @@ export const $clientNamespace: ClientNamespaceDecorator = ( format: {}, target: entity, }); + return; } setScopedDecoratorData(context, $clientNamespace, clientNamespaceKey, entity, value, scope); }; diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 9fae0ae1e3..1a293e6b4a 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -29,6 +29,8 @@ import { } from "@typespec/http"; import { TspLiteralType } from "./internal-utils.js"; +// Types for TCGC lib + export interface TCGCContext { program: Program; emitterName: string; @@ -79,6 +81,8 @@ export interface SdkEmitterOptions { "emitter-name"?: string; } +// Types for TCGC customization decorators + export interface SdkClient { kind: "SdkClient"; name: string; @@ -87,25 +91,6 @@ export interface SdkClient { crossLanguageDefinitionId: string; } -export interface SdkClientType - extends DecoratedType { - __raw: SdkClient | SdkOperationGroup; - kind: "client"; - name: string; - clientNamespace: string; // fully qualified namespace - doc?: string; - summary?: string; - initialization: SdkInitializationType; - methods: SdkMethod[]; - apiVersions: string[]; - /** - * @deprecated Use `clientNamespace` instead. - */ - nameSpace: string; // fully qualified - crossLanguageDefinitionId: string; - parent?: SdkClientType; -} - export interface SdkOperationGroup { kind: "SdkOperationGroup"; type: Namespace | Interface; @@ -114,6 +99,63 @@ export interface SdkOperationGroup { service: Namespace; } +export type AccessFlags = "internal" | "public"; + +/** + * This enum represents the different ways a model can be used in a method. + */ +export enum UsageFlags { + None = 0, + Input = 1 << 1, + Output = 1 << 2, + ApiVersionEnum = 1 << 3, + // Input and Json will also be set when JsonMergePatch is set. + JsonMergePatch = 1 << 4, + // Input will also be set when MultipartFormData is set. + MultipartFormData = 1 << 5, + // Used in spread. + Spread = 1 << 6, + /** + * @deprecated Use `Exception` instead. + */ + // Output will also be set when Error is set. + Error = 1 << 7, + // Set when type is used in conjunction with an application/json content type. + Json = 1 << 8, + // Set when type is used in conjunction with an application/xml content type. + Xml = 1 << 9, + // Set when type is used for exception output. + Exception = 1 << 10, + // Set when type is used as LRO initial response. + LroInitial = 1 << 11, + // Set when type is used as LRO polling response. + LroPolling = 1 << 12, + // Set when type is used as LRO final envelop response. + LroFinalEnvelope = 1 << 13, +} + +/** + * Flags used to indicate how a client is initialized. + * `Individually` means the client is initialized individually. + * `Parent` means the client is initialized by its parent. + */ +export enum InitializedByFlags { + Individually = 1 << 0, + Parent = 1 << 1, +} + +/** + * Options used to indicate how to initialize a client. + * `parameters` is a model that used to . + * `initializedBy` is a flag that indicates how the client is initialized. + */ +export interface ClientInitializationOptions { + parameters?: Model; + initializedBy?: InitializedByFlags; +} + +// Types for TCGC specific type graph + interface DecoratedType { // Client types sourced from TypeSpec decorated types will have this generic decoratores list. // Only decorators in allowed list will be included in this list. @@ -127,6 +169,31 @@ export interface DecoratorInfo { // A dict of the decorator's arguments. For example, `{ encoding: "base64url" }`. arguments: Record; } +export interface SdkClientType + extends DecoratedType { + __raw: SdkClient | SdkOperationGroup; + kind: "client"; + name: string; + clientNamespace: string; // fully qualified namespace + doc?: string; + summary?: string; + /** + * @deprecated Use `clientInitialization.paramters` instead. + */ + initialization: SdkInitializationType; + clientInitialization: SdkClientInitializationType; + methods: SdkMethod[]; + apiVersions: string[]; + /** + * @deprecated Use `clientNamespace` instead. + */ + nameSpace: string; // fully qualified + crossLanguageDefinitionId: string; + // The parent client of this client. The structure follows the definition hierarchy. + parent?: SdkClientType; + // The children of this client. The structure follows the definition hierarchy. + children?: SdkClientType[]; +} interface SdkTypeBase extends DecoratedType { __raw?: Type; @@ -361,8 +428,6 @@ export interface SdkUnionType extends usage: UsageFlags; } -export type AccessFlags = "internal" | "public"; - export interface SdkModelType extends SdkTypeBase { kind: "model"; properties: SdkModelPropertyType[]; @@ -385,6 +450,14 @@ export interface SdkInitializationType extends SdkModelType { properties: SdkParameter[]; } +export interface SdkClientInitializationType extends SdkTypeBase { + kind: "clientinitialization"; + name: string; + isGeneratedName: boolean; + parameters: SdkParameter[]; + initializedBy: InitializedByFlags; +} + export interface SdkCredentialType extends SdkTypeBase { kind: "credential"; scheme: HttpAuth; @@ -760,6 +833,9 @@ export type SdkServiceMethod = | SdkLroServiceMethod | SdkLroPagingServiceMethod; +/** + * @deprecated Use `parent` and `children` property from `SdkClientType` to find client hierarchy instead. + */ export interface SdkClientAccessor extends SdkMethodBase { kind: "clientaccessor"; @@ -768,7 +844,7 @@ export interface SdkClientAccessor = | SdkServiceMethod - | SdkClientAccessor; + | SdkClientAccessor; // eslint-disable-line @typescript-eslint/no-deprecated export interface SdkPackage { name: string; @@ -795,39 +871,6 @@ export type SdkHttpPackage = SdkPackage; export type LanguageScopes = "dotnet" | "java" | "python" | "javascript" | "go" | string; -/** - * This enum represents the different ways a model can be used in a method. - */ -export enum UsageFlags { - None = 0, - Input = 1 << 1, - Output = 1 << 2, - ApiVersionEnum = 1 << 3, - // Input and Json will also be set when JsonMergePatch is set. - JsonMergePatch = 1 << 4, - // Input will also be set when MultipartFormData is set. - MultipartFormData = 1 << 5, - // Used in spread. - Spread = 1 << 6, - /** - * @deprecated Use `Exception` instead. - */ - // Output will also be set when Error is set. - Error = 1 << 7, - // Set when type is used in conjunction with an application/json content type. - Json = 1 << 8, - // Set when type is used in conjunction with an application/xml content type. - Xml = 1 << 9, - // Set when type is used for exception output. - Exception = 1 << 10, - // Set when type is used as LRO initial response. - LroInitial = 1 << 11, - // Set when type is used as LRO polling response. - LroPolling = 1 << 12, - // Set when type is used as LRO final envelop response. - LroFinalEnvelope = 1 << 13, -} - interface SdkExampleBase { kind: string; name: string; diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index 34f5749934..3f7671e2d0 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -67,13 +67,13 @@ export const $lib = createTypeSpecLibrary({ access: { severity: "error", messages: { - default: `Access decorator value must be "public" or "internal".`, + default: `Access value must be "public" or "internal".`, }, }, "invalid-usage": { severity: "error", messages: { - default: `Usage decorator value must be 2 ("input") or 4 ("output").`, + default: `Usage value must be 2 ("input") or 4 ("output").`, }, }, "invalid-encode": { @@ -247,7 +247,13 @@ export const $lib = createTypeSpecLibrary({ "invalid-alternate-source-type": { severity: "error", messages: { - default: `@alternateType only supports scalar types. The source type is '${"typeName"}'.`, + default: paramMessage`@alternateType only supports scalar types. The source type is '${"typeName"}'.`, + }, + }, + "invalid-initialized-by": { + severity: "error", + messages: { + default: paramMessage`Invalid 'initializedBy' value. ${"message"}`, }, }, }, diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 32595e41f8..496542b56a 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -17,7 +17,7 @@ import { resolveVersions } from "@typespec/versioning"; import { getAccess, getClientInitialization, - getClientNameOverride, + getClientInitializationOptions, getClientNamespace, getOverriddenClientMethod, listClients, @@ -28,8 +28,10 @@ import { } from "./decorators.js"; import { getSdkHttpOperation, getSdkHttpParameter } from "./http.js"; import { + InitializedByFlags, SdkBodyModelPropertyType, SdkClient, + SdkClientInitializationType, SdkClientType, SdkEndpointParameter, SdkEndpointType, @@ -43,6 +45,7 @@ import { SdkMethod, SdkMethodParameter, SdkMethodResponse, + SdkModelPropertyType, SdkModelType, SdkNamespace, SdkNullableType, @@ -87,6 +90,7 @@ import { getClientTypeWithDiagnostics, getSdkCredentialParameter, getSdkModelPropertyType, + getSdkModelWithDiagnostics, getTypeSpecBuiltInType, handleAllTypes, } from "./types.js"; @@ -561,17 +565,9 @@ function getSdkInitializationType( client: SdkClient | SdkOperationGroup, ): [SdkInitializationType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let initializationModel = getClientInitialization(context, client.type); - let clientParams = context.__clientToParameters.get(client.type); - if (!clientParams) { - clientParams = []; - context.__clientToParameters.set(client.type, clientParams); - } + let initializationModel = getClientInitialization(context, client.type); // eslint-disable-line @typescript-eslint/no-deprecated 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; @@ -596,6 +592,92 @@ function getSdkInitializationType( return diagnostics.wrap(initializationModel); } +function createSdkClientInitializationType( + context: TCGCContext, + client: SdkClient | SdkOperationGroup, +): [SdkClientInitializationType, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const name = `${client.kind === "SdkClient" ? client.name : client.groupPath.split(".").at(-1)}Options`; + const result: SdkClientInitializationType = { + kind: "clientinitialization", + doc: "Initialization for the client", + parameters: [], + initializedBy: + client.kind === "SdkClient" ? InitializedByFlags.Individually : InitializedByFlags.Parent, + name, + isGeneratedName: true, + decorators: [], + }; + + // customization + const initializationOptions = getClientInitializationOptions(context, client.type); + if (initializationOptions?.parameters) { + const model = diagnostics.pipe( + getSdkModelWithDiagnostics(context, initializationOptions.parameters), + ); + result.doc = model.doc; + result.summary = model.summary; + result.name = model.name; + result.isGeneratedName = model.isGeneratedName; + result.decorators = model.decorators; + result.__raw = model.__raw; + result.parameters = model.properties.map( + (property: SdkModelPropertyType): SdkMethodParameter => { + property.onClient = true; + property.kind = "method"; + return property as SdkMethodParameter; + }, + ); + } + if (initializationOptions?.initializedBy) { + if ( + client.kind === "SdkClient" && + (initializationOptions.initializedBy & InitializedByFlags.Parent) === + InitializedByFlags.Parent + ) { + diagnostics.add( + createDiagnostic({ + code: "invalid-initialized-by", + target: client.type, + format: { + message: + "First level client must have `InitializedBy.individually` specified in `initializedBy`.", + }, + }), + ); + } else if ( + client.kind === "SdkOperationGroup" && + initializationOptions.initializedBy === InitializedByFlags.Individually + ) { + diagnostics.add( + createDiagnostic({ + code: "invalid-initialized-by", + target: client.type, + format: { + message: + "Sub client must have `InitializedBy.parent` or `InitializedBy.individually | InitializedBy.parent` specified in `initializedBy`.", + }, + }), + ); + } else { + result.initializedBy = initializationOptions.initializedBy; + } + } + if (initializationOptions?.parameters) { + // Cache elevated parameter, then we could use it to set `onClient` property for method parameters. + let clientParams = context.__clientToParameters.get(client.type); + if (!clientParams) { + clientParams = []; + context.__clientToParameters.set(client.type, clientParams); + } + for (const param of result.parameters) { + clientParams.push(param); + } + } + + return diagnostics.wrap(result); +} + function getSdkMethodParameter( context: TCGCContext, type: ModelProperty, @@ -625,13 +707,17 @@ function getSdkMethods( const operationGroupClient = diagnostics.pipe( createSdkClientType(context, operationGroup, sdkClientType), ); - const clientInitialization = getClientInitialization(context, operationGroup.type); + if (sdkClientType.children) { + sdkClientType.children.push(operationGroupClient); + } else { + sdkClientType.children = [operationGroupClient]; + } + const clientInitialization = getClientInitialization(context, operationGroup.type); // eslint-disable-line @typescript-eslint/no-deprecated const parameters: SdkParameter[] = []; if (clientInitialization) { for (const property of clientInitialization.properties) { parameters.push(property); } - } else { } const name = `get${operationGroup.type.name}`; retval.push({ @@ -792,17 +878,10 @@ function createSdkClientType( parent?: SdkClientType, ): [SdkClientType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const isClient = client.kind === "SdkClient"; - let name = ""; - if (isClient) { - name = client.name; - } else { - name = getClientNameOverride(context, client.type) ?? client.type.name; - } const sdkClientType: SdkClientType = { __raw: client, kind: "client", - name, + name: client.kind === "SdkClient" ? client.name : getLibraryName(context, client.type), doc: getDoc(context.program, client.type), summary: getSummary(context.program, client.type), methods: [], @@ -810,6 +889,7 @@ function createSdkClientType( nameSpace: getClientNamespaceStringHelper(context, client.service)!, clientNamespace: getClientNamespace(context, client.type), initialization: diagnostics.pipe(getSdkInitializationType(context, client)), + clientInitialization: diagnostics.pipe(createSdkClientInitializationType(context, client)), decorators: diagnostics.pipe(getTypeDecorators(context, client.type)), parent, // if it is client, the crossLanguageDefinitionId is the ${namespace}, if it is operation group, the crosslanguageDefinitionId is the %{namespace}.%{operationGroupName} @@ -820,6 +900,9 @@ function createSdkClientType( getSdkMethods(context, client, sdkClientType), ); addDefaultClientParameters(context, sdkClientType); + // update initialization model properties + + sdkClientType.initialization.properties = [...sdkClientType.clientInitialization.parameters]; // eslint-disable-line @typescript-eslint/no-deprecated return diagnostics.wrap(sdkClientType); } @@ -827,11 +910,12 @@ function addDefaultClientParameters< TServiceOperation extends SdkServiceOperation = SdkHttpOperation, >(context: TCGCContext, client: SdkClientType): void { const diagnostics = createDiagnosticCollector(); + const defaultClientParamters = []; // there will always be an endpoint property - client.initialization.properties.push(diagnostics.pipe(getSdkEndpointParameter(context, client))); + defaultClientParamters.push(diagnostics.pipe(getSdkEndpointParameter(context, client))); const credentialParam = getSdkCredentialParameter(context, client.__raw); if (credentialParam) { - client.initialization.properties.push(credentialParam); + defaultClientParamters.push(credentialParam); } let apiVersionParam = context.__clientToParameters .get(client.__raw.type) @@ -847,7 +931,7 @@ function addDefaultClientParameters< } } if (apiVersionParam) { - client.initialization.properties.push(apiVersionParam); + defaultClientParamters.push(apiVersionParam); } let subId = context.__clientToParameters .get(client.__raw.type) @@ -862,8 +946,12 @@ function addDefaultClientParameters< } } if (subId) { - client.initialization.properties.push(subId); + defaultClientParamters.push(subId); } + client.clientInitialization.parameters = [ + ...defaultClientParamters, + ...client.clientInitialization.parameters, + ]; } function populateApiVersionInformation(context: TCGCContext): void { diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 5371e5ca5d..0d24518d01 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -2193,501 +2193,6 @@ describe("typespec-client-generator-core: decorators", () => { }); }); - describe("@clientInitialization", () => { - it("main client", async () => { - await runner.compileWithCustomization( - ` - @service - namespace MyService; - - op download(@path blobName: string): void; - `, - ` - namespace MyCustomizations; - - model MyClientInitialization { - blobName: string; - } - - @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); - `, - ); - const sdkPackage = runner.context.sdkPackage; - const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 2); - const endpoint = client.initialization.properties.find((x) => x.kind === "endpoint"); - ok(endpoint); - const blobName = client.initialization.properties.find((x) => x.name === "blobName"); - ok(blobName); - strictEqual(blobName.clientDefaultValue, undefined); - strictEqual(blobName.onClient, true); - strictEqual(blobName.optional, false); - - const methods = client.methods; - strictEqual(methods.length, 1); - const download = methods[0]; - strictEqual(download.name, "download"); - strictEqual(download.kind, "basic"); - strictEqual(download.parameters.length, 0); - - const downloadOp = download.operation; - strictEqual(downloadOp.parameters.length, 1); - const blobNameOpParam = downloadOp.parameters[0]; - strictEqual(blobNameOpParam.name, "blobName"); - strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); - strictEqual(blobNameOpParam.correspondingMethodParams[0], blobName); - strictEqual(blobNameOpParam.onClient, true); - }); - - it("On Interface", async () => { - await runner.compileWithBuiltInService( - ` - model clientInitModel - { - p1: string; - } - - @route("/bump") - @clientInitialization(clientInitModel) - interface bumpParameter { - @route("/op1") - @doc("bump parameter") - @post - @convenientAPI(true) - op op1(@path p1: string, @query q1: string): void; - - @route("/op2") - @doc("bump parameter") - @post - @convenientAPI(true) - op op2(@path p1: string): void; - } - `, - ); - const sdkPackage = runner.context.sdkPackage; - const clientAccessor = sdkPackage.clients[0].methods[0]; - strictEqual(clientAccessor.kind, "clientaccessor"); - const bumpParameterClient = clientAccessor.response; - - const methods = bumpParameterClient.methods; - strictEqual(methods.length, 2); - - const op1Method = methods.find((x) => x.name === "op1"); - ok(op1Method); - strictEqual(op1Method.kind, "basic"); - strictEqual(op1Method.parameters.length, 1); - strictEqual(op1Method.parameters[0].name, "q1"); - const op1Op = op1Method.operation; - strictEqual(op1Op.parameters.length, 2); - strictEqual(op1Op.parameters[0].name, "p1"); - strictEqual(op1Op.parameters[0].onClient, true); - strictEqual(op1Op.parameters[1].name, "q1"); - strictEqual(op1Op.parameters[1].onClient, false); - }); - it("subclient", async () => { - await runner.compileWithCustomization( - ` - @service - namespace StorageClient { - - @route("/main") - op download(@path blobName: string): void; - - interface BlobClient { - @route("/blob") - op download(@path blobName: string): void; - } - } - `, - ` - model ClientInitialization { - blobName: string - }; - - @@clientInitialization(StorageClient, ClientInitialization); - @@clientInitialization(StorageClient.BlobClient, ClientInitialization); - `, - ); - const sdkPackage = runner.context.sdkPackage; - const clients = sdkPackage.clients; - strictEqual(clients.length, 1); - const client = clients[0]; - strictEqual(client.name, "StorageClient"); - strictEqual(client.initialization.access, "public"); - strictEqual(client.initialization.properties.length, 2); - ok(client.initialization.properties.find((x) => x.kind === "endpoint")); - const blobName = client.initialization.properties.find((x) => x.name === "blobName"); - ok(blobName); - strictEqual(blobName.onClient, true); - - const methods = client.methods; - strictEqual(methods.length, 2); - - // the main client's function should not have `blobName` as a client method parameter - const mainClientDownload = methods.find((x) => x.kind === "basic" && x.name === "download"); - ok(mainClientDownload); - strictEqual(mainClientDownload.parameters.length, 0); - - const getBlobClient = methods.find((x) => x.kind === "clientaccessor"); - ok(getBlobClient); - strictEqual(getBlobClient.kind, "clientaccessor"); - strictEqual(getBlobClient.name, "getBlobClient"); - strictEqual(getBlobClient.parameters.length, 1); - const blobNameParam = getBlobClient.parameters.find((x) => x.name === "blobName"); - ok(blobNameParam); - strictEqual(blobNameParam.onClient, true); - strictEqual(blobNameParam.optional, false); - strictEqual(blobNameParam.kind, "method"); - - const blobClient = getBlobClient.response; - - strictEqual(blobClient.kind, "client"); - strictEqual(blobClient.name, "BlobClient"); - strictEqual(blobClient.initialization.access, "internal"); - strictEqual(blobClient.initialization.properties.length, 2); - - ok(blobClient.initialization.properties.find((x) => x.kind === "endpoint")); - const blobClientBlobInitializationProp = blobClient.initialization.properties.find( - (x) => x.name === "blobName", - ); - ok(blobClientBlobInitializationProp); - strictEqual(blobClientBlobInitializationProp.kind, "method"); - strictEqual(blobClientBlobInitializationProp.onClient, true); - strictEqual(blobClient.methods.length, 1); - - const download = blobClient.methods[0]; - strictEqual(download.name, "download"); - strictEqual(download.kind, "basic"); - strictEqual(download.parameters.length, 0); - - const downloadOp = download.operation; - strictEqual(downloadOp.parameters.length, 1); - const blobNameOpParam = downloadOp.parameters[0]; - strictEqual(blobNameOpParam.name, "blobName"); - strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); - strictEqual(blobNameOpParam.correspondingMethodParams[0], blobClientBlobInitializationProp); - strictEqual(blobNameOpParam.onClient, true); - }); - it("some methods don't have client initialization params", async () => { - await runner.compileWithCustomization( - ` - @service - namespace MyService; - - op download(@path blobName: string, @header header: int32): void; - op noClientParams(@query query: int32): void; - `, - ` - namespace MyCustomizations; - - model MyClientInitialization { - blobName: string; - } - - @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); - `, - ); - const sdkPackage = runner.context.sdkPackage; - const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 2); - const endpoint = client.initialization.properties.find((x) => x.kind === "endpoint"); - ok(endpoint); - const blobName = client.initialization.properties.find((x) => x.name === "blobName"); - ok(blobName); - strictEqual(blobName.clientDefaultValue, undefined); - strictEqual(blobName.onClient, true); - strictEqual(blobName.optional, false); - - const methods = client.methods; - strictEqual(methods.length, 2); - const download = methods[0]; - strictEqual(download.name, "download"); - strictEqual(download.kind, "basic"); - strictEqual(download.parameters.length, 1); - - const headerParam = download.parameters.find((x) => x.name === "header"); - ok(headerParam); - strictEqual(headerParam.onClient, false); - - const downloadOp = download.operation; - strictEqual(downloadOp.parameters.length, 2); - const blobNameOpParam = downloadOp.parameters[0]; - strictEqual(blobNameOpParam.name, "blobName"); - strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); - strictEqual(blobNameOpParam.correspondingMethodParams[0], blobName); - strictEqual(blobNameOpParam.onClient, true); - - const noClientParamsMethod = methods[1]; - strictEqual(noClientParamsMethod.name, "noClientParams"); - strictEqual(noClientParamsMethod.kind, "basic"); - strictEqual(noClientParamsMethod.parameters.length, 1); - strictEqual(noClientParamsMethod.parameters[0].name, "query"); - strictEqual(noClientParamsMethod.parameters[0].onClient, false); - }); - - it("multiple client params", async () => { - await runner.compileWithCustomization( - ` - @service - namespace MyService; - - op download(@path blobName: string, @path containerName: string): void; - `, - ` - namespace MyCustomizations; - - model MyClientInitialization { - blobName: string; - containerName: string; - } - - @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); - `, - ); - const sdkPackage = runner.context.sdkPackage; - const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 3); - const endpoint = client.initialization.properties.find((x) => x.kind === "endpoint"); - ok(endpoint); - const blobName = client.initialization.properties.find((x) => x.name === "blobName"); - ok(blobName); - strictEqual(blobName.clientDefaultValue, undefined); - strictEqual(blobName.onClient, true); - strictEqual(blobName.optional, false); - - const containerName = client.initialization.properties.find( - (x) => x.name === "containerName", - ); - ok(containerName); - strictEqual(containerName.clientDefaultValue, undefined); - strictEqual(containerName.onClient, true); - - const methods = client.methods; - strictEqual(methods.length, 1); - const download = methods[0]; - strictEqual(download.name, "download"); - strictEqual(download.kind, "basic"); - strictEqual(download.parameters.length, 0); - - const downloadOp = download.operation; - strictEqual(downloadOp.parameters.length, 2); - const blobNameOpParam = downloadOp.parameters[0]; - strictEqual(blobNameOpParam.name, "blobName"); - strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); - strictEqual(blobNameOpParam.correspondingMethodParams[0], blobName); - - const containerNameOpParam = downloadOp.parameters[1]; - strictEqual(containerNameOpParam.name, "containerName"); - strictEqual(containerNameOpParam.correspondingMethodParams.length, 1); - strictEqual(containerNameOpParam.correspondingMethodParams[0], containerName); - }); - - it("@operationGroup with same model on parent client", async () => { - await runner.compile( - ` - @service - namespace MyService; - - @operationGroup - interface MyInterface { - op download(@path blobName: string, @path containerName: string): void; - } - - model MyClientInitialization { - blobName: string; - containerName: string; - } - - @@clientInitialization(MyService, MyClientInitialization); - @@clientInitialization(MyService.MyInterface, MyClientInitialization); - `, - ); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.clients.length, 1); - - const client = sdkPackage.clients[0]; - strictEqual(client.initialization.access, "public"); - strictEqual(client.initialization.properties.length, 3); - ok(client.initialization.properties.find((x) => x.kind === "endpoint")); - const blobName = client.initialization.properties.find((x) => x.name === "blobName"); - ok(blobName); - strictEqual(blobName.clientDefaultValue, undefined); - strictEqual(blobName.onClient, true); - - const containerName = client.initialization.properties.find( - (x) => x.name === "containerName", - ); - ok(containerName); - strictEqual(containerName.clientDefaultValue, undefined); - strictEqual(containerName.onClient, true); - - const methods = client.methods; - strictEqual(methods.length, 1); - const clientAccessor = methods[0]; - strictEqual(clientAccessor.kind, "clientaccessor"); - const og = clientAccessor.response; - strictEqual(og.kind, "client"); - - strictEqual(og.initialization.access, "internal"); - strictEqual(og.initialization.properties.length, 3); - ok(og.initialization.properties.find((x) => x.kind === "endpoint")); - ok(og.initialization.properties.find((x) => x === blobName)); - ok(og.initialization.properties.find((x) => x === containerName)); - - const download = og.methods[0]; - strictEqual(download.name, "download"); - strictEqual(download.kind, "basic"); - strictEqual(download.parameters.length, 0); - - const op = download.operation; - strictEqual(op.parameters.length, 2); - strictEqual(op.parameters[0].correspondingMethodParams[0], blobName); - strictEqual(op.parameters[1].correspondingMethodParams[0], containerName); - strictEqual(op.parameters[0].onClient, true); - strictEqual(op.parameters[1].onClient, true); - }); - - it("redefine client structure", async () => { - await runner.compileWithCustomization( - ` - @service - namespace MyService; - - op uploadContainer(@path containerName: string): void; - op uploadBlob(@path containerName: string, @path blobName: string): void; - `, - ` - namespace MyCustomizations { - model ContainerClientInitialization { - containerName: string; - } - @client({service: MyService}) - @clientInitialization(ContainerClientInitialization) - namespace ContainerClient { - op upload is MyService.uploadContainer; - - - model BlobClientInitialization { - containerName: string; - blobName: string; - } - - @client({service: MyService}) - @clientInitialization(BlobClientInitialization) - namespace BlobClient { - op upload is MyService.uploadBlob; - } - } - } - - `, - ); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.clients.length, 2); - - const containerClient = sdkPackage.clients.find((x) => x.name === "ContainerClient"); - ok(containerClient); - strictEqual(containerClient.initialization.access, "public"); - strictEqual(containerClient.initialization.properties.length, 2); - ok(containerClient.initialization.properties.find((x) => x.kind === "endpoint")); - - const containerName = containerClient.initialization.properties.find( - (x) => x.name === "containerName", - ); - ok(containerName); - - const methods = containerClient.methods; - strictEqual(methods.length, 1); - strictEqual(methods[0].name, "upload"); - strictEqual(methods[0].kind, "basic"); - strictEqual(methods[0].parameters.length, 0); - strictEqual(methods[0].operation.parameters.length, 1); - strictEqual(methods[0].operation.parameters[0].correspondingMethodParams[0], containerName); - - const blobClient = sdkPackage.clients.find((x) => x.name === "BlobClient"); - ok(blobClient); - strictEqual(blobClient.initialization.access, "public"); - strictEqual(blobClient.initialization.properties.length, 3); - ok(blobClient.initialization.properties.find((x) => x.kind === "endpoint")); - - const containerNameOnBlobClient = blobClient.initialization.properties.find( - (x) => x.name === "containerName", - ); - ok(containerNameOnBlobClient); - - const blobName = blobClient.initialization.properties.find((x) => x.name === "blobName"); - ok(blobName); - - const blobMethods = blobClient.methods; - strictEqual(blobMethods.length, 1); - strictEqual(blobMethods[0].name, "upload"); - strictEqual(blobMethods[0].kind, "basic"); - strictEqual(blobMethods[0].parameters.length, 0); - strictEqual(blobMethods[0].operation.parameters.length, 2); - strictEqual( - blobMethods[0].operation.parameters[0].correspondingMethodParams[0], - containerNameOnBlobClient, - ); - strictEqual(blobMethods[0].operation.parameters[1].correspondingMethodParams[0], blobName); - }); - - it("@paramAlias", async () => { - await runner.compileWithCustomization( - ` - @service - namespace MyService; - - op download(@path blob: string): void; - op upload(@path blobName: string): void; - `, - ` - namespace MyCustomizations; - - model MyClientInitialization { - @paramAlias("blob") - blobName: string; - } - - @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); - `, - ); - const sdkPackage = runner.context.sdkPackage; - const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 2); - const endpoint = client.initialization.properties.find((x) => x.kind === "endpoint"); - ok(endpoint); - const blobName = client.initialization.properties.find((x) => x.name === "blobName"); - ok(blobName); - strictEqual(blobName.clientDefaultValue, undefined); - strictEqual(blobName.onClient, true); - strictEqual(blobName.optional, false); - - const methods = client.methods; - strictEqual(methods.length, 2); - const download = methods[0]; - strictEqual(download.name, "download"); - strictEqual(download.kind, "basic"); - strictEqual(download.parameters.length, 0); - - const downloadOp = download.operation; - strictEqual(downloadOp.parameters.length, 1); - strictEqual(downloadOp.parameters[0].name, "blob"); - strictEqual(downloadOp.parameters[0].correspondingMethodParams.length, 1); - strictEqual(downloadOp.parameters[0].correspondingMethodParams[0], blobName); - - const upload = methods[1]; - strictEqual(upload.name, "upload"); - strictEqual(upload.kind, "basic"); - strictEqual(upload.parameters.length, 0); - - const uploadOp = upload.operation; - strictEqual(uploadOp.parameters.length, 1); - strictEqual(uploadOp.parameters[0].name, "blobName"); - strictEqual(uploadOp.parameters[0].correspondingMethodParams.length, 1); - strictEqual(uploadOp.parameters[0].correspondingMethodParams[0], blobName); - }); - }); - describe("scope negation", () => { it("single scope negation", async () => { const runnerWithCSharp = await createSdkTestRunner({ diff --git a/packages/typespec-client-generator-core/test/decorators/client-initialization.test.ts b/packages/typespec-client-generator-core/test/decorators/client-initialization.test.ts new file mode 100644 index 0000000000..12582f32a0 --- /dev/null +++ b/packages/typespec-client-generator-core/test/decorators/client-initialization.test.ts @@ -0,0 +1,752 @@ +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { InitializedByFlags } from "../../src/interfaces.js"; +import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; + +describe("typespec-client-generator-core: @clientInitialization", () => { + let runner: SdkTestRunner; + + beforeEach(async () => { + runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python" }); + }); + + it("change client initialization", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blobName: string): void; + `, + ` + namespace MyCustomizations; + + model MyClientInitialization { + blobName: string; + } + + @@clientInitialization(MyService, {parameters: MyCustomizations.MyClientInitialization}); + `, + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.initialization.access, "public"); + strictEqual(client.initialization.properties.length, 2); + const endpoint = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); + ok(endpoint); + strictEqual( + endpoint, + client.initialization.properties.find((x) => x.kind === "endpoint"), + ); + const blobName = client.clientInitialization.parameters.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual( + blobName, + client.initialization.properties.find((x) => x.name === "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); + strictEqual(blobNameOpParam.onClient, true); + }); + + it("backward compatibility", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blobName: string): void; + `, + ` + namespace MyCustomizations; + + model MyClientInitialization { + blobName: string; + } + + @@clientInitialization(MyService, MyCustomizations.MyClientInitialization); + `, + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.initialization.access, "public"); + strictEqual(client.initialization.properties.length, 2); + const endpoint = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); + ok(endpoint); + strictEqual( + endpoint, + client.initialization.properties.find((x) => x.kind === "endpoint"), + ); + const blobName = client.clientInitialization.parameters.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual( + blobName, + client.initialization.properties.find((x) => x.name === "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); + strictEqual(blobNameOpParam.onClient, true); + }); + + it("client accessor", async () => { + await runner.compileWithBuiltInService( + ` + model clientInitModel + { + p1: string; + } + + @route("/bump") + @clientInitialization({parameters: clientInitModel}) + interface bumpParameter { + @route("/op1") + @doc("bump parameter") + @post + @convenientAPI(true) + op op1(@path p1: string, @query q1: string): void; + + @route("/op2") + @doc("bump parameter") + @post + @convenientAPI(true) + op op2(@path p1: string): void; + } + `, + ); + const sdkPackage = runner.context.sdkPackage; + const clientAccessor = sdkPackage.clients[0].methods[0]; + strictEqual(clientAccessor.kind, "clientaccessor"); + strictEqual(clientAccessor.access, "internal"); + + const bumpParameterClient = clientAccessor.response; + strictEqual(bumpParameterClient.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(bumpParameterClient.initialization.access, "internal"); + + const methods = bumpParameterClient.methods; + strictEqual(methods.length, 2); + + const op1Method = methods.find((x) => x.name === "op1"); + ok(op1Method); + strictEqual(op1Method.kind, "basic"); + strictEqual(op1Method.parameters.length, 1); + strictEqual(op1Method.parameters[0].name, "q1"); + const op1Op = op1Method.operation; + strictEqual(op1Op.parameters.length, 2); + strictEqual(op1Op.parameters[0].name, "p1"); + strictEqual(op1Op.parameters[0].onClient, true); + strictEqual(op1Op.parameters[1].name, "q1"); + strictEqual(op1Op.parameters[1].onClient, false); + }); + + it("subclient", async () => { + await runner.compileWithCustomization( + ` + @service + namespace StorageClient { + + @route("/main") + op download(@path blobName: string): void; + + interface BlobClient { + @route("/blob") + op download(@path blobName: string): void; + } + } + `, + ` + model ClientInitialization { + blobName: string + }; + + @@clientInitialization(StorageClient, {parameters: ClientInitialization}); + @@clientInitialization(StorageClient.BlobClient, {parameters: ClientInitialization}); + `, + ); + const sdkPackage = runner.context.sdkPackage; + const clients = sdkPackage.clients; + strictEqual(clients.length, 1); + const client = clients[0]; + strictEqual(client.name, "StorageClient"); + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.initialization.access, "public"); + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.initialization.properties.length, 2); + const endpoint = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); + ok(endpoint); + strictEqual( + endpoint, + client.initialization.properties.find((x) => x.kind === "endpoint"), + ); + const blobName = client.clientInitialization.parameters.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual( + blobName, + client.initialization.properties.find((x) => x.name === "blobName"), + ); + strictEqual(blobName.onClient, true); + + const methods = client.methods; + strictEqual(methods.length, 2); + + // the main client's function should not have `blobName` as a client method parameter + const mainClientDownload = methods.find((x) => x.kind === "basic" && x.name === "download"); + ok(mainClientDownload); + strictEqual(mainClientDownload.parameters.length, 0); + + const getBlobClient = methods.find((x) => x.kind === "clientaccessor"); + ok(getBlobClient); + strictEqual(getBlobClient.kind, "clientaccessor"); + strictEqual(getBlobClient.name, "getBlobClient"); + strictEqual(getBlobClient.parameters.length, 1); + const blobNameParam = getBlobClient.parameters.find((x) => x.name === "blobName"); + ok(blobNameParam); + strictEqual(blobNameParam.onClient, true); + strictEqual(blobNameParam.optional, false); + strictEqual(blobNameParam.kind, "method"); + + const blobClient = getBlobClient.response; + + strictEqual(blobClient.kind, "client"); + strictEqual(blobClient.name, "BlobClient"); + strictEqual(blobClient.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(blobClient.initialization.access, "internal"); + strictEqual(blobClient.clientInitialization.parameters.length, 2); + strictEqual(blobClient.initialization.properties.length, 2); + + const blobClientEndpoint = blobClient.initialization.properties.find( + (x) => x.kind === "endpoint", + ); + ok(blobClientEndpoint); + strictEqual( + blobClientEndpoint, + blobClient.initialization.properties.find((x) => x.kind === "endpoint"), + ); + const blobClientBlobName = blobClient.clientInitialization.parameters.find( + (x) => x.name === "blobName", + ); + ok(blobClientBlobName); + strictEqual( + blobClientBlobName, + blobClient.initialization.properties.find((x) => x.name === "blobName"), + ); + strictEqual(blobClientBlobName.kind, "method"); + strictEqual(blobClientBlobName.onClient, true); + strictEqual(blobClient.methods.length, 1); + + const download = blobClient.methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 0); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 1); + const blobNameOpParam = downloadOp.parameters[0]; + strictEqual(blobNameOpParam.name, "blobName"); + strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); + strictEqual(blobNameOpParam.correspondingMethodParams[0], blobClientBlobName); + strictEqual(blobNameOpParam.onClient, true); + }); + + 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, {parameters: MyCustomizations.MyClientInitialization}); + `, + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.initialization.access, "public"); + strictEqual(client.initialization.properties.length, 2); + + const endpoint = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); + ok(endpoint); + strictEqual( + endpoint, + client.initialization.properties.find((x) => x.kind === "endpoint"), + ); + const blobName = client.clientInitialization.parameters.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual( + blobName, + client.initialization.properties.find((x) => x.name === "blobName"), + ); + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + strictEqual(blobName.optional, false); + + const methods = client.methods; + strictEqual(methods.length, 2); + const download = methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 1); + + const headerParam = download.parameters.find((x) => x.name === "header"); + ok(headerParam); + strictEqual(headerParam.onClient, false); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 2); + const blobNameOpParam = downloadOp.parameters[0]; + strictEqual(blobNameOpParam.name, "blobName"); + strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); + strictEqual(blobNameOpParam.correspondingMethodParams[0], blobName); + strictEqual(blobNameOpParam.onClient, true); + + const noClientParamsMethod = methods[1]; + strictEqual(noClientParamsMethod.name, "noClientParams"); + strictEqual(noClientParamsMethod.kind, "basic"); + strictEqual(noClientParamsMethod.parameters.length, 1); + strictEqual(noClientParamsMethod.parameters[0].name, "query"); + strictEqual(noClientParamsMethod.parameters[0].onClient, false); + }); + + it("multiple client params", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blobName: string, @path containerName: string): void; + `, + ` + namespace MyCustomizations; + + model MyClientInitialization { + blobName: string; + containerName: string; + } + + @@clientInitialization(MyService, {parameters: MyCustomizations.MyClientInitialization}); + `, + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 3); + strictEqual(client.initialization.access, "public"); + strictEqual(client.initialization.properties.length, 3); + + const endpoint = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); + ok(endpoint); + strictEqual( + endpoint, + client.initialization.properties.find((x) => x.kind === "endpoint"), + ); + const blobName = client.clientInitialization.parameters.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual( + blobName, + client.initialization.properties.find((x) => x.name === "blobName"), + ); + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + strictEqual(blobName.optional, false); + + const containerName = client.clientInitialization.parameters.find( + (x) => x.name === "containerName", + ); + ok(containerName); + strictEqual( + containerName, + client.initialization.properties.find((x) => x.name === "containerName"), + ); + strictEqual(containerName.clientDefaultValue, undefined); + strictEqual(containerName.onClient, true); + + const methods = client.methods; + strictEqual(methods.length, 1); + const download = methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 0); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 2); + const blobNameOpParam = downloadOp.parameters[0]; + strictEqual(blobNameOpParam.name, "blobName"); + strictEqual(blobNameOpParam.correspondingMethodParams.length, 1); + strictEqual(blobNameOpParam.correspondingMethodParams[0], blobName); + + const containerNameOpParam = downloadOp.parameters[1]; + strictEqual(containerNameOpParam.name, "containerName"); + strictEqual(containerNameOpParam.correspondingMethodParams.length, 1); + strictEqual(containerNameOpParam.correspondingMethodParams[0], containerName); + }); + + it("@operationGroup with same model on parent client", async () => { + await runner.compile( + ` + @service + namespace MyService; + + @operationGroup + interface MyInterface { + op download(@path blobName: string, @path containerName: string): void; + } + + model MyClientInitialization { + blobName: string; + containerName: string; + } + + @@clientInitialization(MyService, {parameters: MyClientInitialization}); + @@clientInitialization(MyService.MyInterface, {parameters: MyClientInitialization}); + `, + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + + const client = sdkPackage.clients[0]; + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.initialization.access, "public"); + strictEqual(client.clientInitialization.parameters.length, 3); + strictEqual(client.initialization.properties.length, 3); + + const endpoint = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); + ok(endpoint); + strictEqual( + endpoint, + client.initialization.properties.find((x) => x.kind === "endpoint"), + ); + const blobName = client.clientInitialization.parameters.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual( + blobName, + client.initialization.properties.find((x) => x.name === "blobName"), + ); + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + + const containerName = client.clientInitialization.parameters.find( + (x) => x.name === "containerName", + ); + ok(containerName); + strictEqual( + containerName, + client.initialization.properties.find((x) => x.name === "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.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(og.clientInitialization.parameters.length, 3); + strictEqual(og.initialization.access, "internal"); + strictEqual(og.initialization.properties.length, 3); + + ok(og.clientInitialization.parameters.find((x) => x.kind === "endpoint")); + ok(og.clientInitialization.parameters.find((x) => x === blobName)); + ok(og.clientInitialization.parameters.find((x) => x === containerName)); + 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); + strictEqual(op.parameters[0].onClient, true); + strictEqual(op.parameters[1].onClient, true); + }); + + 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({parameters: ContainerClientInitialization}) + namespace ContainerClient { + op upload is MyService.uploadContainer; + + + model BlobClientInitialization { + containerName: string; + blobName: string; + } + + @client({service: MyService}) + @clientInitialization({parameters: 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.clientInitialization.initializedBy, + InitializedByFlags.Individually, + ); + strictEqual(containerClient.initialization.access, "public"); + strictEqual(containerClient.clientInitialization.parameters.length, 2); + strictEqual(containerClient.initialization.properties.length, 2); + + const endpoint = containerClient.clientInitialization.parameters.find( + (x) => x.kind === "endpoint", + ); + ok(endpoint); + strictEqual( + endpoint, + containerClient.initialization.properties.find((x) => x.kind === "endpoint"), + ); + + const containerName = containerClient.clientInitialization.parameters.find( + (x) => x.name === "containerName", + ); + ok(containerName); + strictEqual( + containerName, + containerClient.initialization.properties.find((x) => x.name === "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.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(blobClient.initialization.access, "public"); + strictEqual(blobClient.clientInitialization.parameters.length, 3); + strictEqual(blobClient.initialization.properties.length, 3); + + const endpointOnBlobClient = blobClient.clientInitialization.parameters.find( + (x) => x.kind === "endpoint", + ); + ok(endpointOnBlobClient); + strictEqual( + endpointOnBlobClient, + blobClient.initialization.properties.find((x) => x.kind === "endpoint"), + ); + + const containerNameOnBlobClient = blobClient.clientInitialization.parameters.find( + (x) => x.name === "containerName", + ); + ok(containerNameOnBlobClient); + strictEqual( + containerNameOnBlobClient, + blobClient.initialization.properties.find((x) => x.name === "containerName"), + ); + + const blobName = blobClient.clientInitialization.parameters.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual( + blobName, + blobClient.initialization.properties.find((x) => x.name === "blobName"), + ); + + const blobMethods = blobClient.methods; + strictEqual(blobMethods.length, 1); + strictEqual(blobMethods[0].name, "upload"); + strictEqual(blobMethods[0].kind, "basic"); + strictEqual(blobMethods[0].parameters.length, 0); + strictEqual(blobMethods[0].operation.parameters.length, 2); + strictEqual( + blobMethods[0].operation.parameters[0].correspondingMethodParams[0], + containerNameOnBlobClient, + ); + strictEqual(blobMethods[0].operation.parameters[1].correspondingMethodParams[0], blobName); + }); + + it("@paramAlias", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blob: string): void; + op upload(@path blobName: string): void; + `, + ` + namespace MyCustomizations; + + model MyClientInitialization { + @paramAlias("blob") + blobName: string; + } + + @@clientInitialization(MyService, {parameters: MyCustomizations.MyClientInitialization}); + `, + ); + const sdkPackage = runner.context.sdkPackage; + const client = sdkPackage.clients[0]; + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.initialization.access, "public"); + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.initialization.properties.length, 2); + + const endpoint = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); + ok(endpoint); + strictEqual( + endpoint, + client.initialization.properties.find((x) => x.kind === "endpoint"), + ); + + const blobName = client.clientInitialization.parameters.find((x) => x.name === "blobName"); + ok(blobName); + strictEqual( + blobName, + client.initialization.properties.find((x) => x.name === "blobName"), + ); + + strictEqual(blobName.clientDefaultValue, undefined); + strictEqual(blobName.onClient, true); + strictEqual(blobName.optional, false); + + const methods = client.methods; + strictEqual(methods.length, 2); + const download = methods[0]; + strictEqual(download.name, "download"); + strictEqual(download.kind, "basic"); + strictEqual(download.parameters.length, 0); + + const downloadOp = download.operation; + strictEqual(downloadOp.parameters.length, 1); + strictEqual(downloadOp.parameters[0].name, "blob"); + strictEqual(downloadOp.parameters[0].correspondingMethodParams.length, 1); + strictEqual(downloadOp.parameters[0].correspondingMethodParams[0], blobName); + + const upload = methods[1]; + strictEqual(upload.name, "upload"); + strictEqual(upload.kind, "basic"); + strictEqual(upload.parameters.length, 0); + + const uploadOp = upload.operation; + strictEqual(uploadOp.parameters.length, 1); + strictEqual(uploadOp.parameters[0].name, "blobName"); + strictEqual(uploadOp.parameters[0].correspondingMethodParams.length, 1); + strictEqual(uploadOp.parameters[0].correspondingMethodParams[0], blobName); + }); + + it("sub client initialized individually", async () => { + await runner.compileWithBuiltInService( + ` + model clientInitModel + { + p1: string; + } + + @route("/bump") + @clientInitialization({parameters: clientInitModel, initializedBy: InitializedBy.individually | InitializedBy.parent}) + interface bumpParameter { + @route("/op1") + @doc("bump parameter") + @post + @convenientAPI(true) + op op1(@path p1: string, @query q1: string): void; + + @route("/op2") + @doc("bump parameter") + @post + @convenientAPI(true) + op op2(@path p1: string): void; + } + `, + ); + const sdkPackage = runner.context.sdkPackage; + const clientAccessor = sdkPackage.clients[0].methods[0]; + strictEqual(clientAccessor.kind, "clientaccessor"); + strictEqual(clientAccessor.access, "internal"); + + const bumpParameterClient = clientAccessor.response; + strictEqual( + bumpParameterClient.clientInitialization.initializedBy, + InitializedByFlags.Individually | InitializedByFlags.Parent, + ); + strictEqual(bumpParameterClient.initialization.access, "internal"); + }); + + it("wrong initializedBy value type", async () => { + const diagnostics = await runner.diagnose(` + @clientInitialization({initializedBy: 4}) + namespace Test { + } + `); + + expectDiagnostics(diagnostics, { + code: "invalid-argument", + }); + }); +}); diff --git a/packages/typespec-client-generator-core/test/packages/client-scenario.test.ts b/packages/typespec-client-generator-core/test/packages/client-scenario.test.ts new file mode 100644 index 0000000000..c7e983c197 --- /dev/null +++ b/packages/typespec-client-generator-core/test/packages/client-scenario.test.ts @@ -0,0 +1,428 @@ +import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing"; +import { AzureResourceManagerTestLibrary } from "@azure-tools/typespec-azure-resource-manager/testing"; +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { OpenAPITestLibrary } from "@typespec/openapi/testing"; +import { ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { InitializedByFlags } from "../../src/interfaces.js"; +import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; + +describe("typespec-client-generator-core: client scenario", () => { + let runner: SdkTestRunner; + + beforeEach(async () => { + runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python" }); + }); + + it("normal client", async () => { + await runner.compile( + ` + @service({ + title: "Pet Store", + }) + namespace PetStore; + + @route("/feed") + op feed(): void; + + @route("/pet") + op pet(): void; + `, + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "PetStoreClient"); + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 1); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + + const methods = client.methods; + strictEqual(methods.length, 2); + strictEqual(methods[0].name, "feed"); + strictEqual(methods[1].name, "pet"); + }); + + it("arm client with operation groups", async () => { + const runnerWithArm = await createSdkTestRunner({ + librariesToAdd: [AzureResourceManagerTestLibrary, AzureCoreTestLibrary, OpenAPITestLibrary], + autoUsings: ["Azure.ResourceManager", "Azure.Core"], + emitterName: "@azure-tools/typespec-java", + }); + await runnerWithArm.compile(` + @armProviderNamespace("My.Service") + @server("http://localhost:3000", "endpoint") + @service({title: "My.Service"}) + @versioned(Versions) + @armCommonTypesVersion(CommonTypes.Versions.v5) + namespace My.Service; + + /** Api versions */ + enum Versions { + /** 2024-04-01-preview api version */ + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + V2024_04_01_PREVIEW: "2024-04-01-preview", + } + + model TestTrackedResource is TrackedResource { + ...ResourceNameParameter; + } + + model TestTrackedResourceProperties { + description?: string; + } + + @armResourceOperations + interface Tests { + get is ArmResourceRead; + } + `); + + const sdkPackage = runnerWithArm.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "ServiceClient"); + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 4); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters[1].name, "credential"); + strictEqual(client.clientInitialization.parameters[2].name, "apiVersion"); + strictEqual(client.clientInitialization.parameters[3].name, "subscriptionId"); + strictEqual(client.methods.length, 1); // client accessor methods which have already deprecated + strictEqual(client.children?.length, 1); + + const tests = client.children?.find((c) => c.name === "Tests"); + ok(tests); + strictEqual(tests.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(tests.clientInitialization.parameters.length, 4); + strictEqual(tests.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(tests.clientInitialization.parameters[1].name, "credential"); + strictEqual(tests.clientInitialization.parameters[2].name, "apiVersion"); + strictEqual(tests.clientInitialization.parameters[3].name, "subscriptionId"); + strictEqual(tests.methods.length, 1); + strictEqual(tests.methods[0].name, "get"); + }); + + it("client with sub clients", async () => { + await runner.compile( + ` + @service({ + title: "Pet Store", + }) + namespace PetStore; + + @route("/pets") + namespace Pets { + @route("/dogs") + interface Dogs { + @route("/feed") + feed(): void; + @route("/pet") + pet(): void; + } + + @route("/cats") + interface Cats { + @route("/feed") + op feed(): void; + @route("/pet") + op pet(): void; + } + } + + @route("/actions") + interface Actions { + @route("/open") + open(): void; + @route("/close") + close(): void; + } + `, + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "PetStoreClient"); + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 1); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(client.methods.length, 2); // client accessor methods which have already deprecated + strictEqual(client.children?.length, 2); + + const pets = client.children?.find((c) => c.name === "Pets"); + ok(pets); + strictEqual(pets.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(pets.clientInitialization.parameters.length, 1); + strictEqual(pets.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(pets?.methods.length, 2); // client accessor methods which have already deprecated + strictEqual(pets?.children?.length, 2); + + const dogs = pets.children?.find((c) => c.name === "Dogs"); + ok(dogs); + strictEqual(dogs.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(dogs.clientInitialization.parameters.length, 1); + strictEqual(dogs.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(dogs?.methods.length, 2); + strictEqual(dogs?.methods[0].name, "feed"); + strictEqual(dogs?.methods[1].name, "pet"); + + const cats = pets.children?.find((c) => c.name === "Cats"); + ok(cats); + strictEqual(cats.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(cats.clientInitialization.parameters.length, 1); + strictEqual(cats.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(cats?.methods.length, 2); + strictEqual(cats?.methods[0].name, "feed"); + strictEqual(cats?.methods[1].name, "pet"); + + const actions = client.children?.find((c) => c.name === "Actions"); + ok(actions); + strictEqual(actions.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(actions.clientInitialization.parameters.length, 1); + strictEqual(actions.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(actions?.methods.length, 2); // client accessor methods which have already deprecated + strictEqual(actions?.methods[0].name, "open"); + strictEqual(actions?.methods[1].name, "close"); + }); + + it("client with sub client and sub client has extra initialization paramters", async () => { + await runner.compileWithCustomization( + ` + @service({ + title: "Azure AI Face API", + }) + namespace Face; + + @route("/largefacelists") + interface FaceListOperations { + op getLargeFaceList(@query largeFaceListId: string): void; + } + + @route("/largepersongroups") + interface PersonGroupOperations { + op getLargePersonGroup(@query largePersonGroupId: string): void; + } + `, + ` + @client( + { + name: "FaceAdministrationClient", + service: Face, + } + ) + namespace FaceAdministrationClient { + model LargeFaceListClientOptions { + largeFaceListId: string; + } + + model LargePersonGroupClientOptions { + largePersonGroupId: string; + } + + @operationGroup + @clientInitialization(LargeFaceListClientOptions) + interface LargeFaceList { + get is Face.FaceListOperations.getLargeFaceList; + } + + @operationGroup + @clientInitialization(LargePersonGroupClientOptions) + interface LargePersonGroup { + get is Face.PersonGroupOperations.getLargePersonGroup; + } + } + `, + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "FaceAdministrationClient"); + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 1); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(client.methods.length, 2); // client accessor methods which have already deprecated + strictEqual(client.children?.length, 2); + + const largeFaceList = client.children?.find((c) => c.name === "LargeFaceList"); + ok(largeFaceList); + strictEqual(largeFaceList.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(largeFaceList.clientInitialization.parameters.length, 2); + strictEqual(largeFaceList.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(largeFaceList.clientInitialization.parameters[1].name, "largeFaceListId"); + strictEqual(largeFaceList?.methods.length, 1); + strictEqual(largeFaceList?.methods[0].name, "get"); + strictEqual(largeFaceList?.methods[0].parameters.length, 0); + + const largePersonGroup = client.children?.find((c) => c.name === "LargePersonGroup"); + ok(largePersonGroup); + strictEqual(largePersonGroup.clientInitialization.initializedBy, InitializedByFlags.Parent); + strictEqual(largePersonGroup.clientInitialization.parameters.length, 2); + strictEqual(largePersonGroup.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(largePersonGroup.clientInitialization.parameters[1].name, "largePersonGroupId"); + strictEqual(largePersonGroup?.methods.length, 1); + strictEqual(largePersonGroup?.methods[0].name, "get"); + strictEqual(largePersonGroup?.methods[0].parameters.length, 0); + }); + + it("client with sub client and sub client can also be initialized individually", async () => { + await runner.compileWithCustomization( + ` + @service({ + title: "Pet Store", + }) + namespace PetStore; + + @route("/pets") + namespace Pets { + @route("/feed") + op feed(): void; + @route("/pet") + op pet(): void; + } + + @route("/actions") + namespace Actions { + @route("/open") + op open(): void; + @route("/close") + op close(): void; + } + `, + ` + @@clientInitialization(PetStore.Pets, + { + initializedBy: InitializedBy.individually | InitializedBy.parent, + } + ); + + @@clientInitialization(PetStore.Actions, + { + initializedBy: InitializedBy.individually | InitializedBy.parent, + } + ); + `, + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "PetStoreClient"); + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 1); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(client.methods.length, 2); // client accessor methods which have already deprecated + strictEqual(client.children?.length, 2); + + const pets = client.children?.find((c) => c.name === "Pets"); + ok(pets); + strictEqual( + pets.clientInitialization.initializedBy, + InitializedByFlags.Individually | InitializedByFlags.Parent, + ); + strictEqual(pets.clientInitialization.parameters.length, 1); + strictEqual(pets.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(pets?.methods.length, 2); + strictEqual(pets.methods[0].name, "feed"); + strictEqual(pets.methods[1].name, "pet"); + + const actions = client.children?.find((c) => c.name === "Actions"); + ok(actions); + strictEqual( + actions.clientInitialization.initializedBy, + InitializedByFlags.Individually | InitializedByFlags.Parent, + ); + strictEqual(actions.clientInitialization.parameters.length, 1); + strictEqual(actions.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(actions?.methods.length, 2); + strictEqual(actions.methods[0].name, "open"); + strictEqual(actions.methods[1].name, "close"); + }); + + it("client with sub client and sub client can also be initialized individually with extra paramters", async () => { + await runner.compileWithCustomization( + ` + @service + namespace ContainerClient { + interface Blob { + @route("/blob") + op download(@path containerName: string, @path blobName: string): void; + } + } + `, + ` + model ContainerClientInitialization { + containerName: string + }; + + model BlobClientInitialization { + containerName: string, + blobName: string + }; + + @@clientInitialization(ContainerClient, {parameters: ContainerClientInitialization}); + @@clientInitialization(ContainerClient.Blob, {parameters: BlobClientInitialization, initializedBy: InitializedBy.individually | InitializedBy.parent}); + `, + ); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "ContainerClient"); + strictEqual(client.clientInitialization.initializedBy, InitializedByFlags.Individually); + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters[1].name, "containerName"); + strictEqual(client.methods.length, 1); // client accessor methods which have already deprecated + strictEqual(client.children?.length, 1); + + const blob = client.children?.find((c) => c.name === "Blob"); + ok(blob); + strictEqual( + blob.clientInitialization.initializedBy, + InitializedByFlags.Individually | InitializedByFlags.Parent, + ); + strictEqual(blob.clientInitialization.parameters.length, 3); + strictEqual(blob.clientInitialization.parameters[0].name, "endpoint"); + strictEqual(blob.clientInitialization.parameters[1].name, "containerName"); + strictEqual(blob.clientInitialization.parameters[2].name, "blobName"); + strictEqual(blob?.methods.length, 1); + strictEqual(blob.methods[0].name, "download"); + strictEqual(blob.methods[0].parameters.length, 0); + }); + + it("first level client could not be initialized by parent", async () => { + await runner.compileWithCustomization( + ` + @service + namespace MyService; + + op download(@path blobName: string): void; + `, + ` + namespace MyCustomizations; + + @@clientInitialization(MyService, {initializedBy: InitializedBy.parent}); + `, + ); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/invalid-initialized-by", + message: + "Invalid 'initializedBy' value. First level client must have `InitializedBy.individually` specified in `initializedBy`.", + }); + }); + + it("sub client could not only be initialized individually", async () => { + await runner.compileWithBuiltInService( + ` + @route("/bump") + @clientInitialization({initializedBy: InitializedBy.individually}) + interface SubClient { + op test(): void; + } + `, + ); + expectDiagnostics(runner.context.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/invalid-initialized-by", + message: + "Invalid 'initializedBy' value. Sub client must have `InitializedBy.parent` or `InitializedBy.individually | InitializedBy.parent` specified in `initializedBy`.", + }); + }); +}); diff --git a/packages/typespec-client-generator-core/test/packages/client.test.ts b/packages/typespec-client-generator-core/test/packages/client.test.ts index 16e66400b4..bdaa156dc2 100644 --- a/packages/typespec-client-generator-core/test/packages/client.test.ts +++ b/packages/typespec-client-generator-core/test/packages/client.test.ts @@ -3,12 +3,11 @@ import { ApiKeyAuth, OAuth2Flow, Oauth2Auth } from "@typespec/http"; import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { - SdkClientType, + InitializedByFlags, SdkCredentialParameter, SdkCredentialType, SdkEndpointParameter, SdkEndpointType, - SdkHttpOperation, } from "../../src/interfaces.js"; import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; @@ -72,9 +71,9 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); - strictEqual(client.initialization.name, "ServiceClientOptions"); - strictEqual(client.initialization.properties.length, 1); - const endpointParam = client.initialization.properties[0]; + strictEqual(client.clientInitialization.name, "ServiceClientOptions"); + strictEqual(client.clientInitialization.parameters.length, 1); + const endpointParam = client.clientInitialization.parameters[0]; strictEqual(endpointParam.kind, "endpoint"); strictEqual(endpointParam.name, "endpoint"); strictEqual(endpointParam.onClient, true); @@ -105,9 +104,9 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); - strictEqual(client.initialization.properties.length, 2); + strictEqual(client.clientInitialization.parameters.length, 2); - const endpointParam = client.initialization.properties.filter( + const endpointParam = client.clientInitialization.parameters.filter( (p): p is SdkEndpointParameter => p.kind === "endpoint", )[0]; strictEqual(endpointParam.type.kind, "endpoint"); @@ -118,7 +117,7 @@ describe("typespec-client-generator-core: client", () => { strictEqual(templateArg.type.kind, "string"); strictEqual(templateArg.clientDefaultValue, "http://localhost:3000"); - const credentialParam = client.initialization.properties.filter( + const credentialParam = client.clientInitialization.parameters.filter( (p): p is SdkCredentialParameter => p.kind === "credential", )[0]; strictEqual(credentialParam.name, "credential"); @@ -148,9 +147,9 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); - strictEqual(client.initialization.properties.length, 2); + strictEqual(client.clientInitialization.parameters.length, 2); - const endpointParam = client.initialization.properties.filter( + const endpointParam = client.clientInitialization.parameters.filter( (p): p is SdkEndpointParameter => p.kind === "endpoint", )[0]; strictEqual(endpointParam.type.kind, "endpoint"); @@ -163,7 +162,7 @@ describe("typespec-client-generator-core: client", () => { strictEqual(templateArg.onClient, true); strictEqual(templateArg.clientDefaultValue, "http://localhost:3000"); - const credentialParam = client.initialization.properties.filter( + const credentialParam = client.clientInitialization.parameters.filter( (p): p is SdkCredentialParameter => p.kind === "credential", )[0]; strictEqual(credentialParam.name, "credential"); @@ -199,9 +198,9 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); - strictEqual(client.initialization.properties.length, 2); + strictEqual(client.clientInitialization.parameters.length, 2); - const endpointParam = client.initialization.properties.filter( + const endpointParam = client.clientInitialization.parameters.filter( (p): p is SdkEndpointParameter => p.kind === "endpoint", )[0]; strictEqual(endpointParam.type.kind, "endpoint"); @@ -212,7 +211,7 @@ describe("typespec-client-generator-core: client", () => { strictEqual(templateArg.name, "endpoint"); strictEqual(templateArg.clientDefaultValue, "http://localhost:3000"); - const credentialParam = client.initialization.properties.filter( + const credentialParam = client.clientInitialization.parameters.filter( (p): p is SdkCredentialParameter => p.kind === "credential", )[0]; strictEqual(credentialParam.name, "credential"); @@ -263,9 +262,9 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); - strictEqual(client.initialization.properties.length, 2); + strictEqual(client.clientInitialization.parameters.length, 2); - const endpointParam = client.initialization.properties.filter( + const endpointParam = client.clientInitialization.parameters.filter( (p): p is SdkEndpointParameter => p.kind === "endpoint", )[0]; strictEqual(endpointParam.clientDefaultValue, undefined); @@ -285,7 +284,7 @@ describe("typespec-client-generator-core: client", () => { strictEqual(templateArg.clientDefaultValue, undefined); strictEqual(templateArg.doc, undefined); - const credentialParam = client.initialization.properties.filter( + const credentialParam = client.clientInitialization.parameters.filter( (p): p is SdkCredentialParameter => p.kind === "credential", )[0]; strictEqual(credentialParam.name, "credential"); @@ -325,11 +324,11 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); - strictEqual(client.initialization.properties.length, 2); + strictEqual(client.clientInitialization.parameters.length, 2); strictEqual(client.apiVersions.length, 1); strictEqual(client.apiVersions[0], "v1.0"); - const endpointParams = client.initialization.properties.filter( + const endpointParams = client.clientInitialization.parameters.filter( (p): p is SdkEndpointParameter => p.kind === "endpoint", ); strictEqual(endpointParams.length, 1); @@ -374,7 +373,7 @@ describe("typespec-client-generator-core: client", () => { strictEqual(apiVersionParam.kind, "path"); deepStrictEqual(client.apiVersions, ["v1.0"]); - const credentialParam = client.initialization.properties.find( + const credentialParam = client.clientInitialization.parameters.find( (p): p is SdkCredentialParameter => p.kind === "credential", ); ok(credentialParam); @@ -408,9 +407,9 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); - strictEqual(client.initialization.properties.length, 1); + strictEqual(client.clientInitialization.parameters.length, 1); - const endpointParams = client.initialization.properties.filter( + const endpointParams = client.clientInitialization.parameters.filter( (p): p is SdkEndpointParameter => p.kind === "endpoint", ); strictEqual(endpointParams.length, 1); @@ -470,9 +469,9 @@ describe("typespec-client-generator-core: client", () => { const sdkPackage = runner.context.sdkPackage; strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 1); + strictEqual(client.clientInitialization.parameters.length, 1); - const endpointParam = client.initialization.properties.filter( + const endpointParam = client.clientInitialization.parameters.filter( (p): p is SdkEndpointParameter => p.kind === "endpoint", )[0]; strictEqual(endpointParam.type.kind, "endpoint"); @@ -530,11 +529,11 @@ describe("typespec-client-generator-core: client", () => { const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); strictEqual(client.crossLanguageDefinitionId, "My.Service"); - strictEqual(client.initialization.properties.length, 3); + strictEqual(client.clientInitialization.parameters.length, 3); strictEqual(client.apiVersions.length, 1); strictEqual(client.apiVersions[0], "2022-12-01-preview"); - const endpointParam = client.initialization.properties.find((x) => x.kind === "endpoint"); + const endpointParam = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); ok(endpointParam); strictEqual(endpointParam.name, "endpoint"); strictEqual(endpointParam.kind, "endpoint"); @@ -551,7 +550,9 @@ describe("typespec-client-generator-core: client", () => { strictEqual(endpointTemplateArg.kind, "path"); strictEqual(endpointTemplateArg.clientDefaultValue, "http://localhost:3000"); - const apiVersionParam = client.initialization.properties.filter((p) => p.isApiVersionParam)[0]; + const apiVersionParam = client.clientInitialization.parameters.filter( + (p) => p.isApiVersionParam, + )[0]; strictEqual(apiVersionParam.name, "apiVersion"); strictEqual(apiVersionParam.onClient, true); strictEqual(apiVersionParam.optional, false); @@ -606,11 +607,11 @@ describe("typespec-client-generator-core: client", () => { const client = sdkPackage.clients[0]; strictEqual(client.name, "ServiceClient"); strictEqual(client.crossLanguageDefinitionId, "My.Service"); - strictEqual(client.initialization.properties.length, 3); + strictEqual(client.clientInitialization.parameters.length, 3); strictEqual(client.apiVersions.length, 2); deepStrictEqual(client.apiVersions, ["2022-12-01-preview", "2022-12-01"]); - const endpointParam = client.initialization.properties.find((x) => x.kind === "endpoint"); + const endpointParam = client.clientInitialization.parameters.find((x) => x.kind === "endpoint"); ok(endpointParam); strictEqual(endpointParam.type.kind, "endpoint"); strictEqual(endpointParam.type.serverUrl, "{endpoint}"); @@ -621,7 +622,9 @@ describe("typespec-client-generator-core: client", () => { strictEqual(templateArg.onClient, true); strictEqual(templateArg.clientDefaultValue, "http://localhost:3000"); - const apiVersionParam = client.initialization.properties.filter((p) => p.isApiVersionParam)[0]; + const apiVersionParam = client.clientInitialization.parameters.filter( + (p) => p.isApiVersionParam, + )[0]; strictEqual(apiVersionParam.name, "apiVersion"); strictEqual(apiVersionParam.onClient, true); strictEqual(apiVersionParam.optional, false); @@ -666,14 +669,15 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const mainClient = sdkPackage.clients.find((c) => c.name === "TestServiceClient"); - const operationGroup = mainClient?.methods.find((c) => c.kind === "clientaccessor") - ?.response as SdkClientType; - ok(mainClient && operationGroup); + ok(mainClient); + ok(mainClient.children); + const operationGroup = mainClient.children[0]; + ok(operationGroup); strictEqual(operationGroup.parent, mainClient); strictEqual(mainClient.methods.length, 1); - strictEqual(mainClient.initialization.properties.length, 1); - strictEqual(mainClient.initialization.properties[0].name, "endpoint"); + strictEqual(mainClient.clientInitialization.parameters.length, 1); + strictEqual(mainClient.clientInitialization.parameters[0].name, "endpoint"); strictEqual(mainClient.crossLanguageDefinitionId, "TestService"); const clientAccessor = mainClient.methods[0]; @@ -687,8 +691,8 @@ describe("typespec-client-generator-core: client", () => { "TestService.MyOperationGroup.getMyOperationGroup", ); - strictEqual(operationGroup.initialization.properties.length, 1); - strictEqual(operationGroup.initialization.access, "internal"); + strictEqual(operationGroup.clientInitialization.parameters.length, 1); + strictEqual(operationGroup.clientInitialization.initializedBy, InitializedByFlags.Parent); strictEqual(operationGroup.methods.length, 1); strictEqual(operationGroup.methods[0].name, "func"); strictEqual( @@ -715,23 +719,20 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const mainClient = sdkPackage.clients[0]; - const fooClient = mainClient.methods.find( - (m) => m.kind === "clientaccessor" && m.name === "getFoo", - )?.response as SdkClientType; - const fooBarClient = fooClient.methods.find((m) => m.kind === "clientaccessor") - ?.response as SdkClientType; - const barClient = mainClient.methods.find( - (m) => m.kind === "clientaccessor" && m.name === "getBar", - )?.response as SdkClientType; - ok(mainClient && fooClient && fooBarClient && barClient); + strictEqual(mainClient.children?.length, 2); + const fooClient = mainClient.children.find((m) => m.name === "Foo"); + strictEqual(fooClient?.children?.length, 1); + const fooBarClient = fooClient.children.find((m) => m.name === "Bar"); + const barClient = mainClient.children.find((m) => m.name === "Bar"); + ok(fooBarClient && barClient); strictEqual(fooClient.parent, mainClient); strictEqual(fooBarClient.parent, fooClient); strictEqual(barClient.parent, mainClient); strictEqual(mainClient.methods.length, 2); - ok(mainClient.initialization); - strictEqual(mainClient.initialization.properties.length, 1); - strictEqual(mainClient.initialization.properties[0].name, "endpoint"); + ok(mainClient.clientInitialization); + strictEqual(mainClient.clientInitialization.parameters.length, 1); + strictEqual(mainClient.clientInitialization.parameters[0].name, "endpoint"); strictEqual(mainClient.crossLanguageDefinitionId, "TestService"); const fooAccessor = mainClient.methods[0]; @@ -750,8 +751,8 @@ describe("typespec-client-generator-core: client", () => { strictEqual(barAccessor.parameters.length, 0); strictEqual(barAccessor.response, barClient); - strictEqual(fooClient.initialization.properties.length, 1); - strictEqual(fooClient.initialization.access, "internal"); + strictEqual(fooClient.clientInitialization.parameters.length, 1); + strictEqual(fooClient.clientInitialization.initializedBy, InitializedByFlags.Parent); strictEqual(fooClient.methods.length, 1); strictEqual(fooClient.crossLanguageDefinitionId, "TestService.Foo"); @@ -763,16 +764,16 @@ describe("typespec-client-generator-core: client", () => { strictEqual(fooBarAccessor.parameters.length, 0); strictEqual(fooBarAccessor.response, fooBarClient); - strictEqual(fooBarClient.initialization.properties.length, 1); - strictEqual(fooBarClient.initialization.access, "internal"); + strictEqual(fooBarClient.clientInitialization.parameters.length, 1); + strictEqual(fooBarClient.clientInitialization.initializedBy, InitializedByFlags.Parent); strictEqual(fooBarClient.crossLanguageDefinitionId, "TestService.Foo.Bar"); strictEqual(fooBarClient.methods.length, 1); strictEqual(fooBarClient.methods[0].kind, "basic"); strictEqual(fooBarClient.methods[0].name, "one"); strictEqual(fooBarClient.methods[0].crossLanguageDefinitionId, "TestService.Foo.Bar.one"); - strictEqual(barClient.initialization.properties.length, 1); - strictEqual(barClient.initialization.access, "internal"); + strictEqual(barClient.clientInitialization.parameters.length, 1); + strictEqual(barClient.clientInitialization.initializedBy, InitializedByFlags.Parent); strictEqual(barClient.crossLanguageDefinitionId, "TestService.Bar"); strictEqual(barClient.methods.length, 1); strictEqual(barClient.methods[0].kind, "basic"); @@ -816,8 +817,8 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 1); - strictEqual(client.initialization.properties[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters.length, 1); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); strictEqual(client.methods.length, 1); @@ -845,8 +846,8 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 1); - strictEqual(client.initialization.properties[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters.length, 1); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); strictEqual(sdkPackage.clients[0].methods.length, 1); const withApiVersion = sdkPackage.clients[0].methods[0]; @@ -890,8 +891,8 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 1); - strictEqual(client.initialization.properties[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters.length, 1); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); const withoutApiVersion = client.methods[0]; strictEqual(withoutApiVersion.kind, "basic"); @@ -920,10 +921,10 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 2); - strictEqual(client.initialization.properties[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); - const clientApiVersionParam = client.initialization.properties[1]; + const clientApiVersionParam = client.clientInitialization.parameters[1]; strictEqual(clientApiVersionParam.name, "apiVersion"); strictEqual(clientApiVersionParam.onClient, true); strictEqual(clientApiVersionParam.optional, false); @@ -953,7 +954,7 @@ describe("typespec-client-generator-core: client", () => { strictEqual(apiVersionParam.correspondingMethodParams.length, 1); strictEqual( apiVersionParam.correspondingMethodParams[0], - client.initialization.properties.find((x) => x.isApiVersionParam), + client.clientInitialization.parameters.find((x) => x.isApiVersionParam), ); }); @@ -974,10 +975,10 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 2); - strictEqual(client.initialization.properties[0].name, "endpoint"); + strictEqual(client.clientInitialization.parameters.length, 2); + strictEqual(client.clientInitialization.parameters[0].name, "endpoint"); - const clientApiVersionParam = client.initialization.properties[1]; + const clientApiVersionParam = client.clientInitialization.parameters[1]; strictEqual(clientApiVersionParam.name, "apiVersion"); strictEqual(clientApiVersionParam.onClient, true); strictEqual(clientApiVersionParam.optional, false); @@ -1009,7 +1010,7 @@ describe("typespec-client-generator-core: client", () => { strictEqual(apiVersionParam.correspondingMethodParams.length, 1); strictEqual( apiVersionParam.correspondingMethodParams[0], - client.initialization.properties.find((x) => x.isApiVersionParam), + client.clientInitialization.parameters.find((x) => x.isApiVersionParam), ); }); @@ -1041,8 +1042,8 @@ describe("typespec-client-generator-core: client", () => { strictEqual(sdkPackage.clients.length, 1); const client = sdkPackage.clients[0]; - strictEqual(client.initialization.properties.length, 1); - const parameter = client.initialization.properties[0]; + strictEqual(client.clientInitialization.parameters.length, 1); + const parameter = client.clientInitialization.parameters[0]; strictEqual(parameter.name, "endpoint"); strictEqual(parameter.type.kind, "union"); diff --git a/packages/typespec-client-generator-core/test/public-utils/get-http-operation-parameter.test.ts b/packages/typespec-client-generator-core/test/public-utils/get-http-operation-parameter.test.ts index 4c1c7859f9..cfe46fb1cc 100644 --- a/packages/typespec-client-generator-core/test/public-utils/get-http-operation-parameter.test.ts +++ b/packages/typespec-client-generator-core/test/public-utils/get-http-operation-parameter.test.ts @@ -439,7 +439,7 @@ describe("typespec-client-generator-core: public-utils getHttpOperationParameter const client = sdkPackage.clients[0]; let httpParam = getHttpOperationParameter( client.methods[0] as SdkServiceMethod, - client.initialization.properties[0] as SdkMethodParameter, + client.initialization.properties[1] as SdkMethodParameter, ); ok(httpParam); strictEqual(httpParam.kind, "path"); @@ -447,7 +447,7 @@ describe("typespec-client-generator-core: public-utils getHttpOperationParameter httpParam = getHttpOperationParameter( client.methods[1] as SdkServiceMethod, - client.initialization.properties[0] as SdkMethodParameter, + client.initialization.properties[1] as SdkMethodParameter, ); ok(httpParam); strictEqual(httpParam.kind, "path"); diff --git a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md new file mode 100644 index 0000000000..d48a726fed --- /dev/null +++ b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md @@ -0,0 +1,59 @@ +--- +title: "Data types" +--- + +## Azure.ClientGenerator.Core + +### `ClientInitializationOptions` {#Azure.ClientGenerator.Core.ClientInitializationOptions} + +Client initialization customization options. + +```typespec +model Azure.ClientGenerator.Core.ClientInitializationOptions +``` + +#### Properties + +| Name | Type | Description | +| -------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| parameters? | `Model` | Redefine the client initialization parameters you would like to add to the client.
By default, we apply endpoint, credential, and api-version parameters. If you specify parameters model, we will append the properties of the model to the parameters list of the client initialization. | +| initializedBy? | `EnumMember \| Union` | Determines how the client could be initialized. Use `InitializedBy` enum to set the value. The value could be `InitializedBy.individually`, `InitializedBy.parent` or `InitializedBy.individually \| InitializedBy.parent`. | + +### `Access` {#Azure.ClientGenerator.Core.Access} + +Access value. + +```typespec +enum Azure.ClientGenerator.Core.Access +``` + +| Name | Value | Description | +| -------- | ------------ | -------------- | +| public | `"public"` | Open to user | +| internal | `"internal"` | Hide from user | + +### `InitializedBy` {#Azure.ClientGenerator.Core.InitializedBy} + +InitializedBy value. + +```typespec +enum Azure.ClientGenerator.Core.InitializedBy +``` + +| Name | Value | Description | +| ------------ | ----- | ------------------------------------------------- | +| individually | `1` | The client could be initialized individually. | +| parent | `2` | The client could be initialized by parent client. | + +### `Usage` {#Azure.ClientGenerator.Core.Usage} + +Usage value. + +```typespec +enum Azure.ClientGenerator.Core.Usage +``` + +| Name | Value | Description | +| ------ | ----- | ---------------- | +| input | `2` | Used in request | +| output | `4` | Used in response | diff --git a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md index 196f10f15c..c91d80c6eb 100644 --- a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md +++ b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md @@ -272,10 +272,10 @@ interface MyInterface {} ### `@clientInitialization` {#@Azure.ClientGenerator.Core.clientInitialization} -Client parameters you would like to add to the client. By default, we apply endpoint, credential, and api-version parameters. If you add clientInitialization, we will append those to the default list of parameters. +Customize the client initialization way. ```typespec -@Azure.ClientGenerator.Core.clientInitialization(options: Model, scope?: valueof string) +@Azure.ClientGenerator.Core.clientInitialization(options: Azure.ClientGenerator.Core.ClientInitializationOptions, scope?: valueof string) ``` #### Target @@ -284,10 +284,10 @@ Client parameters you would like to add to the client. By default, we apply endp #### 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.
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | +| Name | Type | Description | +| ------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| options | [`ClientInitializationOptions`](./data-types.md#Azure.ClientGenerator.Core.ClientInitializationOptions) | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters.
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -304,9 +304,9 @@ 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. +@@clientInitialization(MyService, {parameters: MyServiceClientOptions}) +// The generated client will have `blobName` on its initialization method. We will also +// elevate the existing `blobName` parameter from method level to client level. ``` ### `@clientName` {#@Azure.ClientGenerator.Core.clientName} diff --git a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/index.mdx b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/index.mdx index 09a1f8c54a..896778e760 100644 --- a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/index.mdx +++ b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/index.mdx @@ -56,3 +56,7 @@ npm install --save-peer @azure-tools/typespec-client-generator-core - [`@scope`](./decorators.md#@Azure.ClientGenerator.Core.scope) - [`@usage`](./decorators.md#@Azure.ClientGenerator.Core.usage) - [`@useSystemTextJsonConverter`](./decorators.md#@Azure.ClientGenerator.Core.useSystemTextJsonConverter) + +### Models + +- [`ClientInitializationOptions`](./data-types.md#Azure.ClientGenerator.Core.ClientInitializationOptions)