Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[http/openapi] Use enum-driven visibility analysis APIs #5416

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: internal
packages:
- "@typespec/http"
- "@typespec/openapi3"
witemple-msft marked this conversation as resolved.
Show resolved Hide resolved
---

Updated the OpenAPI3 and HTTP libraries to use the new visibility analysis APIs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Added APIs for getting parameterVisibility and returnTypeVisibility as VisibilityFilter objects.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-server-csharp"
---

Fixed a null visibility constraint when calculating effective type.
103 changes: 101 additions & 2 deletions packages/compiler/lib/std/visibility.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
}

/**
Expand Down Expand Up @@ -306,6 +328,9 @@ model Create<T extends Reflection.Model, NameTemplate extends valueof string = "
* 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.
*
Expand Down Expand Up @@ -337,6 +362,9 @@ model Read<T extends Reflection.Model, NameTemplate extends valueof string = "Re
* 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.
Expand Down Expand Up @@ -369,6 +397,9 @@ model Update<T extends Reflection.Model, NameTemplate extends valueof string = "
* 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.
*
Expand Down Expand Up @@ -398,3 +429,71 @@ model CreateOrUpdate<
> {
...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<Dog>;
* ```
*/
@friendlyName(NameTemplate, T)
@withVisibilityFilter(#{ all: #[Lifecycle.Delete] })
model Delete<T extends Reflection.Model, NameTemplate extends valueof string = "Delete{name}"> {
...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<Dog>;
* ```
*/
@friendlyName(NameTemplate, T)
@withVisibilityFilter(#{ all: #[Lifecycle.Query] })
model Query<T extends Reflection.Model, NameTemplate extends valueof string = "Query{name}"> {
...T;
}
41 changes: 27 additions & 14 deletions packages/compiler/src/core/visibility/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnumMember>;
/**
* 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<EnumMember>;
/**
* 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<EnumMember>;
}
Expand Down Expand Up @@ -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[]) {
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/core/visibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export { getLifecycleVisibilityEnum } from "./lifecycle.js";

export {
VisibilityFilter,
addVisibilityModifiers,
clearVisibilityModifiersForClass,
getVisibility,
Expand Down
8 changes: 8 additions & 0 deletions packages/compiler/src/core/visibility/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -83,6 +87,10 @@ export function normalizeVisibilityToLegacyLifecycleString(
return "read";
case "Update":
return "update";
case "Delete":
return "delete";
case "Query":
return "query";
default:
return undefined;
}
Expand Down
Loading
Loading