Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/tcgc-clientOptions-2026-0-22-16-26-53.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

Add `@clientOption` flag for experimental, language-specific flags
41 changes: 41 additions & 0 deletions packages/typespec-client-generator-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Available ruleSets:
- [`@clientLocation`](#@clientlocation)
- [`@clientName`](#@clientname)
- [`@clientNamespace`](#@clientnamespace)
- [`@clientOption`](#@clientoption)
- [`@convenientAPI`](#@convenientapi)
- [`@deserializeEmptyStringAsNull`](#@deserializeemptystringasnull)
- [`@operationGroup`](#@operationgroup)
Expand Down Expand Up @@ -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.<br />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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -965,4 +997,5 @@ export type AzureClientGeneratorCoreDecorators = {
responseAsBool: ResponseAsBoolDecorator;
clientLocation: ClientLocationDecorator;
clientDoc: ClientDocDecorator;
clientOption: ClientOptionDecorator;
};
32 changes: 32 additions & 0 deletions packages/typespec-client-generator-core/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
1 change: 1 addition & 0 deletions packages/typespec-client-generator-core/src/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export const defaultDecoratorsAllowList = [
"TypeSpec\\.Xml\\..*",
"Azure\\.Core\\.@useFinalStateVia",
"Autorest\\.@example",
"Azure\\.ClientGenerator\\.Core\\.@clientOption",
];
34 changes: 34 additions & 0 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
ClientInitializationDecorator,
ClientNameDecorator,
ClientNamespaceDecorator,
ClientOptionDecorator,
ConvenientAPIDecorator,
DeserializeEmptyStringAsNullDecorator,
OperationGroupDecorator,
Expand Down Expand Up @@ -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);
};
20 changes: 20 additions & 0 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
PagingOperation,
Program,
Type,
Value,
} from "@typespec/compiler";
import { unsafe_Realm } from "@typespec/compiler/experimental";
import {
Expand Down Expand Up @@ -202,6 +203,25 @@ export interface DecoratorInfo {
arguments: Record<string, any>;
}

/**
* 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: Value;
/**
* The language scope this option applies to, if specified.
*/
scope?: string;
}

/**
* Represents a client in the package.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -429,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(
Expand Down
14 changes: 14 additions & 0 deletions packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 32 additions & 0 deletions packages/typespec-client-generator-core/src/public-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Scalar,
Type,
Union,
Value,
createDiagnosticCollector,
getEffectiveModelType,
getFriendlyName,
Expand Down Expand Up @@ -41,8 +42,10 @@ import {
listOperationsInOperationGroup,
} from "./decorators.js";
import {
DecoratorInfo,
SdkBodyParameter,
SdkClient,
SdkClientOption,
SdkClientType,
SdkCookieParameter,
SdkHeaderParameter,
Expand Down Expand Up @@ -897,3 +900,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 Value,
scope: d.arguments.scope as string | undefined,
}));
}
Comment on lines +923 to +931
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be getClientOptions<T extends DecoratedType>(type: T, key): unknown is much useful?

2 changes: 2 additions & 0 deletions packages/typespec-client-generator-core/src/tsp-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
$clientLocation,
$clientName,
$clientNamespace,
$clientOption,
$convenientAPI,
$deserializeEmptyStringAsNull,
$flattenProperty,
Expand Down Expand Up @@ -55,6 +56,7 @@ export const $decorators = {
responseAsBool: $responseAsBool,
clientDoc: $clientDoc,
clientLocation: $clientLocation,
clientOption: $clientOption,
} satisfies AzureClientGeneratorCoreDecorators,

"Azure.ClientGenerator.Core.Legacy": {
Expand Down
Loading