From 1672b8317e4ad69c7b29190cb653c50e8d74c591 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 18 Dec 2024 16:52:11 -0500 Subject: [PATCH 1/7] Additional Delete/Query visibilities and filter versions of returntype/parameter visibility decorator metadata. --- packages/compiler/lib/std/visibility.tsp | 103 +++++++++++++++++- .../compiler/src/core/visibility/lifecycle.ts | 8 ++ packages/compiler/src/lib/visibility.ts | 61 +++++++++++ .../test/decorators/visibility.test.ts | 10 +- packages/compiler/test/visibility.test.ts | 2 +- packages/openapi/src/helpers.ts | 9 +- .../standard-library/built-in-data-types.md | 98 ++++++++++++++++- 7 files changed, 278 insertions(+), 13 deletions(-) diff --git a/packages/compiler/lib/std/visibility.tsp b/packages/compiler/lib/std/visibility.tsp index e6b7641f1c..3de21ddf92 100644 --- a/packages/compiler/lib/std/visibility.tsp +++ b/packages/compiler/lib/std/visibility.tsp @@ -176,8 +176,7 @@ 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. + * These visibilities control whether a property is visible during the various phases of a resource's lifecycle. * * @example * ```typespec @@ -195,9 +194,32 @@ extern dec defaultVisibility(target: Enum, ...visibilities: valueof EnumMember[] * therefore visible in all phases. */ enum Lifecycle { + /** + * The property is visible when a resource is being created. + */ Create, + + /** + * The property is visible when a resource is being read. + */ Read, + + /** + * The property is visible when a resource is being updated. + */ Update, + + /** + * The property is visible when a resource is being deleted. + */ + Delete, + + /** + * The property is visible when a resource is being queried. + * + * In HTTP APIs, this visibility applies to parameters of GET or HEAD operations. + */ + Query, } /** @@ -306,6 +328,9 @@ model Create { ...T; } + +/** + * A copy of the input model `T` with only the properties that are visible during the + * "Delete" resource lifecycle phase. + * + * The "Delete" lifecycle phase is used for properties passed as parameters to operations + * that delete data, like HTTP DELETE operations. + * + * This transformation is recursive, and will include only properties that have the + * `Lifecycle.Delete` 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 DeleteDog is Delete; + * ``` + */ +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ all: #[Lifecycle.Delete] }) +model Delete { + ...T; +} + +/** + * A copy of the input model `T` with only the properties that are visible during the + * "Query" resource lifecycle phase. + * + * The "Query" lifecycle phase is used for properties passed as parameters to operations + * that read data, like HTTP GET or HEAD operations. + * + * This transformation is recursive, and will include only properties that have the + * `Lifecycle.Query` 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 QueryDog is Query; + * ``` + */ +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ all: #[Lifecycle.Query] }) +model Query { + ...T; +} diff --git a/packages/compiler/src/core/visibility/lifecycle.ts b/packages/compiler/src/core/visibility/lifecycle.ts index 0d6addf627..2660c14d17 100644 --- a/packages/compiler/src/core/visibility/lifecycle.ts +++ b/packages/compiler/src/core/visibility/lifecycle.ts @@ -54,6 +54,10 @@ export function normalizeLegacyLifecycleVisibilityString( return lifecycle.members.get("Read")!; case "update": return lifecycle.members.get("Update")!; + case "delete": + return lifecycle.members.get("Delete")!; + case "query": + return lifecycle.members.get("Query")!; default: return undefined; } @@ -83,6 +87,10 @@ export function normalizeVisibilityToLegacyLifecycleString( return "read"; case "Update": return "update"; + case "Delete": + return "delete"; + case "Query": + return "query"; default: return undefined; } diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 5d8f8245a8..51ef36ef8e 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -46,6 +46,7 @@ import { } from "../core/visibility/core.js"; import { getLifecycleVisibilityEnum, + normalizeLegacyLifecycleVisibilityString, normalizeVisibilityToLegacyLifecycleString, } from "../core/visibility/lifecycle.js"; import { isMutableType, mutateSubgraph, Mutator, MutatorFlow } from "../experimental/mutators.js"; @@ -180,6 +181,9 @@ export const $parameterVisibility: ParameterVisibilityDecorator = ( /** * Returns the visibilities of the parameters of the given operation, if provided with `@parameterVisibility`. * + * @deprecated Use `getParameterVisibilityFilter` instead. + * + * @see {@link getParameterVisibilityFilter} * @see {@link $parameterVisibility} */ export function getParameterVisibility(program: Program, entity: Operation): string[] | undefined { @@ -190,6 +194,33 @@ export function getParameterVisibility(program: Program, entity: Operation): str .filter((p) => !!p) as string[]; } +/** + * Get the visibility filter that should apply to the parameters of the given operation, or `undefined` if no parameter + * visibility is set. + * + * @param program - the Program in which the operation is defined + * @param operation - the Operation to get the parameter visibility filter for + * @returns a visibility filter for the parameters of the operation, or `undefined` if no parameter visibility is set + */ +export function getParameterVisibilityFilter( + program: Program, + operation: Operation, +): VisibilityFilter | undefined { + const visibilityConfig = getOperationVisibilityConfig(program, operation); + + if (!visibilityConfig.parameters) return undefined; + + return { + any: new Set( + visibilityConfig.parameters + .map((v) => + typeof v === "string" ? normalizeLegacyLifecycleVisibilityString(program, v) : v, + ) + .filter((v) => !!v), + ), + }; +} + export const $returnTypeVisibility: ReturnTypeVisibilityDecorator = ( context: DecoratorContext, operation: Operation, @@ -218,6 +249,9 @@ export const $returnTypeVisibility: ReturnTypeVisibilityDecorator = ( /** * Returns the visibilities of the return type of the given operation, if provided with `@returnTypeVisibility`. * + * @deprecated Use `getReturnTypeVisibilityFilter` instead. + * + * @see {@link getReturnTypeVisibilityFilter} * @see {@link $returnTypeVisibility} */ export function getReturnTypeVisibility(program: Program, entity: Operation): string[] | undefined { @@ -228,6 +262,33 @@ export function getReturnTypeVisibility(program: Program, entity: Operation): st .filter((p) => !!p) as string[]; } +/** + * Get the visibility filter that should apply to the return type of the given operation, or `undefined` if no return + * type visibility is set. + * + * @param program - the Program in which the operation is defined + * @param operation - the Operation to get the return type visibility filter for + * @returns a visibility filter for the return type of the operation, or `undefined` if no return type visibility is set + */ +export function getReturnTypeVisibilityFilter( + program: Program, + operation: Operation, +): VisibilityFilter | undefined { + const visibilityConfig = getOperationVisibilityConfig(program, operation); + + if (!visibilityConfig.returnType) return undefined; + + return { + any: new Set( + visibilityConfig.returnType + .map((v) => + typeof v === "string" ? normalizeLegacyLifecycleVisibilityString(program, v) : v, + ) + .filter((v) => !!v), + ), + }; +} + // #endregion // #region Core Visibility Decorators diff --git a/packages/compiler/test/decorators/visibility.test.ts b/packages/compiler/test/decorators/visibility.test.ts index af06ede963..737d33427e 100644 --- a/packages/compiler/test/decorators/visibility.test.ts +++ b/packages/compiler/test/decorators/visibility.test.ts @@ -43,11 +43,19 @@ describe("visibility", function () { Read: LifecycleEnum.members.get("Read")!, Create: LifecycleEnum.members.get("Create")!, Update: LifecycleEnum.members.get("Update")!, + Delete: LifecycleEnum.members.get("Delete")!, + Query: LifecycleEnum.members.get("Query")!, }; assertSetsEqual( getVisibilityForClass(runner.program, name, LifecycleEnum), - new Set([Lifecycle.Read, Lifecycle.Create, Lifecycle.Update]), + new Set([ + Lifecycle.Read, + Lifecycle.Create, + Lifecycle.Update, + Lifecycle.Delete, + Lifecycle.Query, + ]), ); assertSetsEqual( diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 837464c1fe..b15d1a3fa9 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -299,7 +299,7 @@ describe("compiler: visibility core", () => { const resetVisibility = getVisibilityForClass(runner.program, x, Lifecycle); - strictEqual(resetVisibility.size, 3); + strictEqual(resetVisibility.size, 5); for (const member of Lifecycle.members.values()) { ok(resetVisibility.has(member)); diff --git a/packages/openapi/src/helpers.ts b/packages/openapi/src/helpers.ts index 411de4eceb..e49a056608 100644 --- a/packages/openapi/src/helpers.ts +++ b/packages/openapi/src/helpers.ts @@ -2,9 +2,10 @@ import { Diagnostic, DiagnosticTarget, getFriendlyName, + getLifecycleVisibilityEnum, getProperty, getTypeName, - getVisibility, + getVisibilityForClass, isGlobalNamespace, isService, isTemplateInstance, @@ -164,11 +165,11 @@ 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); + const Lifecycle = getLifecycleVisibilityEnum(program); + const visibility = getVisibilityForClass(program, property, getLifecycleVisibilityEnum(program)); // note: multiple visibilities that include read are not handled using // readonly: true, but using separate schemas. - return visibility?.length === 1 && visibility[0] === "read"; + return visibility.size === 1 && visibility.has(Lifecycle.members.get("Read")!); } /** 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 80aba54b41..5eb71cd6be 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 @@ -60,6 +60,9 @@ None A copy of the input model `T` with only the properties that are visible during the "Create" or "Update" resource lifecycle phases. +The "CreateOrUpdate" lifecycle phase is used by default for properties passed as parameters to operations +that can create _or_ update data, like HTTP PUT operations. + This transformation is recursive, and will include only properties that have the `Lifecycle.Create` or `Lifecycle.Update` visibility modifier. @@ -105,6 +108,45 @@ model DefaultKeyVisibility | Visibility | The visibility to apply to all properties. | +#### Properties +None + +### `Delete` {#Delete} + +A copy of the input model `T` with only the properties that are visible during the +"Delete" resource lifecycle phase. + +The "Delete" lifecycle phase is used for properties passed as parameters to operations +that delete data, like HTTP DELETE operations. + +This transformation is recursive, and will include only properties that have the +`Lifecycle.Delete` 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 Delete +``` + +#### 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 DeleteDog is Delete; +``` + #### Properties None @@ -213,6 +255,45 @@ model PickProperties | Keys | The property keys to include. | +#### Properties +None + +### `Query` {#Query} + +A copy of the input model `T` with only the properties that are visible during the +"Query" resource lifecycle phase. + +The "Query" lifecycle phase is used for properties passed as parameters to operations +that read data, like HTTP GET or HEAD operations. + +This transformation is recursive, and will include only properties that have the +`Lifecycle.Query` 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 Query +``` + +#### 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 QueryDog is Query; +``` + #### Properties None @@ -221,6 +302,9 @@ None A copy of the input model `T` with only the properties that are visible during the "Read" resource lifecycle phase. +The "Read" lifecycle phase is used for properties returned by operations that read data, like +HTTP GET operations. + This transformation is recursive, and will include only properties that have the `Lifecycle.Read` visibility modifier. @@ -288,6 +372,9 @@ model ServiceOptions A copy of the input model `T` with only the properties that are visible during the "Update" resource lifecycle phase. +The "Update" lifecycle phase is used for properties passed as parameters to operations +that update data, like HTTP PATCH operations. + 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. @@ -414,17 +501,18 @@ enum DurationKnownEncoding 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. +These visibilities control whether a property is visible during the various phases of a resource's lifecycle. ```typespec enum Lifecycle ``` | Name | Value | Description | |------|-------|-------------| -| Create | | | -| Read | | | -| Update | | | +| Create | | The property is visible when a resource is being created. | +| Read | | The property is visible when a resource is being read. | +| Update | | The property is visible when a resource is being updated. | +| Delete | | The property is visible when a resource is being deleted. | +| Query | | The property is visible when a resource is being queried.

