diff --git a/.chronus/changes/scope-decorator-2024-11-18-12-59-12.md b/.chronus/changes/scope-decorator-2024-11-18-12-59-12.md new file mode 100644 index 0000000000..853a1683ec --- /dev/null +++ b/.chronus/changes/scope-decorator-2024-11-18-12-59-12.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Add `@scope` decorator to define the language scope for operation diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index fa09ff3b7f..2c465cf53d 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -83,6 +83,7 @@ options: - [`@override`](#@override) - [`@paramAlias`](#@paramalias) - [`@protocolAPI`](#@protocolapi) +- [`@scope`](#@scope) - [`@usage`](#@usage) - [`@useSystemTextJsonConverter`](#@usesystemtextjsonconverter) @@ -632,6 +633,31 @@ Whether you want to generate an operation as a protocol operation. op test: void; ``` +#### `@scope` + +To define the client scope of an operation. + +```typespec +@Azure.ClientGenerator.Core.scope(scope?: valueof string) +``` + +##### Target + +`Operation` + +##### Parameters + +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | + +##### Examples + +```typespec +@scope("!csharp") +op test: void; +``` + #### `@usage` Override usage for models/enums. 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 010a5b1386..b7558caf6c 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 @@ -565,6 +565,19 @@ export type AlternateTypeDecorator = ( scope?: string, ) => void; +/** + * To define the client scope of an operation. + * + * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". + * @example + * ```typespec + * @scope("!csharp") + * op test: void; + * ``` + */ +export type ScopeDecorator = (context: DecoratorContext, target: Operation, scope?: string) => void; + export type AzureClientGeneratorCoreDecorators = { clientName: ClientNameDecorator; convenientAPI: ConvenientAPIDecorator; @@ -580,4 +593,5 @@ export type AzureClientGeneratorCoreDecorators = { paramAlias: ParamAliasDecorator; clientNamespace: ClientNamespaceDecorator; alternateType: AlternateTypeDecorator; + scope: ScopeDecorator; }; diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 14227bece4..f6c58bf0e4 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -539,3 +539,16 @@ extern dec clientNamespace( * ``` */ extern dec alternateType(source: ModelProperty | Scalar, alternate: Scalar, scope?: valueof string); + +/** + * To define the client scope of an operation. + * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". + * + * @example + * ```typespec + * @scope("!csharp") + * op test: void; + * ``` + */ +extern dec scope(target: Operation, scope?: valueof string); diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 54881dc630..9316fe7e75 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -40,6 +40,7 @@ import { OperationGroupDecorator, ParamAliasDecorator, ProtocolAPIDecorator, + ScopeDecorator, UsageDecorator, } from "../generated-defs/Azure.ClientGenerator.Core.js"; import { @@ -60,6 +61,7 @@ import { getValidApiVersion, isAzureCoreTspModel, negationScopesKey, + scopeKey, } from "./internal-utils.js"; import { createStateSymbol, reportDiagnostic } from "./lib.js"; import { getLibraryName } from "./public-utils.js"; @@ -624,6 +626,10 @@ export function listOperationsInOperationGroup( } for (const op of current.operations.values()) { + if (!IsInScope(context, op)) { + continue; + } + // Skip templated operations and omit operations if ( !isTemplateDeclarationOrInstance(op) && @@ -1115,3 +1121,39 @@ function getNamespaceFullNameWithOverride(context: TCGCContext, namespace: Names } return segments.join("."); } + +export const $scope: ScopeDecorator = ( + context: DecoratorContext, + entity: Operation, + scope?: LanguageScopes, +) => { + const [negationScopes, scopes] = parseScopes(context, scope); + if (negationScopes !== undefined && negationScopes.length > 0) { + // for negation scope, override the previous value + setScopedDecoratorData(context, $scope, negationScopesKey, entity, negationScopes); + } + if (scopes !== undefined && scopes.length > 0) { + // for normal scope, add them incrementally + const targetEntry = context.program.stateMap(scopeKey).get(entity); + setScopedDecoratorData( + context, + $scope, + scopeKey, + entity, + !targetEntry ? scopes : [...Object.values(targetEntry), ...scopes], + ); + } +}; + +function IsInScope(context: TCGCContext, entity: Operation): boolean { + const scopes = getScopedDecoratorData(context, scopeKey, entity); + if (scopes !== undefined && scopes.includes(context.emitterName)) { + return true; + } + + const negationScopes = getScopedDecoratorData(context, negationScopesKey, entity); + if (negationScopes !== undefined && negationScopes.includes(context.emitterName)) { + return false; + } + return true; +} diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index c5e84e9860..a36b3bb029 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -52,6 +52,7 @@ export const AllScopes = Symbol.for("@azure-core/typespec-client-generator-core/ export const clientNameKey = createStateSymbol("clientName"); export const clientNamespaceKey = createStateSymbol("clientNamespace"); export const negationScopesKey = createStateSymbol("negationScopes"); +export const scopeKey = createStateSymbol("scope"); /** * diff --git a/packages/typespec-client-generator-core/src/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index ab416d455e..808b916444 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -11,6 +11,7 @@ import { $operationGroup, $override, $protocolAPI, + $scope, $usage, $useSystemTextJsonConverter, paramAliasDecorator, @@ -36,5 +37,6 @@ export const $decorators = { paramAlias: paramAliasDecorator, clientNamespace: $clientNamespace, alternateType: $alternateType, + scope: $scope, } as AzureClientGeneratorCoreDecorators, }; diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 00d2076345..5371e5ca5d 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -2969,4 +2969,141 @@ describe("typespec-client-generator-core: decorators", () => { ok(testModel); }); }); + + describe("scope decorator", () => { + it("include operation from csharp client", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + model Test { + prop: string; + } + @scope("csharp") + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const client = sdkPackage.clients.find((x) => x.methods.find((m) => m.name === "func")); + const model = sdkPackage.models.find((x) => x.name === "Test"); + ok(client); + ok(model); + }); + + it("exclude operation from csharp client", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + model Test { + prop: string; + } + @scope("!csharp") + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const client = sdkPackage.clients.find((x) => x.methods.find((m) => m.name === "func")); + const model = sdkPackage.models.find((x) => x.name === "Test"); + strictEqual(client, undefined); + strictEqual(model, undefined); + }); + + it("negation scope override", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + const runnerWithJava = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-java", + }); + const spec = ` + @service + namespace MyService { + model Test { + prop: string; + } + @scope("!java") + @scope("!csharp") + op func( + @body body: Test + ): void; + } + `; + await runnerWithCSharp.compile(spec); + const csharpSdkPackage = runnerWithCSharp.context.sdkPackage; + const csharpSdkClient = csharpSdkPackage.clients.find((x) => + x.methods.find((m) => m.name === "func"), + ); + const csharpSdkModel = csharpSdkPackage.models.find((x) => x.name === "Test"); + ok(csharpSdkClient); + ok(csharpSdkModel); + + await runnerWithJava.compile(spec); + const javaSdkPackage = runnerWithJava.context.sdkPackage; + const javaSdkClient = javaSdkPackage.clients.find((x) => + x.methods.find((m) => m.name === "func"), + ); + const javaSdkModel = javaSdkPackage.models.find((x) => x.name === "Test"); + strictEqual(javaSdkClient, undefined); + strictEqual(javaSdkModel, undefined); + }); + + it("no scope decorator", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const client = sdkPackage.clients.find((x) => x.methods.find((m) => m.name === "func")); + const model = sdkPackage.models.find((x) => x.name === "Test"); + ok(client); + ok(model); + }); + + it("negation scope override normal scope", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + model Test { + prop: string; + } + @scope("!csharp") + @scope("csharp") + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const client = sdkPackage.clients.find((x) => x.methods.find((m) => m.name === "func")); + const model = sdkPackage.models.find((x) => x.name === "Test"); + ok(client); + ok(model); + }); + }); }); 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 30a89d2fcf..b35a5cf617 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 @@ -554,6 +554,31 @@ Whether you want to generate an operation as a protocol operation. op test: void; ``` +### `@scope` {#@Azure.ClientGenerator.Core.scope} + +To define the client scope of an operation. + +```typespec +@Azure.ClientGenerator.Core.scope(scope?: valueof string) +``` + +#### Target + +`Operation` + +#### Parameters + +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | + +#### Examples + +```typespec +@scope("!csharp") +op test: void; +``` + ### `@usage` {#@Azure.ClientGenerator.Core.usage} Override usage for models/enums. 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 fd9a9ed4bb..513b74dca6 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 @@ -52,5 +52,6 @@ npm install --save-peer @azure-tools/typespec-client-generator-core - [`@override`](./decorators.md#@Azure.ClientGenerator.Core.override) - [`@paramAlias`](./decorators.md#@Azure.ClientGenerator.Core.paramAlias) - [`@protocolAPI`](./decorators.md#@Azure.ClientGenerator.Core.protocolAPI) +- [`@scope`](./decorators.md#@Azure.ClientGenerator.Core.scope) - [`@usage`](./decorators.md#@Azure.ClientGenerator.Core.usage) - [`@useSystemTextJsonConverter`](./decorators.md#@Azure.ClientGenerator.Core.useSystemTextJsonConverter)