diff --git a/.chronus/changes/add-access-to-namespace-2024-7-6-15-15-43.md b/.chronus/changes/add-access-to-namespace-2024-7-6-15-15-43.md new file mode 100644 index 0000000000..a5b1aa5420 --- /dev/null +++ b/.chronus/changes/add-access-to-namespace-2024-7-6-15-15-43.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Add Namespace as target for @access decorator \ No newline at end of file diff --git a/docs/libraries/typespec-client-generator-core/reference/decorators.md b/docs/libraries/typespec-client-generator-core/reference/decorators.md index fee0493ac5..3e37b8c16d 100644 --- a/docs/libraries/typespec-client-generator-core/reference/decorators.md +++ b/docs/libraries/typespec-client-generator-core/reference/decorators.md @@ -11,6 +11,9 @@ toc_max_heading_level: 3 ### `@access` {#@Azure.ClientGenerator.Core.access} Set explicit access for operations, models and enums. +When setting access for namespaces, +the access info will be propagated to the models defined in the namespace. +If the model has an access override, the model override takes precedence. When setting access for models, the access info wll not be propagated to models' properties, base models or sub models. When setting access for an operation, @@ -28,7 +31,7 @@ the access result is undefined. #### Target -`Model | Operation | Enum | Union` +`Model | Operation | Enum | Union | Namespace` #### Parameters diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index 9ccc9cdf08..f7b452c155 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -29,6 +29,9 @@ npm install @azure-tools/typespec-client-generator-core #### `@access` Set explicit access for operations, models and enums. +When setting access for namespaces, +the access info will be propagated to the models defined in the namespace. +If the model has an access override, the model override takes precedence. When setting access for models, the access info wll not be propagated to models' properties, base models or sub models. When setting access for an operation, @@ -46,7 +49,7 @@ the access result is undefined. ##### Target -`Model | Operation | Enum | Union` +`Model | Operation | Enum | Union | Namespace` ##### Parameters 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 fdbb9f9126..acc59a6241 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 @@ -268,6 +268,9 @@ export type UsageDecorator = ( /** * Set explicit access for operations, models and enums. + * When setting access for namespaces, + * the access info will be propagated to the models defined in the namespace. + * If the model has an access override, the model override takes precedence. * When setting access for models, * the access info wll not be propagated to models' properties, base models or sub models. * When setting access for an operation, @@ -407,7 +410,7 @@ export type UsageDecorator = ( */ export type AccessDecorator = ( context: DecoratorContext, - target: Model | Operation | Enum | Union, + target: Model | Operation | Enum | Union | Namespace, value: EnumMember, scope?: string ) => void; diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index e88fd4f8c0..a8c4ed3953 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -260,6 +260,9 @@ enum Access { /** * Set explicit access for operations, models and enums. + * When setting access for namespaces, + * the access info will be propagated to the models defined in the namespace. + * If the model has an access override, the model override takes precedence. * When setting access for models, * the access info wll not be propagated to models' properties, base models or sub models. * When setting access for an operation, @@ -398,7 +401,7 @@ enum Access { * ``` */ extern dec access( - target: Model | Operation | Enum | Union, + target: Model | Operation | Enum | Union | Namespace, value: EnumMember, scope?: valueof string ); diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 932fcda251..638b20c7c8 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -935,7 +935,7 @@ const accessKey = createStateSymbol("access"); export const $access: AccessDecorator = ( context: DecoratorContext, - entity: Model | Enum | Operation | Union, + entity: Model | Enum | Operation | Union | Namespace, value: EnumMember, scope?: LanguageScopes ) => { @@ -951,16 +951,19 @@ export const $access: AccessDecorator = ( export function getAccessOverride( context: TCGCContext, - entity: Model | Enum | Operation | Union + entity: Model | Enum | Operation | Union | Namespace ): AccessFlags | undefined { - return getScopedDecoratorData(context, accessKey, entity); + const accessOverride = getScopedDecoratorData(context, accessKey, entity); + + if (!accessOverride && entity.namespace) { + return getAccessOverride(context, entity.namespace); + } + + return accessOverride; } -export function getAccess( - context: TCGCContext, - entity: Model | Enum | Operation | Union -): AccessFlags { - const override = getScopedDecoratorData(context, accessKey, entity); +export function getAccess(context: TCGCContext, entity: Model | Enum | Operation | Union) { + const override = getAccessOverride(context, entity); if (override || entity.kind === "Operation") { return override || "public"; } @@ -970,12 +973,13 @@ export function getAccess( return getSdkModel(context, entity).access; case "Enum": return getSdkEnum(context, entity).access; - case "Union": + case "Union": { const type = getSdkUnion(context, entity); if (type.kind === "enum" || type.kind === "model") { return type.access; } return "public"; + } } } diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 5fca450693..0c1bbeca1e 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -2,6 +2,7 @@ import { Enum, Interface, Model, + ModelProperty, Namespace, Operation, ignoreDiagnostics, @@ -1690,16 +1691,110 @@ describe("typespec-client-generator-core: decorators", () => { }); describe("@access", () => { - it("mark an operation as internal", async () => { - const { test } = (await runner.compile(` - @service({title: "Test Service"}) namespace TestService; - @test - @access(Access.internal) - op test(): void; - `)) as { test: Operation }; + describe("namespace access override", () => { + it("should inherit access from parent namespace", async () => { + const { Test } = (await runner.compile(` + @access(Access.public) + @service({title: "Test Service"}) namespace TestService; + @test + model Test { + prop: string; + } + `)) as { Test: Operation }; - const actual = getAccess(runner.context, test); - strictEqual(actual, "internal"); + const actual = getAccess(runner.context, Test); + strictEqual(actual, "public"); + }); + + it("should tag anonymous models with default access", async () => { + const { Test, prop } = (await runner.compile(` + @access(Access.public) + @service({title: "Test Service"}) namespace TestService; + @test + model Test { + @test + prop: { + foo: string; + } + } + `)) as { Test: Operation; prop: ModelProperty }; + + const actual = getAccess(runner.context, Test); + const actualAnonymous = getAccess(runner.context, prop.type as Model); + strictEqual(actual, "public"); + strictEqual(actualAnonymous, "public"); + }); + + it("should tag as internal anonymous models with default access", async () => { + const { Test, prop } = (await runner.compile(` + @access(Access.internal) + @service({title: "Test Service"}) namespace TestService; + @test + model Test { + @test + prop: { + foo: string; + } + } + `)) as { Test: Operation; prop: ModelProperty }; + + const actual = getAccess(runner.context, Test); + const actualAnonymous = getAccess(runner.context, prop.type as Model); + strictEqual(actual, "internal"); + strictEqual(actualAnonymous, "internal"); + }); + + it("should honor the granular override over the namespace one", async () => { + const { Test } = (await runner.compile(` + @access(Access.public) + @service({title: "Test Service"}) namespace TestService; + @access(Access.internal) + @test + model Test { + prop: string; + } + `)) as { Test: Operation }; + + const actual = getAccess(runner.context, Test); + strictEqual(actual, "internal"); + }); + + it("locally mark an operation as internal", async () => { + const { test } = (await runner.compile(` + @access(Access.public) + @service({title: "Test Service"}) namespace TestService; + @test + @access(Access.internal) + op test(): void; + `)) as { test: Operation }; + + const actual = getAccess(runner.context, test); + strictEqual(actual, "internal"); + }); + + it("locally mark an operation as public", async () => { + const { test } = (await runner.compile(` + @access(Access.public) + @service({title: "Test Service"}) namespace TestService; + @test + op test(): void; + `)) as { test: Operation }; + + const actual = getAccess(runner.context, test); + strictEqual(actual, "public"); + }); + + it("mark an operation as internal through the namespace", async () => { + const { test } = (await runner.compile(` + @access(Access.internal) + @service({title: "Test Service"}) namespace TestService; + @test + op test(): void; + `)) as { test: Operation }; + + const actual = getAccess(runner.context, test); + strictEqual(actual, "internal"); + }); }); it("default calculated value of operation is undefined, default value of calculated model is undefined", async () => {