In HTTP APIs, this visibility applies to parameters of GET or HEAD operations. | #### Examples ```typespec From 3721fa3c70c08cc5217aded39845cee3dceeb267 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 19 Dec 2024 00:45:39 -0500 Subject: [PATCH 2/7] Rework HTTP to use new visibility APIs --- packages/compiler/src/core/visibility/core.ts | 41 ++-- .../compiler/src/core/visibility/index.ts | 1 + packages/compiler/src/lib/visibility.ts | 56 ++++- packages/compiler/test/visibility.test.ts | 80 +++++++ packages/http/src/metadata.ts | 224 +++++++++++++++--- 5 files changed, 347 insertions(+), 55 deletions(-) diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index 796705a878..b68ebd5809 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -603,18 +603,31 @@ export function hasVisibility( * AND * * - NONE of the visibilities in the `none` set. + * + * Note: The constraints behave similarly to the `every` and `some` methods of the Array prototype in JavaScript. If the + * `any` constraint is set to an empty set, it will _NEVER_ be satisfied (similarly, `Array.prototype.some` will always + * return `false` for an empty array). If the `none` constraint is set to an empty set, it will _ALWAYS_ be satisfied. + * If the `all` constraint is set to an empty set, it will be satisfied (similarly, `Array.prototype.every` will always + * return `true` for an empty array). + * */ export interface VisibilityFilter { /** * If set, the filter considers a property visible if it has ALL of these visibility modifiers. + * + * If this set is empty, the filter will be satisfied if the other constraints are satisfied. */ all?: Set; /** * If set, the filter considers a property visible if it has ANY of these visibility modifiers. + * + * If this set is empty, the filter will _NEVER_ be satisfied. */ any?: Set; /** * If set, the filter considers a property visible if it has NONE of these visibility modifiers. + * + * If this set is empty, the filter will be satisfied if the other constraints are satisfied. */ none?: Set; } @@ -690,30 +703,30 @@ export function isVisible( return isVisibleLegacy(_filterOrLegacyVisibilities); } - const filter = { ...(_filterOrLegacyVisibilities as VisibilityFilter) }; - filter.all ??= new Set(); - filter.any ??= new Set(); - filter.none ??= new Set(); + const filter = _filterOrLegacyVisibilities as VisibilityFilter; // 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; + if (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) { + // Validate that property has NONE of the excluded visibilities of filter.none + if (filter.none) { + for (const modifier of filter.none) { + if (hasVisibility(program, property, modifier)) return false; + } + } + + if (filter.any) { for (const modifier of filter.any) { - if (hasVisibility(program, property, modifier)) break outer; + if (hasVisibility(program, property, modifier)) return true; } 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[]) { diff --git a/packages/compiler/src/core/visibility/index.ts b/packages/compiler/src/core/visibility/index.ts index 97ae71a0f7..2f4ac16c60 100644 --- a/packages/compiler/src/core/visibility/index.ts +++ b/packages/compiler/src/core/visibility/index.ts @@ -4,6 +4,7 @@ export { getLifecycleVisibilityEnum } from "./lifecycle.js"; export { + VisibilityFilter, addVisibilityModifiers, clearVisibilityModifiersForClass, getVisibility, diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 51ef36ef8e..03010ea36a 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -194,25 +194,62 @@ export function getParameterVisibility(program: Program, entity: Operation): str .filter((p) => !!p) as string[]; } +/** + * A context-specific provider for visibility information that applies when parameter or return type visibility + * constraints are not explicitly specified. Visibility providers are provided by libraries that define implied + * visibility semantics, such as `@typespec/http`. + * + * If you are not working in a protocol that has specific visibility semantics, you can use the + * {@link EmptyVisibilityProvider} from this package as a default provider. It will consider all properties visible by + * default unless otherwise explicitly specified. + */ +export interface VisibilityProvider { + parameters(program: Program, operation: Operation): VisibilityFilter; + returnType(program: Program, operation: Operation): VisibilityFilter; +} + +/** + * An empty visibility provider. This provider returns an empty filter that considers all properties visible. This filter + * is used when no context-specific visibility provider is available. + * + * When working with an HTTP specification, use the `HttpVisibilityProvider` from the `@typespec/http` library instead. + */ +export const EmptyVisibilityProvider: VisibilityProvider = { + parameters: () => ({}), + returnType: () => ({}), +}; + /** * Get the visibility filter that should apply to the parameters of the given operation, or `undefined` if no parameter * visibility is set. * + * If you are not working in a protocol that has specific implicit visibility semantics, you can use the + * {@link EmptyVisibilityProvider} as a default provider. If you working in a protocol or context where parameters have + * implicit visibility transformations (like HTTP), you should use the visibility provider from that library (for HTTP, + * use the `HttpVisibilityProvider` from the `@typespec/http` library). + * * @param program - the Program in which the operation is defined * @param operation - the Operation to get the parameter visibility filter for + * @param defaultProvider - a provider for visibility filters that apply when no visibility constraints are explicitly + * set. Defaults to an empty provider that returns an empty filter if not provided. * @returns a visibility filter for the parameters of the operation, or `undefined` if no parameter visibility is set */ export function getParameterVisibilityFilter( program: Program, operation: Operation, -): VisibilityFilter | undefined { - const visibilityConfig = getOperationVisibilityConfig(program, operation); + defaultProvider: VisibilityProvider, +): VisibilityFilter { + const operationVisibilityConfig = getOperationVisibilityConfig(program, operation); - if (!visibilityConfig.parameters) return undefined; + if (!operationVisibilityConfig.parameters) return defaultProvider.parameters(program, operation); return { + // WARNING: the HTTP library depends on `any` being the only key in the filter object returned by this method. + // if you change this logic, you will need to update the HTTP library to account for differences in the + // returned object. HTTP does not currently have a way to express `all` or `none` constraints in the same + // way that the core visibility system does. any: new Set( - visibilityConfig.parameters + operationVisibilityConfig.parameters .map((v) => typeof v === "string" ? normalizeLegacyLifecycleVisibilityString(program, v) : v, ) @@ -268,17 +305,24 @@ export function getReturnTypeVisibility(program: Program, entity: Operation): st * * @param program - the Program in which the operation is defined * @param operation - the Operation to get the return type visibility filter for + * @param defaultProvider - a provider for visibility filters that apply when no visibility constraints are explicitly + * set. Defaults to an empty provider that returns an empty filter if not provided. * @returns a visibility filter for the return type of the operation, or `undefined` if no return type visibility is set */ export function getReturnTypeVisibilityFilter( program: Program, operation: Operation, -): VisibilityFilter | undefined { + defaultProvider: VisibilityProvider, +): VisibilityFilter { const visibilityConfig = getOperationVisibilityConfig(program, operation); - if (!visibilityConfig.returnType) return undefined; + if (!visibilityConfig.returnType) return defaultProvider.returnType(program, operation); return { + // WARNING: the HTTP library depends on `any` being the only key in the filter object returned by this method. + // if you change this logic, you will need to update the HTTP library to account for differences in the + // returned object. HTTP does not currently have a way to express `all` or `none` constraints in the same + // way that the core visibility system does. any: new Set( visibilityConfig.returnType .map((v) => diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index b15d1a3fa9..987a60d184 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -9,14 +9,17 @@ import { addVisibilityModifiers, clearVisibilityModifiersForClass, DecoratorContext, + EmptyVisibilityProvider, Enum, getLifecycleVisibilityEnum, + getParameterVisibilityFilter, getVisibilityForClass, hasVisibility, isSealed, isVisible, Model, ModelProperty, + Operation, removeVisibilityModifiers, resetVisibilityModifiersForClass, sealVisibilityModifiers, @@ -569,6 +572,83 @@ describe("compiler: visibility core", () => { true, ); }); + + describe("parameter visibility filters", () => { + it("correctly provides empty default visibility filter", async () => { + const { Example, foo } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create) + x: string; + } + + @test op foo(example: Example): void; + `)) as { Example: Model; foo: Operation }; + + const x = Example.properties.get("x")!; + + const filter = getParameterVisibilityFilter(runner.program, foo, EmptyVisibilityProvider); + + strictEqual(filter.all, undefined); + strictEqual(filter.any, undefined); + strictEqual(filter.none, undefined); + + strictEqual(isVisible(runner.program, x, filter), true); + }); + + it("correctly provides visibility filter from operation", async () => { + const { Example, foo } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create) + x: string; + } + + @parameterVisibility(Lifecycle.Update) + @test op foo( + example: Example + ): void; + `)) as { Example: Model; foo: Operation }; + + const x = Example.properties.get("x")!; + + const filter = getParameterVisibilityFilter(runner.program, foo, EmptyVisibilityProvider); + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + strictEqual(filter.all, undefined); + strictEqual(filter.any?.size, 1); + strictEqual(filter.any.has(Lifecycle.members.get("Update")!), true); + strictEqual(filter.none, undefined); + + strictEqual(isVisible(runner.program, x, filter), false); + }); + + it("correctly coerces legacy string in visibility filter", async () => { + const { Example, foo } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create) + x: string; + } + + @parameterVisibility("update") + @test op foo( + example: Example + ): void; + `)) as { Example: Model; foo: Operation }; + + const x = Example.properties.get("x")!; + + const filter = getParameterVisibilityFilter(runner.program, foo, EmptyVisibilityProvider); + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + strictEqual(filter.all, undefined); + strictEqual(filter.any?.size, 1); + strictEqual(filter.any.has(Lifecycle.members.get("Update")!), true); + strictEqual(filter.none, undefined); + + strictEqual(isVisible(runner.program, x, filter), false); + }); + }); }); describe("legacy compatibility", () => { diff --git a/packages/http/src/metadata.ts b/packages/http/src/metadata.ts index 2f78981574..c092bdf548 100644 --- a/packages/http/src/metadata.ts +++ b/packages/http/src/metadata.ts @@ -1,7 +1,9 @@ import { compilerAssert, + EnumMember, getEffectiveModelType, - getParameterVisibility, + getLifecycleVisibilityEnum, + getParameterVisibilityFilter, isVisible as isVisibleCore, Model, ModelProperty, @@ -9,9 +11,12 @@ import { Program, Type, Union, + type VisibilityFilter, + VisibilityProvider, } from "@typespec/compiler"; import { TwoLevelMap } from "@typespec/compiler/utils"; import { + getOperationVerb, includeInapplicableMetadataInPayload, isBody, isBodyIgnore, @@ -23,7 +28,7 @@ import { isQueryParam, isStatusCode, } from "./decorators.js"; -import { HttpVerb } from "./types.js"; +import { HttpVerb, OperationParameterOptions } from "./types.js"; /** * Flags enum representation of well-known visibilities that are used in @@ -84,33 +89,117 @@ function visibilityToArray(visibility: Visibility): readonly string[] { return result; } -function arrayToVisibility(array: readonly string[] | undefined): Visibility | undefined { - if (!array) { - return undefined; - } + +function filterToVisibility(program: Program, filter: VisibilityFilter): Visibility { + const Lifecycle = getLifecycleVisibilityEnum(program); let value = Visibility.None; - for (const item of array) { - switch (item) { - case "read": - value |= Visibility.Read; - break; - case "create": - value |= Visibility.Create; - break; - case "update": - value |= Visibility.Update; - break; - case "delete": - value |= Visibility.Delete; - break; - case "query": - value |= Visibility.Query; - break; - default: - return undefined; + + compilerAssert( + !filter.all, + "Unexpected: `all` constraint in visibility filter passed to filterToVisibility", + ); + compilerAssert( + !filter.none, + "Unexpected: `none` constraint in visibility filter passed to filterToVisibility", + ); + + if (filter.any) { + for (const item of filter.any ?? []) { + if (item.enum !== Lifecycle) continue; + switch (item.name) { + case "Read": + value |= Visibility.Read; + break; + case "Create": + value |= Visibility.Create; + break; + case "Update": + value |= Visibility.Update; + break; + case "Delete": + value |= Visibility.Delete; + break; + case "Query": + value |= Visibility.Query; + break; + default: + compilerAssert( + false, + `Unreachable: unrecognized Lifecycle visibility member: '${item.name}'`, + ); + } } + + return value; + } else { + return Visibility.All; } - return value; +} + +const VISIBILITY_FILTER_CACHE_MAP = new WeakMap>(); + +function getVisibilityFilterCache(program: Program): Map { + let cache = VISIBILITY_FILTER_CACHE_MAP.get(program); + if (!cache) { + cache = new Map(); + VISIBILITY_FILTER_CACHE_MAP.set(program, cache); + } + return cache; +} + +/** + * Convert an HTTP visibility to a visibility filter that can be used to test core visibility and applied to a model. + * + * The Item and Patch visibility flags are ignored. + * + * @param program - the Program we're working in + * @param visibility - the visibility to convert to a filter + * @returns a VisibilityFilter object that selects properties having any of the given visibility flags + */ +function visibilityToFilter(program: Program, visibility: Visibility): VisibilityFilter { + const cache = getVisibilityFilterCache(program); + let filter = cache.get(visibility); + + if (!filter) { + // Item and Patch flags are not real visibilities. + visibility &= ~(Visibility.Item | Visibility.Patch); + + const LifecycleEnum = getLifecycleVisibilityEnum(program); + + const Lifecycle = { + Create: LifecycleEnum.members.get("Create")!, + Read: LifecycleEnum.members.get("Read")!, + Update: LifecycleEnum.members.get("Update")!, + Delete: LifecycleEnum.members.get("Delete")!, + Query: LifecycleEnum.members.get("Query")!, + } as const; + + const any = new Set(); + + if (visibility & Visibility.Read) { + any.add(Lifecycle.Read); + } + if (visibility & Visibility.Create) { + any.add(Lifecycle.Create); + } + if (visibility & Visibility.Update) { + any.add(Lifecycle.Update); + } + if (visibility & Visibility.Delete) { + any.add(Lifecycle.Delete); + } + if (visibility & Visibility.Query) { + any.add(Lifecycle.Query); + } + + compilerAssert(any.size > 0 || visibility === Visibility.None, "invalid visibility"); + + filter = { any }; + + cache.set(visibility, filter); + } + + return filter; } /** @@ -130,7 +219,7 @@ function arrayToVisibility(array: readonly string[] | undefined): Visibility | u * */ export function getVisibilitySuffix( visibility: Visibility, - canonicalVisibility: Visibility | undefined = Visibility.None, + canonicalVisibility: Visibility = Visibility.None, ) { let suffix = ""; @@ -168,8 +257,7 @@ function getDefaultVisibilityForVerb(verb: HttpVerb): Visibility { case "delete": return Visibility.Delete; default: - const _assertNever: never = verb; - compilerAssert(false, "unreachable"); + compilerAssert(false, `Unreachable: unrecognized HTTP verb: '${verb satisfies never}'`); } } @@ -195,6 +283,71 @@ export function getRequestVisibility(verb: HttpVerb): Visibility { return visibility; } +/** + * A visibility provider for HTTP operations. Pass this value as a provider to the `getParameterVisibilityFilter` and + * `getReturnTypeVisibilityFilter` functions in the TypeSpec core to get the applicable parameter and return type + * visibility filters for an HTTP operation. + * + * When created with a verb, this provider will use the default visibility for that verb. + * + * @param verb - the HTTP verb for the operation + * + * @see {@link VisibilityProvider} + * @see {@link getParameterVisibilityFilter} + * @see {@link getReturnTypeVisibilityFilter} + */ +export function HttpVisibilityProvider(verb: HttpVerb): VisibilityProvider; +/** + * A visibility provider for HTTP operations. Pass this value as a provider to the `getParameterVisibilityFilter` and + * `getReturnTypeVisibilityFilter` functions in the TypeSpec core to get the applicable parameter and return type + * visibility filters for an HTTP operation. + * + * When created with an options object, this provider will use the `verbSelector` function to determine the verb for the + * operation and use the default visibility for that verb, or the configured HTTP verb for the operation, and finally + * the GET verb if the verbSelector function is not defined and no HTTP verb is configured. + * + * @param options - an options object with a `verbSelector` function that returns the HTTP verb for the operation + * + * @see {@link VisibilityProvider} + * @see {@link getParameterVisibilityFilter} + * @see {@link getReturnTypeVisibilityFilter} + */ +export function HttpVisibilityProvider(options: OperationParameterOptions): VisibilityProvider; +/** + * A visibility provider for HTTP operations. Pass this value as a provider to the `getParameterVisibilityFilter` and + * `getReturnTypeVisibilityFilter` functions in the TypeSpec core to get the applicable parameter and return type + * visibility filters for an HTTP operation. + * + * When created without any arguments, this provider will use the configured verb for the operation or the GET verb if + * no HTTP verb is configured and use the default visibility for that selected verb. + * + * @see {@link VisibilityProvider} + * @see {@link getParameterVisibilityFilter} + * @see {@link getReturnTypeVisibilityFilter} + */ +export function HttpVisibilityProvider(): VisibilityProvider; +export function HttpVisibilityProvider( + verbOrParameterOptions?: HttpVerb | OperationParameterOptions, +): VisibilityProvider { + const hasVerb = typeof verbOrParameterOptions === "string"; + + return { + parameters: (program, operation) => { + const verb = hasVerb + ? (verbOrParameterOptions as HttpVerb) + : (verbOrParameterOptions?.verbSelector?.(program, operation) ?? + getOperationVerb(program, operation) ?? + "get"); + return visibilityToFilter(program, getDefaultVisibilityForVerb(verb)); + }, + returnType: (program, _) => { + const Read = getLifecycleVisibilityEnum(program).members.get("Read")!; + // For return types, we always use Read visibility in HTTP. + return { any: new Set([Read]) }; + }, + }; +} + /** * Returns the applicable parameter visibility or visibilities for the request if `@requestVisibility` was used. * Otherwise, returns the default visibility based on the HTTP verb for the operation. @@ -207,10 +360,12 @@ export function resolveRequestVisibility( operation: Operation, verb: HttpVerb, ): Visibility { - const parameterVisibility = getParameterVisibility(program, operation); - const parameterVisibilityArray = arrayToVisibility(parameterVisibility); - const defaultVisibility = getDefaultVisibilityForVerb(verb); - let visibility = parameterVisibilityArray ?? defaultVisibility; + const parameterVisibilityFilter = getParameterVisibilityFilter( + program, + operation, + HttpVisibilityProvider(verb), + ); + let visibility = filterToVisibility(program, parameterVisibilityFilter); // 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") { @@ -237,8 +392,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)); + return isVisibleCore(program, property, visibilityToFilter(program, visibility)); } /** From a0b2361a46745e420cebfe67aab04ff3ab614e0b Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 19 Dec 2024 01:04:27 -0500 Subject: [PATCH 3/7] [hsc] fix a null visibility constraint in effective type calculation --- packages/http-server-csharp/src/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-server-csharp/src/service.ts b/packages/http-server-csharp/src/service.ts index 2eefa32289..537e224c77 100644 --- a/packages/http-server-csharp/src/service.ts +++ b/packages/http-server-csharp/src/service.ts @@ -811,7 +811,7 @@ export async function $onEmit(context: EmitContext) code`${this.emitter.emitTypeReference( this.#metaInfo.getEffectivePayloadType( bodyParam.type, - Visibility.Create & Visibility.Update, + Visibility.Create | Visibility.Update, ), )} body`, ); From 42242b2d273ae08e73cb30da51fd661d72d8dfb5 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 19 Dec 2024 13:37:03 -0500 Subject: [PATCH 4/7] Make fallback HttpVisibilityProvider logic more accurate --- packages/http/src/metadata.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/http/src/metadata.ts b/packages/http/src/metadata.ts index c092bdf548..0194060d0d 100644 --- a/packages/http/src/metadata.ts +++ b/packages/http/src/metadata.ts @@ -28,6 +28,7 @@ import { isQueryParam, isStatusCode, } from "./decorators.js"; +import { getHttpOperation } from "./operations.js"; import { HttpVerb, OperationParameterOptions } from "./types.js"; /** @@ -333,11 +334,17 @@ export function HttpVisibilityProvider( return { parameters: (program, operation) => { - const verb = hasVerb + let verb = hasVerb ? (verbOrParameterOptions as HttpVerb) : (verbOrParameterOptions?.verbSelector?.(program, operation) ?? - getOperationVerb(program, operation) ?? - "get"); + getOperationVerb(program, operation)); + + if (!verb) { + const [httpOperation] = getHttpOperation(program, operation); + + verb = httpOperation.verb; + } + return visibilityToFilter(program, getDefaultVisibilityForVerb(verb)); }, returnType: (program, _) => { @@ -360,6 +367,10 @@ export function resolveRequestVisibility( operation: Operation, verb: HttpVerb, ): Visibility { + // WARNING: This is the only place where we call HttpVisibilityProvider _WITHIN_ the HTTP implementation itself. We + // _must_ provide the verb directly to the function as the first argument. If the verb is not provided directly, the + // provider calls getHttpOperation to resolve the verb. Since the current function is called from getHttpOperation, it + // will cause a stack overflow if the version of HttpVisibilityProvider we use here has to resolve the verb itself. const parameterVisibilityFilter = getParameterVisibilityFilter( program, operation, From d8516850988db272854a096cf2b991c9a8bd1534 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 19 Dec 2024 13:37:17 -0500 Subject: [PATCH 5/7] Chronus --- ...itemple-msft-http-visibility-enum-2024-11-19-13-8-4.md | 8 ++++++++ ...temple-msft-http-visibility-enum-2024-11-19-13-8-52.md | 7 +++++++ ...temple-msft-http-visibility-enum-2024-11-19-13-9-45.md | 7 +++++++ 3 files changed, 22 insertions(+) create mode 100644 .chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md create mode 100644 .chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-52.md create mode 100644 .chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-9-45.md diff --git a/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md b/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md new file mode 100644 index 0000000000..eb762a58a8 --- /dev/null +++ b/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md @@ -0,0 +1,8 @@ +--- +changeKind: internal +packages: + - "@typespec/http" + - "@typespec/openapi3" +--- + +Updated the OpenAPI3 and HTTP libraries to use the new visibility analysis APIs. \ No newline at end of file diff --git a/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-52.md b/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-52.md new file mode 100644 index 0000000000..7efb81f915 --- /dev/null +++ b/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-52.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Added APIs for getting parameterVisibility and returnTypeVisibility as VisibilityFilter objects. \ No newline at end of file diff --git a/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-9-45.md b/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-9-45.md new file mode 100644 index 0000000000..4f9e22eb7b --- /dev/null +++ b/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-9-45.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-server-csharp" +--- + +Fixed a null visibility constraint when calculating effective type. \ No newline at end of file From be4057f0f43416d362b11aa8cfab7d992e8f1f19 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 19 Dec 2024 13:53:42 -0500 Subject: [PATCH 6/7] Update .chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md --- .../witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md b/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md index eb762a58a8..0a004cf70e 100644 --- a/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md +++ b/.chronus/changes/witemple-msft-http-visibility-enum-2024-11-19-13-8-4.md @@ -2,7 +2,7 @@ changeKind: internal packages: - "@typespec/http" - - "@typespec/openapi3" + - "@typespec/openapi" --- Updated the OpenAPI3 and HTTP libraries to use the new visibility analysis APIs. \ No newline at end of file From f03067da72d0b7d71933c581f5036f6cf7fffd18 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 19 Dec 2024 14:47:07 -0500 Subject: [PATCH 7/7] Docs --- .../docs/docs/language-basics/visibility.md | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/website/src/content/docs/docs/language-basics/visibility.md b/website/src/content/docs/docs/language-basics/visibility.md index dd523a840d..18f44970ef 100644 --- a/website/src/content/docs/docs/language-basics/visibility.md +++ b/website/src/content/docs/docs/language-basics/visibility.md @@ -7,6 +7,11 @@ title: Visibility 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. +**Note** ⚠️: Enum-based visibility as described in this document _replaces_ visibility strings that you may have used +in the past. The system is backwards-compatible with visibility strings, but you should use enum-based visibility for +new specifications. String-based visibility (e.g. `@visibility("create")`) may be deprecated and removed in future +versions of TypeSpec. + ## Basic concepts - Visibility applies to _model properties_ only. It is used to determine when an emitter should include or exclude a @@ -19,7 +24,7 @@ of a model within different operations or contexts. ## 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: +whether properties are visible when passing a resource to or reading a resource from an API endpoint. For example: ```typespec model Example { @@ -43,8 +48,8 @@ model Example { /** * 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. + * By default, properties are visible in all lifecycle phases, so this property + * is present in all lifecycle phases. */ description: string; } @@ -166,6 +171,22 @@ Notice: - 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 modifiers + +The following visibility modifiers are available in the `Lifecycle` visibility class: + +- `Create`: The property is visible when the resource is created. This visibility is checked, for example, when a property + is a parameter in an HTTP `POST` operation. +- `Read`: The property is visible when the resource is read. This visibility is checked, for example, when a property is + returned in an HTTP `GET` operation. +- `Update`: The property is visible when the resource is updated. This visibility is checked, for example, when a property + is a parameter in an HTTP `PATCH` or `PUT` operation. +- `Delete`: The property is visible when a resource is deleted. This visibility is checked, for example, when a property + is a parameter in an HTTP `DELETE` operation. +- `Query`: The property is visible when a resource is passed as a parameter in a query. This visibility is checked, for + example, when a property is a parameter in an HTTP `GET` operation (**this should not be confused with an HTTP query + parameter defined using `@query`**). + ### Lifecycle visibility transforms You can explicitly compute the shape of a model within a _specific_ lifecycle phase by using the four built-in @@ -179,6 +200,10 @@ templates for lifecycle transforms: 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. +- `Delete`: creates a copy of `T` with only the properties that have the `Lifecycle.Delete` modifier enabled, + recursively. +- `Query`: creates a copy of `T` with only the properties that have the `Lifecycle.Query` modifier enabled, + recursively. For example: