Skip to content

Commit

Permalink
Add @scope decorator (#1998)
Browse files Browse the repository at this point in the history
Resolves #964

The scope parameter of `@scope` decorator is not working as the normal
decorator.
For a normal decorator, we need to get the decorator value from state
map based on the scope and the current emitter name. If the value hit
from the state map with current emitter name, it's a positive case, if
not hit, it's a negative case.
```
@clientName("Renamed", csharp)
model A;
```
With the normal decorator scope handling, if the decorator is absent it
is a negative case. For the case below, we don't need to handle
`@clientname` decorator, it is a negative case.
```
model A;
```


But for `@scope` decorator, when it is absent, it means `AllScopes`,
which is also a positive case. For below two cases, python emitter
should include this operation, both of them are positive case.
```
op func: void
```
and 
```
@scope("!csharp")
op func: void
```

For incremental applying, the mechanism should be similar to the normal
decorator scope handling.
- for the same scope, the later decorator value wins regardless it's
defined with normal scope or scope negation
  • Loading branch information
live1206 authored Jan 3, 2025
1 parent 5a35942 commit 9d16af1
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .chronus/changes/scope-decorator-2024-11-18-12-59-12.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 `@scope` decorator to define the language scope for operation
26 changes: 26 additions & 0 deletions packages/typespec-client-generator-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ options:
- [`@override`](#@override)
- [`@paramAlias`](#@paramalias)
- [`@protocolAPI`](#@protocolapi)
- [`@scope`](#@scope)
- [`@usage`](#@usage)
- [`@useSystemTextJsonConverter`](#@usesystemtextjsonconverter)

Expand Down Expand Up @@ -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<br />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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -580,4 +593,5 @@ export type AzureClientGeneratorCoreDecorators = {
paramAlias: ParamAliasDecorator;
clientNamespace: ClientNamespaceDecorator;
alternateType: AlternateTypeDecorator;
scope: ScopeDecorator;
};
13 changes: 13 additions & 0 deletions packages/typespec-client-generator-core/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
42 changes: 42 additions & 0 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
OperationGroupDecorator,
ParamAliasDecorator,
ProtocolAPIDecorator,
ScopeDecorator,
UsageDecorator,
} from "../generated-defs/Azure.ClientGenerator.Core.js";
import {
Expand All @@ -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";
Expand Down Expand Up @@ -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) &&
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");

/**
*
Expand Down
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 @@ -11,6 +11,7 @@ import {
$operationGroup,
$override,
$protocolAPI,
$scope,
$usage,
$useSystemTextJsonConverter,
paramAliasDecorator,
Expand All @@ -36,5 +37,6 @@ export const $decorators = {
paramAlias: paramAliasDecorator,
clientNamespace: $clientNamespace,
alternateType: $alternateType,
scope: $scope,
} as AzureClientGeneratorCoreDecorators,
};
137 changes: 137 additions & 0 deletions packages/typespec-client-generator-core/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<br />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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 9d16af1

Please sign in to comment.