From 3ea0e396f2640e0b0baa573506514f222f62edc0 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 22 Jan 2026 16:26:25 -0500 Subject: [PATCH 01/12] add clientOption decorator --- .../Azure.ClientGenerator.Core.ts | 31 ++ .../lib/decorators.tsp | 25 ++ .../src/configs.ts | 1 + .../src/decorators.ts | 34 ++ .../src/internal-utils.ts | 8 + .../typespec-client-generator-core/src/lib.ts | 14 + .../src/tsp-index.ts | 2 + .../test/decorators/client-option.test.ts | 424 ++++++++++++++++++ .../test/decorators/general-list.test.ts | 39 +- 9 files changed, 569 insertions(+), 9 deletions(-) create mode 100644 packages/typespec-client-generator-core/test/decorators/client-option.test.ts 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..758025b763 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,36 @@ 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. + * + * **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" + * @clientOption("enableFeatureFoo", true, "python") + * model MyModel { + * prop: string; + * } + * ``` + */ +export type ClientOptionDecorator = ( + context: DecoratorContext, + target: Type, + name: string, + value: Type, + scope?: string, +) => DecoratorValidatorCallbacks | void; + export type AzureClientGeneratorCoreDecorators = { clientName: ClientNameDecorator; convenientAPI: ConvenientAPIDecorator; @@ -965,4 +995,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..b6985884b6 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -1022,3 +1022,28 @@ 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. + * + * **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" + * @clientOption("enableFeatureFoo", true, "python") + * model MyModel { + * prop: string; + * } + * ``` + */ +extern dec clientOption(target: unknown, name: valueof string, value: 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..da35be5e52 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: Type, + 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/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/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..5c1e116052 --- /dev/null +++ b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts @@ -0,0 +1,424 @@ +import { Model } from "@typespec/compiler"; +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { createSdkTestRunner, SdkTestRunner } from "../test-host.js"; + +let runner: SdkTestRunner; + +beforeEach(async () => { + runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python" }); +}); + +describe("@clientOption diagnostics", () => { + it("should emit client-option warning always", async () => { + const diagnostics = await runner.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 runner.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 runner.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 in decorators array", () => { + it("should appear in decorators array for model", async () => { + const { Test: _Test } = (await runner.compile(` + @service + @test namespace MyService { + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("enableFeatureFoo", true, "python") + @test + model Test { + id: string; + } + + op getTest(): Test; + } + `)) as { Test: Model }; + + const sdkModel = runner.context.sdkPackage.models.find((m) => m.name === "Test"); + strictEqual(sdkModel !== undefined, true); + + const clientOptionDecorator = sdkModel!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + strictEqual(clientOptionDecorator !== undefined, true); + deepStrictEqual(clientOptionDecorator!.arguments, { + name: "enableFeatureFoo", + value: true, + scope: "python", + }); + }); + + it("should support multiple @clientOption decorators on same target", async () => { + const { Test: _Test } = (await runner.compile(` + @service + @test namespace MyService { + #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; + } + `)) as { Test: Model }; + + const sdkModel = runner.context.sdkPackage.models.find((m) => m.name === "Test"); + strictEqual(sdkModel !== undefined, true); + + const clientOptionDecorators = sdkModel!.decorators.filter( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + strictEqual(clientOptionDecorators.length, 2); + + // Verify each decorator has the correct name and value + const fooDecorator = clientOptionDecorators.find((d) => d.arguments.name === "enableFeatureFoo"); + ok(fooDecorator, "enableFeatureFoo decorator should exist"); + strictEqual(fooDecorator!.arguments.name, "enableFeatureFoo"); + strictEqual(fooDecorator!.arguments.value, true); + strictEqual(fooDecorator!.arguments.scope, "python"); + + const barDecorator = clientOptionDecorators.find((d) => d.arguments.name === "enableFeatureBar"); + ok(barDecorator, "enableFeatureBar decorator should exist"); + strictEqual(barDecorator!.arguments.name, "enableFeatureBar"); + strictEqual(barDecorator!.arguments.value, "value"); + strictEqual(barDecorator!.arguments.scope, "python"); + }); + + it("should support different value types", async () => { + const { TestBool: _TestBool, TestString: _TestString, TestNumber: _TestNumber } = + (await runner.compile(` + @service + @test namespace MyService { + #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; + } + `)) as { TestBool: Model; TestString: Model; TestNumber: Model }; + + // Verify boolean value type + const sdkModelBool = runner.context.sdkPackage.models.find((m) => m.name === "TestBool"); + ok(sdkModelBool, "TestBool model should exist"); + const boolDecorator = sdkModelBool!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(boolDecorator, "clientOption decorator should exist on TestBool"); + strictEqual(boolDecorator!.arguments.name, "boolOption"); + strictEqual(boolDecorator!.arguments.value, true); + strictEqual(typeof boolDecorator!.arguments.value, "boolean"); + + // Verify string value type + const sdkModelString = runner.context.sdkPackage.models.find((m) => m.name === "TestString"); + ok(sdkModelString, "TestString model should exist"); + const stringDecorator = sdkModelString!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(stringDecorator, "clientOption decorator should exist on TestString"); + strictEqual(stringDecorator!.arguments.name, "stringOption"); + strictEqual(stringDecorator!.arguments.value, "someValue"); + strictEqual(typeof stringDecorator!.arguments.value, "string"); + + // Verify number value type + const sdkModelNumber = runner.context.sdkPackage.models.find((m) => m.name === "TestNumber"); + ok(sdkModelNumber, "TestNumber model should exist"); + const numberDecorator = sdkModelNumber!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(numberDecorator, "clientOption decorator should exist on TestNumber"); + strictEqual(numberDecorator!.arguments.name, "numberOption"); + strictEqual(numberDecorator!.arguments.value, 42); + strictEqual(typeof numberDecorator!.arguments.value, "number"); + }); + + it("should appear in decorators array for operation", async () => { + await runner.compile(` + @service + @test namespace MyService { + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("operationFlag", "customValue", "python") + @test + op testOp(): string; + } + `); + + const sdkMethod = runner.context.sdkPackage.clients[0].methods.find( + (m) => m.kind === "basic" && m.name === "testOp", + ); + ok(sdkMethod, "SDK method should exist"); + + const clientOptionDecorator = sdkMethod!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(clientOptionDecorator, "clientOption decorator should be present"); + deepStrictEqual(clientOptionDecorator!.arguments, { + name: "operationFlag", + value: "customValue", + scope: "python", + }); + }); + + it("should appear in decorators array for enum", async () => { + await runner.compile(` + @service + @test namespace MyService { + #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 sdkEnum = runner.context.sdkPackage.enums.find((e) => e.name === "TestEnum"); + ok(sdkEnum, "SDK enum should exist"); + + const clientOptionDecorator = sdkEnum!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(clientOptionDecorator, "clientOption decorator should be present"); + deepStrictEqual(clientOptionDecorator!.arguments, { + name: "enumFlag", + value: true, + scope: "python", + }); + }); + + it("should appear in decorators array for model property", async () => { + await runner.compile(` + @service + @test namespace MyService { + @test + model Test { + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("propertyFlag", "propValue", "python") + myProp: string; + } + + op getTest(): Test; + } + `); + + const sdkModel = runner.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 clientOptionDecorator = sdkProperty!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(clientOptionDecorator, "clientOption decorator should be present on property"); + deepStrictEqual(clientOptionDecorator!.arguments, { + name: "propertyFlag", + value: "propValue", + scope: "python", + }); + }); + + it("should respect scope filtering - decorator appears when scope matches emitter", async () => { + await runner.compile(` + @service + @test namespace MyService { + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("pythonOnlyFlag", true, "python") + @test + model Test { + id: string; + } + + op getTest(): Test; + } + `); + + // Runner is configured with python emitter + const sdkModel = runner.context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const clientOptionDecorator = sdkModel!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(clientOptionDecorator, "clientOption decorator should be present for matching scope"); + + // Verify the name and value are correctly captured + strictEqual(clientOptionDecorator!.arguments.name, "pythonOnlyFlag"); + strictEqual(clientOptionDecorator!.arguments.value, true); + strictEqual(clientOptionDecorator!.arguments.scope, "python"); + }); + + it("should not include decorator when scope does not match emitter", async () => { + await runner.compile(` + @service + @test namespace MyService { + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("javaOnlyFlag", true, "java") + @test + model Test { + id: string; + } + + op getTest(): Test; + } + `); + + // Runner is configured with python emitter, but decorator is scoped to java + // The decorator should NOT appear in the decorators array + const sdkModel = runner.context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const clientOptionDecorator = sdkModel!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + strictEqual( + clientOptionDecorator, + undefined, + "clientOption decorator should not be present when scope doesn't match emitter", + ); + }); + + it("should include all argument fields in decorator info", async () => { + await runner.compile(` + @service + @test namespace MyService { + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("testOption", "testValue", "python") + @test + model Test { + id: string; + } + + op getTest(): Test; + } + `); + + const sdkModel = runner.context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const clientOptionDecorator = sdkModel!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(clientOptionDecorator, "clientOption decorator should be present"); + + // Verify the decorator has the correct name format + strictEqual( + clientOptionDecorator!.name, + "Azure.ClientGenerator.Core.@clientOption", + "Decorator name should be fully qualified", + ); + + // Verify all arguments are present + ok("name" in clientOptionDecorator!.arguments, "arguments should have 'name' field"); + ok("value" in clientOptionDecorator!.arguments, "arguments should have 'value' field"); + ok("scope" in clientOptionDecorator!.arguments, "arguments should have 'scope' field"); + + // Verify argument values + strictEqual(clientOptionDecorator!.arguments.name, "testOption"); + strictEqual(clientOptionDecorator!.arguments.value, "testValue"); + strictEqual(clientOptionDecorator!.arguments.scope, "python"); + }); + + it("should handle decorator without scope argument", async () => { + await runner.compile(` + @service + @test namespace MyService { + #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 sdkModel = runner.context.sdkPackage.models.find((m) => m.name === "Test"); + ok(sdkModel, "SDK model should exist"); + + const clientOptionDecorator = sdkModel!.decorators.find( + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", + ); + ok(clientOptionDecorator, "clientOption decorator should be present"); + strictEqual(clientOptionDecorator!.arguments.name, "noScopeOption"); + strictEqual(clientOptionDecorator!.arguments.value, 123); + // scope should be undefined when not provided + strictEqual(clientOptionDecorator!.arguments.scope, undefined); + }); +}); 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 e1a7fe07a0..91a109e272 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 @@ -105,9 +105,9 @@ it.skip("decorator arg type not supported", async function () { }); }); -it("multiple same decorators", async function () { +it("multiple same decorators with matching scope", async function () { runner = await createSdkTestRunner( - {}, + { emitterName: "@azure-tools/typespec-python" }, { additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@clientName"] }, ); @@ -117,14 +117,8 @@ it("multiple same decorators", async function () { op test(): void; `); + // Only the decorator with matching scope (python) should be included deepStrictEqual(runner.context.sdkPackage.clients[0].methods[0].decorators, [ - { - name: "Azure.ClientGenerator.Core.@clientName", - arguments: { - rename: "testForJava", - scope: "java", - }, - }, { name: "Azure.ClientGenerator.Core.@clientName", arguments: { @@ -136,6 +130,33 @@ it("multiple same decorators", async function () { expectDiagnostics(runner.context.diagnostics, []); }); +it("multiple same decorators without scope", async function () { + runner = await createSdkTestRunner( + {}, + { additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@clientName"] }, + ); + + await runner.compileWithBuiltInService(` + @clientName("testNoScope1") + @clientName("testNoScope2") + op test(): void; + `); + + // Decorators without scope should all be included + const decorators = runner.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(runner.context.diagnostics, []); +}); + it("decorators on a namespace", async function () { runner = await createSdkTestRunner({}, { additionalDecorators: ["TypeSpec\\.@service"] }); From 07afed44ce0254163c6835d9b4cce1be0dcc304e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 22 Jan 2026 16:27:02 -0500 Subject: [PATCH 02/12] add changeset --- .chronus/changes/tcgc-clientOptions-2026-0-22-16-26-53.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/tcgc-clientOptions-2026-0-22-16-26-53.md 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 From e20ce1f34c2a96da810df4d7f6feefafdb70372d Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 26 Jan 2026 14:38:52 -0500 Subject: [PATCH 03/12] switch to valueof unknown for value --- .../generated-defs/Azure.ClientGenerator.Core.ts | 2 +- packages/typespec-client-generator-core/lib/decorators.tsp | 2 +- packages/typespec-client-generator-core/src/decorators.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 758025b763..471d0a150e 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 @@ -970,7 +970,7 @@ export type ClientOptionDecorator = ( context: DecoratorContext, target: Type, name: string, - value: Type, + value: unknown, scope?: string, ) => DecoratorValidatorCallbacks | void; diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index b6985884b6..9ad2f960ca 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -1046,4 +1046,4 @@ extern dec clientDoc( * } * ``` */ -extern dec clientOption(target: unknown, name: valueof string, value: unknown, scope?: valueof string); +extern dec clientOption(target: unknown, name: valueof string, value: valueof unknown, scope?: valueof string); diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index da35be5e52..6b3cffbd1d 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -1864,7 +1864,7 @@ export const $clientOption: ClientOptionDecorator = ( context: DecoratorContext, target: Type, name: string, - value: Type, + value: unknown, scope?: LanguageScopes, ) => { // Always emit warning that this is experimental From 2977dbaf5f9d9de76c97ee4abdfad29b2aa829b0 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 26 Jan 2026 14:41:49 -0500 Subject: [PATCH 04/12] add suppression comments --- .../generated-defs/Azure.ClientGenerator.Core.ts | 2 +- packages/typespec-client-generator-core/lib/decorators.tsp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 471d0a150e..add250fad8 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 @@ -959,7 +959,7 @@ export type ClientDocDecorator = ( * 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" + * #suppress "@azure-tools/typespec-client-generator-core/client-option" "preview feature for python" * @clientOption("enableFeatureFoo", true, "python") * model MyModel { * prop: string; diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 9ad2f960ca..646dc936f3 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -1039,7 +1039,7 @@ extern dec clientDoc( * * @example Apply an experimental option for Python * ```typespec - * #suppress "@azure-tools/typespec-client-generator-core/client-option" + * #suppress "@azure-tools/typespec-client-generator-core/client-option" "preview feature for python" * @clientOption("enableFeatureFoo", true, "python") * model MyModel { * prop: string; From 569575308643f0a6abb2dd2e6c73186f55fd30d6 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 26 Jan 2026 14:51:50 -0500 Subject: [PATCH 05/12] add client options link --- .../12clientOptions.mdx | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx 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..f16f080f1d --- /dev/null +++ b/website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx @@ -0,0 +1,173 @@ +--- +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" +@clientOption("enableFeatureFoo", true, "python") +#suppress "@azure-tools/typespec-client-generator-core/client-option" +@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 + +Language emitters can access client options through the `decorators` array on SDK types. Each decorator entry contains: + +- `name`: The fully qualified decorator name (`Azure.ClientGenerator.Core.@clientOption`) +- `arguments`: An object with `name`, `value`, and optionally `scope` fields + +```typescript +// Example: Reading client options in an emitter +const sdkModel = context.sdkPackage.models.find(m => m.name === "MyModel"); +const clientOptions = sdkModel.decorators.filter( + d => d.name === "Azure.ClientGenerator.Core.@clientOption" +); + +for (const option of clientOptions) { + const optionName = option.arguments.name; // e.g., "enableFeatureFoo" + const optionValue = option.arguments.value; // e.g., true + const scope = option.arguments.scope; // e.g., "python" + + // Apply emitter-specific behavior based on the option +} +``` + +## 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. From a6977259a5dcdf83766a5fd652e96b2fb26566f0 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 26 Jan 2026 14:52:50 -0500 Subject: [PATCH 06/12] add doc --- packages/typespec-client-generator-core/lib/decorators.tsp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 646dc936f3..352bf8a4b2 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -1027,6 +1027,8 @@ extern dec clientDoc( * 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). From 7a01d25143bc6913bd720f1cee11f4f54861f354 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 26 Jan 2026 14:59:43 -0500 Subject: [PATCH 07/12] add helper method --- .../Azure.ClientGenerator.Core.ts | 2 + .../src/interfaces.ts | 19 ++ .../src/public-utils.ts | 31 +++ .../test/decorators/client-option.test.ts | 209 ++++++------------ .../12clientOptions.mdx | 68 +++++- 5 files changed, 177 insertions(+), 152 deletions(-) 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 add250fad8..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 @@ -949,6 +949,8 @@ export type ClientDocDecorator = ( * 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). * 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/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/test/decorators/client-option.test.ts b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts index e57d07ba6f..8ae12f5c41 100644 --- a/packages/typespec-client-generator-core/test/decorators/client-option.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts @@ -1,6 +1,7 @@ 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", () => { @@ -60,8 +61,8 @@ describe("@clientOption diagnostics", () => { }); }); -describe("@clientOption in decorators array", () => { - it("should appear in decorators array for model", async () => { +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") @@ -78,20 +79,18 @@ describe("@clientOption in decorators array", () => { }); const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); - strictEqual(sdkModel !== undefined, true); + ok(sdkModel, "SDK model should exist"); - const clientOptionDecorator = sdkModel!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - strictEqual(clientOptionDecorator !== undefined, true); - deepStrictEqual(clientOptionDecorator!.arguments, { + const clientOptions = getClientOptions(sdkModel.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { name: "enableFeatureFoo", value: true, scope: "python", }); }); - it("should support multiple @clientOption decorators on same target", async () => { + 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") @@ -110,29 +109,23 @@ describe("@clientOption in decorators array", () => { }); const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); - strictEqual(sdkModel !== undefined, true); - - const clientOptionDecorators = sdkModel!.decorators.filter( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - strictEqual(clientOptionDecorators.length, 2); - - // Verify each decorator has the correct name and value - const fooDecorator = clientOptionDecorators.find( - (d) => d.arguments.name === "enableFeatureFoo", - ); - ok(fooDecorator, "enableFeatureFoo decorator should exist"); - strictEqual(fooDecorator!.arguments.name, "enableFeatureFoo"); - strictEqual(fooDecorator!.arguments.value, true); - strictEqual(fooDecorator!.arguments.scope, "python"); + ok(sdkModel, "SDK model should exist"); - const barDecorator = clientOptionDecorators.find( - (d) => d.arguments.name === "enableFeatureBar", - ); - ok(barDecorator, "enableFeatureBar decorator should exist"); - strictEqual(barDecorator!.arguments.name, "enableFeatureBar"); - strictEqual(barDecorator!.arguments.value, "value"); - strictEqual(barDecorator!.arguments.scope, "python"); + 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 () => { @@ -170,38 +163,32 @@ describe("@clientOption in decorators array", () => { // Verify boolean value type const sdkModelBool = context.sdkPackage.models.find((m) => m.name === "TestBool"); ok(sdkModelBool, "TestBool model should exist"); - const boolDecorator = sdkModelBool!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(boolDecorator, "clientOption decorator should exist on TestBool"); - strictEqual(boolDecorator!.arguments.name, "boolOption"); - strictEqual(boolDecorator!.arguments.value, true); - strictEqual(typeof boolDecorator!.arguments.value, "boolean"); + 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 stringDecorator = sdkModelString!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(stringDecorator, "clientOption decorator should exist on TestString"); - strictEqual(stringDecorator!.arguments.name, "stringOption"); - strictEqual(stringDecorator!.arguments.value, "someValue"); - strictEqual(typeof stringDecorator!.arguments.value, "string"); + 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 numberDecorator = sdkModelNumber!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(numberDecorator, "clientOption decorator should exist on TestNumber"); - strictEqual(numberDecorator!.arguments.name, "numberOption"); - strictEqual(numberDecorator!.arguments.value, 42); - strictEqual(typeof numberDecorator!.arguments.value, "number"); + 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 appear in decorators array for operation", async () => { + 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") @@ -218,18 +205,16 @@ describe("@clientOption in decorators array", () => { ); ok(sdkMethod, "SDK method should exist"); - const clientOptionDecorator = sdkMethod!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(clientOptionDecorator, "clientOption decorator should be present"); - deepStrictEqual(clientOptionDecorator!.arguments, { + const clientOptions = getClientOptions(sdkMethod.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { name: "operationFlag", value: "customValue", scope: "python", }); }); - it("should appear in decorators array for enum", async () => { + 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") @@ -250,18 +235,16 @@ describe("@clientOption in decorators array", () => { const sdkEnum = context.sdkPackage.enums.find((e) => e.name === "TestEnum"); ok(sdkEnum, "SDK enum should exist"); - const clientOptionDecorator = sdkEnum!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(clientOptionDecorator, "clientOption decorator should be present"); - deepStrictEqual(clientOptionDecorator!.arguments, { + const clientOptions = getClientOptions(sdkEnum.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { name: "enumFlag", value: true, scope: "python", }); }); - it("should appear in decorators array for model property", async () => { + it("should return client options for model property", async () => { const { program } = await SimpleTesterWithService.compile(` @test model Test { @@ -280,21 +263,19 @@ describe("@clientOption in decorators array", () => { 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"); + const sdkProperty = sdkModel.properties.find((p) => p.name === "myProp"); ok(sdkProperty, "SDK property should exist"); - const clientOptionDecorator = sdkProperty!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(clientOptionDecorator, "clientOption decorator should be present on property"); - deepStrictEqual(clientOptionDecorator!.arguments, { + const clientOptions = getClientOptions(sdkProperty.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { name: "propertyFlag", value: "propValue", scope: "python", }); }); - it("should respect scope filtering - decorator appears when scope matches emitter", async () => { + 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") @@ -314,18 +295,14 @@ describe("@clientOption in decorators array", () => { const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); ok(sdkModel, "SDK model should exist"); - const clientOptionDecorator = sdkModel!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(clientOptionDecorator, "clientOption decorator should be present for matching scope"); - - // Verify the name and value are correctly captured - strictEqual(clientOptionDecorator!.arguments.name, "pythonOnlyFlag"); - strictEqual(clientOptionDecorator!.arguments.value, true); - strictEqual(clientOptionDecorator!.arguments.scope, "python"); + 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 not include decorator when scope does not match emitter", async () => { + 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") @@ -342,63 +319,15 @@ describe("@clientOption in decorators array", () => { emitterName: "@azure-tools/typespec-python", }); - // The decorator should NOT appear in the decorators array const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); ok(sdkModel, "SDK model should exist"); - const clientOptionDecorator = sdkModel!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - strictEqual( - clientOptionDecorator, - undefined, - "clientOption decorator should not be present when scope doesn't match emitter", - ); + // The decorator should NOT appear - getClientOptions should return empty array + const clientOptions = getClientOptions(sdkModel.decorators); + strictEqual(clientOptions.length, 0); }); - it("should include all argument fields in decorator info", async () => { - const { program } = await SimpleTesterWithService.compile(` - #suppress "@azure-tools/typespec-client-generator-core/client-option" - @clientOption("testOption", "testValue", "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 clientOptionDecorator = sdkModel!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(clientOptionDecorator, "clientOption decorator should be present"); - - // Verify the decorator has the correct name format - strictEqual( - clientOptionDecorator!.name, - "Azure.ClientGenerator.Core.@clientOption", - "Decorator name should be fully qualified", - ); - - // Verify all arguments are present - ok("name" in clientOptionDecorator!.arguments, "arguments should have 'name' field"); - ok("value" in clientOptionDecorator!.arguments, "arguments should have 'value' field"); - ok("scope" in clientOptionDecorator!.arguments, "arguments should have 'scope' field"); - - // Verify argument values - strictEqual(clientOptionDecorator!.arguments.name, "testOption"); - strictEqual(clientOptionDecorator!.arguments.value, "testValue"); - strictEqual(clientOptionDecorator!.arguments.scope, "python"); - }); - - it("should handle decorator without scope argument", async () => { + 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" @@ -418,13 +347,11 @@ describe("@clientOption in decorators array", () => { const sdkModel = context.sdkPackage.models.find((m) => m.name === "Test"); ok(sdkModel, "SDK model should exist"); - const clientOptionDecorator = sdkModel!.decorators.find( - (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", - ); - ok(clientOptionDecorator, "clientOption decorator should be present"); - strictEqual(clientOptionDecorator!.arguments.name, "noScopeOption"); - strictEqual(clientOptionDecorator!.arguments.value, 123); + 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(clientOptionDecorator!.arguments.scope, undefined); + strictEqual(clientOptions[0].scope, undefined); }); }); 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 index f16f080f1d..1109649d3f 100644 --- a/website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx +++ b/website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx @@ -105,24 +105,70 @@ model NumericExample { id: string; } ## How Emitters Access Client Options -Language emitters can access client options through the `decorators` array on SDK types. Each decorator entry contains: +TCGC provides the `getClientOptions` helper function to easily extract client options from any SDK type that has a `decorators` array. -- `name`: The fully qualified decorator name (`Azure.ClientGenerator.Core.@clientOption`) -- `arguments`: An object with `name`, `value`, and optionally `scope` fields +### Using the getClientOptions Helper (Recommended) ```typescript -// Example: Reading client options in an emitter +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 = sdkModel.decorators.filter( - d => d.name === "Azure.ClientGenerator.Core.@clientOption" -); +const clientOptions = getClientOptions(sdkModel.decorators); for (const option of clientOptions) { - const optionName = option.arguments.name; // e.g., "enableFeatureFoo" - const optionValue = option.arguments.value; // e.g., true - const scope = option.arguments.scope; // e.g., "python" + 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" +); - // Apply emitter-specific behavior based on the option +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" } ``` From 84f360d4d0191d726bc905baad17f9a966ec443a Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 26 Jan 2026 15:05:57 -0500 Subject: [PATCH 08/12] format --- .../lib/decorators.tsp | 9 ++- .../12clientOptions.mdx | 65 ++++++++++--------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 352bf8a4b2..3c52b3f5ab 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -1027,7 +1027,7 @@ extern dec clientDoc( * 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 @@ -1048,4 +1048,9 @@ extern dec clientDoc( * } * ``` */ -extern dec clientOption(target: unknown, name: valueof string, value: valueof unknown, scope?: valueof string); +extern dec clientOption( + target: unknown, + name: valueof string, + value: valueof unknown, + scope?: valueof string +); 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 index 1109649d3f..5a6c4eb7cb 100644 --- a/website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx +++ b/website/src/content/docs/docs/howtos/Generate client libraries/12clientOptions.mdx @@ -21,11 +21,11 @@ The `@clientOption` decorator allows spec authors to pass arbitrary key-value op ### 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 | +| 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 @@ -76,8 +76,8 @@ You can apply multiple `@clientOption` decorators to the same target: ```typespec title=client.tsp #suppress "@azure-tools/typespec-client-generator-core/client-option" -@clientOption("enableFeatureFoo", true, "python") #suppress "@azure-tools/typespec-client-generator-core/client-option" +@clientOption("enableFeatureFoo", true, "python") @clientOption("customSerializerMode", "strict", "python") model MyModel { id: string; @@ -91,18 +91,23 @@ 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; } +model BoolExample { + id: string; +} #suppress "@azure-tools/typespec-client-generator-core/client-option" @clientOption("stringOption", "customValue", "csharp") -model StringExample { id: string; } +model StringExample { + id: string; +} #suppress "@azure-tools/typespec-client-generator-core/client-option" @clientOption("numericOption", 42, "java") -model NumericExample { id: string; } +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. @@ -113,7 +118,7 @@ TCGC provides the `getClientOptions` helper function to easily extract client op 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 sdkModel = context.sdkPackage.models.find((m) => m.name === "MyModel"); const clientOptions = getClientOptions(sdkModel.decorators); for (const option of clientOptions) { @@ -160,15 +165,15 @@ const clientOptions = getClientOptions(sdkClient.decorators); 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 sdkModel = context.sdkPackage.models.find((m) => m.name === "MyModel"); const clientOptionDecorators = sdkModel.decorators.filter( - d => d.name === "Azure.ClientGenerator.Core.@clientOption" + (d) => d.name === "Azure.ClientGenerator.Core.@clientOption", ); for (const decorator of clientOptionDecorators) { - const optionName = decorator.arguments.name; // e.g., "enableFeatureFoo" + const optionName = decorator.arguments.name; // e.g., "enableFeatureFoo" const optionValue = decorator.arguments.value; // e.g., true - const scope = decorator.arguments.scope; // e.g., "python" + const scope = decorator.arguments.scope; // e.g., "python" } ``` @@ -178,33 +183,33 @@ Language emitters should document which client options they support. The followi ### Python -| Option Name | Value Type | Target | Description | -|------------|------------|--------|-------------| -| *Coming soon* | | | | +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | ### C# (.NET) -| Option Name | Value Type | Target | Description | -|------------|------------|--------|-------------| -| *Coming soon* | | | | +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | ### Java -| Option Name | Value Type | Target | Description | -|------------|------------|--------|-------------| -| *Coming soon* | | | | +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | ### TypeScript/JavaScript -| Option Name | Value Type | Target | Description | -|------------|------------|--------|-------------| -| *Coming soon* | | | | +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | ### Go -| Option Name | Value Type | Target | Description | -|------------|------------|--------|-------------| -| *Coming soon* | | | | +| Option Name | Value Type | Target | Description | +| ------------- | ---------- | ------ | ----------- | +| _Coming soon_ | | | | ## Best Practices From cbbac633e8af9044991f384ee53cc03f8883976c Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Jan 2026 13:40:43 -0500 Subject: [PATCH 09/12] add tests for array and dict --- .../test/decorators/client-option.test.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) 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 index 8ae12f5c41..c3de15c325 100644 --- a/packages/typespec-client-generator-core/test/decorators/client-option.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts @@ -354,4 +354,149 @@ describe("@clientOption with getClientOptions getter", () => { // scope should be undefined when not provided strictEqual(clientOptions[0].scope, undefined); }); + + it("should support array value type", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("arrayOption", #["item1", "item2", "item3"], "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); + strictEqual(clientOptions[0].name, "arrayOption"); + ok(Array.isArray(clientOptions[0].value), "value should be an array"); + deepStrictEqual(clientOptions[0].value, ["item1", "item2", "item3"]); + strictEqual(clientOptions[0].scope, "python"); + }); + + it("should support object/map value type", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("objectOption", #{key1: "value1", key2: "value2"}, "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); + strictEqual(clientOptions[0].name, "objectOption"); + ok( + typeof clientOptions[0].value === "object" && !Array.isArray(clientOptions[0].value), + "value should be an object", + ); + deepStrictEqual(clientOptions[0].value, { key1: "value1", key2: "value2" }); + strictEqual(clientOptions[0].scope, "python"); + }); + + it("should support nested object and array values", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("nestedOption", #{ + stringField: "hello", + numberField: 42, + arrayField: #[1, 2, 3], + nestedObject: #{inner: "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, 1); + strictEqual(clientOptions[0].name, "nestedOption"); + + const value = clientOptions[0].value as unknown as Record; + strictEqual(value.stringField, "hello"); + strictEqual(value.numberField, 42); + deepStrictEqual(value.arrayField, [1, 2, 3]); + deepStrictEqual(value.nestedObject, { inner: "value" }); + strictEqual(clientOptions[0].scope, "python"); + }); + + it("should support array of numbers", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("numberArrayOption", #[1, 2, 3, 4, 5], "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); + strictEqual(clientOptions[0].name, "numberArrayOption"); + ok(Array.isArray(clientOptions[0].value), "value should be an array"); + deepStrictEqual(clientOptions[0].value, [1, 2, 3, 4, 5]); + }); + + it("should support array of mixed types", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("mixedArrayOption", #["string", 42, 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); + strictEqual(clientOptions[0].name, "mixedArrayOption"); + ok(Array.isArray(clientOptions[0].value), "value should be an array"); + deepStrictEqual(clientOptions[0].value, ["string", 42, true]); + }); }); From c87d570b00001e5bc5fb053540d0080fbf642d1b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Jan 2026 13:56:20 -0500 Subject: [PATCH 10/12] build --- .../typespec-client-generator-core/README.md | 41 +++++++++++++++++++ .../reference/decorators.md | 40 ++++++++++++++++++ .../reference/index.mdx | 1 + 3 files changed, 82 insertions(+) diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index 8209e21156..a16a4d3f68 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -123,6 +123,7 @@ Available ruleSets: - [`@clientLocation`](#@clientlocation) - [`@clientName`](#@clientname) - [`@clientNamespace`](#@clientnamespace) +- [`@clientOption`](#@clientoption) - [`@convenientAPI`](#@convenientapi) - [`@deserializeEmptyStringAsNull`](#@deserializeemptystringasnull) - [`@operationGroup`](#@operationgroup) @@ -846,6 +847,46 @@ model Test { } ``` +#### `@clientOption` + +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). + +```typespec +@Azure.ClientGenerator.Core.clientOption(name: valueof string, value: valueof unknown, scope?: valueof string) +``` + +##### Target + +The type you want to apply the option to. +`unknown` + +##### Parameters + +| Name | Type | Description | +| ----- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `valueof string` | The name of the option (e.g., "enableFeatureFoo"). | +| value | `valueof unknown` | The value of the option. Can be any type; emitters will cast as needed. | +| scope | `valueof string` | 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. | + +##### Examples + +###### 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; +} +``` + #### `@convenientAPI` Whether you want to generate an operation as a convenient method. 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 62027ac60f..64c1c6173f 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 @@ -720,6 +720,46 @@ model Test { } ``` +### `@clientOption` {#@Azure.ClientGenerator.Core.clientOption} + +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). + +```typespec +@Azure.ClientGenerator.Core.clientOption(name: valueof string, value: valueof unknown, scope?: valueof string) +``` + +#### Target + +The type you want to apply the option to. +`unknown` + +#### Parameters + +| Name | Type | Description | +| ----- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `valueof string` | The name of the option (e.g., "enableFeatureFoo"). | +| value | `valueof unknown` | The value of the option. Can be any type; emitters will cast as needed. | +| scope | `valueof string` | 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. | + +#### Examples + +##### 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; +} +``` + ### `@convenientAPI` {#@Azure.ClientGenerator.Core.convenientAPI} Whether you want to generate an operation as a convenient method. 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 68770d5090..c3300ed697 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 @@ -50,6 +50,7 @@ npm install --save-peer @azure-tools/typespec-client-generator-core - [`@clientLocation`](./decorators.md#@Azure.ClientGenerator.Core.clientLocation) - [`@clientName`](./decorators.md#@Azure.ClientGenerator.Core.clientName) - [`@clientNamespace`](./decorators.md#@Azure.ClientGenerator.Core.clientNamespace) +- [`@clientOption`](./decorators.md#@Azure.ClientGenerator.Core.clientOption) - [`@convenientAPI`](./decorators.md#@Azure.ClientGenerator.Core.convenientAPI) - [`@deserializeEmptyStringAsNull`](./decorators.md#@Azure.ClientGenerator.Core.deserializeEmptyStringAsNull) - [`@operationGroup`](./decorators.md#@Azure.ClientGenerator.Core.operationGroup) From cec0d0aa0d8d7ad2c94ddc8c555507b80221d683 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Jan 2026 14:10:19 -0500 Subject: [PATCH 11/12] switch over to Value --- packages/typespec-client-generator-core/src/interfaces.ts | 3 ++- packages/typespec-client-generator-core/src/internal-utils.ts | 2 +- packages/typespec-client-generator-core/src/public-utils.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 841ed56629..c10d86d1fb 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -14,6 +14,7 @@ import { PagingOperation, Program, Type, + Value, } from "@typespec/compiler"; import { unsafe_Realm } from "@typespec/compiler/experimental"; import { @@ -214,7 +215,7 @@ export interface SdkClientOption { /** * The value of the client option. */ - value: string | boolean | number; + value: Value; /** * The language scope this option applies to, if specified. */ diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index d825fa2c60..2a115ff1e9 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -437,7 +437,7 @@ function getDecoratorArgValue( if (arg.kind === "EnumMember") { return diagnostics.wrap(diagnostics.pipe(getClientTypeWithDiagnostics(context, arg))); } - if (arg.kind === "String" || arg.kind === "Number" || arg.kind === "Boolean") { + if (arg.kind === "String" || arg.kind === "Number" || arg.kind === "Boolean" || arg.kind === "Value") { return diagnostics.wrap(arg.value); } diagnostics.add( diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index f73b0d35c4..8964c674a9 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -10,6 +10,7 @@ import { Scalar, Type, Union, + Value, createDiagnosticCollector, getEffectiveModelType, getFriendlyName, @@ -924,7 +925,7 @@ export function getClientOptions(decorators: DecoratorInfo[]): SdkClientOption[] .filter((d) => d.name === CLIENT_OPTION_DECORATOR_NAME) .map((d) => ({ name: d.arguments.name as string, - value: d.arguments.value as string | boolean | number, + value: d.arguments.value as Value, scope: d.arguments.scope as string | undefined, })); } From 38d267c7794c79e6f2ccfd05ecc6dbfc177b0437 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Jan 2026 14:16:53 -0500 Subject: [PATCH 12/12] add tests for namespace and interface --- .../test/decorators/client-option.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) 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 index c3de15c325..52390afdc9 100644 --- a/packages/typespec-client-generator-core/test/decorators/client-option.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/client-option.test.ts @@ -499,4 +499,66 @@ describe("@clientOption with getClientOptions getter", () => { ok(Array.isArray(clientOptions[0].value), "value should be an array"); deepStrictEqual(clientOptions[0].value, ["string", 42, true]); }); + + it("should return client options for namespace", async () => { + const { program } = await SimpleTester.compile(` + @server("http://localhost:3000", "endpoint") + @service + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("namespaceFlag", "nsValue", "python") + namespace MyService { + model TestModel { + id: string; + } + op getTest(): TestModel; + } + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + // The service namespace should have the decorator + const sdkNamespace = context.sdkPackage.namespaces.find((ns) => ns.name === "MyService"); + ok(sdkNamespace, "SDK namespace should exist"); + + const clientOptions = getClientOptions(sdkNamespace.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { + name: "namespaceFlag", + value: "nsValue", + scope: "python", + }); + }); + + it("should return client options for interface (operation group)", async () => { + const { program } = await SimpleTesterWithService.compile(` + #suppress "@azure-tools/typespec-client-generator-core/client-option" + @clientOption("interfaceFlag", true, "python") + @test + interface MyOperations { + op doSomething(): string; + } + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + + // Interfaces become sub-clients (children of the main client) + const client = context.sdkPackage.clients[0]; + ok(client, "Client should exist"); + + // Find the sub-client for the interface + const subClient = client.children?.find((c) => c.name === "MyOperations"); + ok(subClient, "Sub-client for interface should exist"); + + const clientOptions = getClientOptions(subClient.decorators); + strictEqual(clientOptions.length, 1); + deepStrictEqual(clientOptions[0], { + name: "interfaceFlag", + value: true, + scope: "python", + }); + }); });