Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: internal
packages:
- "@typespec/compiler"
- "@typespec/http"
---

Replaced visibility and merge-patch transforms with invocations of `internal` functions for greater accuracy.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

Fixed a bug that would prevent template parameters from assigning to values in some cases.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Added a new template `FilterVisibility` to support more accurate visibility transforms. This replaces the `@withVisibilityFilter` decorator, which is now deprecated and slated for removal in a future version of TypeSpec.
19 changes: 19 additions & 0 deletions packages/compiler/generated-defs/TypeSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
DecoratorValidatorCallbacks,
Enum,
EnumValue,
FunctionContext,
Interface,
Model,
ModelProperty,
Expand Down Expand Up @@ -1206,3 +1207,21 @@ export type TypeSpecDecorators = {
withVisibilityFilter: WithVisibilityFilterDecorator;
withLifecycleUpdate: WithLifecycleUpdateDecorator;
};

export type ApplyVisibilityFilterFunctionImplementation = (
context: FunctionContext,
input: Model,
filter: VisibilityFilter,
nameTemplate?: string,
) => Model;

export type ApplyLifecycleUpdateFunctionImplementation = (
context: FunctionContext,
input: Model,
nameTemplate?: string,
) => Model;

export type TypeSpecFunctions = {
applyVisibilityFilter: ApplyVisibilityFilterFunctionImplementation;
applyLifecycleUpdate: ApplyLifecycleUpdateFunctionImplementation;
};
9 changes: 7 additions & 2 deletions packages/compiler/generated-defs/TypeSpec.ts-test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// An error in the imports would mean that the decorator is not exported or
// doesn't have the right name.

import { $decorators } from "../src/index.js";
import type { TypeSpecDecorators } from "./TypeSpec.js";
import { $decorators, $functions } from "../src/index.js";
import type { TypeSpecDecorators, TypeSpecFunctions } from "./TypeSpec.js";

/**
* An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ...
*/
const _decs: TypeSpecDecorators = $decorators["TypeSpec"];

/**
* An error here would mean that the exported function is not using the same signature. Make sure to have export const $funcName: FuncNameFunction = (...) => ...
*/
const _funcs: TypeSpecFunctions = $functions["TypeSpec"];
113 changes: 75 additions & 38 deletions packages/compiler/lib/std/visibility.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,60 @@ model VisibilityFilter {
* }
* ```
*/
#deprecated "withVisibilityFilter is deprecated and will be removed in a future release. Use the `FilterVisibility` template or Lifecycle specific templates (e.g. `Read`, `Create`, `Update`, etc.) instead."
extern dec withVisibilityFilter(
target: Model,
filter: valueof VisibilityFilter,
nameTemplate?: valueof string
);

/**
* A copy of the input model `M` with only the properties that match the given visibility filter.
*
* 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`.
*
* If a `nameTemplate` is provided, newly-created type instances will be named according
* to the template. See the `@friendlyName` decorator for more information on the template
* syntax. The transformed type is provided as the argument to the template.
*
* @template M the model to apply the visibility filter to.
* @template Filter the visibility filter to apply to the properties of the target model.
* @template NameTemplate the name template to use when renaming new type instances.
*
* @example
* ```typespec
* model Dog {
* @visibility(CustomVisibility.A)
* id: int32;
* @removeVisibility(CustomVisibility.A)
* name: string;
* }
*
* enum CustomVisibility {
* A,
* B,
* }
*
* const customFilter: VisibilityFilter = #{ all: #[CustomVisibility.A] };
*
* // This model will have the `id` property but not the `name` property, since `id` has the CustomVisibility.A visibility and `name` does not.
* model DogRead is FilterVisibility<Dog, customFilter, "Read{name}">;
* ```
*/
alias FilterVisibility<
M extends Model,
Filter extends valueof VisibilityFilter,
NameTemplate extends valueof string
> = applyVisibilityFilter(M, Filter, NameTemplate);

#suppress "experimental-feature"
internal extern fn applyVisibilityFilter(
input: Model,
filter: valueof VisibilityFilter,
nameTemplate?: valueof string
): Model;

/**
* Transforms the `target` model to include only properties that are visible during the
* "Update" lifecycle phase.
Expand Down Expand Up @@ -338,8 +386,12 @@ extern dec withVisibilityFilter(
* }
* ```
*/
#deprecated "withLifecycleUpdate is deprecated and will be removed in a future release. Use the `Update` template instead."
extern dec withLifecycleUpdate(target: Model, nameTemplate?: valueof string);

#suppress "experimental-feature"
internal extern fn applyLifecycleUpdate(input: Model, nameTemplate?: valueof string): Model;

