Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5521f84
Initial
witemple-msft Mar 3, 2026
85af641
Finish port of visibility mutators to functions
witemple-msft Mar 3, 2026
494351c
Fix completions for internal items
witemple-msft Mar 3, 2026
5592314
Fix JSON schema test for more correct visibility transform.
witemple-msft Mar 3, 2026
c0881b4
Migrate http merge-patch to functions
witemple-msft Mar 3, 2026
e68a348
Remove some commented code
witemple-msft Mar 3, 2026
8262ede
Docs changes
witemple-msft Mar 3, 2026
80eb975
Update expectations to account for drift
witemple-msft Mar 3, 2026
7ce6f7d
Merge remote-tracking branch 'upstream/main' into witemple-msft/trans…
witemple-msft Mar 3, 2026
f8c4ea0
Chronus
witemple-msft Mar 3, 2026
f889584
format
witemple-msft Mar 3, 2026
41a36df
More chronus
witemple-msft Mar 3, 2026
18a4857
Add replacement template for @withVisibilityFilter
witemple-msft Mar 3, 2026
ca987ff
tests and chronus
witemple-msft Mar 3, 2026
022e07d
docs changes
witemple-msft Mar 3, 2026
316e320
Set media type hint on transformed models.
witemple-msft Mar 4, 2026
4e9ba38
Address feedback
witemple-msft Mar 4, 2026
b14f1df
tests
witemple-msft Mar 4, 2026
83bf32c
Merge remote-tracking branch 'upstream/main' into witemple-msft/trans…
witemple-msft Mar 4, 2026
fd0496a
format
witemple-msft Mar 4, 2026
4e9698c
Always set merge-patch hint on outputs.
witemple-msft Mar 4, 2026
531e6ab
Merge remote-tracking branch 'upstream/main' into witemple-msft/trans…
witemple-msft Mar 17, 2026
47586cb
Change how media type hint is applied slightly so it will propagate.
witemple-msft Mar 17, 2026
9e38f95
Merge remote-tracking branch 'upstream/main' into witemple-msft/trans…
witemple-msft Mar 17, 2026
8b34b3b
More imperative -> decorator fixes.
witemple-msft Mar 17, 2026
3f47113
Regression test against loss of metadata
witemple-msft Mar 17, 2026
f6b7dd3
Merge remote-tracking branch 'upstream/main' into witemple-msft/trans…
witemple-msft Mar 17, 2026
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