diff --git a/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-50-9.md b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-50-9.md new file mode 100644 index 0000000000..082815372a --- /dev/null +++ b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-50-9.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Adds support for enum-driven visibility in the compiler core. diff --git a/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md new file mode 100644 index 0000000000..a2c7cd59bc --- /dev/null +++ b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md @@ -0,0 +1,8 @@ +--- +changeKind: internal +packages: + - "@typespec/http" + - "@typespec/openapi" +--- + +Mask deprecation on getVisibility diff --git a/packages/compiler/.scripts/gen-extern-signature.ts b/packages/compiler/.scripts/gen-extern-signature.ts index c34facd1ce..9350af479d 100644 --- a/packages/compiler/.scripts/gen-extern-signature.ts +++ b/packages/compiler/.scripts/gen-extern-signature.ts @@ -41,7 +41,10 @@ for (const [namespace, diagnostics] of resolved) { const files = await generateExternDecorators(program, "@typespec/compiler", { namespaces }); for (const [name, content] of Object.entries(files)) { - const updatedContent = content.replace(/from "\@typespec\/compiler"/g, `from "../src/index.js"`); + const updatedContent = content.replace( + /from "\@typespec\/compiler"/g, + name.endsWith(".ts-test.ts") ? `from "../src/index.js"` : `from "../src/core/index.js"`, + ); const prettierConfig = await resolveConfig(root); await NodeHost.writeFile( diff --git a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts index 0819a6017d..5ec2e246c2 100644 --- a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts +++ b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts @@ -1,4 +1,4 @@ -import type { DecoratorContext, Type } from "../src/index.js"; +import type { DecoratorContext, Type } from "../src/core/index.js"; export type GetterDecorator = (context: DecoratorContext, target: Type) => void; diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 97ec7d4921..8597322567 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -12,7 +12,7 @@ import type { Type, Union, UnionVariant, -} from "../src/index.js"; +} from "../src/core/index.js"; export interface ExampleOptions { readonly title?: string; @@ -24,6 +24,12 @@ export interface OperationExample { readonly returnType?: unknown; } +export interface VisibilityFilter { + readonly any?: readonly EnumValue[]; + readonly all?: readonly EnumValue[]; + readonly none?: readonly EnumValue[]; +} + /** * Specify how to encode the target type. * @@ -114,12 +120,22 @@ export type WithoutDefaultValuesDecorator = (context: DecoratorContext, target: /** * Set the visibility of key properties in a model if not already set. * - * @param visibility The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. + * This will set the visibility modifiers of all key properties in the model if the visibility is not already _explicitly_ set, + * but will not change the visibility of any properties that have visibility set _explicitly_, even if the visibility + * is the same as the default visibility. + * + * Visibility may be explicitly set using any of the following decorators: + * + * - `@visibility` + * - `@removeVisibility` + * - `@invisible` + * + * @param visibility The desired default visibility value. If a key property already has visibility set, it will not be changed. */ export type WithDefaultKeyVisibilityDecorator = ( context: DecoratorContext, target: Model, - visibility: string, + visibility: string | EnumValue, ) => void; /** @@ -617,87 +633,6 @@ export type OpExampleDecorator = ( options?: ExampleOptions, ) => void; -/** - * Indicates that a property is only considered to be present or applicable ("visible") with - * the in the given named contexts ("visibilities"). When a property has no visibilities applied - * to it, it is implicitly visible always. - * - * As far as the TypeSpec core library is concerned, visibilities are open-ended and can be arbitrary - * strings, but the following visibilities are well-known to standard libraries and should be used - * with standard emitters that interpret them as follows: - * - * - "read": output of any operation. - * - "create": input to operations that create an entity.. - * - "query": input to operations that read data. - * - "update": input to operations that update data. - * - "delete": input to operations that delete data. - * - * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) - * - * @param visibilities List of visibilities which apply to this property. - * @example - * ```typespec - * model Dog { - * // the service will generate an ID, so you don't need to send it. - * @visibility("read") id: int32; - * // the service will store this secret name, but won't ever return it - * @visibility("create", "update") secretName: string; - * // the regular name is always present - * name: string; - * } - * ``` - */ -export type VisibilityDecorator = ( - context: DecoratorContext, - target: ModelProperty, - ...visibilities: string[] -) => void; - -/** - * Removes properties that are not considered to be present or applicable - * ("visible") in the given named contexts ("visibilities"). Can be used - * together with spread to effectively spread only visible properties into - * a new model. - * - * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) - * - * When using an emitter that applies visibility automatically, it is generally - * not necessary to use this decorator. - * - * @param visibilities List of visibilities which apply to this property. - * @example - * ```typespec - * model Dog { - * @visibility("read") id: int32; - * @visibility("create", "update") secretName: string; - * name: string; - * } - * - * // The spread operator will copy all the properties of Dog into DogRead, - * // and @withVisibility will then remove those that are not visible with - * // create or update visibility. - * // - * // In this case, the id property is removed, and the name and secretName - * // properties are kept. - * @withVisibility("create", "update") - * model DogCreateOrUpdate { - * ...Dog; - * } - * - * // In this case the id and name properties are kept and the secretName property - * // is removed. - * @withVisibility("read") - * model DogRead { - * ...Dog; - * } - * ``` - */ -export type WithVisibilityDecorator = ( - context: DecoratorContext, - target: Model, - ...visibilities: string[] -) => void; - /** * Mark this operation as a `list` operation that returns a paginated list of items. */ @@ -864,6 +799,133 @@ export type InspectTypeNameDecorator = ( text: string, ) => void; +/** + * Indicates that a property is only considered to be present or applicable ("visible") with + * the in the given named contexts ("visibilities"). When a property has no visibilities applied + * to it, it is implicitly visible always. + * + * As far as the TypeSpec core library is concerned, visibilities are open-ended and can be arbitrary + * strings, but the following visibilities are well-known to standard libraries and should be used + * with standard emitters that interpret them as follows: + * + * - "read": output of any operation. + * - "create": input to operations that create an entity.. + * - "query": input to operations that read data. + * - "update": input to operations that update data. + * - "delete": input to operations that delete data. + * + * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) + * + * @param visibilities List of visibilities which apply to this property. + * @example + * ```typespec + * model Dog { + * // the service will generate an ID, so you don't need to send it. + * @visibility(Lifecycle.Read) id: int32; + * // the service will store this secret name, but won't ever return it + * @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; + * // the regular name is always present + * name: string; + * } + * ``` + */ +export type VisibilityDecorator = ( + context: DecoratorContext, + target: ModelProperty, + ...visibilities: (string | EnumValue)[] +) => void; + +/** + * Indicates that a property is not visible in the given visibility class. + * + * This decorator removes all active visibility modifiers from the property within + * the given visibility class. + * + * @param visibilityClass The visibility class to make the property invisible within. + * @example + * ```typespec + * model Example { + * @invisible(Lifecycle) + * hidden_property: string; + * } + * ``` + */ +export type InvisibleDecorator = ( + context: DecoratorContext, + target: ModelProperty, + visibilityClass: Enum, +) => void; + +/** + * Removes visibility modifiers from a property. + * + * If the visibility modifiers for a visibility class have not been initialized, + * this decorator will use the default visibility modifiers for the visibility + * class as the default modifier set. + * + * @param target The property to remove visibility from. + * @param visibilities The visibility modifiers to remove from the target property. + * @example + * ```typespec + * model Example { + * // This property will have the Create and Update visibilities, but not the + * // Read visibility, since it is removed. + * @removeVisibility(Lifecycle.Read) + * secret_property: string; + * } + * ``` + */ +export type RemoveVisibilityDecorator = ( + context: DecoratorContext, + target: ModelProperty, + ...visibilities: EnumValue[] +) => void; + +/** + * Removes properties that are not considered to be present or applicable + * ("visible") in the given named contexts ("visibilities"). Can be used + * together with spread to effectively spread only visible properties into + * a new model. + * + * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) + * + * When using an emitter that applies visibility automatically, it is generally + * not necessary to use this decorator. + * + * @param visibilities List of visibilities which apply to this property. + * @example + * ```typespec + * model Dog { + * @visibility("read") id: int32; + * @visibility("create", "update") secretName: string; + * name: string; + * } + * + * // The spread operator will copy all the properties of Dog into DogRead, + * // and @withVisibility will then remove those that are not visible with + * // create or update visibility. + * // + * // In this case, the id property is removed, and the name and secretName + * // properties are kept. + * @withVisibility("create", "update") + * model DogCreateOrUpdate { + * ...Dog; + * } + * + * // In this case the id and name properties are kept and the secretName property + * // is removed. + * @withVisibility("read") + * model DogRead { + * ...Dog; + * } + * ``` + */ +export type WithVisibilityDecorator = ( + context: DecoratorContext, + target: Model, + ...visibilities: (string | EnumValue)[] +) => void; + /** * Sets which visibilities apply to parameters for the given operation. * @@ -872,7 +934,7 @@ export type InspectTypeNameDecorator = ( export type ParameterVisibilityDecorator = ( context: DecoratorContext, target: Operation, - ...visibilities: string[] + ...visibilities: (string | EnumValue)[] ) => void; /** @@ -883,9 +945,83 @@ export type ParameterVisibilityDecorator = ( export type ReturnTypeVisibilityDecorator = ( context: DecoratorContext, target: Operation, - ...visibilities: string[] + ...visibilities: (string | EnumValue)[] ) => void; +/** + * Declares the default visibility modifiers for a visibility class. + * + * The default modifiers are used when a property does not have any visibility decorators + * applied to it. + * + * The modifiers passed to this decorator _MUST_ be members of the target Enum. + * + * @param visibilities the list of modifiers to use as the default visibility modifiers. + */ +export type DefaultVisibilityDecorator = ( + context: DecoratorContext, + target: Enum, + ...visibilities: EnumValue[] +) => void; + +/** + * Applies the given visibility filter to the properties of the target model. + * + * This transformation is recursive, so it will also apply the filter to any nested + * or referenced models that are the types of any properties in the `target`. + * + * @param target The model to apply the visibility filter to. + * @param filter The visibility filter to apply to the properties of the target model. + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * @withVisibilityFilter(#{ all: #[Lifecycle.Read] }) + * model DogRead { + * ...Dog + * } + * ``` + */ +export type WithVisibilityFilterDecorator = ( + context: DecoratorContext, + target: Model, + filter: VisibilityFilter, +) => void; + +/** + * Transforms the `target` model to include only properties that are visible during the + * "Update" lifecycle phase. + * + * Any nested models of optional properties will be transformed into the "CreateOrUpdate" + * lifecycle phase instead of the "Update" lifecycle phase, so that nested models may be + * fully updated. + * + * @param target The model to apply the transformation to. + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * @visibility(Lifecycle.Create, Lifecycle.Update) + * secretName: string; + * + * name: string; + * } + * + * @withLifecycleUpdate + * model DogUpdate { + * ...Dog + * } + * ``` + */ +export type WithLifecycleUpdateDecorator = (context: DecoratorContext, target: Model) => void; + export type TypeSpecDecorators = { encode: EncodeDecorator; doc: DocDecorator; @@ -922,8 +1058,6 @@ export type TypeSpecDecorators = { discriminator: DiscriminatorDecorator; example: ExampleDecorator; opExample: OpExampleDecorator; - visibility: VisibilityDecorator; - withVisibility: WithVisibilityDecorator; list: ListDecorator; offset: OffsetDecorator; pageIndex: PageIndexDecorator; @@ -936,6 +1070,13 @@ export type TypeSpecDecorators = { lastLink: LastLinkDecorator; inspectType: InspectTypeDecorator; inspectTypeName: InspectTypeNameDecorator; + visibility: VisibilityDecorator; + invisible: InvisibleDecorator; + removeVisibility: RemoveVisibilityDecorator; + withVisibility: WithVisibilityDecorator; parameterVisibility: ParameterVisibilityDecorator; returnTypeVisibility: ReturnTypeVisibilityDecorator; + defaultVisibility: DefaultVisibilityDecorator; + withVisibilityFilter: WithVisibilityFilterDecorator; + withLifecycleUpdate: WithLifecycleUpdateDecorator; }; diff --git a/packages/compiler/lib/std/decorators.tsp b/packages/compiler/lib/std/decorators.tsp index c6c8fdaad2..f3e45413f0 100644 --- a/packages/compiler/lib/std/decorators.tsp +++ b/packages/compiler/lib/std/decorators.tsp @@ -572,93 +572,6 @@ extern dec opExample( example: valueof OperationExample, options?: valueof ExampleOptions ); -/** - * Indicates that a property is only considered to be present or applicable ("visible") with - * the in the given named contexts ("visibilities"). When a property has no visibilities applied - * to it, it is implicitly visible always. - * - * As far as the TypeSpec core library is concerned, visibilities are open-ended and can be arbitrary - * strings, but the following visibilities are well-known to standard libraries and should be used - * with standard emitters that interpret them as follows: - * - * - "read": output of any operation. - * - "create": input to operations that create an entity.. - * - "query": input to operations that read data. - * - "update": input to operations that update data. - * - "delete": input to operations that delete data. - * - * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) - * - * @param visibilities List of visibilities which apply to this property. - * - * @example - * - * ```typespec - * model Dog { - * // the service will generate an ID, so you don't need to send it. - * @visibility("read") id: int32; - * // the service will store this secret name, but won't ever return it - * @visibility("create", "update") secretName: string; - * // the regular name is always present - * name: string; - * } - * ``` - */ -extern dec visibility(target: ModelProperty, ...visibilities: valueof string[]); - -/** - * Removes properties that are not considered to be present or applicable - * ("visible") in the given named contexts ("visibilities"). Can be used - * together with spread to effectively spread only visible properties into - * a new model. - * - * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) - * - * When using an emitter that applies visibility automatically, it is generally - * not necessary to use this decorator. - * - * @param visibilities List of visibilities which apply to this property. - * - * @example - * ```typespec - * model Dog { - * @visibility("read") id: int32; - * @visibility("create", "update") secretName: string; - * name: string; - * } - * - * // The spread operator will copy all the properties of Dog into DogRead, - * // and @withVisibility will then remove those that are not visible with - * // create or update visibility. - * // - * // In this case, the id property is removed, and the name and secretName - * // properties are kept. - * @withVisibility("create", "update") - * model DogCreateOrUpdate { - * ...Dog; - * } - * - * // In this case the id and name properties are kept and the secretName property - * // is removed. - * @withVisibility("read") - * model DogRead { - * ...Dog; - * } - * ``` - */ -extern dec withVisibility(target: Model, ...visibilities: valueof string[]); - -/** - * Set the visibility of key properties in a model if not already set. - * - * @param visibility The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. - */ -extern dec withDefaultKeyVisibility(target: Model, visibility: valueof string); - -/** - * Returns the model with non-updateable properties removed. - */ -extern dec withUpdateableProperties(target: Model); /** * Returns the model with required properties removed. @@ -848,15 +761,3 @@ extern dec inspectType(target: unknown, text: valueof string); * @param text Custom text to log */ extern dec inspectTypeName(target: unknown, text: valueof string); - -/** - * Sets which visibilities apply to parameters for the given operation. - * @param visibilities List of visibility strings which apply to this operation. - */ -extern dec parameterVisibility(target: Operation, ...visibilities: valueof string[]); - -/** - * Sets which visibilities apply to the return type for the given operation. - * @param visibilities List of visibility strings which apply to this operation. - */ -extern dec returnTypeVisibility(target: Operation, ...visibilities: valueof string[]); diff --git a/packages/compiler/lib/std/main.tsp b/packages/compiler/lib/std/main.tsp index e0abf3ef19..4bc071fa50 100644 --- a/packages/compiler/lib/std/main.tsp +++ b/packages/compiler/lib/std/main.tsp @@ -3,3 +3,4 @@ import "./types.tsp"; import "./decorators.tsp"; import "./reflection.tsp"; import "./projected-names.tsp"; +import "./visibility.tsp"; diff --git a/packages/compiler/lib/std/visibility.tsp b/packages/compiler/lib/std/visibility.tsp new file mode 100644 index 0000000000..e6b7641f1c --- /dev/null +++ b/packages/compiler/lib/std/visibility.tsp @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +import "../../dist/src/lib/tsp-index.js"; + +using TypeSpec.Reflection; + +namespace TypeSpec; + +/** + * Indicates that a property is only considered to be present or applicable ("visible") with + * the in the given named contexts ("visibilities"). When a property has no visibilities applied + * to it, it is implicitly visible always. + * + * As far as the TypeSpec core library is concerned, visibilities are open-ended and can be arbitrary + * strings, but the following visibilities are well-known to standard libraries and should be used + * with standard emitters that interpret them as follows: + * + * - "read": output of any operation. + * - "create": input to operations that create an entity.. + * - "query": input to operations that read data. + * - "update": input to operations that update data. + * - "delete": input to operations that delete data. + * + * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) + * + * @param visibilities List of visibilities which apply to this property. + * + * @example + * + * ```typespec + * model Dog { + * // the service will generate an ID, so you don't need to send it. + * @visibility(Lifecycle.Read) id: int32; + * // the service will store this secret name, but won't ever return it + * @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; + * // the regular name is always present + * name: string; + * } + * ``` + */ +extern dec visibility(target: ModelProperty, ...visibilities: valueof (string | EnumMember)[]); + +/** + * Indicates that a property is not visible in the given visibility class. + * + * This decorator removes all active visibility modifiers from the property within + * the given visibility class. + * + * @param visibilityClass The visibility class to make the property invisible within. + * + * @example + * ```typespec + * model Example { + * @invisible(Lifecycle) + * hidden_property: string; + * } + * ``` + */ +extern dec invisible(target: ModelProperty, visibilityClass: Enum); + +/** + * Removes visibility modifiers from a property. + * + * If the visibility modifiers for a visibility class have not been initialized, + * this decorator will use the default visibility modifiers for the visibility + * class as the default modifier set. + * + * @param target The property to remove visibility from. + * @param visibilities The visibility modifiers to remove from the target property. + * + * @example + * ```typespec + * model Example { + * // This property will have the Create and Update visibilities, but not the + * // Read visibility, since it is removed. + * @removeVisibility(Lifecycle.Read) + * secret_property: string; + * } + * ``` + */ +extern dec removeVisibility(target: ModelProperty, ...visibilities: valueof EnumMember[]); + +/** + * Removes properties that are not considered to be present or applicable + * ("visible") in the given named contexts ("visibilities"). Can be used + * together with spread to effectively spread only visible properties into + * a new model. + * + * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) + * + * When using an emitter that applies visibility automatically, it is generally + * not necessary to use this decorator. + * + * @param visibilities List of visibilities which apply to this property. + * + * @example + * ```typespec + * model Dog { + * @visibility("read") id: int32; + * @visibility("create", "update") secretName: string; + * name: string; + * } + * + * // The spread operator will copy all the properties of Dog into DogRead, + * // and @withVisibility will then remove those that are not visible with + * // create or update visibility. + * // + * // In this case, the id property is removed, and the name and secretName + * // properties are kept. + * @withVisibility("create", "update") + * model DogCreateOrUpdate { + * ...Dog; + * } + * + * // In this case the id and name properties are kept and the secretName property + * // is removed. + * @withVisibility("read") + * model DogRead { + * ...Dog; + * } + * ``` + */ +extern dec withVisibility(target: Model, ...visibilities: valueof (string | EnumMember)[]); + +/** + * Set the visibility of key properties in a model if not already set. + * + * This will set the visibility modifiers of all key properties in the model if the visibility is not already _explicitly_ set, + * but will not change the visibility of any properties that have visibility set _explicitly_, even if the visibility + * is the same as the default visibility. + * + * Visibility may be explicitly set using any of the following decorators: + * + * - `@visibility` + * - `@removeVisibility` + * - `@invisible` + * + * @param visibility The desired default visibility value. If a key property already has visibility set, it will not be changed. + */ +extern dec withDefaultKeyVisibility(target: Model, visibility: valueof string | EnumMember); + +/** + * Sets which visibilities apply to parameters for the given operation. + * + * @param visibilities List of visibility strings which apply to this operation. + */ +extern dec parameterVisibility(target: Operation, ...visibilities: valueof (string | EnumMember)[]); + +/** + * Sets which visibilities apply to the return type for the given operation. + * @param visibilities List of visibility strings which apply to this operation. + */ +extern dec returnTypeVisibility( + target: Operation, + ...visibilities: valueof (string | EnumMember)[] +); + +/** + * Returns the model with non-updateable properties removed. + */ +extern dec withUpdateableProperties(target: Model); + +/** + * Declares the default visibility modifiers for a visibility class. + * + * The default modifiers are used when a property does not have any visibility decorators + * applied to it. + * + * The modifiers passed to this decorator _MUST_ be members of the target Enum. + * + * @param visibilities the list of modifiers to use as the default visibility modifiers. + */ +extern dec defaultVisibility(target: Enum, ...visibilities: valueof EnumMember[]); + +/** + * A visibility class for resource lifecycle phases. + * + * These visibilities control whether a property is visible during the create, read, and update phases of a resource's + * lifecycle. + * + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) id: int32; + * @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; + * name: string; + * } + * ``` + * + * In this example, the `id` property is only visible during the read phase, and the `secretName` property is only visible + * during the create and update phases. This means that the server will return the `id` property when returning a `Dog`, + * but the client will not be able to set or update it. In contrast, the `secretName` property can be set when creating + * or updating a `Dog`, but the server will never return it. The `name` property has no visibility modifiers and is + * therefore visible in all phases. + */ +enum Lifecycle { + Create, + Read, + Update, +} + +/** + * A visibility filter, used to specify which properties should be included when + * using the `withVisibilityFilter` decorator. + * + * The filter matches any property with ALL of the following: + * - If the `any` key is present, the property must have at least one of the specified visibilities. + * - If the `all` key is present, the property must have all of the specified visibilities. + * - If the `none` key is present, the property must have none of the specified visibilities. + */ +model VisibilityFilter { + any?: EnumMember[]; + all?: EnumMember[]; + none?: EnumMember[]; +} + +/** + * Applies the given visibility filter to the properties of the target model. + * + * This transformation is recursive, so it will also apply the filter to any nested + * or referenced models that are the types of any properties in the `target`. + * + * @param target The model to apply the visibility filter to. + * @param filter The visibility filter to apply to the properties of the target model. + * + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * @withVisibilityFilter(#{ all: #[Lifecycle.Read] }) + * model DogRead { + * ...Dog + * } + * ``` + */ +extern dec withVisibilityFilter(target: Model, filter: valueof VisibilityFilter); + +/** + * Transforms the `target` model to include only properties that are visible during the + * "Update" lifecycle phase. + * + * Any nested models of optional properties will be transformed into the "CreateOrUpdate" + * lifecycle phase instead of the "Update" lifecycle phase, so that nested models may be + * fully updated. + * + * @param target The model to apply the transformation to. + * + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * @visibility(Lifecycle.Create, Lifecycle.Update) + * secretName: string; + * + * name: string; + * } + * + * @withLifecycleUpdate + * model DogUpdate { + * ...Dog + * } + * ``` + */ +extern dec withLifecycleUpdate(target: Model); + +/** + * A copy of the input model `T` with only the properties that are visible during the + * "Create" resource lifecycle phase. + * + * This transformation is recursive, and will include only properties that have the + * `Lifecycle.Create` visibility modifier. + * + * If a `NameTemplate` is provided, the new model will be named according to the template. + * The template uses the same syntax as the `@friendlyName` decorator. + * + * @template T The model to transform. + * @template NameTemplate The name template to use for the new model. + * + * * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * model CreateDog is Create; + * ``` + */ +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ all: #[Lifecycle.Create] }) +model Create { + ...T; +} + +/** + * A copy of the input model `T` with only the properties that are visible during the + * "Read" resource lifecycle phase. + * + * This transformation is recursive, and will include only properties that have the + * `Lifecycle.Read` visibility modifier. + * + * If a `NameTemplate` is provided, the new model will be named according to the template. + * The template uses the same syntax as the `@friendlyName` decorator. + * + * @template T The model to transform. + * @template NameTemplate The name template to use for the new model. + * + * * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * model ReadDog is Read; + * ``` + */ +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ all: #[Lifecycle.Read] }) +model Read { + ...T; +} + +/** + * A copy of the input model `T` with only the properties that are visible during the + * "Update" resource lifecycle phase. + * + * This transformation will include only the properties that have the `Lifecycle.Update` + * visibility modifier, and the types of all properties will be replaced with the + * equivalent `CreateOrUpdate` transformation. + * + * If a `NameTemplate` is provided, the new model will be named according to the template. + * The template uses the same syntax as the `@friendlyName` decorator. + * + * @template T The model to transform. + * @template NameTemplate The name template to use for the new model. + * + * * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * model UpdateDog is Update; + * ``` + */ +@friendlyName(NameTemplate, T) +@withLifecycleUpdate +model Update { + ...T; +} + +/** + * A copy of the input model `T` with only the properties that are visible during the + * "Create" or "Update" resource lifecycle phases. + * + * This transformation is recursive, and will include only properties that have the + * `Lifecycle.Create` or `Lifecycle.Update` visibility modifier. + * + * If a `NameTemplate` is provided, the new model will be named according to the template. + * The template uses the same syntax as the `@friendlyName` decorator. + * + * @template T The model to transform. + * @template NameTemplate The name template to use for the new model. + * + * * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * model CreateOrUpdateDog is CreateOrUpdate; + * ``` + */ +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ any: #[Lifecycle.Create, Lifecycle.Update] }) +model CreateOrUpdate< + T extends Reflection.Model, + NameTemplate extends valueof string = "CreateOrUpdate{name}" +> { + ...T; +} diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index a7bc378065..8f19d0ccbf 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -69,3 +69,4 @@ export * from "./semantic-walker.js"; export { createSourceFile, getSourceFileKindFromExt } from "./source-file.js"; export * from "./type-utils.js"; export * from "./types.js"; +export * from "./visibility/index.js"; diff --git a/packages/compiler/src/core/logger/logger.ts b/packages/compiler/src/core/logger/logger.ts index ea61fa0236..59566a2041 100644 --- a/packages/compiler/src/core/logger/logger.ts +++ b/packages/compiler/src/core/logger/logger.ts @@ -1,4 +1,4 @@ -import { getSourceLocation } from "../index.js"; +import { getSourceLocation } from "../diagnostics.js"; import type { Logger, LogInfo, LogLevel, LogSink, ProcessedLog } from "../types.js"; const LogLevels = { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index e915655011..f577f932c9 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -1017,6 +1017,28 @@ const diagnostics = { }, }, + // #region Visibility + "visibility-sealed": { + severity: "error", + messages: { + default: paramMessage`Visibility of property '${"propName"}' is sealed and cannot be changed.`, + }, + }, + "visibility-mixed-legacy": { + severity: "error", + messages: { + default: + "Cannot apply both string (legacy) visibility modifiers and enum-based visibility modifiers to a property.", + }, + }, + "default-visibility-not-member": { + severity: "error", + messages: { + default: "The default visibility modifiers of a class must be members of the class enum.", + }, + }, + // #endregion + // #region CLI "no-compatible-vs-installed": { severity: "error", diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts new file mode 100644 index 0000000000..796705a878 --- /dev/null +++ b/packages/compiler/src/core/visibility/core.ts @@ -0,0 +1,726 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +// TypeSpec Visibility System +// -------------------------- + +// This module defines the core visibility system of the TypeSpec language. The +// visibility system is used to decide when properties of a _conceptual resource_ +// are present. The system is based on the concept of _visibility classes_, +// represented by TypeSpec enums. Each visibility class has a set of _visibility +// modifiers_ that can be applied to a model property, each modifier represented +// by a member of the visibility class enum. +// +// Each visibility class has a _default modifier set_ that is used when no +// modifiers are specified for a property, and each property has an _active +// modifier set_ that is used when analyzing the visibility of the property. +// +// Visibility can be _sealed_ for a program, property, or visibility class +// within a property. Once visibility is sealed, it cannot be unsealed, and any +// attempts to modify a sealed visibility will fail. + +import { compilerAssert } from "../diagnostics.js"; +import { reportDiagnostic } from "../messages.js"; +import type { Program } from "../program.js"; +import type { DecoratorContext, Enum, EnumMember, ModelProperty } from "../types.js"; +import { + getLifecycleVisibilityEnum, + normalizeLegacyLifecycleVisibilityString, +} from "./lifecycle.js"; + +import type { VisibilityFilter as GeneratedVisibilityFilter } from "../../../generated-defs/TypeSpec.js"; +import { useStateMap, useStateSet } from "../../lib/utils.js"; + +export { GeneratedVisibilityFilter }; + +/** + * A set of active visibility modifiers per visibility class. + */ +type VisibilityModifiers = Map>; + +/** + * The global visibility store. + * + * This store is used to track the visibility modifiers + */ +const [getVisibilityStore, setVisibilityStore] = useStateMap( + "visibilityStore", +); + +/** + * Returns the visibility modifiers for a given `property` within a `program`. + */ +function getOrInitializeVisibilityModifiers( + program: Program, + property: ModelProperty, +): VisibilityModifiers { + let visibilityModifiers = getVisibilityStore(program, property); + + if (!visibilityModifiers) { + visibilityModifiers = new Map(); + setVisibilityStore(program, property, visibilityModifiers); + } + + return visibilityModifiers; +} + +/** + * Returns the active visibility modifier set for a given `property` and `visibilityClass`. + * + * If no visibility modifiers have been set for the given `property` and `visibilityClass`, the function will use the + * provided `defaultSet` to initialize the visibility modifiers. + * + * @param program - the program in which the property occurs + * @param property - the property to get visibility modifiers for + * @param visibilityClass - the visibility class to get visibility modifiers for + * @param defaultSet - the default set to use if no set has been initialized + * @returns the active visibility modifier set for the given property and visibility class + */ +function getOrInitializeActiveModifierSetForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, + defaultSet: Set, +): Set { + const visibilityModifiers = getOrInitializeVisibilityModifiers(program, property); + let visibilityModifierSet = visibilityModifiers.get(visibilityClass); + + if (!visibilityModifierSet) { + visibilityModifierSet = defaultSet; + visibilityModifiers.set(visibilityClass, visibilityModifierSet); + } + + return visibilityModifierSet; +} + +/** + * If a Program is in this set, visibility is sealed for all properties in that Program. + */ +const VISIBILITY_PROGRAM_SEALS = new WeakSet(); + +const [isVisibilitySealedForProperty, sealVisibilityForProperty] = useStateSet( + "propertyVisibilitySealed", +); + +const [getSealedVisibilityClasses, setSealedVisibilityClasses] = useStateMap< + ModelProperty, + Set +>("sealedVisibilityClasses"); + +/** + * Seals visibility modifiers for a property in a given visibility class. + * + * @param program - the program in which the property occurs + * @param property - the property to seal visibility modifiers for + * @param visibilityClass - the visibility class to seal visibility modifiers for + */ +function sealVisibilityModifiersForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, +) { + let sealedClasses = getSealedVisibilityClasses(program, property); + + if (!sealedClasses) { + sealedClasses = new Set(); + setSealedVisibilityClasses(program, property, sealedClasses); + } + + sealedClasses.add(visibilityClass); +} + +/** + * Stores the default modifier set for a given visibility class. + */ +const [getDefaultModifiers, setDefaultModifiers] = useStateMap>( + "defaultVisibilityModifiers", +); + +/** + * Gets the default modifier set for a visibility class. If no default modifier set has been set, this function will + * initialize the default modifier set to ALL the visibility class's members. + * + * @param program - the program in which the visibility class occurs + * @param visibilityClass - the visibility class to get the default modifier set for + * @returns the default modifier set for the visibility class + */ +function getDefaultModifierSetForClass(program: Program, visibilityClass: Enum): Set { + const cached = getDefaultModifiers(program, visibilityClass); + + if (cached) return cached; + + const defaultModifierSet = new Set(visibilityClass.members.values()); + + setDefaultModifiers(program, visibilityClass, defaultModifierSet); + + return defaultModifierSet; +} + +/** + * Set the default visibility modifier set for a visibility class. + * + * This function may only be called ONCE per visibility class and must be called + * before the default modifier set is used by any operation. + */ +export function setDefaultModifierSetForVisibilityClass( + program: Program, + visibilityClass: Enum, + defaultSet: Set, +) { + compilerAssert( + !getDefaultModifiers(program, visibilityClass), + "The default modifier set for a visibility class may only be set once.", + ); + + setDefaultModifiers(program, visibilityClass, defaultSet); +} + +/** + * Convert a sequence of visibility modifiers into a map of visibility classes to their respective modifiers in the + * sequence. + * + * @param modifiers - the visibility modifiers to group + * @returns a map of visibility classes to their respective modifiers in the input list + */ +function groupModifiersByVisibilityClass(modifiers: EnumMember[]): Map> { + const enumMap = new Map>(); + + // Prepare new modifier sets for each visibility class + for (const modifier of modifiers) { + const visibilityClass = modifier.enum; + + let modifierSet = enumMap.get(visibilityClass); + + if (!modifierSet) { + modifierSet = new Set(); + enumMap.set(visibilityClass, modifierSet); + } + + modifierSet.add(modifier); + } + + return enumMap; +} + +// #region Legacy Visibility API + +const [getLegacyVisibility, setLegacyVisibilityModifiers, getLegacyVisibilityStateMap] = + useStateMap("legacyVisibility"); + +export { getLegacyVisibility }; + +/** + * Sets the legacy visibility modifiers for a property. + * + * This function will also set the visibility modifiers for the property in the Lifecycle visibility class for any + * strings in the visibility array that are recognized as lifecycle visibility strings. + * + * Calling this function twice on the same property will result in a failed compiler assertion. + * + * @param program - the program in which the property occurs + * @param property - the property to set visibility modifiers for + * @param visibilities - the legacy visibility strings to set + */ +export function setLegacyVisibility( + context: DecoratorContext, + property: ModelProperty, + visibilities: string[], +) { + const { program } = context; + + setLegacyVisibilityModifiers(program, property, visibilities); + + const lifecycleClass = getLifecycleVisibilityEnum(program); + + clearVisibilityModifiersForClass(program, property, lifecycleClass, context); + + const isEmpty = + visibilities.length === 0 || (visibilities.length === 1 && visibilities[0] === "none"); + + if (!isEmpty) { + const lifecycleVisibilities = visibilities + .map((v) => normalizeLegacyLifecycleVisibilityString(program, v)) + .filter((v) => !!v); + + addVisibilityModifiers(program, property, lifecycleVisibilities); + } +} + +/** + * Removes legacy visibility modifiers from a property. + * + * @param program - the program in which the property occurs + * @param property - the property to remove visibility modifiers from + */ +export function clearLegacyVisibility(program: Program, property: ModelProperty) { + getLegacyVisibilityStateMap(program).delete(property); +} + +/** + * Returns the legacy visibility modifiers for a property. + * + * For a property using the enum-driven visibility system, the active Lifecycle visibility modifiers will be converted + * to strings for backwards compatibility as follows: + * + * - If Lifecycle visibility is not explicitly set, and no legacy visibility is set, this function will return `undefined`. + * - If the property has no active Lifecycle visibility modifiers, this function will return `["none"]`. + * - Otherwise, this function will return an array of lowercase strings representing the active Lifecycle visibility + * modifiers ("create", "read", "update"). + * + * @deprecated Use `getVisibilityForClass` instead. + * + * @param program - the program in which the property occurs + * @param property - the property to get legacy visibility modifiers for + */ +export function getVisibility(program: Program, property: ModelProperty): string[] | undefined { + const legacyModifiers = getLegacyVisibility(program, property); + + if (legacyModifiers) return legacyModifiers; + + // Now check for applied lifecycle visibility modifiers and coerce them if necessary. + + const lifecycleModifiers = getVisibilityStore(program, property)?.get( + getLifecycleVisibilityEnum(program), + ); + + // Visibility is completely uninitialized, so return undefined to mimic legacy behavior. + if (!lifecycleModifiers) return undefined; + + // Visibility has been cleared explicitly, so return [] to mimic legacy behavior. + if (lifecycleModifiers.size === 0) return []; + + // Otherwise we just convert the modifiers to strings. + return Array.from(lifecycleModifiers).map((v) => v.name.toLowerCase()); +} + +// #endregion + +// #region Visibility Management API + +/** + * Check if a property has had its visibility modifiers sealed. + * + * If the property has been sealed globally, this function will return true. If the property has been sealed for the + * given visibility class, this function will return true. + * + * Otherwise, this function returns false. + * + * @param property - the property to check + * @param visibilityClass - the optional visibility class to check + * @returns true if the property is sealed for the given visibility class, false otherwise + */ +export function isSealed( + program: Program, + property: ModelProperty, + visibilityClass?: Enum, +): boolean { + if (VISIBILITY_PROGRAM_SEALS.has(program)) return true; + + const classSealed = visibilityClass + ? getSealedVisibilityClasses(program, property)?.has(visibilityClass) + : false; + + return classSealed || isVisibilitySealedForProperty(program, property); +} + +/** + * Seals a property's visibility modifiers. + * + * If the `visibilityClass` is provided, the property's visibility modifiers will be sealed for that visibility class + * only. Otherwise, the property's visibility modifiers will be sealed for all visibility classes (globally). + * + * @param property - the property to seal + * @param visibilityClass - the optional visibility class to seal the property for + */ +export function sealVisibilityModifiers( + program: Program, + property: ModelProperty, + visibilityClass?: Enum, +) { + if (visibilityClass) { + sealVisibilityModifiersForClass(program, property, visibilityClass); + } else { + sealVisibilityForProperty(program, property); + } +} + +/** + * Seals a program's visibility modifiers. + * + * This affects all properties in the program and prevents any further modifications to visibility modifiers within the + * program. + * + * Once the modifiers for a program are sealed, they cannot be unsealed. + * + * @param program - the program to seal + */ +export function sealVisibilityModifiersForProgram(program: Program) { + VISIBILITY_PROGRAM_SEALS.add(program); +} + +/** + * Add visibility modifiers to a property. + * + * This function will add all the `modifiers` to the active set of visibility modifiers for the given `property`. + * + * If no set of active modifiers exists for the given `property`, an empty set will be created for the property. + * + * If the visibility modifiers for `property` in the given modifier's visibility class have been sealed, this function + * will issue a diagnostic and ignore that modifier, but it will still add the rest of the modifiers whose classes have + * not been sealed. + * + * @param program - the program in which the ModelProperty occurs + * @param property - the property to add visibility modifiers to + * @param modifiers - the visibility modifiers to add + * @param context - the optional decorator context to use for displaying diagnostics + */ +export function addVisibilityModifiers( + program: Program, + property: ModelProperty, + modifiers: EnumMember[], + context?: DecoratorContext, +) { + const modifiersByClass = groupModifiersByVisibilityClass(modifiers); + + for (const [visibilityClass, newModifiers] of modifiersByClass.entries()) { + const target = context?.decoratorTarget ?? property; + if (isSealed(program, property, visibilityClass)) { + reportDiagnostic(program, { + code: "visibility-sealed", + format: { + propName: property.name, + }, + target, + }); + continue; + } + + const modifierSet = getOrInitializeActiveModifierSetForClass( + program, + property, + visibilityClass, + /* defaultSet: */ new Set(), + ); + + for (const modifier of newModifiers) { + modifierSet.add(modifier); + } + } +} + +/** + * Remove visibility modifiers from a property. + * + * This function will remove all the `modifiers` from the active set of visibility modifiers for the given `property`. + * + * If no set of active modifiers exists for the given `property`, the default set for the modifier's visibility class + * will be used. + * + * If the visibility modifiers for `property` in the given modifier's visibility class have been sealed, this function + * will issue a diagnostic and ignore that modifier, but it will still remove the rest of the modifiers whose classes + * have not been sealed. + * + * @param program - the program in which the ModelProperty occurs + * @param property - the property to remove visibility modifiers from + * @param modifiers - the visibility modifiers to remove + * @param context - the optional decorator context to use for displaying diagnostics + */ +export function removeVisibilityModifiers( + program: Program, + property: ModelProperty, + modifiers: EnumMember[], + context?: DecoratorContext, +) { + const modifiersByClass = groupModifiersByVisibilityClass(modifiers); + + for (const [visibilityClass, newModifiers] of modifiersByClass.entries()) { + const target = context?.decoratorTarget ?? property; + if (isSealed(program, property, visibilityClass)) { + reportDiagnostic(program, { + code: "visibility-sealed", + format: { + propName: property.name, + }, + target, + }); + continue; + } + + const modifierSet = getOrInitializeActiveModifierSetForClass( + program, + property, + visibilityClass, + /* defaultSet: */ getDefaultModifierSetForClass(program, visibilityClass), + ); + + for (const modifier of newModifiers) { + modifierSet.delete(modifier); + } + } +} + +/** + * Clears the visibility modifiers for a property in a given visibility class. + * + * If the visibility modifiers for the given class are sealed, this function will issue a diagnostic and leave the + * visibility modifiers unchanged. + * + * @param program - the program in which the ModelProperty occurs + * @param property - the property to clear visibility modifiers for + * @param visibilityClass - the visibility class to clear visibility modifiers for + * @param context - the optional decorator context to use for displaying diagnostics + */ +export function clearVisibilityModifiersForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, + context?: DecoratorContext, +) { + const target = context?.decoratorTarget ?? property; + if (isSealed(program, property, visibilityClass)) { + reportDiagnostic(program, { + code: "visibility-sealed", + format: { + propName: property.name, + }, + target, + }); + return; + } + + const modifierSet = getOrInitializeActiveModifierSetForClass( + program, + property, + visibilityClass, + /* defaultSet: */ new Set(), + ); + + modifierSet.clear(); +} + +/** + * Resets the visibility modifiers for a property in a given visibility class. + * + * This does not clear the modifiers. It resets them to the _uninitialized_ state. + * + * This is useful when cloning properties and you want to reset the visibility modifiers on the clone. + * + * If the visibility modifiers for this property and given visibility class are sealed, this function will issue a + * diagnostic and leave the visibility modifiers unchanged. + * + * @param program - the program in which the property occurs + * @param property - the property to reset visibility modifiers for + * @param visibilityClass - the visibility class to reset visibility modifiers for + * @param context - the optional decorator context to use for displaying diagnostics + */ +export function resetVisibilityModifiersForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, + context?: DecoratorContext, +) { + const target = context?.decoratorTarget ?? property; + + if (isSealed(program, property, visibilityClass)) { + reportDiagnostic(program, { + code: "visibility-sealed", + format: { + propName: property.name, + }, + target, + }); + return; + } + + getOrInitializeVisibilityModifiers(program, property).delete(visibilityClass); +} + +// #endregion + +// #region Visibility Analysis API + +/** + * Returns the active visibility modifiers for a property in a given visibility class. + * + * This function is infallible. If the visibility modifiers for the given class have not been set explicitly, it will + * return the default visibility modifiers for the class. + * + * @param program - the program in which the property occurs + * @param property - the property to get visibility modifiers for + * @param visibilityClass - the visibility class to get visibility modifiers for + * @returns the set of active modifiers (enum members) for the property and visibility class + */ +export function getVisibilityForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, +): Set { + return getOrInitializeActiveModifierSetForClass( + program, + property, + visibilityClass, + /* defaultSet: */ getDefaultModifierSetForClass(program, visibilityClass), + ); +} + +/** + * Determines if a property has a specified visibility modifier. + * + * If no visibility modifiers have been set for the visibility class of the modifier, the visibility class's default + * modifier set is used. + * + * @param program - the program in which the property occurs + * @param property - the property to check + * @param modifier - the visibility modifier to check for + * @returns true if the property has the visibility modifier, false otherwise + */ +export function hasVisibility( + program: Program, + property: ModelProperty, + modifier: EnumMember, +): boolean { + const activeSet = getOrInitializeActiveModifierSetForClass( + program, + property, + modifier.enum, + /* defaultSet: */ getDefaultModifierSetForClass(program, modifier.enum), + ); + + return activeSet?.has(modifier) ?? false; +} + +/** + * A visibility filter that can be used to determine if a property is visible. + * + * The filter is defined by three sets of visibility modifiers. The filter is satisfied if the property has: + * + * - ALL of the visibilities in the `all` set. + * + * AND + * + * - ANY of the visibilities in the `any` set. + * + * AND + * + * - NONE of the visibilities in the `none` set. + */ +export interface VisibilityFilter { + /** + * If set, the filter considers a property visible if it has ALL of these visibility modifiers. + */ + all?: Set; + /** + * If set, the filter considers a property visible if it has ANY of these visibility modifiers. + */ + any?: Set; + /** + * If set, the filter considers a property visible if it has NONE of these visibility modifiers. + */ + none?: Set; +} + +export const VisibilityFilter = { + /** + * Convert a TypeSpec `GeneratedVisibilityFilter` value to a `VisibilityFilter`. + * + * @param filter - the decorator argument filter to convert + * @returns a `VisibilityFilter` object that can be consumed by the visibility APIs + */ + fromDecoratorArgument(filter: GeneratedVisibilityFilter): VisibilityFilter { + return { + all: filter.all && new Set(filter.all.map((v) => v.value)), + any: filter.any && new Set(filter.any.map((v) => v.value)), + none: filter.none && new Set(filter.none.map((v) => v.value)), + }; + }, + /** + * Extracts the unique visibility classes referred to by the modifiers in a + * visibility filter. + * + * @param filter - the visibility filter to extract visibility classes from + * @returns a set of visibility classes referred to by the filter + */ + getVisibilityClasses(filter: VisibilityFilter): Set { + const classes = new Set(); + if (filter.all) filter.all.forEach((v) => classes.add(v.enum)); + if (filter.any) filter.any.forEach((v) => classes.add(v.enum)); + if (filter.none) filter.none.forEach((v) => classes.add(v.enum)); + return classes; + }, +}; + +/** + * Determines if a property is visible according to the given visibility filter. + * + * @see VisibilityFilter + * + * @param program - the program in which the property occurs + * @param property - the property to check + * @param filter - the visibility filter to use + * @returns true if the property is visible according to the filter, false otherwise + */ +export function isVisible( + program: Program, + property: ModelProperty, + filter: VisibilityFilter, +): boolean; + +/** + * Determines if a property has any of the specified (legacy) visibility strings. + * + * @deprecated Calling `isVisible` with an array of legacy visibility strings is deprecated. Use a `VisibilityFilter` + * object instead. + * + * @param program - the program in which the property occurs + * @param property - the property to check + * @param visibilities - the visibility strings to check for + */ +export function isVisible( + program: Program, + property: ModelProperty, + visibilities: readonly string[], +): boolean; + +export function isVisible( + program: Program, + property: ModelProperty, + _filterOrLegacyVisibilities: VisibilityFilter | readonly string[], +): boolean { + if (Array.isArray(_filterOrLegacyVisibilities)) { + return isVisibleLegacy(_filterOrLegacyVisibilities); + } + + const filter = { ...(_filterOrLegacyVisibilities as VisibilityFilter) }; + filter.all ??= new Set(); + filter.any ??= new Set(); + filter.none ??= new Set(); + + // Validate that property has ALL of the required visibilities of filter.all + for (const modifier of filter.all) { + if (!hasVisibility(program, property, modifier)) return false; + } + + // Validate that property has ANY of the required visibilities of filter.any + outer: while (filter.any.size > 0) { + for (const modifier of filter.any) { + if (hasVisibility(program, property, modifier)) break outer; + } + + return false; + } + + // Validate that property has NONE of the excluded visibilities of filter.none + for (const modifier of filter.none) { + if (hasVisibility(program, property, modifier)) return false; + } + + return true; + + function isVisibleLegacy(visibilities: readonly string[]) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + const propertyVisibilities = getVisibility(program, property); + return !propertyVisibilities || propertyVisibilities.some((v) => visibilities.includes(v)); + } +} + +// #endregion diff --git a/packages/compiler/src/core/visibility/index.ts b/packages/compiler/src/core/visibility/index.ts new file mode 100644 index 0000000000..97ae71a0f7 --- /dev/null +++ b/packages/compiler/src/core/visibility/index.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +export { getLifecycleVisibilityEnum } from "./lifecycle.js"; + +export { + addVisibilityModifiers, + clearVisibilityModifiersForClass, + getVisibility, + getVisibilityForClass, + hasVisibility, + isSealed, + isVisible, + removeVisibilityModifiers, + resetVisibilityModifiersForClass, + sealVisibilityModifiers, + sealVisibilityModifiersForProgram, +} from "./core.js"; diff --git a/packages/compiler/src/core/visibility/lifecycle.ts b/packages/compiler/src/core/visibility/lifecycle.ts new file mode 100644 index 0000000000..0d6addf627 --- /dev/null +++ b/packages/compiler/src/core/visibility/lifecycle.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +import { compilerAssert } from "../diagnostics.js"; +import type { Program } from "../program.js"; +import type { Enum, EnumMember } from "../types.js"; + +/** + * A cache for the `TypeSpec.Visibility.Lifecycle` enum per Program instance. + */ +const LIFECYCLE_ENUM_CACHE = new WeakMap(); + +/** + * Returns the instance of `TypeSpec.Visibility.Lifecycle` for the given `program`. + * + * @param program - the program to get the lifecycle visibility enum for + * @returns a reference to the lifecycle visibility enum + */ +export function getLifecycleVisibilityEnum(program: Program): Enum { + const cached = LIFECYCLE_ENUM_CACHE.get(program); + + if (cached) return cached; + + const [type, diagnostics] = program.resolveTypeReference("TypeSpec.Lifecycle"); + + compilerAssert( + diagnostics.length === 0, + "Encountered diagnostics when resolving the `TypeSpec.Lifecycle` visibility class enum", + ); + + compilerAssert(type!.kind === "Enum", "Expected `TypeSpec.Visibility.Lifecycle` to be an enum"); + + LIFECYCLE_ENUM_CACHE.set(program, type); + + return type; +} + +/** + * Returns the member of `Lifecycle` that corresponds to the given legacy `visibility` string. + * + * @param program - the program to get the lifecycle visibility enum for + * @param visibility - the visibility string to normalize + * @returns the corresponding member of `Lifecycle` or `undefined` if the visibility string is not recognized + */ +export function normalizeLegacyLifecycleVisibilityString( + program: Program, + visibility: string, +): EnumMember | undefined { + const lifecycle = getLifecycleVisibilityEnum(program); + switch (visibility) { + case "create": + return lifecycle.members.get("Create")!; + case "read": + return lifecycle.members.get("Read")!; + case "update": + return lifecycle.members.get("Update")!; + default: + return undefined; + } +} + +/** + * Returns the legacy visibility string that corresponds to the given `visibility` member of `Lifecycle`. + * + * If the given `visibility` member is not a member of `Lifecycle`, the function will return `undefined`. + * + * @param program - the program to get the lifecycle visibility enum for + * @param visibility - the visibility modifier to normalize + * @returns the corresponding legacy visibility string or `undefined` if the visibility member is not recognized + */ +export function normalizeVisibilityToLegacyLifecycleString( + program: Program, + visibility: EnumMember, +): string | undefined { + const lifecycle = getLifecycleVisibilityEnum(program); + + if (visibility.enum !== lifecycle) return undefined; + + switch (visibility.name) { + case "Create": + return "create"; + case "Read": + return "read"; + case "Update": + return "update"; + default: + return undefined; + } +} diff --git a/packages/compiler/src/experimental/mutators.ts b/packages/compiler/src/experimental/mutators.ts index e668004914..919b9f27a9 100644 --- a/packages/compiler/src/experimental/mutators.ts +++ b/packages/compiler/src/experimental/mutators.ts @@ -101,6 +101,25 @@ export type MutableType = Exclude< | Projection | Namespace >; + +/** + * Determines if a type is mutable. + */ +export function isMutableType(type: Type): type is MutableType { + switch (type.kind) { + case "TemplateParameter": + case "Intrinsic": + case "Function": + case "Decorator": + case "FunctionParameter": + case "Object": + case "Projection": + return false; + default: + return true; + } +} + /** @experimental */ export type MutableTypeWithNamespace = MutableType | Namespace; const typeId = CustomKeyMap.objectKeyer(); diff --git a/packages/compiler/src/experimental/typekit/define-kit.ts b/packages/compiler/src/experimental/typekit/define-kit.ts index 1f1711464a..47b21eec65 100644 --- a/packages/compiler/src/experimental/typekit/define-kit.ts +++ b/packages/compiler/src/experimental/typekit/define-kit.ts @@ -1,10 +1,10 @@ import { type Program } from "../../core/program.js"; -let currentProgram: Program | undefined; +const CURRENT_PROGRAM = Symbol.for("TypeSpec.currentProgram"); /** @experimental */ export function setCurrentProgram(program: Program): void { - currentProgram = program; + (globalThis as any)[CURRENT_PROGRAM] = program; } /** @experimental */ @@ -21,7 +21,7 @@ export function createTypekit(): TypekitPrototype { Object.defineProperty(tk, "program", { get() { - return currentProgram; + return (globalThis as any)[CURRENT_PROGRAM]; }, }); diff --git a/packages/compiler/src/experimental/typekit/kits/model-property.ts b/packages/compiler/src/experimental/typekit/kits/model-property.ts index 65ddbd2c46..a8b456f40d 100644 --- a/packages/compiler/src/experimental/typekit/kits/model-property.ts +++ b/packages/compiler/src/experimental/typekit/kits/model-property.ts @@ -1,5 +1,6 @@ -import type { ModelProperty, Scalar, Type } from "../../../core/types.js"; -import { EncodeData, getEncode, getFormat, getVisibility } from "../../../lib/decorators.js"; +import type { Enum, EnumMember, ModelProperty, Scalar, Type } from "../../../core/types.js"; +import { getVisibilityForClass } from "../../../core/visibility/core.js"; +import { EncodeData, getEncode, getFormat } from "../../../lib/decorators.js"; import { defineKit } from "../define-kit.js"; export interface ModelPropertyKit { @@ -26,11 +27,10 @@ export interface ModelPropertyKit { */ getFormat(property: ModelProperty): string | undefined; - // todo: update this with Will's proposal. /** * Get the visibility of the model property. */ - getVisibility(property: ModelProperty): string[] | undefined; + getVisibilityForClass(property: ModelProperty, visibilityClass: Enum): Set; } interface TypeKit { @@ -63,8 +63,8 @@ defineKit({ return getFormat(this.program, type) ?? getFormat(this.program, type.type as Scalar); }, - getVisibility(property) { - return getVisibility(this.program, property); + getVisibilityForClass(property, visibilityClass) { + return getVisibilityForClass(this.program, property, visibilityClass); }, }, }); diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 63222cce48..4440ddf759 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -22,20 +22,14 @@ import type { MinValueExclusiveDecorator, OpExampleDecorator, OverloadDecorator, - ParameterVisibilityDecorator, PatternDecorator, ProjectedNameDecorator, - ReturnTypeVisibilityDecorator, ReturnsDocDecorator, SecretDecorator, SummaryDecorator, TagDecorator, - VisibilityDecorator, - WithDefaultKeyVisibilityDecorator, WithOptionalPropertiesDecorator, WithPickedPropertiesDecorator, - WithUpdateablePropertiesDecorator, - WithVisibilityDecorator, WithoutDefaultValuesDecorator, WithoutOmittedPropertiesDecorator, } from "../../generated-defs/TypeSpec.js"; @@ -43,7 +37,6 @@ import { getPropertyType, isIntrinsicType, validateDecoratorNotOnType, - validateDecoratorTarget, } from "../core/decorator-utils.js"; import { getDeprecationDetails, markDeprecated } from "../core/deprecation.js"; import { @@ -105,12 +98,14 @@ import { UnionVariant, Value, } from "../core/types.js"; +import { setKey } from "./key.js"; import { useStateMap, useStateSet } from "./utils.js"; export { $encodedName, resolveEncodedName } from "./encoded-names.js"; export { serializeValueAsJson } from "./examples.js"; export { getPagingOperation, isList, type PagingOperation, type PagingProperty } from "./paging.js"; export * from "./service.js"; +export * from "./visibility.js"; export { ExampleOptions }; export const namespace = "TypeSpec"; @@ -126,7 +121,7 @@ function replaceTemplatedStringFromProperties(formatString: string, sourceObject }); } -function createStateSymbol(name: string) { +export function createStateSymbol(name: string) { return Symbol.for(`TypeSpec.${name}`); } @@ -807,46 +802,10 @@ function validateEncodeData(context: DecoratorContext, target: Type, encodeData: export { getEncode }; -// -- @visibility decorator --------------------- - -const [getVisibility, setVisibility, getVisibilityStateMap] = useStateMap( - "visibilitySettings", -); -export const $visibility: VisibilityDecorator = ( - context: DecoratorContext, - target: ModelProperty, - ...visibilities: string[] -) => { - validateDecoratorUniqueOnNode(context, target, $visibility); - - setVisibility(context.program, target, visibilities); -}; - -export { getVisibility }; - -function clearVisibilities(program: Program, target: Type) { - getVisibilityStateMap(program).delete(target); -} - -export const $withVisibility: WithVisibilityDecorator = ( - context: DecoratorContext, - target: Model, - ...visibilities: string[] -) => { - filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, visibilities)); - [...target.properties.values()].forEach((p) => clearVisibilities(context.program, p)); -}; - -export function isVisible( - program: Program, - property: ModelProperty, - visibilities: readonly string[], +export function filterModelPropertiesInPlace( + model: Model, + filter: (prop: ModelProperty) => boolean, ) { - const propertyVisibilities = getVisibility(program, property); - return !propertyVisibilities || propertyVisibilities.some((v) => visibilities.includes(v)); -} - -function filterModelPropertiesInPlace(model: Model, filter: (prop: ModelProperty) => boolean) { for (const [key, prop] of model.properties) { if (!filter(prop)) { model.properties.delete(key); @@ -864,19 +823,6 @@ export const $withOptionalProperties: WithOptionalPropertiesDecorator = ( target.properties.forEach((p) => (p.optional = true)); }; -// -- @withUpdateableProperties decorator ---------------------- - -export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( - context: DecoratorContext, - target: Type, -) => { - if (!validateDecoratorTarget(context, target, "@withUpdateableProperties", "Model")) { - return; - } - - filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, ["update"])); -}; - // -- @withoutOmittedProperties decorator ---------------------- export const $withoutOmittedProperties: WithoutOmittedPropertiesDecorator = ( @@ -1087,8 +1033,6 @@ function isEnumMemberAssignableToType(program: Program, typeName: Type, member: } export { getKnownValues }; -const [getKey, setKey] = useStateMap("key"); - /** * `@key` - mark a model property as the key to identify instances of that type * @@ -1117,46 +1061,7 @@ export const $key: KeyDecorator = ( setKey(context.program, entity, altName || entity.name); }; -export function isKey(program: Program, property: ModelProperty) { - return getKey(program, property) !== undefined; -} - -export function getKeyName(program: Program, property: ModelProperty): string | undefined { - return getKey(program, property); -} - -export const $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator = ( - context: DecoratorContext, - entity: Model, - visibility: string, -) => { - const keyProperties: ModelProperty[] = []; - entity.properties.forEach((prop: ModelProperty) => { - // Keep track of any key property without a visibility - if (isKey(context.program, prop) && !getVisibility(context.program, prop)) { - keyProperties.push(prop); - } - }); - - // For each key property without a visibility, clone it and add the specified - // default visibility value - keyProperties.forEach((keyProp) => { - entity.properties.set( - keyProp.name, - context.program.checker.cloneType(keyProp, { - decorators: [ - ...keyProp.decorators, - { - decorator: $visibility, - args: [ - { value: context.program.checker.createLiteralType(visibility), jsValue: visibility }, - ], - }, - ], - }), - ); - }); -}; +export { getKeyName, isKey } from "./key.js"; /** * Mark a type as deprecated @@ -1369,49 +1274,6 @@ export const $discriminator: DiscriminatorDecorator = ( setDiscriminator(context.program, entity, discriminator); }; -const [getParameterVisibility, setParameterVisibility] = useStateMap( - "parameterVisibility", -); - -export const $parameterVisibility: ParameterVisibilityDecorator = ( - context: DecoratorContext, - entity: Operation, - ...visibilities: string[] -) => { - validateDecoratorUniqueOnNode(context, entity, $parameterVisibility); - setParameterVisibility(context.program, entity, visibilities); -}; - -export { - /** - * Returns the visibilities of the parameters of the given operation, if provided with `@parameterVisibility`. - * - * @see {@link $parameterVisibility} - */ - getParameterVisibility, -}; - -const [getReturnTypeVisibility, setReturnTypeVisibility] = useStateMap( - "returnTypeVisibility", -); -export const $returnTypeVisibility: ReturnTypeVisibilityDecorator = ( - context: DecoratorContext, - entity: Operation, - ...visibilities: string[] -) => { - validateDecoratorUniqueOnNode(context, entity, $returnTypeVisibility); - setReturnTypeVisibility(context.program, entity, visibilities); -}; - -export { - /** - * Returns the visibilities of the return type of the given operation, if provided with `@returnTypeVisibility`. - * - * @see {@link $returnTypeVisibility} - */ - getReturnTypeVisibility, -}; - export interface Example extends ExampleOptions { readonly value: Value; } diff --git a/packages/compiler/src/lib/key.ts b/packages/compiler/src/lib/key.ts new file mode 100644 index 0000000000..2e7ba1a96a --- /dev/null +++ b/packages/compiler/src/lib/key.ts @@ -0,0 +1,11 @@ +import { Program } from "../core/index.js"; +import { ModelProperty, Type } from "../core/types.js"; +import { useStateMap } from "./utils.js"; + +const [getKey, setKey] = useStateMap("key"); + +export function isKey(program: Program, property: ModelProperty) { + return getKey(program, property) !== undefined; +} + +export { getKey as getKeyName, setKey }; diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index 2061e3ff42..f3f5cd39e8 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -24,21 +24,15 @@ import { $minValueExclusive, $opExample, $overload, - $parameterVisibility, $pattern, $projectedName, - $returnTypeVisibility, $returnsDoc, $secret, $service, $summary, $tag, - $visibility, - $withDefaultKeyVisibility, $withOptionalProperties, $withPickedProperties, - $withUpdateableProperties, - $withVisibility, $withoutDefaultValues, $withoutOmittedProperties, } from "./decorators.js"; @@ -54,6 +48,19 @@ import { pageSizeDecorator, prevLinkDecorator, } from "./paging.js"; +import { + $defaultVisibility, + $invisible, + $parameterVisibility, + $removeVisibility, + $returnTypeVisibility, + $visibility, + $withDefaultKeyVisibility, + $withLifecycleUpdate, + $withUpdateableProperties, + $withVisibility, + $withVisibilityFilter, +} from "./visibility.js"; /** @internal */ export const $decorators = { @@ -65,7 +72,6 @@ export const $decorators = { withoutOmittedProperties: $withoutOmittedProperties, withPickedProperties: $withPickedProperties, withoutDefaultValues: $withoutDefaultValues, - withDefaultKeyVisibility: $withDefaultKeyVisibility, summary: $summary, returnsDoc: $returnsDoc, errorsDoc: $errorsDoc, @@ -93,10 +99,16 @@ export const $decorators = { discriminator: $discriminator, example: $example, opExample: $opExample, - visibility: $visibility, - withVisibility: $withVisibility, inspectType: $inspectType, inspectTypeName: $inspectTypeName, + visibility: $visibility, + removeVisibility: $removeVisibility, + invisible: $invisible, + defaultVisibility: $defaultVisibility, + withVisibility: $withVisibility, + withVisibilityFilter: $withVisibilityFilter, + withLifecycleUpdate: $withLifecycleUpdate, + withDefaultKeyVisibility: $withDefaultKeyVisibility, parameterVisibility: $parameterVisibility, returnTypeVisibility: $returnTypeVisibility, diff --git a/packages/compiler/src/lib/utils.ts b/packages/compiler/src/lib/utils.ts index 910745ba1d..215a456f5f 100644 --- a/packages/compiler/src/lib/utils.ts +++ b/packages/compiler/src/lib/utils.ts @@ -1,4 +1,4 @@ -import type { Type } from "../core/types.js"; +import type { Model, ModelProperty, Type } from "../core/types.js"; import { unsafe_useStateMap, unsafe_useStateSet } from "../experimental/state-accessor.js"; function createStateSymbol(name: string) { @@ -12,3 +12,21 @@ export function useStateMap(key: string | symbol) { export function useStateSet(key: string | symbol) { return unsafe_useStateSet(typeof key === "string" ? createStateSymbol(key) : key); } + +/** + * Filters the properties of a model by removing them from the model instance if + * a given `filter` predicate is not satisfied. + * + * @param model - the model to filter properties on + * @param filter - the predicate to filter properties with + */ +export function filterModelPropertiesInPlace( + model: Model, + filter: (prop: ModelProperty) => boolean, +) { + for (const [key, prop] of model.properties) { + if (!filter(prop)) { + model.properties.delete(key); + } + } +} diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts new file mode 100644 index 0000000000..b9821c6059 --- /dev/null +++ b/packages/compiler/src/lib/visibility.ts @@ -0,0 +1,579 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import type { + DefaultVisibilityDecorator, + InvisibleDecorator, + ParameterVisibilityDecorator, + RemoveVisibilityDecorator, + ReturnTypeVisibilityDecorator, + VisibilityDecorator, + WithDefaultKeyVisibilityDecorator, + WithLifecycleUpdateDecorator, + WithUpdateablePropertiesDecorator, + WithVisibilityDecorator, + WithVisibilityFilterDecorator, +} from "../../generated-defs/TypeSpec.js"; +import { validateDecoratorTarget, validateDecoratorUniqueOnNode } from "../core/decorator-utils.js"; +import { reportDiagnostic } from "../core/messages.js"; +import type { Program } from "../core/program.js"; +import { + DecoratorApplication, + DecoratorContext, + DecoratorFunction, + Enum, + EnumMember, + EnumValue, + Model, + ModelProperty, + Operation, + Type, + UnionVariant, +} from "../core/types.js"; +import { + addVisibilityModifiers, + clearLegacyVisibility, + clearVisibilityModifiersForClass, + GeneratedVisibilityFilter, + getLegacyVisibility, + getVisibility, + isVisible, + removeVisibilityModifiers, + resetVisibilityModifiersForClass, + setDefaultModifierSetForVisibilityClass, + setLegacyVisibility, + VisibilityFilter, +} from "../core/visibility/core.js"; +import { + getLifecycleVisibilityEnum, + normalizeVisibilityToLegacyLifecycleString, +} from "../core/visibility/lifecycle.js"; +import { isMutableType, mutateSubgraph, Mutator, MutatorFlow } from "../experimental/mutators.js"; +import { isKey } from "./key.js"; +import { filterModelPropertiesInPlace, useStateMap } from "./utils.js"; + +// #region Legacy Visibility Utilities + +/** + * Takes a list of visibilities that possibly include both legacy visibility + * strings and visibility class members, and returns two lists containing only + * each type. + * + * @param visibilities - The list of visibilities to split + * @returns a tuple containing visibility enum members in the first position and + * legacy visibility strings in the second position + */ +function splitLegacyVisibility(visibilities: (string | EnumValue)[]): [EnumMember[], string[]] { + const legacyVisibilities = [] as string[]; + const modifiers = [] as EnumMember[]; + + for (const visibility of visibilities) { + if (typeof visibility === "string") { + legacyVisibilities.push(visibility); + } else { + modifiers.push(visibility.value); + } + } + + return [modifiers, legacyVisibilities] as const; +} + +export const $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator = ( + context: DecoratorContext, + entity: Model, + visibility: string | EnumValue, +) => { + const keyProperties: ModelProperty[] = []; + entity.properties.forEach((prop: ModelProperty) => { + // Keep track of any key property without a visibility + // eslint-disable-next-line @typescript-eslint/no-deprecated + if (isKey(context.program, prop) && !getVisibility(context.program, prop)) { + keyProperties.push(prop); + } + }); + + // For each key property without a visibility, clone it and add the specified + // default visibility value + keyProperties.forEach((keyProp) => { + entity.properties.set( + keyProp.name, + context.program.checker.cloneType(keyProp, { + decorators: [ + ...keyProp.decorators, + { + decorator: $visibility, + args: [ + { + value: + typeof visibility === "string" + ? context.program.checker.createLiteralType(visibility) + : visibility, + jsValue: visibility, + }, + ], + }, + ], + }), + ); + }); +}; + +/** + * Visibility configuration of an operation. + */ +interface OperationVisibilityConfig { + /** + * Stored parameter visibility configuration. + */ + parameters?: string[] | EnumMember[]; + /** + * Stored return type visibility configuration. + */ + returnType?: string[] | EnumMember[]; +} + +const [getOperationVisibilityConfigRaw, setOperationVisibilityConfigRaw] = useStateMap< + Operation, + OperationVisibilityConfig +>("operationVisibilityConfig"); + +function getOperationVisibilityConfig( + program: Program, + operation: Operation, +): OperationVisibilityConfig { + let config = getOperationVisibilityConfigRaw(program, operation); + + if (!config) { + config = {}; + + setOperationVisibilityConfigRaw(program, operation, config); + } + + return config; +} + +export const $parameterVisibility: ParameterVisibilityDecorator = ( + context: DecoratorContext, + operation: Operation, + ...visibilities: (string | EnumValue)[] +) => { + validateDecoratorUniqueOnNode(context, operation, $parameterVisibility); + + const [modifiers, legacyVisibilities] = splitLegacyVisibility(visibilities); + + if (modifiers.length > 0 && legacyVisibilities.length > 0) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + target: context.decoratorTarget, + }); + + return; + } + + if (modifiers.length > 0) { + getOperationVisibilityConfig(context.program, operation).parameters = modifiers; + } else { + getOperationVisibilityConfig(context.program, operation).parameters = legacyVisibilities; + } +}; + +/** + * Returns the visibilities of the parameters of the given operation, if provided with `@parameterVisibility`. + * + * @see {@link $parameterVisibility} + */ +export function getParameterVisibility(program: Program, entity: Operation): string[] | undefined { + return getOperationVisibilityConfig(program, entity) + .parameters?.map((p) => + typeof p === "string" ? p : normalizeVisibilityToLegacyLifecycleString(program, p), + ) + .filter((p) => !!p) as string[]; +} + +export const $returnTypeVisibility: ReturnTypeVisibilityDecorator = ( + context: DecoratorContext, + operation: Operation, + ...visibilities: (string | EnumValue)[] +) => { + validateDecoratorUniqueOnNode(context, operation, $parameterVisibility); + + const [modifiers, legacyVisibilities] = splitLegacyVisibility(visibilities); + + if (modifiers.length > 0 && legacyVisibilities.length > 0) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + target: context.decoratorTarget, + }); + + return; + } + + if (modifiers.length > 0) { + getOperationVisibilityConfig(context.program, operation).returnType = modifiers; + } else { + getOperationVisibilityConfig(context.program, operation).returnType = legacyVisibilities; + } +}; + +/** + * Returns the visibilities of the return type of the given operation, if provided with `@returnTypeVisibility`. + * + * @see {@link $returnTypeVisibility} + */ +export function getReturnTypeVisibility(program: Program, entity: Operation): string[] | undefined { + return getOperationVisibilityConfig(program, entity) + .returnType?.map((p) => + typeof p === "string" ? p : normalizeVisibilityToLegacyLifecycleString(program, p), + ) + .filter((p) => !!p) as string[]; +} + +// #endregion + +// #region Core Visibility Decorators + +// -- @visibility decorator --------------------- + +export const $visibility: VisibilityDecorator = ( + context: DecoratorContext, + target: ModelProperty, + ...visibilities: (string | EnumValue)[] +) => { + const [modifiers, legacyVisibilities] = splitLegacyVisibility(visibilities); + + if (legacyVisibilities.length > 0 || visibilities.length === 0) { + const isUnique = validateDecoratorUniqueOnNode(context, target, $visibility); + + if (modifiers.length > 0) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + target: context.decoratorTarget, + }); + + return; + } + + // Only attempt to set the legacy visibility modifiers if the visibility invocation is unique. Otherwise, a compiler + // assertion will fail inside the legacy visibility management API. + if (isUnique) setLegacyVisibility(context, target, legacyVisibilities); + } else { + if (getLegacyVisibility(context.program, target)) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + target: context.decoratorTarget, + }); + } + addVisibilityModifiers(context.program, target, modifiers, context); + } +}; + +// -- @removeVisibility decorator --------------------- + +export const $removeVisibility: RemoveVisibilityDecorator = ( + context: DecoratorContext, + target: ModelProperty, + ...visibilities: EnumValue[] +) => { + removeVisibilityModifiers( + context.program, + target, + visibilities.map((v) => v.value), + ); +}; + +// -- @invisible decorator --------------------- + +export const $invisible: InvisibleDecorator = ( + context: DecoratorContext, + target: ModelProperty, + visibilityClass: Enum, +) => { + clearVisibilityModifiersForClass(context.program, target, visibilityClass); +}; + +// -- @defaultVisibility decorator ------------------ + +export const $defaultVisibility: DefaultVisibilityDecorator = ( + context: DecoratorContext, + target: Enum, + ...visibilities: EnumValue[] +) => { + validateDecoratorUniqueOnNode(context, target, $defaultVisibility); + + const modifierSet = new Set(); + + for (const visibility of visibilities) { + if (visibility.value.enum !== target) { + reportDiagnostic(context.program, { + code: "default-visibility-not-member", + target: context.decoratorTarget, + }); + } else { + modifierSet.add(visibility.value); + } + } + + setDefaultModifierSetForVisibilityClass(context.program, target, modifierSet); +}; + +// #endregion + +// #region Legacy Visibility Transforms + +// -- @withVisibility decorator --------------------- + +export const $withVisibility: WithVisibilityDecorator = ( + context: DecoratorContext, + target: Model, + ...visibilities: (string | EnumValue)[] +) => { + const [modifiers, legacyVisibilities] = splitLegacyVisibility(visibilities); + + if (legacyVisibilities.length > 0) { + if (modifiers.length > 0) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + target: context.decoratorTarget, + }); + + return; + } + + // eslint-disable-next-line @typescript-eslint/no-deprecated + filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, legacyVisibilities)); + + for (const p of target.properties.values()) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + const legacyModifiers = getVisibility(context.program, p); + + if (legacyModifiers && legacyModifiers.length > 0) { + clearLegacyVisibility(context.program, p); + } else { + resetVisibilityModifiersForClass( + context.program, + p, + getLifecycleVisibilityEnum(context.program), + ); + } + } + } else { + const filter: VisibilityFilter = { + all: new Set(modifiers), + }; + + const visibilityClasses = new Set(modifiers.map((m) => m.enum)); + filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, filter)); + for (const p of target.properties.values()) { + for (const c of visibilityClasses) { + resetVisibilityModifiersForClass(context.program, p, c); + } + } + } +}; + +// -- @withUpdateableProperties decorator ---------------------- + +/** + * Filters a model for properties that are updateable. + * + * @param context - the program context + * @param target - Model to filter for updateable properties + */ +export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( + context: DecoratorContext, + target: Type, +) => { + if (!validateDecoratorTarget(context, target, "@withUpdateableProperties", "Model")) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-deprecated + filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, ["update"])); +}; + +// #endregion + +// #region Mutator Driven Transforms + +// -- @withVisibilityFilter decorator ---------------------- + +export const $withVisibilityFilter: WithVisibilityFilterDecorator = ( + context: DecoratorContext, + target: Model, + _filter: GeneratedVisibilityFilter, +) => { + const filter = VisibilityFilter.fromDecoratorArgument(_filter); + + const vfMutator: Mutator = createVisibilityFilterMutator(filter, { + decoratorFn: $withVisibilityFilter, + }); + + const { type } = mutateSubgraph(context.program, [vfMutator], target); + + target.properties = (type as Model).properties; +}; + +// -- @withLifecycleUpdate decorator ---------------------- + +export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( + context: DecoratorContext, + target: Model, +) => { + const lifecycle = getLifecycleVisibilityEnum(context.program); + const lifecycleUpdate: VisibilityFilter = { + all: new Set([lifecycle.members.get("Update")!]), + }; + + const lifecycleCreateOrUpdate: VisibilityFilter = { + any: new Set([lifecycle.members.get("Create")!, lifecycle.members.get("Update")!]), + }; + + const createOrUpdateMutator = createVisibilityFilterMutator(lifecycleCreateOrUpdate); + + const updateMutator = createVisibilityFilterMutator(lifecycleUpdate, { + recur: createOrUpdateMutator, + decoratorFn: $withLifecycleUpdate, + }); + + const { type } = mutateSubgraph(context.program, [updateMutator], target); + + target.properties = (type as Model).properties; +}; + +/** + * Options for the `createVisibilityFilterMutator` function. + */ +interface CreateVisibilityFilterMutatorOptions { + /** + * A mutator to apply to the type of visible properties of the model. If not provided, applies the constructed + * visibility filter mutator recursively. + */ + recur?: Mutator; + + /** + * Optionally, a decorator function to remove from the model's decorators, if present. + * + * This allows removing a decorator like `withVisibilityFilter` from the model after it has been applied + * to avoid an infinite loop. + */ + decoratorFn?: DecoratorFunction; +} + +/** + * Create a mutator that applies a visibility filter to a type. + * + * @param filter - The visibility filter to apply + * @param options - optional settings for the mutator + * @returns + */ +function createVisibilityFilterMutator( + filter: VisibilityFilter, + options: CreateVisibilityFilterMutatorOptions = {}, +): Mutator { + const visibilityClasses = VisibilityFilter.getVisibilityClasses(filter); + const mpMutator: Mutator = { + name: "VisibilityFilterProperty", + ModelProperty: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (prop, clone, program) => { + // We need to create a copy of the decorators array to avoid modifying the original. + // Decorators are _NOT_ cloned by the type kit, so we have to be careful not to modify the decorator arguments + // of the original type. + const decorators: DecoratorApplication[] = []; + + for (const decorator of prop.decorators) { + const decFn = decorator.decorator; + if (decFn === $visibility || decFn === $removeVisibility) { + const nextArgs = decorator.args.filter((arg) => { + if (arg.value.entityKind !== "Value") return false; + + const isString = arg.value.valueKind === "StringValue"; + const isOperativeVisibility = + arg.value.valueKind === "EnumValue" && visibilityClasses.has(arg.value.value.enum); + + return !(isString || isOperativeVisibility); + }); + + if (nextArgs.length > 0) { + decorators.push({ + ...decorator, + args: nextArgs, + }); + } + } else if (decFn !== $invisible) { + decorators.push(decorator); + } + } + + clone.decorators = decorators; + + for (const visibilityClass of visibilityClasses) { + resetVisibilityModifiersForClass(program, clone, visibilityClass); + } + + if (isMutableType(prop.type)) { + clone.type = mutateSubgraph(program, [options.recur ?? self], prop.type).type; + } + }, + }, + }; + const self: Mutator = { + name: "VisibilityFilter", + Union: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (union, clone, program) => { + for (const [key, member] of union.variants) { + if (member.type.kind === "Model" || member.type.kind === "Union") { + const variant: UnionVariant = { + ...member, + type: mutateSubgraph(program, [self], member.type).type, + }; + clone.variants.set(key, variant); + } + } + }, + }, + Model: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (model, clone, program, realm) => { + for (const [key, prop] of model.properties) { + if (!isVisible(program, prop, filter)) { + // Property is not visible, remove it + clone.properties.delete(key); + realm.remove(clone); + } else { + const mutated = mutateSubgraph(program, [mpMutator], prop); + + clone.properties.set(key, mutated.type as ModelProperty); + } + } + + if (options.decoratorFn) { + clone.decorators = clone.decorators.filter((d) => d.decorator !== options.decoratorFn); + } + }, + }, + ModelProperty: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (prop, clone, program) => { + if (isMutableType(prop.type)) { + clone.type = mutateSubgraph(program, [self], prop.type).type; + } + }, + }, + Tuple: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (tuple, clone, program) => { + for (const [index, element] of tuple.values.entries()) { + if (isMutableType(element)) { + clone.values[index] = mutateSubgraph(program, [self], element).type; + } + } + }, + }, + }; + + return self; +} + +// #endregion diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index ca84c4a5ec..9b640a7551 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -1,14 +1,6 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { - Model, - ModelProperty, - Namespace, - Operation, - Scalar, - getVisibility, - isSecret, -} from "../../src/index.js"; +import { Model, ModelProperty, Namespace, Operation, Scalar, isSecret } from "../../src/index.js"; import { getDoc, getEncode, @@ -866,44 +858,6 @@ describe("compiler: built-in decorators", () => { }); }); - describe("@withDefaultKeyVisibility", () => { - it("sets the default visibility on a key property when not already present", async () => { - const { TestModel } = (await runner.compile( - ` - model OriginalModel { - @key - name: string; - } - - @test - model TestModel is DefaultKeyVisibility { - } `, - )) as { TestModel: Model }; - - deepStrictEqual(getVisibility(runner.program, TestModel.properties.get("name")!), ["read"]); - }); - - it("allows visibility applied to a key property to override the default", async () => { - const { TestModel } = (await runner.compile( - ` - model OriginalModel { - @key - @visibility("read", "update") - name: string; - } - - @test - model TestModel is DefaultKeyVisibility { - } `, - )) as { TestModel: Model }; - - deepStrictEqual(getVisibility(runner.program, TestModel.properties.get("name")!), [ - "read", - "update", - ]); - }); - }); - describe("@overload", () => { it("emits an error when @overload is given something other than an operation", async () => { const diagnostics = await runner.diagnose(` diff --git a/packages/compiler/test/decorators/visibility.test.ts b/packages/compiler/test/decorators/visibility.test.ts new file mode 100644 index 0000000000..af06ede963 --- /dev/null +++ b/packages/compiler/test/decorators/visibility.test.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { DecoratorContext, Enum, Model, ModelProperty } from "../../src/core/types.js"; +import { getVisibility, getVisibilityForClass } from "../../src/core/visibility/core.js"; +import { $visibility, getLifecycleVisibilityEnum } from "../../src/index.js"; +import { BasicTestRunner, createTestRunner } from "../../src/testing/index.js"; + +function assertSetsEqual(a: Set, b: Set): void { + strictEqual(a.size, b.size); + + for (const item of a) { + ok(b.has(item)); + } +} + +describe("visibility", function () { + let runner: BasicTestRunner; + + beforeEach(async () => { + runner = await createTestRunner(); + }); + + it("default visibility", async () => { + const { name, Dummy } = (await runner.compile(` + @test + @defaultVisibility(Dummy.B) + enum Dummy { + A, + B, + } + + model TestModel { + @test + name: string; + }`)) as { name: ModelProperty; Dummy: Enum }; + + const LifecycleEnum = getLifecycleVisibilityEnum(runner.program); + + const Lifecycle = { + Read: LifecycleEnum.members.get("Read")!, + Create: LifecycleEnum.members.get("Create")!, + Update: LifecycleEnum.members.get("Update")!, + }; + + assertSetsEqual( + getVisibilityForClass(runner.program, name, LifecycleEnum), + new Set([Lifecycle.Read, Lifecycle.Create, Lifecycle.Update]), + ); + + assertSetsEqual( + getVisibilityForClass(runner.program, name, Dummy), + new Set([Dummy.members.get("B")!]), + ); + }); +}); + +describe("visibility (legacy)", function () { + let runner: BasicTestRunner; + + beforeEach(async () => { + runner = await createTestRunner(); + }); + + describe("@withDefaultKeyVisibility", () => { + it("sets the default visibility on a key property when not already present", async () => { + const { TestModel } = (await runner.compile( + ` + model OriginalModel { + @key + name: string; + } + + @test + model TestModel is DefaultKeyVisibility { + } `, + )) as { TestModel: Model }; + + deepStrictEqual(getVisibility(runner.program, TestModel.properties.get("name")!), ["read"]); + }); + + it("allows visibility applied to a key property to override the default", async () => { + const { TestModel } = (await runner.compile( + ` + model OriginalModel { + @key + @visibility("read", "update") + name: string; + } + + @test + model TestModel is DefaultKeyVisibility { + } `, + )) as { TestModel: Model }; + + deepStrictEqual(getVisibility(runner.program, TestModel.properties.get("name")!), [ + "read", + "update", + ]); + }); + + it("allows overriding legacy visibility", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility("read") + name: string + } + `)) as { Example: Model }; + + const name = Example.properties.get("name")!; + + const decCtx = { + program: runner.program, + } as DecoratorContext; + + $visibility(decCtx, name, "create"); + + deepStrictEqual(getVisibility(runner.program, name), ["create"]); + }); + }); +}); diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts new file mode 100644 index 0000000000..837464c1fe --- /dev/null +++ b/packages/compiler/test/visibility.test.ts @@ -0,0 +1,1228 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { getVisibility, VisibilityFilter } from "../src/core/visibility/core.js"; +import { + $visibility, + addVisibilityModifiers, + clearVisibilityModifiersForClass, + DecoratorContext, + Enum, + getLifecycleVisibilityEnum, + getVisibilityForClass, + hasVisibility, + isSealed, + isVisible, + Model, + ModelProperty, + removeVisibilityModifiers, + resetVisibilityModifiersForClass, + sealVisibilityModifiers, + sealVisibilityModifiersForProgram, +} from "../src/index.js"; +import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../src/testing/index.js"; + +describe("compiler: visibility core", () => { + let runner: BasicTestRunner; + + beforeEach(async () => { + runner = await createTestRunner(); + }); + + it("produces correct lifecycle visibility enum reference", async () => { + const { lifecycle } = (await runner.compile(` + model X { + @test lifecycle: TypeSpec.Lifecycle; + } + `)) as { lifecycle: ModelProperty }; + + const lifecycleEnum = getLifecycleVisibilityEnum(runner.program); + + strictEqual(lifecycleEnum, lifecycle.type); + strictEqual(lifecycleEnum, runner.program.resolveTypeReference("TypeSpec.Lifecycle")[0]); + }); + + describe("visibility seals", () => { + it("seals visibility modifiers for a program", async () => { + const { Example, Dummy } = (await runner.compile(` + @test model Example { + x: string; + } + + @test enum Dummy {} + `)) as { Example: Model; Dummy: Enum }; + + const x = Example.properties.get("x")!; + + const lifecycle = getLifecycleVisibilityEnum(runner.program); + + ok(!isSealed(runner.program, x)); + ok(!isSealed(runner.program, x, lifecycle)); + ok(!isSealed(runner.program, x, Dummy)); + + sealVisibilityModifiersForProgram(runner.program); + + ok(isSealed(runner.program, x)); + ok(isSealed(runner.program, x, lifecycle)); + ok(isSealed(runner.program, x, Dummy)); + }); + + it("seals visibility modifiers for a visibility class", async () => { + const { Example, Dummy } = (await runner.compile(` + @test model Example { + x: string; + } + + @test enum Dummy {} + `)) as { Example: Model; Dummy: Enum }; + + const x = Example.properties.get("x")!; + + const lifecycle = getLifecycleVisibilityEnum(runner.program); + + ok(!isSealed(runner.program, x)); + ok(!isSealed(runner.program, x, lifecycle)); + ok(!isSealed(runner.program, x, Dummy)); + + sealVisibilityModifiers(runner.program, x, lifecycle); + + ok(!isSealed(runner.program, x)); + ok(isSealed(runner.program, x, lifecycle)); + ok(!isSealed(runner.program, x, Dummy)); + }); + + it("seals visibility modifiers for a property", async () => { + const { Example, Dummy } = (await runner.compile(` + @test model Example { + x: string; + y: string; + } + + @test enum Dummy {} + `)) as { Example: Model; Dummy: Enum }; + + const x = Example.properties.get("x")!; + const y = Example.properties.get("y")!; + + const lifecycle = getLifecycleVisibilityEnum(runner.program); + + ok(!isSealed(runner.program, x)); + ok(!isSealed(runner.program, x, lifecycle)); + ok(!isSealed(runner.program, x, Dummy)); + + ok(!isSealed(runner.program, y)); + ok(!isSealed(runner.program, y, lifecycle)); + ok(!isSealed(runner.program, y, Dummy)); + + sealVisibilityModifiers(runner.program, x); + + ok(isSealed(runner.program, x)); + ok(isSealed(runner.program, x, lifecycle)); + ok(isSealed(runner.program, x, Dummy)); + + ok(!isSealed(runner.program, y)); + ok(!isSealed(runner.program, y, lifecycle)); + ok(!isSealed(runner.program, y, Dummy)); + }); + + it("correctly diagnoses modifying sealed visibility", async () => { + const { Example } = (await runner.compile(` + @test model Example { + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + const Create = Lifecycle.members.get("Create")!; + + sealVisibilityModifiersForProgram(runner.program); + + addVisibilityModifiers(runner.program, x, [Create]); + removeVisibilityModifiers(runner.program, x, [Create]); + clearVisibilityModifiersForClass(runner.program, x, Lifecycle); + + ok(runner.program.diagnostics.length === 3); + + expectDiagnostics(runner.program.diagnostics, [ + { + code: "visibility-sealed", + message: "Visibility of property 'x' is sealed and cannot be changed.", + }, + { + code: "visibility-sealed", + message: "Visibility of property 'x' is sealed and cannot be changed.", + }, + { + code: "visibility-sealed", + message: "Visibility of property 'x' is sealed and cannot be changed.", + }, + ]); + }); + }); + + describe("visibility modifiers", () => { + it("default visibility modifiers are all modifiers", async () => { + const { Example, Dummy } = (await runner.compile(` + @test model Example { + x: string; + } + + @test + @defaultVisibility(Dummy.A) + enum Dummy { + A, + B, + } + `)) as { Example: Model; Dummy: Enum }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, Lifecycle.members.size); + for (const member of Lifecycle.members.values()) { + ok(visibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } + + const dummyVisibility = getVisibilityForClass(runner.program, x, Dummy); + + strictEqual(dummyVisibility.size, 1); + ok(dummyVisibility.has(Dummy.members.get("A")!)); + ok(hasVisibility(runner.program, x, Dummy.members.get("A")!)); + ok(!dummyVisibility.has(Dummy.members.get("B")!)); + ok(!hasVisibility(runner.program, x, Dummy.members.get("B")!)); + }); + + it("adds a visibility modifier", async () => { + const { Example } = (await runner.compile(` + @test model Example { + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + const Create = Lifecycle.members.get("Create")!; + + addVisibilityModifiers(runner.program, x, [Create]); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 1); + + for (const member of Lifecycle.members.values()) { + if (member !== Create) { + ok(!visibility.has(member)); + ok(!hasVisibility(runner.program, x, member)); + } else { + ok(visibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } + } + }); + + it("removes a visibility modifier", async () => { + const { Example } = (await runner.compile(` + @test model Example { + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + const Create = Lifecycle.members.get("Create")!; + + removeVisibilityModifiers(runner.program, x, [Create]); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, Lifecycle.members.size - 1); + + for (const member of Lifecycle.members.values()) { + if (member !== Create) { + ok(visibility.has(member)); + } else { + ok(!visibility.has(member)); + } + } + }); + + it("clears visibility modifiers for a class", async () => { + const { Example } = (await runner.compile(` + @test model Example { + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + clearVisibilityModifiersForClass(runner.program, x, Lifecycle); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 0); + + for (const member of Lifecycle.members.values()) { + ok(!visibility.has(member)); + ok(!hasVisibility(runner.program, x, member)); + } + }); + + it("resets visibility modifiers for a class", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create) + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 1); + ok(visibility.has(Lifecycle.members.get("Create")!)); + ok(hasVisibility(runner.program, x, Lifecycle.members.get("Create")!)); + + resetVisibilityModifiersForClass(runner.program, x, Lifecycle); + + const resetVisibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(resetVisibility.size, 3); + + for (const member of Lifecycle.members.values()) { + ok(resetVisibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } + }); + + it("preserves visibility for other classes", async () => { + const { Example, Dummy } = (await runner.compile(` + @test model Example { + x: string; + } + + @test enum Dummy { + A, + B, + } + `)) as { Example: Model; Dummy: Enum }; + + const x = Example.properties.get("x")!; + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + clearVisibilityModifiersForClass(runner.program, x, Dummy); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, Lifecycle.members.size); + + for (const member of Lifecycle.members.values()) { + ok(visibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } + }); + }); + + describe("visibility filters", () => { + type LifecycleVisibilityName = "Create" | "Read" | "Update"; + interface VisibilityFilterScenario { + name: string; + expect: boolean; + visibility: Array; + filter: StringVisibilityFilter; + } + + interface StringVisibilityFilter { + all?: LifecycleVisibilityName[]; + any?: LifecycleVisibilityName[]; + none?: LifecycleVisibilityName[]; + } + + const SCENARIOS: VisibilityFilterScenario[] = [ + { + name: "simple property - all - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { all: ["Read"] }, + }, + { + name: "simple property - all - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Update"] }, + }, + { + name: "simple property - partial all - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Create", "Update"] }, + }, + { + name: "unmodified visibility - all - visible", + expect: true, + visibility: [], + filter: { all: ["Create", "Read", "Update"] }, + }, + { + name: "simple property - any - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { any: ["Read"] }, + }, + { + name: "simple property - partial any - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { any: ["Create", "Update"] }, + }, + { + name: "simple property - any - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { any: ["Update"] }, + }, + { + name: "simple property - none - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { none: ["Update"] }, + }, + { + name: "simple property - none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { none: ["Create"] }, + }, + { + name: "simple property - partial none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { none: ["Create", "Update"] }, + }, + { + name: "unmodified visibility - none - not visible", + expect: false, + visibility: [], + filter: { none: ["Create"] }, + }, + { + name: "simple property - all/any - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { all: ["Read"], any: ["Create"] }, + }, + { + name: "simple property - all/any - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Read"], any: ["Update"] }, + }, + { + name: "simple property - all/none - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { all: ["Read"], none: ["Update"] }, + }, + { + name: "simple property - all/none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Read"], none: ["Create"] }, + }, + { + name: "simple property - any/none - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { any: ["Read"], none: ["Update"] }, + }, + { + name: "simple property - any/none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { any: ["Read"], none: ["Create"] }, + }, + { + name: "simple property - all/any/none - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { all: ["Read"], any: ["Create"], none: ["Update"] }, + }, + { + name: "simple property - all/any/none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Read"], any: ["Create"], none: ["Create"] }, + }, + ]; + + for (const scenario of SCENARIOS) { + it(scenario.name, async () => { + const visibilityDecorator = + scenario.visibility.length > 0 + ? `@visibility(${scenario.visibility.map((v) => `Lifecycle.${v}`).join(", ")})` + : ""; + const { Example } = (await runner.compile(` + @test model Example { + ${visibilityDecorator} + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const filter = Object.fromEntries( + Object.entries(scenario.filter).map(([k, vis]) => [ + k, + new Set((vis as LifecycleVisibilityName[]).map((v) => Lifecycle.members.get(v)!)), + ]), + ) as VisibilityFilter; + + strictEqual(isVisible(runner.program, x, filter), scenario.expect); + }); + } + + it("mixed visibility classes in filter", async () => { + const { Example, Dummy: DummyEnum } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create, Dummy.B) + x: string; + } + + @test + @defaultVisibility(Dummy.A) + enum Dummy { + A, + B, + } + `)) as { Example: Model; Dummy: Enum }; + + const x = Example.properties.get("x")!; + const LifecycleEnum = getLifecycleVisibilityEnum(runner.program); + + const Lifecycle = { + Create: LifecycleEnum.members.get("Create")!, + Read: LifecycleEnum.members.get("Read")!, + Update: LifecycleEnum.members.get("Update")!, + }; + + const Dummy = { + A: DummyEnum.members.get("A")!, + B: DummyEnum.members.get("B")!, + }; + + strictEqual( + isVisible(runner.program, x, { + all: new Set([Lifecycle.Create, Dummy.B]), + }), + true, + ); + + strictEqual( + isVisible(runner.program, x, { + any: new Set([Dummy.A]), + }), + false, + ); + + strictEqual( + isVisible(runner.program, x, { + none: new Set([Lifecycle.Update]), + }), + true, + ); + + strictEqual( + isVisible(runner.program, x, { + all: new Set([Lifecycle.Create]), + none: new Set([Dummy.A]), + }), + true, + ); + + strictEqual( + isVisible(runner.program, x, { + all: new Set([Lifecycle.Create, Dummy.B]), + none: new Set([Dummy.A]), + }), + true, + ); + + strictEqual( + isVisible(runner.program, x, { + all: new Set([Lifecycle.Create]), + any: new Set([Dummy.A, Dummy.B]), + none: new Set([Lifecycle.Update]), + }), + true, + ); + }); + }); + + describe("legacy compatibility", () => { + it("converts legacy visibility strings to modifiers", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility("create") + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 1); + + for (const member of Lifecycle.members.values()) { + if (member.name === "Create") { + ok(visibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } else { + ok(!visibility.has(member)); + ok(!hasVisibility(runner.program, x, member)); + } + } + }); + + it("isVisible correctly coerces legacy visibility modifiers", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create, Lifecycle.Update) + x: string; + y: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + ok(isVisible(runner.program, x, ["create"])); + ok(isVisible(runner.program, x, ["update"])); + ok(!isVisible(runner.program, x, ["read"])); + + const y = Example.properties.get("y")!; + + ok(isVisible(runner.program, y, ["create", "update"])); + ok(isVisible(runner.program, y, ["read"])); + }); + + it("getVisibility correctly coerces visibility modifiers", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create, Lifecycle.Update) + x: string; + y: string; + @invisible(Lifecycle) + z: string; + @visibility(Lifecycle.Create, Lifecycle.Update, Lifecycle.Read) + a: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + const y = Example.properties.get("y")!; + const z = Example.properties.get("z")!; + const a = Example.properties.get("a")!; + + const xVisibility = getVisibility(runner.program, x); + const yVisibility = getVisibility(runner.program, y); + const zVisibility = getVisibility(runner.program, z); + const aVisibility = getVisibility(runner.program, a); + + deepStrictEqual(xVisibility, ["create", "update"]); + strictEqual(yVisibility, undefined); + deepStrictEqual(zVisibility, []); + deepStrictEqual(aVisibility, ["create", "update", "read"]); + }); + + it("correctly preseves explicitness of empty visibility", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 0); + + const legacyVisibility = getVisibility(runner.program, x); + + deepStrictEqual(legacyVisibility, []); + }); + + it("correctly coerces visibility modifiers after rewriting", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility("create") + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const LifecycleEnum = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, LifecycleEnum); + + strictEqual(visibility.size, 1); + + const Lifecycle = { + Create: LifecycleEnum.members.get("Create")!, + Read: LifecycleEnum.members.get("Read")!, + Update: LifecycleEnum.members.get("Update")!, + }; + + ok(visibility.has(Lifecycle.Create)); + ok(!visibility.has(Lifecycle.Read)); + ok(!visibility.has(Lifecycle.Update)); + + const legacyVisibility = getVisibility(runner.program, x); + + deepStrictEqual(legacyVisibility, ["create"]); + + // Now change the visibility imperatively using the legacy API + $visibility({ program: runner.program } as DecoratorContext, x, "read"); + + const updatedVisibility = getVisibilityForClass(runner.program, x, LifecycleEnum); + + strictEqual(updatedVisibility.size, 1); + + ok(!updatedVisibility.has(Lifecycle.Create)); + ok(updatedVisibility.has(Lifecycle.Read)); + ok(!updatedVisibility.has(Lifecycle.Update)); + + const updatedLegacyVisibility = getVisibility(runner.program, x); + + deepStrictEqual(updatedLegacyVisibility, ["read"]); + }); + }); + + describe("lifecycle transforms", () => { + async function compileWithTransform( + transform: "Create" | "Read" | "Update" | "CreateOrUpdate", + legacy: boolean = false, + ) { + const Lifecycle = { + Read: legacy ? `"read"` : "Lifecycle.Read", + Create: legacy ? `"create"` : "Lifecycle.Create", + Update: legacy ? `"update"` : "Lifecycle.Update", + }; + const { Result } = (await runner.compile(` + model Example { + @visibility(${Lifecycle.Read}) + r: string; + + cru: string; + + @visibility(${Lifecycle.Create}, ${Lifecycle.Read}) + cr: string; + + @visibility(${Lifecycle.Create}, ${Lifecycle.Update}) + cu: string; + + @visibility(${Lifecycle.Create}) + c: string; + + @visibility(${Lifecycle.Update}, ${Lifecycle.Read}) + ru: string; + + @visibility(${Lifecycle.Update}) + u: string; + + ${legacy ? `@visibility("none")` : `@invisible(Lifecycle)`} + invisible: string; + + nested: { + @visibility(${Lifecycle.Read}) + r: string; + + cru: string; + + @visibility(${Lifecycle.Create}, ${Lifecycle.Read}) + cr: string; + + @visibility(${Lifecycle.Create}, ${Lifecycle.Update}) + cu: string; + + @visibility(${Lifecycle.Create}) + c: string; + + @visibility(${Lifecycle.Update}, ${Lifecycle.Read}) + ru: string; + + @visibility(${Lifecycle.Update}) + u: string; + + ${legacy ? `@visibility("none")` : `@invisible(Lifecycle)`} + invisible: string; + }; + } + + // This ensures the transforms are non-side-effecting. + model ReadExample is Read; + + @test model Result is ${transform}; + `)) as { Result: Model }; + + return Result; + } + + function getProperties(model: Model) { + return { + c: model.properties.get("c"), + cr: model.properties.get("cr"), + cu: model.properties.get("cu"), + cru: model.properties.get("cru"), + r: model.properties.get("r"), + ru: model.properties.get("ru"), + u: model.properties.get("u"), + invisible: model.properties.get("invisible"), + }; + } + + it("correctly applies Read transform", async () => { + const Result = await compileWithTransform("Read"); + const props = getProperties(Result); + + validateReadTransform(props, Result, getProperties); + }); + + it("correctly applies Create transform", async () => { + const Result = await compileWithTransform("Create"); + const props = getProperties(Result); + + validateCreateTransform(props, Result, getProperties); + }); + + it("correctly applies Update transform", async () => { + const Result = await compileWithTransform("Update"); + const props = getProperties(Result); + + validateUpdateTransform(props, Result, getProperties); + }); + + it("correctly applies CreateOrUpdate transform", async () => { + const Result = await compileWithTransform("CreateOrUpdate"); + const props = getProperties(Result); + + // Properties that only have read visibility are removed + validateCreateOrUpdateTransform(props, Result, getProperties); + }); + + it("correctly transforms a union", async () => { + const { Result } = (await runner.compile(` + model Example { + example: A | B; + } + + model A { + @visibility(Lifecycle.Read) + a: string; + } + + model B { + @invisible(Lifecycle) + b: string; + } + + @test + model Result is Read; + `)) as { Result: Model }; + + const example = Result.properties.get("example"); + + ok(example); + + const union = example.type; + + strictEqual(union.kind, "Union"); + + const [A, B] = [...union.variants.values()].map((v) => v.type); + + strictEqual(A.kind, "Model"); + strictEqual(B.kind, "Model"); + + const a = A.properties.get("a"); + const b = B.properties.get("b"); + + ok(a); + + strictEqual(b, undefined); + }); + + it("correctly transforms a model property reference", async () => { + const { Result } = (await runner.compile(` + model Example { + a: ExampleRef.a; + } + + model ExampleRef { + a: A; + } + + model A { + @visibility(Lifecycle.Read) + a: string; + @visibility(Lifecycle.Create) + b: string; + } + + @test + model Result is Create; + `)) as { Result: Model }; + + const example = Result.properties.get("a"); + + ok(example); + + const ref = example.type; + + strictEqual(ref.kind, "ModelProperty"); + + const A = ref.type; + + ok(A.kind === "Model"); + + const a = A.properties.get("a"); + const b = A.properties.get("b"); + + strictEqual(a, undefined); + ok(b); + }); + + it("correctly transforms a tuple", async () => { + const { Result } = (await runner.compile(` + model Example { + example: [A, B]; + } + + model A { + @visibility(Lifecycle.Read) + a: string; + } + + model B { + @invisible(Lifecycle) + b: string; + } + + @test + model Result is Read; + `)) as { Result: Model }; + + const example = Result.properties.get("example"); + + ok(example); + + const tuple = example.type; + + strictEqual(tuple.kind, "Tuple"); + + const [A, B] = tuple.values; + + strictEqual(A.kind, "Model"); + strictEqual(B.kind, "Model"); + + const a = A.properties.get("a"); + const b = B.properties.get("b"); + + ok(a); + + strictEqual(b, undefined); + }); + + describe("legacy compatibility", () => { + it("correctly applies Read transform", async () => { + const Result = await compileWithTransform("Read", true); + const props = getProperties(Result); + + validateReadTransform(props, Result, getProperties); + }); + + it("correctly applies Create transform", async () => { + const Result = await compileWithTransform("Create", true); + const props = getProperties(Result); + + validateCreateTransform(props, Result, getProperties); + }); + + it("correctly applies Update transform", async () => { + const Result = await compileWithTransform("Update", true); + const props = getProperties(Result); + + validateUpdateTransform(props, Result, getProperties); + }); + + it("correctly applies CreateOrUpdate transform", async () => { + const Result = await compileWithTransform("CreateOrUpdate", true); + const props = getProperties(Result); + + // Properties that only have read visibility are removed + validateCreateOrUpdateTransform(props, Result, getProperties); + }); + }); + }); + + describe("withVisibilityFilter transforms", () => { + it("correctly makes transformed models immune from further transformation", async () => { + const { ExampleRead, ExampleReadCreate } = (await runner.compile(` + model Example { + @visibility(Lifecycle.Read) + id: string; + } + + @test model ExampleRead is Read; + + @test model ExampleReadCreate is Create; + `)) as { ExampleRead: Model; ExampleReadCreate: Model }; + + const idRead = ExampleRead.properties.get("id")!; + + ok(idRead); + + ok(!idRead.decorators.some((d) => d.decorator === $visibility)); + + // Property should remain present in the Create transform of this model. + const idReadCreate = ExampleReadCreate.properties.get("id")!; + + ok(idReadCreate); + + strictEqual(idRead.type, idReadCreate.type); + }); + }); +}); +function validateCreateOrUpdateTransform( + props: { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, + Result: Model, + getProperties: (model: Model) => { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, +) { + strictEqual(props.r, undefined); + + strictEqual(props.invisible, undefined); + + // All other visible properties are preserved + ok(props.c); + ok(props.cr); + ok(props.cu); + ok(props.cru); + ok(props.ru); + ok(props.u); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); + ok(nestedProps.ru); + ok(nestedProps.u); + + strictEqual(nestedProps.invisible, undefined); +} + +function validateUpdateTransform( + props: { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, + Result: Model, + getProperties: (model: Model) => { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, +) { + strictEqual(props.r, undefined); + strictEqual(props.c, undefined); + strictEqual(props.cr, undefined); + + strictEqual(props.invisible, undefined); + + ok(props.cu); + ok(props.cru); + ok(props.ru); + ok(props.u); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + // Nested properties work differently in Lifecycle Update transforms, requiring nested create-only properties to + // additionally be visible + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); + ok(nestedProps.ru); + ok(nestedProps.u); +} + +function validateCreateTransform( + props: { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, + Result: Model, + getProperties: (model: Model) => { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, +) { + strictEqual(props.r, undefined); + strictEqual(props.ru, undefined); + strictEqual(props.u, undefined); + + strictEqual(props.invisible, undefined); + + ok(props.c); + ok(props.cr); + ok(props.cu); + ok(props.cru); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + strictEqual(nestedProps.ru, undefined); + strictEqual(nestedProps.u, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); +} + +function validateReadTransform( + props: { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, + Result: Model, + getProperties: (model: Model) => { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, +) { + strictEqual(props.c, undefined); + strictEqual(props.cu, undefined); + strictEqual(props.u, undefined); + + strictEqual(props.invisible, undefined); + + // All properties that have Read visibility are preserved + ok(props.r); + ok(props.cr); + ok(props.cru); + ok(props.ru); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.c, undefined); + strictEqual(nestedProps.cu, undefined); + strictEqual(nestedProps.u, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.r); + ok(nestedProps.cr); + ok(nestedProps.cru); + ok(nestedProps.ru); +} diff --git a/packages/http/src/metadata.ts b/packages/http/src/metadata.ts index 093f22407b..2f78981574 100644 --- a/packages/http/src/metadata.ts +++ b/packages/http/src/metadata.ts @@ -207,9 +207,10 @@ export function resolveRequestVisibility( operation: Operation, verb: HttpVerb, ): Visibility { - const parameterVisibility = arrayToVisibility(getParameterVisibility(program, operation)); + const parameterVisibility = getParameterVisibility(program, operation); + const parameterVisibilityArray = arrayToVisibility(parameterVisibility); const defaultVisibility = getDefaultVisibilityForVerb(verb); - let visibility = parameterVisibility ?? defaultVisibility; + let visibility = parameterVisibilityArray ?? defaultVisibility; // If the verb is PATCH, then we need to add the patch flag to the visibility in order for // later processes to properly apply it if (verb === "patch") { @@ -236,6 +237,7 @@ export function isMetadata(program: Program, property: ModelProperty) { * Determines if the given property is visible with the given visibility. */ export function isVisible(program: Program, property: ModelProperty, visibility: Visibility) { + // eslint-disable-next-line @typescript-eslint/no-deprecated return isVisibleCore(program, property, visibilityToArray(visibility)); } diff --git a/packages/http/src/operations.ts b/packages/http/src/operations.ts index 3d2a182b06..26657269d2 100644 --- a/packages/http/src/operations.ts +++ b/packages/http/src/operations.ts @@ -253,6 +253,7 @@ function validateProgram(program: Program, diagnostics: DiagnosticCollector) { // itself as that would be a layering violation, putting a REST // interpretation of visibility into the core. function checkForUnsupportedVisibility(property: ModelProperty) { + // eslint-disable-next-line @typescript-eslint/no-deprecated if (getVisibility(program, property)?.includes("write")) { // NOTE: Check for name equality instead of function equality // to deal with multiple copies of core being used. diff --git a/packages/openapi/src/helpers.ts b/packages/openapi/src/helpers.ts index 7bc1b1fe6c..411de4eceb 100644 --- a/packages/openapi/src/helpers.ts +++ b/packages/openapi/src/helpers.ts @@ -164,6 +164,7 @@ export function resolveOperationId(program: Program, operation: Operation) { * designate a read-only property. */ export function isReadonlyProperty(program: Program, property: ModelProperty) { + // eslint-disable-next-line @typescript-eslint/no-deprecated const visibility = getVisibility(program, property); // note: multiple visibilities that include read are not handled using // readonly: true, but using separate schemas. diff --git a/website/src/content/current-sidebar.ts b/website/src/content/current-sidebar.ts index eadf0c0776..f89d2084ee 100644 --- a/website/src/content/current-sidebar.ts +++ b/website/src/content/current-sidebar.ts @@ -84,6 +84,7 @@ const sidebar: SidebarItem[] = [ "language-basics/alias", "language-basics/values", "language-basics/type-relations", + "language-basics/visibility", ], }, { diff --git a/website/src/content/docs/docs/language-basics/visibility.md b/website/src/content/docs/docs/language-basics/visibility.md new file mode 100644 index 0000000000..dd523a840d --- /dev/null +++ b/website/src/content/docs/docs/language-basics/visibility.md @@ -0,0 +1,364 @@ +--- +id: visibility +title: Visibility +--- + +**Visibility** is a language feature that allows you to share a model between multiple operations and define in which contexts +properties of the model are "visible." Visibility is a very powerful feature that allows you to define different "views" +of a model within different operations or contexts. + +## Basic concepts + +- Visibility applies to _model properties_ only. It is used to determine when an emitter should include or exclude a + property in a certain context. +- Visibility is defined using a _visibility class_. A visibility class is an `enum` that defines the visibility modifiers + (or flags) that can be applied to a property. Any `enum` can serve as a visibility class. +- Visibility classes have a _default_ visibility, which is the set of visibility modifiers that are applied _by default_ + to a property if the visibility is not explicitly set. + +## Lifecycle visibility + +TypeSpec provides a built-in visibility called "resource lifecycle visibility." This visibility allows you to declare +whether properties are visible when a creating, updating, or reading a resource from an API endpoint. For example: + +```typespec +model Example { + /** + * The unique identifier of this resource. + * + * The ID is automatically generated by the service, so it cannot be set when the resource is created or updated, + * but the server will return it when the resource is read. + */ + @visibility(Lifecycle.Read) + id: string; + + /** + * The name of this resource. + * + * The name can be set when the resource is created, but may not be changed. + */ + @visibility(Lifecycle.Create, Lifecycle.Read) + name: string; + + /** + * The description of this resource. + * + * By default, properties are visible in all three lifecycle phases, so this + * property can be set when the resource is created, updated, and read. + */ + description: string; +} +``` + +In the above example, each property of the `Example` model has a lifecycle visibility that instructs emitters to include +or exclude the property when creating, updating, or reading the `Example` resource. + +TypeSpec's HTTP library, OpenAPI emitter, and other standard functionality use the `Lifecycle` visibility to create +different views of the `Example` model based on which lifecycle phase is used in a particular operation. + +In the following example, the type of the input and output of each operation is affected by the lifecycle visibility +of the properties in the `Example` model. + +```typespec +@route("/example") +interface Examples { + /** + * When an operation uses the POST verb, it uses the `Create` lifecycle visibility to determine which properties + * are visible. + */ + @post create(@body example: Example): Created | Error; + + /** + * When an operation uses the GET verb, it uses the `Read` lifecycle visibility to determine which properties + * are visible. + */ + @get read(@path id: string): Ok | Error; + + /** + * When an operation uses the PATCH verb, it uses the `Update` lifecycle visibility to determine which properties + * are visible. + */ + @patch update(@path id: string, @body example: Example): Ok | Error; +} +``` + +The above interface generates the following OpenAPIv3 schemas: + +```yml +paths: + /example: + post: + parameters: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Example" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Example" + /example/{id}: + get: + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Example" + patch: + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Example" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ExampleUpdate" +components: + schemas: + Example: + type: object + required: + - id + - name + - description + properties: + id: + type: string + readOnly: true + name: + type: string + description: + type: string + ExampleUpdate: + type: object + properties: + description: + type: string +``` + +Notice: + +- The `id` property is marked `readOnly: true` because it is only visible when reading the resource. +- The `ExampleUpdate` schema only includes the `description` property because it is the only property that is visible + when updating the resource. +- Each of the `paths` reference the correct schema based on the lifecycle phase that the operations use. +- The TypeSpec model is only defined _once_, and any changes in the output schemas are derived from the lifecycle + visibility of the properties in the model. + +### Lifecycle visibility transforms + +You can explicitly compute the shape of a model within a _specific_ lifecycle phase by using the four built-in +templates for lifecycle transforms: + +- `Create`: creates a copy of `T` with only the properties that are visible in the `Create` lifecycle + phase, recursively. +- `Read`: creates a copy of `T` with only the properties that are visible in the `Read` lifecycle phase, + recursively. +- `Update`: creates a copy of `T` with only the properties that are visible in the `Update` lifecycle + phase, with the types of the properties set to `CreateOrUpdate`, recursively. +- `CreateOrUpdate`: creates a copy of `T` with only the properties that have _either_ the `Create` or `Update` + visibility modifiers enabled, recursively. + +For example: + +```typespec +model Example { + @visibility(Lifecycle.Create) + id: string; + + @visibility(Lifecycle.Create, Lifecycle.Read) + name: string; + + @visibility(Lifecycle.Update) + description: string; +} + +model ReadExample is Read; + +model CreateExample is Create; + +model UpdateExample is Update; + +model CreateOrUpdateExample is CreateOrUpdate; +``` + +When you use these templates, the resulting models have no `Lifecycle` visibility modifiers applied, so that any +emitters or libraries that use lifecycle visibility will not alter them further. + +## Visibility modifiers + +Each property has its own set of _active visibility modifiers_ for each visibility class. The active modifiers can be +changed using the decorators described in this section. + +**Note**: Changing the visibility for one visibility class _does not_ affect other visibility classes. If you change the +visibility for the `Lifecycle` visibility class, it will not affect the modifiers that are active for _any_ other +visibility classes. + +### `@visibility` + +The `@visibility` decorator _enables_ visibility modifiers. It takes a list of visibility modifiers as arguments and +sets them on the property. For example: + +```typespec +@visibility(Lifecycle.Create, Lifecycle.Read) +name: string; +``` + +In this example, the `name` property has the `Create` and `Read` visibility modifiers enabled. + +If visibility has _already_ been set explicitly on a property, the `@visibility` decorator _ADDS_ its own visibility +modifiers to the currently-active modifiers. It does not _replace_ the existing modifiers. For example: + +```typespec +@visibility(Lifecycle.Create) +@visibility(Lifecycle.Read) +name: string; +``` + +In this example, the `name` property has both the `Create` and `Read` visibility modifiers enabled, but _not_ the `Update` +visibility modifier. The `@visibility` decorator starts from an _empty_ set of modifiers and adds the `Create` modifier, +then adds the `Read` modifier. + +### `@removeVisibility` + +The `@removeVisibility` decorator _disables_ visibility modifiers. It takes a list of visibility modifiers as arguments +and removes them from the property. For example: + +```typespec +@removeVisibility(Lifecycle.Update) +name: string; +``` + +This use of `@removeVisibility` is equivalent to the above examples with the `@visibility` decorator, but it uses the `@removeVisibility` +decorator to remove the `Update` visibility modifier from the `name` property rather than adding the `Create` and `Read` +visibility modifiers. The `@removeVisibility` decorator starts from the _default_ set of visibility modifiers and removes +the `Update` modifier. + +If the visibility has _already_ been set on a property, the `@removeVisibility` decorator _removes_ its visibility from +the currently-active modifiers. It does not _replace_ the existing modifiers. For example: + +```typespec +@removeVisibility(Lifecycle.Update) +@removeVisibility(Lifecycle.Create) +id: string; +``` + +In this example, the `id` property has the `Update` and `Create` visibility modifiers removed, but it retains the `Read` +visibility modifier. + +### `@invisible` + +The `@invisible` decorator _disables all visibility modifiers_ on a property within a given visibility class. For example: + +```typespec +@invisible(Lifecycle) +invisible: string; +``` + +In this example, the `invisible` property has _no_ visibility modifiers enabled in the `Lifecycle` visibility class. + +## Visibility filters + +The `@withVisibilityFilter` decorator allows you to transform a model by applying a visibility filter to it. A +visibility filter is an object that defines constraints on which visibility modifiers must be enabled/disabled for a +property to be visible. For example: + +```typespec +model Example { + @visibility(Lifecycle.Create) + id: string; + + @visibility(Lifecycle.Create, Lifecycle.Read) + name: string; + + @visibility(Lifecycle.Update) + description: string; +} + +@withVisibilityFilter(#{ all: [Lifecycle.Create, Lifecycle.Read] }) +model CreateAndReadExample { + ...Example; +} + +@withVisibilityFilter(#{ any: [Lifecycle.Create, Lifecycle.Update] }) +model CreateOrUpdateExample { + ...Example; +} + +@withVisibilityFilter(#{ none: [Lifecycle.Update] }) +model NonUpdateExample { + ...Example; +} +``` + +In the above example, the `CreateAndReadExample` model is a copy of the `Example` model with only the the properties +that have _BOTH_ the `Create` and `Read` visibility modifiers enabled (i.e. only the `name` property). The +`CreateOrUpdateExample` model is a copy of the `Example` model with only the properties that have _EITHER_ the `Create` +or `Update` visibility modifiers enabled (i.e. the `id` and `name` properties). The `NonUpdateExample` model is a copy +of the `Example` model with only the properties that _do not_ have the `Update` visibility modifier enabled (i.e. the +`id` and `name` properties). + +**Note**: For `Lifecycle` visibility, you should ordinarily use the `Create`, `Read`, `Update`, and `CreateOrUpdate` +templates instead of `@withVisibilityFilter` directly, but you can use `@withVisibilityFilter` to create custom "views" +of a model that use visibility classes other than `Lifecycle` or custom filter logic. + +## Visibility classes + +Any TypeSpec `enum` can serve as a visibility class. The members of the `enum` define the visibility modifiers in the +class. For example, the following is the definition of the `Lifecycle` visibility class defined in the TypeSpec standard +library: + +```typespec +enum Lifecycle { + Create, + Read, + Update, +} +``` + +This visibility class defines three visibility modifiers: `Create`, `Read`, and `Update`. By default, all properties +have _ALL_ three visibilities in the `Lifecycle` enum enabled. + +### Setting default visibility + +You can set the default visibility for a visibility class by declaring it on the enum using the `@defaultVisibility` +decorator: + +```typespec +@defaultVisibility(Example.A) +enum Example { + A, + B, +} +``` + +In this example, any property that does not declare an `Example` visibility modifier will have the `A` visibility by +default. + +**Note**: While you can define your own visibility classes, emitters _will not recognize them_ unless they have been +programmed to do so. You can leverage custom visibility classes in your own emitters, but they will have no effect on +the standard emitters unless those emitters choose to adopt and recognize those visibility classes as meaningful. The +`Lifecycle` visibility class is a standard visibility class that is recognized by several emitters. You can, however, +use your own visibility classes with the built in `@withVisibilityFilter` decorator to transform your models in whatever +ways you see fit. diff --git a/website/src/content/docs/docs/standard-library/built-in-data-types.md b/website/src/content/docs/docs/standard-library/built-in-data-types.md index c167efacde..80aba54b41 100644 --- a/website/src/content/docs/docs/standard-library/built-in-data-types.md +++ b/website/src/content/docs/docs/standard-library/built-in-data-types.md @@ -16,6 +16,78 @@ model Array | Element | The type of the array elements | +#### Properties +None + +### `Create` {#Create} + +A copy of the input model `T` with only the properties that are visible during the +"Create" resource lifecycle phase. + +This transformation is recursive, and will include only properties that have the +`Lifecycle.Create` visibility modifier. + +If a `NameTemplate` is provided, the new model will be named according to the template. +The template uses the same syntax as the `@friendlyName` decorator. +```typespec +model Create +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| T | The model to transform. | +| NameTemplate | The name template to use for the new model.

* | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +model CreateDog is Create; +``` + +#### Properties +None + +### `CreateOrUpdate` {#CreateOrUpdate} + +A copy of the input model `T` with only the properties that are visible during the +"Create" or "Update" resource lifecycle phases. + +This transformation is recursive, and will include only properties that have the +`Lifecycle.Create` or `Lifecycle.Update` visibility modifier. + +If a `NameTemplate` is provided, the new model will be named according to the template. +The template uses the same syntax as the `@friendlyName` decorator. +```typespec +model CreateOrUpdate +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| T | The model to transform. | +| NameTemplate | The name template to use for the new model.

* | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +model CreateOrUpdateDog is CreateOrUpdate; +``` + #### Properties None @@ -141,6 +213,42 @@ model PickProperties | Keys | The property keys to include. | +#### Properties +None + +### `Read` {#Read} + +A copy of the input model `T` with only the properties that are visible during the +"Read" resource lifecycle phase. + +This transformation is recursive, and will include only properties that have the +`Lifecycle.Read` visibility modifier. + +If a `NameTemplate` is provided, the new model will be named according to the template. +The template uses the same syntax as the `@friendlyName` decorator. +```typespec +model Read +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| T | The model to transform. | +| NameTemplate | The name template to use for the new model.

* | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +model ReadDog is Read; +``` + #### Properties None @@ -175,6 +283,43 @@ model ServiceOptions | title? | [`string`](#string) | Title of the service. | | version? | [`string`](#string) | Version of the service. | +### `Update` {#Update} + +A copy of the input model `T` with only the properties that are visible during the +"Update" resource lifecycle phase. + +This transformation will include only the properties that have the `Lifecycle.Update` +visibility modifier, and the types of all properties will be replaced with the +equivalent `CreateOrUpdate` transformation. + +If a `NameTemplate` is provided, the new model will be named according to the template. +The template uses the same syntax as the `@friendlyName` decorator. +```typespec +model Update +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| T | The model to transform. | +| NameTemplate | The name template to use for the new model.

* | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +model UpdateDog is Update; +``` + +#### Properties +None + ### `UpdateableProperties` {#UpdateableProperties} Represents a collection of updateable properties. @@ -191,6 +336,27 @@ model UpdateableProperties #### Properties None +### `VisibilityFilter` {#VisibilityFilter} + +A visibility filter, used to specify which properties should be included when +using the `withVisibilityFilter` decorator. + +The filter matches any property with ALL of the following: +- If the `any` key is present, the property must have at least one of the specified visibilities. +- If the `all` key is present, the property must have all of the specified visibilities. +- If the `none` key is present, the property must have none of the specified visibilities. +```typespec +model VisibilityFilter +``` + + +#### Properties +| Name | Type | Description | +|------|------|-------------| +| any? | `EnumMember[]` | | +| all? | `EnumMember[]` | | +| none? | `EnumMember[]` | | + ### `ArrayEncoding` {#ArrayEncoding} Encoding for serializing arrays @@ -244,6 +410,38 @@ enum DurationKnownEncoding | seconds | `"seconds"` | Encode to integer or float | +### `Lifecycle` {#Lifecycle} + +A visibility class for resource lifecycle phases. + +These visibilities control whether a property is visible during the create, read, and update phases of a resource's +lifecycle. +```typespec +enum Lifecycle +``` + +| Name | Value | Description | +|------|-------|-------------| +| Create | | | +| Read | | | +| Update | | | +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) id: int32; + @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; + name: string; +} +``` + +In this example, the `id` property is only visible during the read phase, and the `secretName` property is only visible +during the create and update phases. This means that the server will return the `id` property when returning a `Dog`, +but the client will not be able to set or update it. In contrast, the `secretName` property can be set when creating +or updating a `Dog`, but the server will never return it. The `name` property has no visibility modifiers and is +therefore visible in all phases. + + ### `boolean` {#boolean} Boolean with `true` and `false` values. diff --git a/website/src/content/docs/docs/standard-library/built-in-decorators.md b/website/src/content/docs/docs/standard-library/built-in-decorators.md index e167e661c9..531888f1ce 100644 --- a/website/src/content/docs/docs/standard-library/built-in-decorators.md +++ b/website/src/content/docs/docs/standard-library/built-in-decorators.md @@ -30,6 +30,29 @@ model Page { ``` +### `@defaultVisibility` {#@defaultVisibility} + +Declares the default visibility modifiers for a visibility class. + +The default modifiers are used when a property does not have any visibility decorators +applied to it. + +The modifiers passed to this decorator _MUST_ be members of the target Enum. +```typespec +@defaultVisibility(...visibilities: valueof EnumMember[]) +``` + +#### Target + +`Enum` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| visibilities | `valueof EnumMember[]` | the list of modifiers to use as the default visibility modifiers. | + + + ### `@deprecated` {#@deprecated} :::caution **Deprecated**: @deprecated decorator is deprecated. Use the `#deprecated` directive instead. @@ -399,6 +422,35 @@ A debugging decorator used to inspect a type name. +### `@invisible` {#@invisible} + +Indicates that a property is not visible in the given visibility class. + +This decorator removes all active visibility modifiers from the property within +the given visibility class. +```typespec +@invisible(visibilityClass: Enum) +``` + +#### Target + +`ModelProperty` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| visibilityClass | `Enum` | The visibility class to make the property invisible within. | + +#### Examples + +```typespec +model Example { + @invisible(Lifecycle) + hidden_property: string; +} +``` + + ### `@key` {#@key} Mark a model property as the key to identify instances of that type @@ -878,7 +930,7 @@ model Page { Sets which visibilities apply to parameters for the given operation. ```typespec -@parameterVisibility(...visibilities: valueof string[]) +@parameterVisibility(...visibilities: valueof string | EnumMember[]) ``` #### Target @@ -888,7 +940,7 @@ Sets which visibilities apply to parameters for the given operation. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibilities | `valueof string[]` | List of visibility strings which apply to this operation. | +| visibilities | `valueof string \| EnumMember[]` | List of visibility strings which apply to this operation. | @@ -986,6 +1038,38 @@ model Certificate { ``` +### `@removeVisibility` {#@removeVisibility} + +Removes visibility modifiers from a property. + +If the visibility modifiers for a visibility class have not been initialized, +this decorator will use the default visibility modifiers for the visibility +class as the default modifier set. +```typespec +@removeVisibility(...visibilities: valueof EnumMember[]) +``` + +#### Target +The property to remove visibility from. +`ModelProperty` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| visibilities | `valueof EnumMember[]` | The visibility modifiers to remove from the target property. | + +#### Examples + +```typespec +model Example { + // This property will have the Create and Update visibilities, but not the + // Read visibility, since it is removed. + @removeVisibility(Lifecycle.Read) + secret_property: string; +} +``` + + ### `@returnsDoc` {#@returnsDoc} Attach a documentation string to describe the successful return types of an operation. @@ -1015,7 +1099,7 @@ op get(): Pet | NotFound; Sets which visibilities apply to the return type for the given operation. ```typespec -@returnTypeVisibility(...visibilities: valueof string[]) +@returnTypeVisibility(...visibilities: valueof string | EnumMember[]) ``` #### Target @@ -1025,7 +1109,7 @@ Sets which visibilities apply to the return type for the given operation. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibilities | `valueof string[]` | List of visibility strings which apply to this operation. | +| visibilities | `valueof string \| EnumMember[]` | List of visibility strings which apply to this operation. | @@ -1149,7 +1233,7 @@ with standard emitters that interpret them as follows: See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) ```typespec -@visibility(...visibilities: valueof string[]) +@visibility(...visibilities: valueof string | EnumMember[]) ``` #### Target @@ -1159,16 +1243,16 @@ See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operati #### Parameters | Name | Type | Description | |------|------|-------------| -| visibilities | `valueof string[]` | List of visibilities which apply to this property. | +| visibilities | `valueof string \| EnumMember[]` | List of visibilities which apply to this property. | #### Examples ```typespec model Dog { // the service will generate an ID, so you don't need to send it. - @visibility("read") id: int32; + @visibility(Lifecycle.Read) id: int32; // the service will store this secret name, but won't ever return it - @visibility("create", "update") secretName: string; + @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; // the regular name is always present name: string; } @@ -1178,8 +1262,18 @@ model Dog { ### `@withDefaultKeyVisibility` {#@withDefaultKeyVisibility} Set the visibility of key properties in a model if not already set. + +This will set the visibility modifiers of all key properties in the model if the visibility is not already _explicitly_ set, +but will not change the visibility of any properties that have visibility set _explicitly_, even if the visibility +is the same as the default visibility. + +Visibility may be explicitly set using any of the following decorators: + +- `@visibility` +- `@removeVisibility` +- `@invisible` ```typespec -@withDefaultKeyVisibility(visibility: valueof string) +@withDefaultKeyVisibility(visibility: valueof string | EnumMember) ``` #### Target @@ -1189,8 +1283,47 @@ Set the visibility of key properties in a model if not already set. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibility | [valueof `string`](#string) | The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. | +| visibility | `valueof string \| EnumMember` | The desired default visibility value. If a key property already has visibility set, it will not be changed. | + + + +### `@withLifecycleUpdate` {#@withLifecycleUpdate} + +Transforms the `target` model to include only properties that are visible during the +"Update" lifecycle phase. + +Any nested models of optional properties will be transformed into the "CreateOrUpdate" +lifecycle phase instead of the "Update" lifecycle phase, so that nested models may be +fully updated. +```typespec +@withLifecycleUpdate +``` + +#### Target +The model to apply the transformation to. +`Model` + +#### Parameters +None + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + @visibility(Lifecycle.Create, Lifecycle.Update) + secretName: string; + + name: string; +} +@withLifecycleUpdate +model DogUpdate { + ...Dog +} +``` ### `@withOptionalProperties` {#@withOptionalProperties} @@ -1289,7 +1422,7 @@ See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operati When using an emitter that applies visibility automatically, it is generally not necessary to use this decorator. ```typespec -@withVisibility(...visibilities: valueof string[]) +@withVisibility(...visibilities: valueof string | EnumMember[]) ``` #### Target @@ -1299,7 +1432,7 @@ not necessary to use this decorator. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibilities | `valueof string[]` | List of visibilities which apply to this property. | +| visibilities | `valueof string \| EnumMember[]` | List of visibilities which apply to this property. | #### Examples @@ -1329,3 +1462,39 @@ model DogRead { } ``` + +### `@withVisibilityFilter` {#@withVisibilityFilter} + +Applies the given visibility filter to the properties of the target model. + +This transformation is recursive, so it will also apply the filter to any nested +or referenced models that are the types of any properties in the `target`. +```typespec +@withVisibilityFilter(filter: valueof VisibilityFilter) +``` + +#### Target +The model to apply the visibility filter to. +`Model` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| filter | [valueof `VisibilityFilter`](./built-in-data-types.md#VisibilityFilter) | The visibility filter to apply to the properties of the target model. | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +@withVisibilityFilter(#{ all: #[Lifecycle.Read] }) +model DogRead { + ...Dog +} +``` +