/**
* A copy of the input model `T` with only the properties that are visible during the
* "Create" resource lifecycle phase.
Expand All @@ -366,12 +418,10 @@ extern dec withLifecycleUpdate(target: Model, nameTemplate?: valueof string);
* model CreateDog is Create<Dog>;
* ```
*/
@doc("")
@friendlyName(NameTemplate, T)
@withVisibilityFilter(#{ all: #[Lifecycle.Create] }, NameTemplate)
model Create<T extends Reflection.Model, NameTemplate extends valueof string = "Create{name}"> {
...T;
}
alias Create<
T extends Model,
NameTemplate extends valueof string = "Create{name}"
> = applyVisibilityFilter(T, #{ all: #[Lifecycle.Create] }, NameTemplate);

/**
* A copy of the input model `T` with only the properties that are visible during the
Expand Down Expand Up @@ -405,12 +455,10 @@ model Create<T extends Reflection.Model, NameTemplate extends valueof string = "
* model ReadDog is Read<Dog>;
* ```
*/
@doc("")
@friendlyName(NameTemplate, T)
@withVisibilityFilter(#{ all: #[Lifecycle.Read] }, NameTemplate)
model Read<T extends Reflection.Model, NameTemplate extends valueof string = "Read{name}"> {
...T;
}
alias Read<
T extends Model,
NameTemplate extends valueof string = "Read{name}"
> = applyVisibilityFilter(T, #{ all: #[Lifecycle.Read] }, NameTemplate);

/**
* A copy of the input model `T` with only the properties that are visible during the
Expand Down Expand Up @@ -445,12 +493,10 @@ model Read<T extends Reflection.Model, NameTemplate extends valueof string = "Re
* model UpdateDog is Update<Dog>;
* ```
*/
@doc("")
@friendlyName(NameTemplate, T)
@withLifecycleUpdate(NameTemplate)
model Update<T extends Reflection.Model, NameTemplate extends valueof string = "Update{name}"> {
...T;
}
alias Update<
T extends Model,
NameTemplate extends valueof string = "Update{name}"
> = applyLifecycleUpdate(T, NameTemplate);

/**
* A copy of the input model `T` with only the properties that are visible during the
Expand Down Expand Up @@ -487,15 +533,10 @@ model Update<T extends Reflection.Model, NameTemplate extends valueof string = "
* model CreateOrUpdateDog is CreateOrUpdate<Dog>;
* ```
*/
@doc("")
@friendlyName(NameTemplate, T)
@withVisibilityFilter(#{ any: #[Lifecycle.Create, Lifecycle.Update] }, NameTemplate)
model CreateOrUpdate<
T extends Reflection.Model,
alias CreateOrUpdate<
T extends Model,
NameTemplate extends valueof string = "CreateOrUpdate{name}"
> {
...T;
}
> = applyVisibilityFilter(T, #{ any: #[Lifecycle.Create, Lifecycle.Update] }, NameTemplate);

/**
* A copy of the input model `T` with only the properties that are visible during the
Expand Down Expand Up @@ -531,12 +572,10 @@ model CreateOrUpdate<
* model DeleteDog is Delete<Dog>;
* ```
*/
@doc("")
@friendlyName(NameTemplate, T)
@withVisibilityFilter(#{ all: #[Lifecycle.Delete] }, NameTemplate)
model Delete<T extends Reflection.Model, NameTemplate extends valueof string = "Delete{name}"> {
...T;
}
alias Delete<
T extends Model,
NameTemplate extends valueof string = "Delete{name}"
> = applyVisibilityFilter(T, #{ all: #[Lifecycle.Delete] }, NameTemplate);

/**
* A copy of the input model `T` with only the properties that are visible during the
Expand Down Expand Up @@ -580,9 +619,7 @@ model Delete<T extends Reflection.Model, NameTemplate extends valueof string = "
* model QueryDog is Query<Dog>;
* ```
*/
@doc("")
@friendlyName(NameTemplate, T)
@withVisibilityFilter(#{ all: #[Lifecycle.Query] }, NameTemplate)
model Query<T extends Reflection.Model, NameTemplate extends valueof string = "Query{name}"> {
...T;
}
alias Query<
T extends Model,
NameTemplate extends valueof string = "Query{name}"
> = applyVisibilityFilter(T, #{ all: #[Lifecycle.Query] }, NameTemplate);
24 changes: 21 additions & 3 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,12 +785,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
if (entity.valueKind === "Function") return entity;
return constraint ? inferScalarsFromConstraints(entity, constraint.type) : entity;
}
// If a template parameter that can be a value is used in a template declaration then we allow it but we return null because we don't have an actual value.
// If a template parameter that can be a value is used where a value is expected,
// synthesize a template value placeholder even when the template parameter is mapped
// from an outer template declaration.
if (
entity.kind === "TemplateParameter" &&
entity.constraint?.valueType &&
entity.constraint.type === undefined &&
ctx.mapper === undefined
entity.constraint.type === undefined
) {
// We must also observe that the template parameter is used here.
// ctx.observeTemplateParameter(entity);
Expand Down Expand Up @@ -5140,6 +5141,23 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
}

if (entity.entityKind === "Type") {
if (
entity.kind === "TemplateParameter" &&
entity.constraint?.valueType &&
entity.constraint.type === undefined
) {
return [
createValue(
{
entityKind: "Value",
valueKind: "TemplateValue",
type: entity.constraint.valueType,
},
entity.constraint.valueType,
) as any,
[],
];
}
return [
null,
[
Expand Down
10 changes: 9 additions & 1 deletion packages/compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export {
serializeValueAsJson,
Service,
ServiceDetails,
setMediaTypeHint,
VisibilityProvider,
type BytesKnownEncoding,
type DateTimeKnownEncoding,
Expand Down Expand Up @@ -232,7 +233,7 @@ export {
export type { PackageJson } from "./types/package-json.js";

import { $decorators as intrinsicDecorators } from "./lib/intrinsic/tsp-index.js";
import { $decorators as stdDecorators } from "./lib/tsp-index.js";
import { $decorators as stdDecorators, $functions as stdFunctions } from "./lib/tsp-index.js";
/** @internal for Typespec compiler */
export const $decorators = {
TypeSpec: {
Expand All @@ -243,6 +244,13 @@ export const $decorators = {
},
};

/** @internal for Typespec compiler */
export const $functions = {
TypeSpec: {
...stdFunctions.TypeSpec,
},
};

export { applyCodeFix, applyCodeFixes, resolveCodeFix } from "./core/code-fixes.js";
export { createAddDecoratorCodeFix } from "./core/compiler-code-fixes/create-add-decorator/create-add-decorator.codefix.js";
export {
Expand Down
35 changes: 33 additions & 2 deletions packages/compiler/src/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export function isErrorModel(program: Program, target: Type): boolean {

// -- @mediaTypeHint decorator --------------

const [_getMediaTypeHint, setMediaTypeHint] = useStateMap<MediaTypeHintable, string>(
const [_getMediaTypeHint, _setMediaTypeHint] = useStateMap<MediaTypeHintable, string>(
createStateSymbol("mediaTypeHint"),
);

Expand All @@ -461,9 +461,40 @@ export const $mediaTypeHint: MediaTypeHintDecorator = (
});
}

setMediaTypeHint(context.program, target, mediaType);
_setMediaTypeHint(context.program, target, mediaType);
};

/**
* Sets the default media type hint for the given target type.
*
* This value is a hint _ONLY_. Emitters are not required to use it, but may use it to get the default media type
* associated with a TypeSpec type.
*
* If a type already has a default media type hint set, this function will override it with the new value.
*
* WARNING: this function _will throw an error_ if the provided media type string is not recognized as a valid
* MIME type.
*
* @param program - the Program containing the target
* @param target - the target to set the MIME type hint for
* @param mediaType - the default media type hint to set for the target
* @throws if the provided media type string is not recognized as a valid MIME type
*/
export function setMediaTypeHint(
program: Program,
target: MediaTypeHintable,
mediaType: string,
): void {
const mimeTypeObj = parseMimeType(mediaType);

compilerAssert(
mimeTypeObj !== undefined,
`Invalid MIME type '${mediaType}' provided to setMediaTypeHint`,
);

_setMediaTypeHint(program, target, mediaType);
}

/**
* Get the default media type hint for the given target type.
*
Expand Down
12 changes: 11 additions & 1 deletion packages/compiler/src/lib/tsp-index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TypeSpecDecorators } from "../../generated-defs/TypeSpec.js";
import { TypeSpecDecorators, TypeSpecFunctions } from "../../generated-defs/TypeSpec.js";
import {
$discriminator,
$doc,
Expand Down Expand Up @@ -59,8 +59,18 @@ import {
$withUpdateableProperties,
$withVisibility,
$withVisibilityFilter,
applyLifecycleUpdate,
applyVisibilityFilter,
} from "./visibility.js";

/** @internal */
export const $functions = {
TypeSpec: {
applyVisibilityFilter,
applyLifecycleUpdate,
} satisfies TypeSpecFunctions,
};

/** @internal */
export const $decorators = {
TypeSpec: {
Expand Down
Loading
Loading