diff --git a/.chronus/changes/tcgc-clientOptions-2026-0-22-16-26-53.md b/.chronus/changes/tcgc-clientOptions-2026-0-22-16-26-53.md new file mode 100644 index 0000000000..cde7450d54 --- /dev/null +++ b/.chronus/changes/tcgc-clientOptions-2026-0-22-16-26-53.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Add `@clientOption` flag for experimental, language-specific flags \ No newline at end of file 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 e1916ff437..900292748a 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 @@ -944,6 +944,38 @@ export type ClientDocDecorator = ( scope?: string, ) => DecoratorValidatorCallbacks | void; +/** + * Pass experimental flags or options to emitters without requiring TCGC reshipping. + * This decorator is intended for temporary workarounds or experimental features and requires + * suppression to acknowledge its experimental nature. + * + * See supported client options for each language emitter here https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/12clientOptions/ + * + * **Warning**: This decorator always emits a warning that must be suppressed, and an additional + * warning if no scope is provided (since options are typically language-specific). + * + * @param target The type you want to apply the option to. + * @param name The name of the option (e.g., "enableFeatureFoo"). + * @param value The value of the option. Can be any type; emitters will cast as needed. + * @param scope Specifies the target language emitters that the decorator should apply. If not set, the decorator will be applied to all language emitters by default. + * You can use "!" to exclude specific languages, for example: !(java, python) or !java, !python. + * @example Apply an experimental option for Python + * ```typespec + * #suppress "@azure-tools/typespec-client-generator-core/client-option" "preview feature for python" + * @clientOption("enableFeatureFoo", true, "python") + * model MyModel { + * prop: string; + * } + * ``` + */ +export type ClientOptionDecorator = ( + context: DecoratorContext, + target: Type, + name: string, + value: unknown, + scope?: string, +) => DecoratorValidatorCallbacks | void; + export type AzureClientGeneratorCoreDecorators = { clientName: ClientNameDecorator; convenientAPI: ConvenientAPIDecorator; @@ -965,4 +997,5 @@ export type AzureClientGeneratorCoreDecorators = { responseAsBool: ResponseAsBoolDecorator; clientLocation: ClientLocationDecorator; clientDoc: ClientDocDecorator; + clientOption: ClientOptionDecorator; }; diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index ce7bb149e2..3c52b3f5ab 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -1022,3 +1022,35 @@ extern dec clientDoc( mode: EnumMember, scope?: valueof string ); + +/** + * Pass experimental flags or options to emitters without requiring TCGC reshipping. + * This decorator is intended for temporary workarounds or experimental features and requires + * suppression to acknowledge its experimental nature. + * + * See supported client options for each language emitter here https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/12clientOptions/ + * + * **Warning**: This decorator always emits a warning that must be suppressed, and an additional + * warning if no scope is provided (since options are typically language-specific). + * + * @param target The type you want to apply the option to. + * @param name The name of the option (e.g., "enableFeatureFoo"). + * @param value The value of the option. Can be any type; emitters will cast as needed. + * @param scope Specifies the target language emitters that the decorator should apply. If not set, the decorator will be applied to all language emitters by default. + * You can use "!" to exclude specific languages, for example: !(java, python) or !java, !python. + * + * @example Apply an experimental option for Python + * ```typespec + * #suppress "@azure-tools/typespec-client-generator-core/client-option" "preview feature for python" + * @clientOption("enableFeatureFoo", true, "python") + * model MyModel { + * prop: string; + * } + * ``` + */ +extern dec clientOption( + target: unknown, + name: valueof string, + value: valueof unknown, + scope?: valueof string +); diff --git a/packages/typespec-client-generator-core/src/configs.ts b/packages/typespec-client-generator-core/src/configs.ts index 5978f59ade..a93123e8fc 100644 --- a/packages/typespec-client-generator-core/src/configs.ts +++ b/packages/typespec-client-generator-core/src/configs.ts @@ -2,4 +2,5 @@ export const defaultDecoratorsAllowList = [ "TypeSpec\\.Xml\\..*", "Azure\\.Core\\.@useFinalStateVia", "Autorest\\.@example", + "Azure\\.ClientGenerator\\.Core\\.@clientOption", ]; diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 7b58ae3492..6b3cffbd1d 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -45,6 +45,7 @@ import { ClientInitializationDecorator, ClientNameDecorator, ClientNamespaceDecorator, + ClientOptionDecorator, ConvenientAPIDecorator, DeserializeEmptyStringAsNullDecorator, OperationGroupDecorator, @@ -1851,3 +1852,36 @@ export function isInScope(context: TCGCContext, entity: Operation | ModelPropert } return true; } + +export const clientOptionKey = createStateSymbol("ClientOption"); + +/** + * `@clientOption` decorator implementation. + * Pass experimental flags or options to emitters without requiring TCGC reshipping. + * The decorator data is stored as {name, value} and exposed via the decorators array. + */ +export const $clientOption: ClientOptionDecorator = ( + context: DecoratorContext, + target: Type, + name: string, + value: unknown, + scope?: LanguageScopes, +) => { + // Always emit warning that this is experimental + reportDiagnostic(context.program, { + code: "client-option", + target: target, + }); + + // Emit additional warning if scope is not provided + if (scope === undefined) { + reportDiagnostic(context.program, { + code: "client-option-requires-scope", + target: target, + }); + } + + // Store the option data - each decorator application is stored separately + // The decorator info will be exposed via the decorators array on SDK types + setScopedDecoratorData(context, $clientOption, clientOptionKey, target, { name, value }, scope); +}; diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 25ae9a9c14..841ed56629 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -202,6 +202,25 @@ export interface DecoratorInfo { arguments: Record; } +/** + * Represents a client option set via the `@clientOption` decorator. + * This is a convenience type for accessing client options without parsing the decorators array directly. + */ +export interface SdkClientOption { + /** + * The name of the client option. + */ + name: string; + /** + * The value of the client option. + */ + value: string | boolean | number; + /** + * The language scope this option applies to, if specified. + */ + scope?: string; +} + /** * Represents a client in the package. */ diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index b6bd1461ae..d825fa2c60 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -402,6 +402,14 @@ export function getTypeDecorators( getDecoratorArgValue(context, decorator.args[i].jsValue, type, decoratorName), ); } + + // Filter by scope - only include decorators that match the current emitter or have no scope + const scopeArg = decoratorInfo.arguments["scope"]; + if (scopeArg !== undefined && scopeArg !== context.emitterName) { + // Skip this decorator if it has a scope that doesn't match the current emitter + continue; + } + retval.push(decoratorInfo); } } diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index ba17212419..26a2a00bcb 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -482,6 +482,20 @@ export const $lib = createTypeSpecLibrary({ default: "All services must have the same server and auth definitions.", }, }, + "client-option": { + severity: "warning", + messages: { + default: + "@clientOption is experimental and should only be used for temporary workarounds. This usage must be suppressed.", + }, + }, + "client-option-requires-scope": { + severity: "warning", + messages: { + default: + "@clientOption should be applied with a specific language scope since it is highly likely this is language-specific.", + }, + }, }, emitter: { options: TCGCEmitterOptionsSchema, diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index cf3cd3ec51..f73b0d35c4 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -41,8 +41,10 @@ import { listOperationsInOperationGroup, } from "./decorators.js"; import { + DecoratorInfo, SdkBodyParameter, SdkClient, + SdkClientOption, SdkClientType, SdkCookieParameter, SdkHeaderParameter, @@ -897,3 +899,32 @@ export function getNamespaceFromType( } return undefined; } + +const CLIENT_OPTION_DECORATOR_NAME = "Azure.ClientGenerator.Core.@clientOption"; + +/** + * Get all client options from a decorated SDK type. + * This is a convenience function for extracting `@clientOption` decorator data + * from the decorators array on SDK types. + * + * @param decorators - The decorators array from an SDK type (model, enum, operation, property, etc.) + * @returns An array of client options with their name, value, and optional scope + * + * @example + * ```typescript + * const sdkModel = context.sdkPackage.models.find(m => m.name === "MyModel"); + * const clientOptions = getClientOptions(sdkModel.decorators); + * for (const option of clientOptions) { + * console.log(`Option: ${option.name} = ${option.value}`); + * } + * ``` + */ +export function getClientOptions(decorators: DecoratorInfo[]): SdkClientOption[] { + return decorators + .filter((d) => d.name === CLIENT_OPTION_DECORATOR_NAME) + .map((d) => ({ + name: d.arguments.name as string, + value: d.arguments.value as string | boolean | number, + scope: d.arguments.scope as string | undefined, + })); +} diff --git a/packages/typespec-client-generator-core/src/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index 6d4799088e..4604443cc5 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -12,6 +12,7 @@ import { $clientLocation, $clientName, $clientNamespace, + $clientOption, $convenientAPI, $deserializeEmptyStringAsNull, $flattenProperty, @@ -55,6 +56,7 @@ export const $decorators = { responseAsBool: $responseAsBool, clientDoc: $clientDoc, clientLocation: $clientLocation, + clientOption: $clientOption, } satisfies AzureClientGeneratorCoreDecorators, "Azure.ClientGenerator.Core.Legacy": { diff --git a/packages/typespec-client-generator-core/test/decorators/client-option.test.ts b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts new file mode 100644 index 0000000000..8ae12f5c41 --- /dev/null +++ b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts @@ -0,0 +1,357 @@ +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { getClientOptions } from "../../src/public-utils.js"; +import { createSdkContextForTester, SimpleTester, SimpleTesterWithService } from "../tester.js"; + +describe("@clientOption diagnostics", () => { + it("should emit client-option warning always", async () => { + const diagnostics = await SimpleTester.diagnose(` + @service + namespace MyService; + + @clientOption("enableFeatureFoo", true, "python") + model Test { + id: string; + } + `); + + expectDiagnostics(diagnostics, { + code: "@azure-tools/typespec-client-generator-core/client-option", + }); + }); + + it("should emit both client-option and client-option-requires-scope warnings when scope is missing", async () => { + const diagnostics = await SimpleTester.diagnose(` + @service + namespace MyService; + + @clientOption("enableFeatureFoo", true) + model Test { + id: string; + } + `); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/client-option", + }, + { + code: "@azure-tools/typespec-client-generator-core/client-option-requires-scope", + }, + ]); + }); + + it("should only emit client-option warning when scope is provided", async () => { + const diagnostics = await SimpleTester.diagnose(` + @service + namespace MyService; + + @clientOption("enableFeatureFoo", true, "python") + model Test { + id: string; + } + `); + + // Should only have the client-option warning, not client-option-requires-scope + strictEqual(diagnostics.length, 1); + expectDiagnostics(diagnostics, { + code: "@azure-tools/typespec-client-generator-core/client-option", + }); + }); +}); + +describe("@clientOption with getClientOptions getter", () => { + it("should return client options for model", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("enableFeatureFoo", true, "python") + @test + model Test { + id: string; + } + + op getTest(): Test; + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const clientOptions = getClientOptions(sdkModel.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { + name: "enableFeatureFoo", + value: true, + scope: "python", + }); + }); + + it("should return multiple client options on same target", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("enableFeatureFoo", true, "python") + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("enableFeatureBar", "value", "python") + @test + model Test { + id: string; + } + + op getTest(): Test; + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const clientOptions = getClientOptions(sdkModel.decorators); + strictEqual(clientOptions.length, 2); + + // Verify each option has the correct name and value + const fooOption = clientOptions.find((o) => o.name === "enableFeatureFoo"); + ok(fooOption, "enableFeatureFoo option should exist"); + strictEqual(fooOption.name, "enableFeatureFoo"); + strictEqual(fooOption.value, true); + strictEqual(fooOption.scope, "python"); + + const barOption = clientOptions.find((o) => o.name === "enableFeatureBar"); + ok(barOption, "enableFeatureBar option should exist"); + strictEqual(barOption.name, "enableFeatureBar"); + strictEqual(barOption.value, "value"); + strictEqual(barOption.scope, "python"); + }); + + it("should support different value types", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("boolOption", true, "python") + @test + model TestBool { + id: string; + } + + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("stringOption", "someValue", "python") + @test + model TestString { + id: string; + } + + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("numberOption", 42, "python") + @test + model TestNumber { + id: string; + } + + @route("/bool") op getBool(): TestBool; + @route("/string") op getString(): TestString; + @route("/number") op getNumber(): TestNumber; + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + // Verify boolean value type + const sdkModelBool = context.sdkPackage.models.find((m) => m.name === "TestBool"); + ok(sdkModelBool, "TestBool model should exist"); + const boolOptions = getClientOptions(sdkModelBool.decorators); + strictEqual(boolOptions.length, 1); + strictEqual(boolOptions[0].name, "boolOption"); + strictEqual(boolOptions[0].value, true); + strictEqual(typeof boolOptions[0].value, "boolean"); + + // Verify string value type + const sdkModelString = context.sdkPackage.models.find((m) => m.name === "TestString"); + ok(sdkModelString, "TestString model should exist"); + const stringOptions = getClientOptions(sdkModelString.decorators); + strictEqual(stringOptions.length, 1); + strictEqual(stringOptions[0].name, "stringOption"); + strictEqual(stringOptions[0].value, "someValue"); + strictEqual(typeof stringOptions[0].value, "string"); + + // Verify number value type + const sdkModelNumber = context.sdkPackage.models.find((m) => m.name === "TestNumber"); + ok(sdkModelNumber, "TestNumber model should exist"); + const numberOptions = getClientOptions(sdkModelNumber.decorators); + strictEqual(numberOptions.length, 1); + strictEqual(numberOptions[0].name, "numberOption"); + strictEqual(numberOptions[0].value, 42); + strictEqual(typeof numberOptions[0].value, "number"); + }); + + it("should return client options for operation", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("operationFlag", "customValue", "python") + @test + op testOp(): string; + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + const sdkMethod = context.sdkPackage.clients[0].methods.find( + (m) => m.kind === "basic" && m.name === "testOp", + ); + ok(sdkMethod, "SDK method should exist"); + + const clientOptions = getClientOptions(sdkMethod.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { + name: "operationFlag", + value: "customValue", + scope: "python", + }); + }); + + it("should return client options for enum", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("enumFlag", true, "python") + @usage(Usage.input) + @test + enum TestEnum { + One, + Two, + } + + op getTest(@query value: TestEnum): string; + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + const sdkEnum = context.sdkPackage.enums.find((e) => e.name === "TestEnum"); + ok(sdkEnum, "SDK enum should exist"); + + const clientOptions = getClientOptions(sdkEnum.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { + name: "enumFlag", + value: true, + scope: "python", + }); + }); + + it("should return client options for model property", async () => { + const { program } = await SimpleTesterWithService.compile(` + @test + model Test { + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("propertyFlag", "propValue", "python") + myProp: string; + } + + op getTest(): Test; + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const sdkProperty = sdkModel.properties.find((p) => p.name === "myProp"); + ok(sdkProperty, "SDK property should exist"); + + const clientOptions = getClientOptions(sdkProperty.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { + name: "propertyFlag", + value: "propValue", + scope: "python", + }); + }); + + it("should return options when scope matches emitter", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("pythonOnlyFlag", true, "python") + @test + model Test { + id: string; + } + + op getTest(): Test; + `); + + // Configure with python emitter + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const clientOptions = getClientOptions(sdkModel.decorators); + strictEqual(clientOptions.length, 1); + strictEqual(clientOptions[0].name, "pythonOnlyFlag"); + strictEqual(clientOptions[0].value, true); + strictEqual(clientOptions[0].scope, "python"); + }); + + it("should return empty array when scope does not match emitter", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("javaOnlyFlag", true, "java") + @test + model Test { + id: string; + } + + op getTest(): Test; + `); + + // Configure with python emitter, but decorator is scoped to java + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + // The decorator should NOT appear - getClientOptions should return empty array + const clientOptions = getClientOptions(sdkModel.decorators); + strictEqual(clientOptions.length, 0); + }); + + it("should handle option without scope argument", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + #suppress "@azure-tools/typespec-client-generator-core/client-option-requires-scope" + @clientOption("noScopeOption", 123) + @test + model Test { + id: string; + } + + op getTest(): Test; + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const clientOptions = getClientOptions(sdkModel.decorators); + strictEqual(clientOptions.length, 1); + strictEqual(clientOptions[0].name, "noScopeOption"); + strictEqual(clientOptions[0].value, 123); + // scope should be undefined when not provided + strictEqual(clientOptions[0].scope, undefined); + }); +}); diff --git a/packages/typespec-client-generator-core/test/decorators/deserialize-empty-string-as-null.test.ts b/packages/typespec-client-generator-core/test/decorators/deserialize-empty-string-as-null.test.ts index ff98e28dcc..f75a3b37a1 100644 --- a/packages/typespec-client-generator-core/test/decorators/deserialize-empty-string-as-null.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/deserialize-empty-string-as-null.test.ts @@ -21,7 +21,7 @@ describe("deserialized empty string as null", () => { const context = await createSdkContextForTester( program, - {}, + { emitterName: "@azure-tools/typespec-csharp" }, { additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@deserializeEmptyStringAsNull"], }, @@ -54,7 +54,7 @@ describe("deserialized empty string as null", () => { const context = await createSdkContextForTester( program, - {}, + { emitterName: "@azure-tools/typespec-csharp" }, { additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@deserializeEmptyStringAsNull"], }, diff --git a/packages/typespec-client-generator-core/test/decorators/general-list.test.ts b/packages/typespec-client-generator-core/test/decorators/general-list.test.ts index f8bfba37c7..4887c9c1d7 100644 --- a/packages/typespec-client-generator-core/test/decorators/general-list.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/general-list.test.ts @@ -123,13 +123,6 @@ it("multiple same decorators", async function () { ); deepStrictEqual(context.sdkPackage.clients[0].methods[0].decorators, [ - { - name: "Azure.ClientGenerator.Core.@clientName", - arguments: { - rename: "testForJava", - scope: "java", - }, - }, { name: "Azure.ClientGenerator.Core.@clientName", arguments: { @@ -141,6 +134,34 @@ it("multiple same decorators", async function () { expectDiagnostics(context.diagnostics, []); }); +it("multiple same decorators without scope", async function () { + const { program } = await SimpleTesterWithService.compile(` + @clientName("testNoScope1") + @clientName("testNoScope2") + op test(): void; + `); + + const context = await createSdkContextForTester( + program, + {}, + { additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@clientName"] }, + ); + + // Decorators without scope should all be included + const decorators = context.sdkPackage.clients[0].methods[0].decorators; + strictEqual(decorators.length, 2); + + const decorator1 = decorators.find((d) => d.arguments.rename === "testNoScope1"); + const decorator2 = decorators.find((d) => d.arguments.rename === "testNoScope2"); + + ok(decorator1, "testNoScope1 decorator should exist"); + ok(decorator2, "testNoScope2 decorator should exist"); + strictEqual(decorator1!.name, "Azure.ClientGenerator.Core.@clientName"); + strictEqual(decorator2!.name, "Azure.ClientGenerator.Core.@clientName"); + + expectDiagnostics(context.diagnostics, []); +}); + it("decorators on a namespace", async function () { const { program } = await SimpleTesterWithService.compile(` op test(): void; @@ -228,7 +249,7 @@ describe("xml scenario", () => { model Foo { @ns("https://example.com/ns1", "ns1") bar1: string; - + @ns("https://example.com/ns2", "ns2") bar2: string; } @@ -276,12 +297,12 @@ describe("xml scenario", () => { ns1: "https://example.com/ns1", ns2: "https://example.com/ns2", } - + @Xml.ns(Namespaces.ns1) model Foo { @Xml.ns(Namespaces.ns1) bar1: string; - + @Xml.ns(Namespaces.ns2) bar2: string; } @@ -364,7 +385,7 @@ describe("csharp only decorator", () => { const context = await createSdkContextForTester( program, - {}, + { emitterName: "@azure-tools/typespec-csharp" }, { additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@useSystemTextJsonConverter"] }, ); diff --git a/website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx b/website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx new file mode 100644 index 0000000000..5a6c4eb7cb --- /dev/null +++ b/website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx @@ -0,0 +1,224 @@ +--- +title: Client Options +llmstxt: true +--- + +import { ClientTabs, ClientTabItem } from "@components/client-tabs"; + +This page documents how to use the `@clientOption` decorator to pass language-specific configuration options to emitters. For an overview of the setup, please visit the setup page. + +:::caution +The `@clientOption` decorator is intended for advanced scenarios where language-specific emitter behavior needs to be configured. Using this decorator always produces a warning to ensure intentional usage. Use standard TCGC decorators when possible. +::: + +## Overview + +The `@clientOption` decorator allows spec authors to pass arbitrary key-value options to specific language emitters. This enables fine-grained control over code generation behavior that may vary between languages. + +```typespec +@clientOption(name: string, value: string | boolean | number, scope?: string) +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ----------------------------- | --------------------------------------------------------------------------------------- | +| `name` | `string` | The name of the option to set | +| `value` | `string \| boolean \| number` | The value for the option | +| `scope` | `string` (optional) | The target language scope. **Required** - omitting scope produces an additional warning | + +## Usage + +### Basic Usage + +Apply the decorator to models, operations, enums, or properties with a language-specific scope: + + + +```typespec title=client.tsp +import "@azure-tools/typespec-client-generator-core"; + +using Azure.ClientGenerator.Core; + +#suppress "@azure-tools/typespec-client-generator-core/client-option" +@clientOption("enableFeatureFoo", true, "python") +model MyModel { + id: string; +} +``` + +```python +# The Python emitter can read this option from the model's decorators array +# and apply the appropriate code generation behavior +``` + +```csharp +// C# emitter does not see this option (scoped to Python only) +``` + +```typescript +// TypeScript emitter does not see this option (scoped to Python only) +``` + +```java +// Java emitter does not see this option (scoped to Python only) +``` + +```go +// Go emitter does not see this option (scoped to Python only) +``` + + + +### Multiple Options + +You can apply multiple `@clientOption` decorators to the same target: + +```typespec title=client.tsp +#suppress "@azure-tools/typespec-client-generator-core/client-option" +#suppress "@azure-tools/typespec-client-generator-core/client-option" +@clientOption("enableFeatureFoo", true, "python") +@clientOption("customSerializerMode", "strict", "python") +model MyModel { + id: string; +} +``` + +### Different Value Types + +The decorator supports string, boolean, and numeric values: + +```typespec title=client.tsp +#suppress "@azure-tools/typespec-client-generator-core/client-option" +@clientOption("booleanOption", true, "python") +model BoolExample { + id: string; +} + +#suppress "@azure-tools/typespec-client-generator-core/client-option" +@clientOption("stringOption", "customValue", "csharp") +model StringExample { + id: string; +} + +#suppress "@azure-tools/typespec-client-generator-core/client-option" +@clientOption("numericOption", 42, "java") +model NumericExample { + id: string; +} +``` + +## How Emitters Access Client Options + +TCGC provides the `getClientOptions` helper function to easily extract client options from any SDK type that has a `decorators` array. + +### Using the getClientOptions Helper (Recommended) + +```typescript +import { getClientOptions } from "@azure-tools/typespec-client-generator-core"; + +// Get client options from a model +const sdkModel = context.sdkPackage.models.find((m) => m.name === "MyModel"); +const clientOptions = getClientOptions(sdkModel.decorators); + +for (const option of clientOptions) { + console.log(`Option: ${option.name} = ${option.value}`); + // option.name: string - The option name (e.g., "enableFeatureFoo") + // option.value: string | boolean | number - The option value + // option.scope?: string - The language scope (e.g., "python") +} +``` + +The `getClientOptions` function returns an array of `SdkClientOption` objects: + +```typescript +interface SdkClientOption { + name: string; + value: string | boolean | number; + scope?: string; +} +``` + +### Works with Any Decorated SDK Type + +The helper works with any SDK type that has a decorators array: + +```typescript +// Models +const modelOptions = getClientOptions(sdkModel.decorators); + +// Enums +const enumOptions = getClientOptions(sdkEnum.decorators); + +// Operations/Methods +const methodOptions = getClientOptions(sdkMethod.decorators); + +// Properties +const propertyOptions = getClientOptions(sdkProperty.decorators); + +// Clients +const clientOptions = getClientOptions(sdkClient.decorators); +``` + +### Alternative: Manual Decorator Filtering + +If you need more control, you can also filter the decorators array directly: + +```typescript +const sdkModel = context.sdkPackage.models.find((m) => m.name === "MyModel"); +const clientOptionDecorators = sdkModel.decorators.filter( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", +); + +for (const decorator of clientOptionDecorators) { + const optionName = decorator.arguments.name; // e.g., "enableFeatureFoo" + const optionValue = decorator.arguments.value; // e.g., true + const scope = decorator.arguments.scope; // e.g., "python" +} +``` + +## Supported Client Options by Language + +Language emitters should document which client options they support. The following sections list the supported options for each language. + +### Python + +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | + +### C# (.NET) + +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | + +### Java + +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | + +### TypeScript/JavaScript + +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | + +### Go + +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | + +## Best Practices + +1. **Always specify a scope**: The decorator is designed for language-specific behavior. Omitting the scope produces an additional warning. + +2. **Suppress the warning intentionally**: Use `#suppress "@azure-tools/typespec-client-generator-core/client-option"` to acknowledge that you're using this advanced feature. + +3. **Document usage**: When using client options, document why they're needed so future maintainers understand the intent. + +4. **Prefer standard decorators**: Use standard TCGC decorators like `@clientName`, `@access`, `@usage`, etc. when they can achieve the desired behavior. Reserve `@clientOption` for cases where no standard decorator exists. + +5. **Coordinate with emitter teams**: Before using a client option, verify with the target language emitter team that the option is supported and understand its behavior.