diff --git a/.chronus/changes/json-schema-auto-decorators-2026-9-2.md b/.chronus/changes/json-schema-auto-decorators-2026-9-2.md new file mode 100644 index 00000000000..a90cfbf3f1b --- /dev/null +++ b/.chronus/changes/json-schema-auto-decorators-2026-9-2.md @@ -0,0 +1,27 @@ +--- +changeKind: deprecation +packages: + - "@typespec/json-schema" +--- + +The metadata-only decorators of this library are now declared as `auto dec`, so the compiler +synthesizes their implementation and provides typed accessors. This affects `@baseUri`, `@id`, +`@oneOf`, `@multipleOf`, `@contains`, `@minContains`, `@maxContains`, `@uniqueItems`, +`@minProperties`, `@maxProperties`, `@contentEncoding`, `@contentMediaType`, `@contentSchema` and +`@prefixItems`. + +Nothing changes for TypeSpec authors. For JavaScript consumers, the `$baseUri`-style implementation +functions and their `BaseUriDecorator`-style signature types are deprecated: they are no longer what +the compiler invokes. Use the generated `set*` accessor to apply a decorator programmatically: + +```ts +// Before +context.call($minContains, target, 2); + +// After +import { setMinContains } from "@typespec/json-schema"; +setMinContains(program, target, 2); +``` + +Applying one of these decorators twice on the same declaration now reports a `duplicate-decorator` +warning. The last application still wins. diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts index cbf82755d92..16bdfd86466 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts @@ -1,12 +1,16 @@ -import type { - DecoratorContext, - DecoratorValidatorCallbacks, - ModelProperty, - Namespace, - Numeric, - Scalar, - Type, - Union, +import { + type DecoratorContext, + type DecoratorValidatorCallbacks, + getAutoDecoratorValue, + hasAutoDecorator, + type ModelProperty, + type Namespace, + type Numeric, + type Program, + type Scalar, + setAutoDecorator, + type Type, + type Union, } from "@typespec/compiler"; /** @@ -25,203 +29,272 @@ export type JsonSchemaDecorator = ( ) => DecoratorValidatorCallbacks | void; /** - * Set the base URI for any schemas emitted from types within this namespace. + * Specify a custom property to add to the emitted schema. This is useful for adding custom keywords + * and other vendor-specific extensions. Scalar values need to be specified using `typeof` to be converted to a schema. + * + * For example, `@extension("x-schema", typeof "foo")` will emit a JSON schema value for `x-schema`, + * whereas `@extension("x-schema", "foo")` will emit the raw code `"foo"`. + * + * The value will be treated as a raw value if any of the following are true: + * - The value is a scalar value (e.g. string, number, boolean, etc.) + * - The value is wrapped in the `Json` template + * - The value is provided using the value syntax (e.g. `#{}`, `#[]`) * - * @param baseUri The base URI. Schema IDs inside this namespace are relative to this URI. + * For example, `@extension("x-schema", { x: "value" })` will emit a JSON schema value for `x-schema`, + * whereas `@extension("x-schema", #{x: "value"})` and `@extension("x-schema", Json<{x: "value"}>)` + * will emit the raw JSON code `{x: "value"}`. + * + * @param key The name of the keyword of vendor extension, e.g. `x-custom`. + * @param value The value of the keyword. */ -export type BaseUriDecorator = ( +export type ExtensionDecorator = ( context: DecoratorContext, - target: Namespace, - baseUri: string, + target: Type, + key: string, + value: Type | unknown, ) => DecoratorValidatorCallbacks | void; +export type TypeSpecJsonSchemaDecorators = { + jsonSchema: JsonSchemaDecorator; + extension: ExtensionDecorator; +}; + +/** Set the base URI for any schemas emitted from types within this namespace. */ +export function getBaseUri(program: Program, target: Namespace): string | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.baseUri", target)?.["baseUri"] as any; +} + +/** Set the base URI for any schemas emitted from types within this namespace. */ +export function setBaseUri(program: Program, target: Namespace, baseUri: string): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.baseUri", target, { baseUri: baseUri }); +} + /** * Specify the JSON Schema id. If this model or a parent namespace has a base URI, * the provided ID will be relative to that base URI. * * By default, the id will be constructed based on the declaration's name. - * - * @param id The id of the JSON schema for this declaration. */ -export type IdDecorator = ( - context: DecoratorContext, - target: Type, - id: string, -) => DecoratorValidatorCallbacks | void; +export function getId(program: Program, target: Type): string | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.id", target)?.["id"] as any; +} /** - * Specify that `oneOf` should be used instead of `anyOf` for that union. - */ -export type OneOfDecorator = ( - context: DecoratorContext, - target: Union | ModelProperty, -) => DecoratorValidatorCallbacks | void; - -/** - * Specify that the numeric type must be a multiple of some numeric value. + * Specify the JSON Schema id. If this model or a parent namespace has a base URI, + * the provided ID will be relative to that base URI. * - * @param value The numeric type must be a multiple of this value. + * By default, the id will be constructed based on the declaration's name. */ -export type MultipleOfDecorator = ( - context: DecoratorContext, +export function setId(program: Program, target: Type, id: string): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.id", target, { id: id }); +} + +/** Check if the `@TypeSpec.JsonSchema.oneOf` decorator was applied on the given target. */ +export function isOneOf(program: Program, target: Union | ModelProperty): boolean { + return hasAutoDecorator(program, "TypeSpec.JsonSchema.oneOf", target); +} + +/** Specify that `oneOf` should be used instead of `anyOf` for that union. */ +export function setOneOf(program: Program, target: Union | ModelProperty): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.oneOf", target); +} + +/** Specify that the numeric type must be a multiple of some numeric value. */ +export function getMultipleOf( + program: Program, + target: Scalar | ModelProperty, +): Numeric | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.multipleOf", target)?.["value"] as any; +} + +/** Specify that the numeric type must be a multiple of some numeric value. */ +export function setMultipleOf( + program: Program, target: Scalar | ModelProperty, value: Numeric, -) => DecoratorValidatorCallbacks | void; +): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.multipleOf", target, { value: value }); +} /** * Specify that the array must contain at least one instance of the provided type. * Use `@minContains` and `@maxContains` to customize how many instances to expect. - * - * @param value The type the array must contain. */ -export type ContainsDecorator = ( - context: DecoratorContext, - target: Type | ModelProperty, - value: Type, -) => DecoratorValidatorCallbacks | void; +export function getContains(program: Program, target: Type | ModelProperty): Type | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.contains", target)?.["value"] as any; +} + +/** + * Specify that the array must contain at least one instance of the provided type. + * Use `@minContains` and `@maxContains` to customize how many instances to expect. + */ +export function setContains(program: Program, target: Type | ModelProperty, value: Type): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.contains", target, { value: value }); +} /** * Used in conjunction with the `@contains` decorator, * specifies that the array must contain at least a certain number of the types provided by the `@contains` decorator. - * - * @param value The minimum number of instances the array must contain */ -export type MinContainsDecorator = ( - context: DecoratorContext, - target: Type | ModelProperty, - value: number, -) => DecoratorValidatorCallbacks | void; +export function getMinContains(program: Program, target: Type | ModelProperty): number | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.minContains", target)?.[ + "value" + ] as any; +} /** * Used in conjunction with the `@contains` decorator, - * specifies that the array must contain at most a certain number of the types provided by the `@contains` decorator. - * - * @param value The maximum number of instances the array must contain + * specifies that the array must contain at least a certain number of the types provided by the `@contains` decorator. */ -export type MaxContainsDecorator = ( - context: DecoratorContext, +export function setMinContains( + program: Program, target: Type | ModelProperty, value: number, -) => DecoratorValidatorCallbacks | void; +): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.minContains", target, { value: value }); +} /** - * Specify that every item in the array must be unique. + * Used in conjunction with the `@contains` decorator, + * specifies that the array must contain at most a certain number of the types provided by the `@contains` decorator. */ -export type UniqueItemsDecorator = ( - context: DecoratorContext, - target: Type | ModelProperty, -) => DecoratorValidatorCallbacks | void; +export function getMaxContains(program: Program, target: Type | ModelProperty): number | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.maxContains", target)?.[ + "value" + ] as any; +} /** - * Specify the minimum number of properties this object can have. - * - * @param value The minimum number of properties this object can have. + * Used in conjunction with the `@contains` decorator, + * specifies that the array must contain at most a certain number of the types provided by the `@contains` decorator. */ -export type MinPropertiesDecorator = ( - context: DecoratorContext, +export function setMaxContains( + program: Program, target: Type | ModelProperty, value: number, -) => DecoratorValidatorCallbacks | void; +): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.maxContains", target, { value: value }); +} -/** - * Specify the maximum number of properties this object can have. - * - * @param value The maximum number of properties this object can have. - */ -export type MaxPropertiesDecorator = ( - context: DecoratorContext, +/** Check if the `@TypeSpec.JsonSchema.uniqueItems` decorator was applied on the given target. */ +export function isUniqueItems(program: Program, target: Type | ModelProperty): boolean { + return hasAutoDecorator(program, "TypeSpec.JsonSchema.uniqueItems", target); +} + +/** Specify that every item in the array must be unique. */ +export function setUniqueItems(program: Program, target: Type | ModelProperty): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.uniqueItems", target); +} + +/** Specify the minimum number of properties this object can have. */ +export function getMinProperties( + program: Program, + target: Type | ModelProperty, +): number | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.minProperties", target)?.[ + "value" + ] as any; +} + +/** Specify the minimum number of properties this object can have. */ +export function setMinProperties( + program: Program, target: Type | ModelProperty, value: number, -) => DecoratorValidatorCallbacks | void; +): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.minProperties", target, { value: value }); +} -/** - * Specify the encoding used for the contents of a string. - * - * @param value - * - * - */ -export type ContentEncodingDecorator = ( - context: DecoratorContext, +/** Specify the maximum number of properties this object can have. */ +export function getMaxProperties( + program: Program, + target: Type | ModelProperty, +): number | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.maxProperties", target)?.[ + "value" + ] as any; +} + +/** Specify the maximum number of properties this object can have. */ +export function setMaxProperties( + program: Program, + target: Type | ModelProperty, + value: number, +): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.maxProperties", target, { value: value }); +} + +/** Specify the encoding used for the contents of a string. */ +export function getContentEncoding( + program: Program, + target: Scalar | ModelProperty, +): string | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.contentEncoding", target)?.[ + "value" + ] as any; +} + +/** Specify the encoding used for the contents of a string. */ +export function setContentEncoding( + program: Program, target: Scalar | ModelProperty, value: string, -) => DecoratorValidatorCallbacks | void; +): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.contentEncoding", target, { value: value }); +} -/** - * Specify that the target array must begin with the provided types. - * - * @param value A tuple containing the types that must be present at the start of the array - */ -export type PrefixItemsDecorator = ( - context: DecoratorContext, - target: Type | ModelProperty, - value: Type, -) => DecoratorValidatorCallbacks | void; +/** Specify that the target array must begin with the provided types. */ +export function getPrefixItems(program: Program, target: Type | ModelProperty): Type | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.prefixItems", target)?.[ + "value" + ] as any; +} -/** - * Specify the content type of content stored in a string. - * - * @param value The media type of the string contents - */ -export type ContentMediaTypeDecorator = ( - context: DecoratorContext, +/** Specify that the target array must begin with the provided types. */ +export function setPrefixItems(program: Program, target: Type | ModelProperty, value: Type): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.prefixItems", target, { value: value }); +} + +/** Specify the content type of content stored in a string. */ +export function getContentMediaType( + program: Program, + target: Scalar | ModelProperty, +): string | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.contentMediaType", target)?.[ + "value" + ] as any; +} + +/** Specify the content type of content stored in a string. */ +export function setContentMediaType( + program: Program, target: Scalar | ModelProperty, value: string, -) => DecoratorValidatorCallbacks | void; +): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.contentMediaType", target, { value: value }); +} /** * Specify the schema for the contents of a string when interpreted according to the content's * media type and encoding. - * - * @param value The schema of the string contents */ -export type ContentSchemaDecorator = ( - context: DecoratorContext, +export function getContentSchema( + program: Program, target: Scalar | ModelProperty, - value: Type, -) => DecoratorValidatorCallbacks | void; +): Type | undefined { + return getAutoDecoratorValue(program, "TypeSpec.JsonSchema.contentSchema", target)?.[ + "value" + ] as any; +} /** - * Specify a custom property to add to the emitted schema. This is useful for adding custom keywords - * and other vendor-specific extensions. Scalar values need to be specified using `typeof` to be converted to a schema. - * - * For example, `@extension("x-schema", typeof "foo")` will emit a JSON schema value for `x-schema`, - * whereas `@extension("x-schema", "foo")` will emit the raw code `"foo"`. - * - * The value will be treated as a raw value if any of the following are true: - * - The value is a scalar value (e.g. string, number, boolean, etc.) - * - The value is wrapped in the `Json` template - * - The value is provided using the value syntax (e.g. `#{}`, `#[]`) - * - * For example, `@extension("x-schema", { x: "value" })` will emit a JSON schema value for `x-schema`, - * whereas `@extension("x-schema", #{x: "value"})` and `@extension("x-schema", Json<{x: "value"}>)` - * will emit the raw JSON code `{x: "value"}`. - * - * @param key The name of the keyword of vendor extension, e.g. `x-custom`. - * @param value The value of the keyword. + * Specify the schema for the contents of a string when interpreted according to the content's + * media type and encoding. */ -export type ExtensionDecorator = ( - context: DecoratorContext, - target: Type, - key: string, - value: Type | unknown, -) => DecoratorValidatorCallbacks | void; - -export type TypeSpecJsonSchemaDecorators = { - jsonSchema: JsonSchemaDecorator; - baseUri: BaseUriDecorator; - id: IdDecorator; - oneOf: OneOfDecorator; - multipleOf: MultipleOfDecorator; - contains: ContainsDecorator; - minContains: MinContainsDecorator; - maxContains: MaxContainsDecorator; - uniqueItems: UniqueItemsDecorator; - minProperties: MinPropertiesDecorator; - maxProperties: MaxPropertiesDecorator; - contentEncoding: ContentEncodingDecorator; - prefixItems: PrefixItemsDecorator; - contentMediaType: ContentMediaTypeDecorator; - contentSchema: ContentSchemaDecorator; - extension: ExtensionDecorator; -}; +export function setContentSchema( + program: Program, + target: Scalar | ModelProperty, + value: Type, +): void { + setAutoDecorator(program, "TypeSpec.JsonSchema.contentSchema", target, { value: value }); +} diff --git a/packages/json-schema/lib/main.tsp b/packages/json-schema/lib/main.tsp index e403f599563..b9fa06e5693 100644 --- a/packages/json-schema/lib/main.tsp +++ b/packages/json-schema/lib/main.tsp @@ -18,7 +18,7 @@ extern dec jsonSchema(target: unknown, baseUri?: valueof string); * * @param baseUri The base URI. Schema IDs inside this namespace are relative to this URI. */ -extern dec baseUri(target: Reflection.Namespace, baseUri: valueof string); +auto dec baseUri(target: Reflection.Namespace, baseUri: valueof string); /** * Specify the JSON Schema id. If this model or a parent namespace has a base URI, @@ -28,19 +28,19 @@ extern dec baseUri(target: Reflection.Namespace, baseUri: valueof string); * * @param id The id of the JSON schema for this declaration. */ -extern dec id(target: unknown, id: valueof string); +auto dec id(target: unknown, id: valueof string); /** * Specify that `oneOf` should be used instead of `anyOf` for that union. */ -extern dec oneOf(target: Reflection.Union | Reflection.ModelProperty); +auto dec oneOf(target: Reflection.Union | Reflection.ModelProperty); /** * Specify that the numeric type must be a multiple of some numeric value. * * @param value The numeric type must be a multiple of this value. */ -extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric); +auto dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric); /** * Specify that the array must contain at least one instance of the provided type. @@ -48,7 +48,7 @@ extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof * * @param value The type the array must contain. */ -extern dec contains(target: unknown[] | Reflection.ModelProperty, value: unknown); +auto dec contains(target: unknown[] | Reflection.ModelProperty, value: unknown); /** * Used in conjunction with the `@contains` decorator, @@ -56,7 +56,7 @@ extern dec contains(target: unknown[] | Reflection.ModelProperty, value: unknown * * @param value The minimum number of instances the array must contain */ -extern dec minContains(target: unknown[] | Reflection.ModelProperty, value: valueof int32); +auto dec minContains(target: unknown[] | Reflection.ModelProperty, value: valueof int32); /** * Used in conjunction with the `@contains` decorator, @@ -64,39 +64,39 @@ extern dec minContains(target: unknown[] | Reflection.ModelProperty, value: valu * * @param value The maximum number of instances the array must contain */ -extern dec maxContains(target: unknown[] | Reflection.ModelProperty, value: valueof int32); +auto dec maxContains(target: unknown[] | Reflection.ModelProperty, value: valueof int32); /** * Specify that every item in the array must be unique. */ -extern dec uniqueItems(target: unknown[] | Reflection.ModelProperty); +auto dec uniqueItems(target: unknown[] | Reflection.ModelProperty); /** * Specify the minimum number of properties this object can have. * * @param value The minimum number of properties this object can have. */ -extern dec minProperties(target: Record | Reflection.ModelProperty, value: valueof int32); +auto dec minProperties(target: Record | Reflection.ModelProperty, value: valueof int32); /** * Specify the maximum number of properties this object can have. * * @param value The maximum number of properties this object can have. */ -extern dec maxProperties(target: Record | Reflection.ModelProperty, value: valueof int32); +auto dec maxProperties(target: Record | Reflection.ModelProperty, value: valueof int32); /** * Specify the encoding used for the contents of a string. * @param value */ -extern dec contentEncoding(target: string | Reflection.ModelProperty, value: valueof string); +auto dec contentEncoding(target: string | Reflection.ModelProperty, value: valueof string); /** * Specify that the target array must begin with the provided types. * * @param value A tuple containing the types that must be present at the start of the array */ -extern dec prefixItems(target: unknown[] | Reflection.ModelProperty, value: unknown[]); +auto dec prefixItems(target: unknown[] | Reflection.ModelProperty, value: unknown[]); /** * Specify the content type of content stored in a string. @@ -104,7 +104,7 @@ extern dec prefixItems(target: unknown[] | Reflection.ModelProperty, value: unkn * @param value The media type of the string contents * */ -extern dec contentMediaType(target: string | Reflection.ModelProperty, value: valueof string); +auto dec contentMediaType(target: string | Reflection.ModelProperty, value: valueof string); /** * Specify the schema for the contents of a string when interpreted according to the content's @@ -112,7 +112,7 @@ extern dec contentMediaType(target: string | Reflection.ModelProperty, value: va * * @param value The schema of the string contents */ -extern dec contentSchema(target: string | Reflection.ModelProperty, value: unknown); +auto dec contentSchema(target: string | Reflection.ModelProperty, value: unknown); /** * Specify a custom property to add to the emitted schema. This is useful for adding custom keywords diff --git a/packages/json-schema/src/back-compat.ts b/packages/json-schema/src/back-compat.ts new file mode 100644 index 00000000000..5a1918f662b --- /dev/null +++ b/packages/json-schema/src/back-compat.ts @@ -0,0 +1,285 @@ +import type { + DecoratorContext, + DecoratorValidatorCallbacks, + ModelProperty, + Namespace, + Numeric, + Scalar, + Type, + Union, +} from "@typespec/compiler"; +import { + setBaseUri, + setContains, + setContentEncoding, + setContentMediaType, + setContentSchema, + setId, + setMaxContains, + setMaxProperties, + setMinContains, + setMinProperties, + setMultipleOf, + setOneOf, + setPrefixItems, + setUniqueItems, +} from "../generated-defs/TypeSpec.JsonSchema.js"; + +/** + * The metadata-only decorators of this library are declared as `auto dec` and no longer have a + * JavaScript implementation. The `$name` functions and `NameDecorator` types below are kept so + * that existing code importing them keeps compiling, but they are no longer the implementation + * the compiler invokes. Use the corresponding `setName` accessor instead. + */ + +/* eslint-disable @typescript-eslint/no-deprecated */ + +/** + * Signature of the `@baseUri` decorator. + * @deprecated `@baseUri` is now an `auto dec` and has no JavaScript implementation. Use `setBaseUri` instead. + */ +export type BaseUriDecorator = ( + context: DecoratorContext, + target: Namespace, + baseUri: string, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@id` decorator. + * @deprecated `@id` is now an `auto dec` and has no JavaScript implementation. Use `setId` instead. + */ +export type IdDecorator = ( + context: DecoratorContext, + target: Type, + id: string, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@oneOf` decorator. + * @deprecated `@oneOf` is now an `auto dec` and has no JavaScript implementation. Use `setOneOf` instead. + */ +export type OneOfDecorator = ( + context: DecoratorContext, + target: Union | ModelProperty, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@multipleOf` decorator. + * @deprecated `@multipleOf` is now an `auto dec` and has no JavaScript implementation. Use `setMultipleOf` instead. + */ +export type MultipleOfDecorator = ( + context: DecoratorContext, + target: Scalar | ModelProperty, + value: Numeric, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@contains` decorator. + * @deprecated `@contains` is now an `auto dec` and has no JavaScript implementation. Use `setContains` instead. + */ +export type ContainsDecorator = ( + context: DecoratorContext, + target: Type | ModelProperty, + value: Type, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@minContains` decorator. + * @deprecated `@minContains` is now an `auto dec` and has no JavaScript implementation. Use `setMinContains` instead. + */ +export type MinContainsDecorator = ( + context: DecoratorContext, + target: Type | ModelProperty, + value: number, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@maxContains` decorator. + * @deprecated `@maxContains` is now an `auto dec` and has no JavaScript implementation. Use `setMaxContains` instead. + */ +export type MaxContainsDecorator = ( + context: DecoratorContext, + target: Type | ModelProperty, + value: number, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@uniqueItems` decorator. + * @deprecated `@uniqueItems` is now an `auto dec` and has no JavaScript implementation. Use `setUniqueItems` instead. + */ +export type UniqueItemsDecorator = ( + context: DecoratorContext, + target: Type | ModelProperty, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@minProperties` decorator. + * @deprecated `@minProperties` is now an `auto dec` and has no JavaScript implementation. Use `setMinProperties` instead. + */ +export type MinPropertiesDecorator = ( + context: DecoratorContext, + target: Type | ModelProperty, + value: number, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@maxProperties` decorator. + * @deprecated `@maxProperties` is now an `auto dec` and has no JavaScript implementation. Use `setMaxProperties` instead. + */ +export type MaxPropertiesDecorator = ( + context: DecoratorContext, + target: Type | ModelProperty, + value: number, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@contentEncoding` decorator. + * @deprecated `@contentEncoding` is now an `auto dec` and has no JavaScript implementation. Use `setContentEncoding` instead. + */ +export type ContentEncodingDecorator = ( + context: DecoratorContext, + target: Scalar | ModelProperty, + value: string, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@prefixItems` decorator. + * @deprecated `@prefixItems` is now an `auto dec` and has no JavaScript implementation. Use `setPrefixItems` instead. + */ +export type PrefixItemsDecorator = ( + context: DecoratorContext, + target: Type | ModelProperty, + value: Type, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@contentMediaType` decorator. + * @deprecated `@contentMediaType` is now an `auto dec` and has no JavaScript implementation. Use `setContentMediaType` instead. + */ +export type ContentMediaTypeDecorator = ( + context: DecoratorContext, + target: Scalar | ModelProperty, + value: string, +) => DecoratorValidatorCallbacks | void; + +/** + * Signature of the `@contentSchema` decorator. + * @deprecated `@contentSchema` is now an `auto dec` and has no JavaScript implementation. Use `setContentSchema` instead. + */ +export type ContentSchemaDecorator = ( + context: DecoratorContext, + target: Scalar | ModelProperty, + value: Type, +) => DecoratorValidatorCallbacks | void; + +/** + * Implementation of the `@baseUri` decorator. + * @deprecated `@baseUri` is now an `auto dec` and has no JavaScript implementation. Use `setBaseUri` instead. + */ +export const $baseUri: BaseUriDecorator = (context, target, baseUri) => { + setBaseUri(context.program, target, baseUri); +}; + +/** + * Implementation of the `@id` decorator. + * @deprecated `@id` is now an `auto dec` and has no JavaScript implementation. Use `setId` instead. + */ +export const $id: IdDecorator = (context, target, id) => { + setId(context.program, target, id); +}; + +/** + * Implementation of the `@oneOf` decorator. + * @deprecated `@oneOf` is now an `auto dec` and has no JavaScript implementation. Use `setOneOf` instead. + */ +export const $oneOf: OneOfDecorator = (context, target) => { + setOneOf(context.program, target); +}; + +/** + * Implementation of the `@multipleOf` decorator. + * @deprecated `@multipleOf` is now an `auto dec` and has no JavaScript implementation. Use `setMultipleOf` instead. + */ +export const $multipleOf: MultipleOfDecorator = (context, target, value) => { + setMultipleOf(context.program, target, value); +}; + +/** + * Implementation of the `@contains` decorator. + * @deprecated `@contains` is now an `auto dec` and has no JavaScript implementation. Use `setContains` instead. + */ +export const $contains: ContainsDecorator = (context, target, value) => { + setContains(context.program, target, value); +}; + +/** + * Implementation of the `@minContains` decorator. + * @deprecated `@minContains` is now an `auto dec` and has no JavaScript implementation. Use `setMinContains` instead. + */ +export const $minContains: MinContainsDecorator = (context, target, value) => { + setMinContains(context.program, target, value); +}; + +/** + * Implementation of the `@maxContains` decorator. + * @deprecated `@maxContains` is now an `auto dec` and has no JavaScript implementation. Use `setMaxContains` instead. + */ +export const $maxContains: MaxContainsDecorator = (context, target, value) => { + setMaxContains(context.program, target, value); +}; + +/** + * Implementation of the `@uniqueItems` decorator. + * @deprecated `@uniqueItems` is now an `auto dec` and has no JavaScript implementation. Use `setUniqueItems` instead. + */ +export const $uniqueItems: UniqueItemsDecorator = (context, target) => { + setUniqueItems(context.program, target); +}; + +/** + * Implementation of the `@minProperties` decorator. + * @deprecated `@minProperties` is now an `auto dec` and has no JavaScript implementation. Use `setMinProperties` instead. + */ +export const $minProperties: MinPropertiesDecorator = (context, target, value) => { + setMinProperties(context.program, target, value); +}; + +/** + * Implementation of the `@maxProperties` decorator. + * @deprecated `@maxProperties` is now an `auto dec` and has no JavaScript implementation. Use `setMaxProperties` instead. + */ +export const $maxProperties: MaxPropertiesDecorator = (context, target, value) => { + setMaxProperties(context.program, target, value); +}; + +/** + * Implementation of the `@contentEncoding` decorator. + * @deprecated `@contentEncoding` is now an `auto dec` and has no JavaScript implementation. Use `setContentEncoding` instead. + */ +export const $contentEncoding: ContentEncodingDecorator = (context, target, value) => { + setContentEncoding(context.program, target, value); +}; + +/** + * Implementation of the `@prefixItems` decorator. + * @deprecated `@prefixItems` is now an `auto dec` and has no JavaScript implementation. Use `setPrefixItems` instead. + */ +export const $prefixItems: PrefixItemsDecorator = (context, target, value) => { + setPrefixItems(context.program, target, value); +}; + +/** + * Implementation of the `@contentMediaType` decorator. + * @deprecated `@contentMediaType` is now an `auto dec` and has no JavaScript implementation. Use `setContentMediaType` instead. + */ +export const $contentMediaType: ContentMediaTypeDecorator = (context, target, value) => { + setContentMediaType(context.program, target, value); +}; + +/** + * Implementation of the `@contentSchema` decorator. + * @deprecated `@contentSchema` is now an `auto dec` and has no JavaScript implementation. Use `setContentSchema` instead. + */ +export const $contentSchema: ContentSchemaDecorator = (context, target, value) => { + setContentSchema(context.program, target, value); +}; diff --git a/packages/json-schema/src/decorators.ts b/packages/json-schema/src/decorators.ts index 7abddb43681..b3b862b75c3 100644 --- a/packages/json-schema/src/decorators.ts +++ b/packages/json-schema/src/decorators.ts @@ -5,6 +5,7 @@ import { isType, type Model, type Namespace, + type Numeric, type Program, type Scalar, serializeValueAsJson, @@ -18,24 +19,22 @@ import { import { useStateMap, useStateSet } from "@typespec/compiler/utils"; import type { ValidatesRawJsonDecorator } from "../generated-defs/TypeSpec.JsonSchema.Private.js"; import type { - ContainsDecorator, - ContentEncodingDecorator, - ContentMediaTypeDecorator, - ContentSchemaDecorator, ExtensionDecorator, - IdDecorator, JsonSchemaDecorator, - MaxContainsDecorator, - MaxPropertiesDecorator, - MinContainsDecorator, - MinPropertiesDecorator, - MultipleOfDecorator, - OneOfDecorator, - PrefixItemsDecorator, - UniqueItemsDecorator, +} from "../generated-defs/TypeSpec.JsonSchema.js"; +import { + getBaseUri as getBaseUriOnNamespace, + getContentEncoding as getContentEncodingOnScalar, + getContentMediaType as getContentMediaTypeOnScalar, + getContentSchema as getContentSchemaOnScalar, + getMultipleOf as getMultipleOfOnScalar, + getPrefixItems as getPrefixItemsAsType, + isOneOf as isOneOfOnUnion, + isUniqueItems, + setBaseUri, + setId, } from "../generated-defs/TypeSpec.JsonSchema.js"; import { JsonSchemaStateKeys } from "./lib.js"; -import { createDataDecorator } from "./utils.js"; /** * TypeSpec Types that can create a json schmea declaration @@ -56,20 +55,51 @@ export const $jsonSchema: JsonSchemaDecorator = ( markJsonSchema(context.program, target); if (baseUriOrId) { if (target.kind === "Namespace") { - context.call($baseUri, target, baseUriOrId); + setBaseUri(context.program, target, baseUriOrId); } else { - context.call($id, target, baseUriOrId); + setId(context.program, target, baseUriOrId); } } }; -export const [ - /** Get base uri set via `@baseUri` decorator */ - getBaseUri, - setBaseUri, - /** {@inheritdoc BaseUriDecorator} */ - $baseUri, -] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.baseURI"]); +/** + * Accessors for the metadata-only decorators are generated from their `auto dec` + * declarations in `lib/main.tsp`. They are re-exported here when their generated + * signature already matches the historical one, and wrapped below when the + * historical signature was wider or returned a different shape. + */ +export { + getContains, + getId, + getMaxContains, + getMaxProperties, + getMinContains, + getMinProperties, + setContains, + setContentEncoding, + setContentMediaType, + setContentSchema, + setId, + setMaxContains, + setMaxProperties, + setMinContains, + setMinProperties, + setMultipleOf, + setOneOf, + setPrefixItems, + setUniqueItems, +} from "../generated-defs/TypeSpec.JsonSchema.js"; +export { setBaseUri }; + +/** + * Get base uri set via `@baseUri` decorator. + * + * Accepts any type so that {@link findBaseUri} can probe a declaration before walking + * up its enclosing namespaces; only namespaces can carry the decorator. + */ +export function getBaseUri(program: Program, target: Type): string | undefined { + return getBaseUriOnNamespace(program, target as Namespace); +} /** Find base uri for the given type. */ export function findBaseUri( @@ -146,133 +176,51 @@ export function getJsonSchemaTypes(program: Program): (JsonSchemaDeclarationType return types; } -export const [ - /** Get value set by `@multipleOf` decorator as a `Numeric` type. */ - getMultipleOfAsNumeric, - setMultipleOf, - /** {@inheritdoc MultipleOfDecorator} */ - - $multipleOf, -] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.multipleOf"]); +/** Get value set by `@multipleOf` decorator as a `Numeric` type. */ +export function getMultipleOfAsNumeric(program: Program, target: Type): Numeric | undefined { + return getMultipleOfOnScalar(program, target as Scalar); +} /** Get value set by `@multipleOf` decorator as a `number` type. If the value is not representable as a number or not set, returns undefined. */ export function getMultipleOf(program: Program, target: Type): number | undefined { return getMultipleOfAsNumeric(program, target)?.asNumber() ?? undefined; } -export const [ - /** Get id as set with `@id` decorator. */ - getId, - setId, - /** {@inheritdoc IdDecorator} */ - $id, -] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.id"]); - -export const [ - /** Check if given type is annotated with `@oneOf` decorator */ - isOneOf, - markOneOf, -] = useStateSet(JsonSchemaStateKeys["JsonSchema.oneOf"]); - -/** {@inheritdoc OneOfDecorator} */ -export const $oneOf: OneOfDecorator = (context: DecoratorContext, target: Type) => { - markOneOf(context.program, target); -}; - -export const [ - /** Get contains value set by `@contains` decorator */ - getContains, - setContains, - /** {@inheritdoc ContainsDecorator} */ - $contains, -] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.contains"]); - -export const [ - /** Get value set by `@minContains` decorator */ - getMinContains, - setMinContains, - /** {@inheritdoc MinContainsDecorator} */ - $minContains, -] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.minContains"]); - -export const [ - /** Get value set by `@maxContains` decorator */ - getMaxContains, - setMaxContains, - /** {@inheritdoc MaxContainsDecorator} */ - $maxContains, -] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.maxContains"]); - -export const [ - /** Check if the given array is annotated with `@uniqueItems` decorator */ - getUniqueItems, - setUniqueItems, -] = useStateMap(JsonSchemaStateKeys["JsonSchema.uniqueItems"]); -/** {@inheritdoc UniqueItemsDecorator} */ -export const $uniqueItems: UniqueItemsDecorator = (context: DecoratorContext, target: Type) => - setUniqueItems(context.program, target, true); - -export const [ - /** Get minimum number of properties set by `@minProperties` decorator */ - getMinProperties, - setMinProperties, - /** {@inheritdoc MinPropertiesDecorator} */ - $minProperties, -] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.minProperties"]); - -export const [ - /** Get maximum number of properties set by `@maxProperties` decorator */ - - getMaxProperties, - setMaxProperties, - /** {@inheritdoc MaxPropertiesDecorator} */ - $maxProperties, -] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.maxProperties"]); +/** Check if given type is annotated with `@oneOf` decorator */ +export function isOneOf(program: Program, target: Type): boolean { + return isOneOfOnUnion(program, target as Union); +} -export const [ - /** Get content encoding as configured by `@contentEncoding` decorator. */ - getContentEncoding, - setContentEncoding, - /** {@inheritdoc ContentEncodingDecorator} */ - $contentEncoding, -] = createDataDecorator( - JsonSchemaStateKeys["JsonSchema.contentEncoding"], -); +/** + * Check if the given array is annotated with `@uniqueItems` decorator. + * + * Returns `true` when the decorator is applied and `undefined` otherwise, so that callers + * can distinguish "not set" from "set" when building constraint objects. + */ +export function getUniqueItems(program: Program, target: Type): true | undefined { + return isUniqueItems(program, target) ? true : undefined; +} -export const [ - /** Get content media type as configured by `@contentMediaType` decorator. */ - getContentMediaType, - setContentMediaType, - /** {@inheritdoc ContentMediaTypeDecorator} */ - $contentMediaType, -] = createDataDecorator( - JsonSchemaStateKeys["JsonSchema.contentMediaType"], -); +/** Get content encoding as configured by `@contentEncoding` decorator. */ +export function getContentEncoding(program: Program, target: Type): string | undefined { + return getContentEncodingOnScalar(program, target as Scalar); +} -export const [ - /** Get content schema set with `@contentSchema` decorator */ - getContentSchema, - setContentSchema, - /** {@inheritdoc ContentSchemaDecorator} */ - $contentSchema, -] = createDataDecorator( - JsonSchemaStateKeys["JsonSchema.contentSchema"], -); +/** Get content media type as configured by `@contentMediaType` decorator. */ +export function getContentMediaType(program: Program, target: Type): string | undefined { + return getContentMediaTypeOnScalar(program, target as Scalar); +} -export const [ - /** Get prefix items set with `@prefixItems` decorator */ - getPrefixItems, - setPrefixItems, -] = useStateMap(JsonSchemaStateKeys["JsonSchema.prefixItems"]); +/** Get content schema set with `@contentSchema` decorator */ +export function getContentSchema(program: Program, target: Type): Type | undefined { + return getContentSchemaOnScalar(program, target as Scalar); +} -/** {@inheritdoc PrefixItemsDecorator} */ -export const $prefixItems: PrefixItemsDecorator = ( - context: DecoratorContext, - target: Type, - value: Type, -) => { - setPrefixItems(context.program, target, value as Tuple); // This cast is incorrect and would cause a crash https://github.com/microsoft/typespec/issues/4742 -}; +/** Get prefix items set with `@prefixItems` decorator */ +export function getPrefixItems(program: Program, target: Type): Tuple | undefined { + // This cast is incorrect and would cause a crash https://github.com/microsoft/typespec/issues/4742 + return getPrefixItemsAsType(program, target) as Tuple | undefined; +} /** * Data type containing information about an extension. diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index bd181361ae8..f5039360b4d 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -1,12 +1,15 @@ +export type { + ExtensionDecorator, + JsonSchemaDecorator, +} from "../generated-defs/TypeSpec.JsonSchema.js"; +/* eslint-disable @typescript-eslint/no-deprecated */ export type { BaseUriDecorator, ContainsDecorator, ContentEncodingDecorator, ContentMediaTypeDecorator, ContentSchemaDecorator, - ExtensionDecorator, IdDecorator, - JsonSchemaDecorator, MaxContainsDecorator, MaxPropertiesDecorator, MinContainsDecorator, @@ -15,7 +18,8 @@ export type { OneOfDecorator, PrefixItemsDecorator, UniqueItemsDecorator, -} from "../generated-defs/TypeSpec.JsonSchema.js"; +} from "./back-compat.js"; +/* eslint-enable @typescript-eslint/no-deprecated */ /** @internal */ export { JsonSchemaEmitter } from "./json-schema-emitter.js"; @@ -25,15 +29,14 @@ export type { JSONSchemaEmitterOptions } from "./lib.js"; /** @internal */ export const namespace = "TypeSpec.JsonSchema"; +/* eslint-disable @typescript-eslint/no-deprecated */ export { $baseUri, $contains, $contentEncoding, $contentMediaType, $contentSchema, - $extension, $id, - $jsonSchema, $maxContains, $maxProperties, $minContains, @@ -42,6 +45,11 @@ export { $oneOf, $prefixItems, $uniqueItems, +} from "./back-compat.js"; +/* eslint-enable @typescript-eslint/no-deprecated */ +export { + $extension, + $jsonSchema, findBaseUri, getBaseUri, getContains, diff --git a/packages/json-schema/src/lib.ts b/packages/json-schema/src/lib.ts index ccca2095106..581f443de58 100644 --- a/packages/json-schema/src/lib.ts +++ b/packages/json-schema/src/lib.ts @@ -179,38 +179,6 @@ export const $lib = createTypeSpecLibrary({ }, state: { JsonSchema: { description: "State indexing types marked with @jsonSchema" }, - "JsonSchema.baseURI": { description: "Contains data configured with @baseUri decorator" }, - "JsonSchema.multipleOf": { description: "Contains data configured with @multipleOf decorator" }, - "JsonSchema.id": { description: "Contains data configured with @id decorator" }, - "JsonSchema.oneOf": { description: "Contains data configured with @oneOf decorator" }, - "JsonSchema.contains": { description: "Contains data configured with @contains decorator" }, - "JsonSchema.minContains": { - description: "Contains data configured with @minContains decorator", - }, - "JsonSchema.maxContains": { - description: "Contains data configured with @maxContains decorator", - }, - "JsonSchema.uniqueItems": { - description: "Contains data configured with @uniqueItems decorator", - }, - "JsonSchema.minProperties": { - description: "Contains data configured with @minProperties decorator", - }, - "JsonSchema.maxProperties": { - description: "Contains data configured with @maxProperties decorator", - }, - "JsonSchema.contentEncoding": { - description: "Contains data configured with @contentEncoding decorator", - }, - "JsonSchema.contentSchema": { - description: "Contains data configured with @contentSchema decorator", - }, - "JsonSchema.contentMediaType": { - description: "Contains data configured with @contentMediaType decorator", - }, - "JsonSchema.prefixItems": { - description: "Contains data configured with @prefixItems decorator", - }, "JsonSchema.extension": { description: "Contains data configured with @extension decorator" }, }, } as const); diff --git a/packages/json-schema/src/tsp-index.ts b/packages/json-schema/src/tsp-index.ts index 6d96dd80add..c92a910bbb8 100644 --- a/packages/json-schema/src/tsp-index.ts +++ b/packages/json-schema/src/tsp-index.ts @@ -1,24 +1,6 @@ import type { TypeSpecJsonSchemaDecorators } from "../generated-defs/TypeSpec.JsonSchema.js"; import type { TypeSpecJsonSchemaPrivateDecorators } from "../generated-defs/TypeSpec.JsonSchema.Private.js"; -import { - $baseUri, - $contains, - $contentEncoding, - $contentMediaType, - $contentSchema, - $extension, - $id, - $jsonSchema, - $maxContains, - $maxProperties, - $minContains, - $minProperties, - $multipleOf, - $oneOf, - $prefixItems, - $uniqueItems, - $validatesRawJson, -} from "./decorators.js"; +import { $extension, $jsonSchema, $validatesRawJson } from "./decorators.js"; export { $flags, $lib } from "./lib.js"; @@ -26,20 +8,6 @@ export { $flags, $lib } from "./lib.js"; export const $decorators = { "TypeSpec.JsonSchema": { jsonSchema: $jsonSchema, - baseUri: $baseUri, - id: $id, - oneOf: $oneOf, - multipleOf: $multipleOf, - contains: $contains, - minContains: $minContains, - maxContains: $maxContains, - uniqueItems: $uniqueItems, - minProperties: $minProperties, - maxProperties: $maxProperties, - contentEncoding: $contentEncoding, - prefixItems: $prefixItems, - contentMediaType: $contentMediaType, - contentSchema: $contentSchema, extension: $extension, } satisfies TypeSpecJsonSchemaDecorators, "TypeSpec.JsonSchema.Private": { diff --git a/packages/json-schema/src/utils.ts b/packages/json-schema/src/utils.ts index 396d871116b..d7ec44febc7 100644 --- a/packages/json-schema/src/utils.ts +++ b/packages/json-schema/src/utils.ts @@ -1,25 +1,4 @@ -import { - isTemplateDeclaration, - type DecoratorFunction, - type Model, - type Type, -} from "@typespec/compiler"; -import { useStateMap } from "@typespec/compiler/utils"; - -export function createDataDecorator< - T extends DecoratorFunction, - Target extends Type = Parameters[1], ->(key: symbol, validate?: (...args: Parameters) => boolean) { - const [getData, setData] = useStateMap[2]>(key); - const decorator = (...args: Parameters) => { - if (validate && !validate(...args)) { - return; - } - const [context, target, value] = args; - setData(context.program, target, value); - }; - return [getData, setData, decorator as T] as const; -} +import { isTemplateDeclaration, type Model } from "@typespec/compiler"; export function includeDerivedModel(model: Model): boolean { return ( diff --git a/packages/json-schema/test/auto-decorators.test.ts b/packages/json-schema/test/auto-decorators.test.ts new file mode 100644 index 00000000000..be343187fc1 --- /dev/null +++ b/packages/json-schema/test/auto-decorators.test.ts @@ -0,0 +1,95 @@ +import { t } from "@typespec/compiler/testing"; +import { expect, it } from "vitest"; +import { + getBaseUri, + getContentEncoding, + getId, + getMinContains, + getMultipleOf, + getMultipleOfAsNumeric, + getPrefixItems, + getUniqueItems, + isOneOf, +} from "../src/decorators.js"; +import { ApiTester, emitSchema } from "./utils.js"; + +// The metadata-only decorators of this library are declared as `auto dec`, which is gated behind +// the experimental `auto-decorators` compiler feature. The library opts itself in via its own +// tspconfig.yaml, so a consumer project must be able to use them without enabling anything. +it("consumers can use the decorators without enabling the auto-decorators feature", async () => { + await emitSchema(` + @oneOf + union Pet { + cat: string, + dog: int32, + } + + @id("custom-id") + model Foo { + @uniqueItems tags: string[]; + @multipleOf(10) count: int32; + @contentEncoding("base64url") blob: string; + } + `); +}); + +it("exposes the stored values through the public accessors", async () => { + const { Foo, Pet, tags, count, blob, program } = await ApiTester.compile(t.code` + @id("custom-id") + model ${t.model("Foo")} { + @uniqueItems ${t.modelProperty("tags")}: string[]; + @multipleOf(10) ${t.modelProperty("count")}: int32; + @contentEncoding("base64url") ${t.modelProperty("blob")}: string; + } + + @oneOf + union ${t.union("Pet")} { + cat: string, + dog: int32, + } + `); + + expect(getId(program, Foo)).toEqual("custom-id"); + expect(isOneOf(program, Pet)).toBe(true); + expect(getUniqueItems(program, tags)).toBe(true); + expect(getMultipleOf(program, count)).toEqual(10); + expect(getMultipleOfAsNumeric(program, count)?.asNumber()).toEqual(10); + expect(getContentEncoding(program, blob)).toEqual("base64url"); +}); + +it("returns undefined from accessors when the decorator is not applied", async () => { + const { plain, program } = await ApiTester.compile(t.code` + model Foo { + ${t.modelProperty("plain")}: string[]; + } + `); + + expect(getUniqueItems(program, plain)).toBeUndefined(); + expect(getMinContains(program, plain)).toBeUndefined(); + expect(getPrefixItems(program, plain)).toBeUndefined(); + expect(isOneOf(program, plain)).toBe(false); +}); + +it("resolves the base uri from the enclosing namespace", async () => { + const { Foo, program } = await ApiTester.compile(t.code` + @baseUri("https://example.com/schemas/") + namespace Schemas { + model ${t.model("Foo")} {} + } + `); + + expect(getBaseUri(program, Foo.namespace!)).toEqual("https://example.com/schemas/"); + expect(getBaseUri(program, Foo)).toBeUndefined(); +}); + +it("warns when a metadata decorator is applied twice on the same declaration", async () => { + const [, diagnostics] = await ApiTester.compileAndDiagnose(` + model Foo { + @minContains(1) + @minContains(2) + values: string[]; + } + `); + + expect(diagnostics.map((d) => d.code)).toContain("duplicate-decorator"); +}); diff --git a/packages/json-schema/tspconfig.yaml b/packages/json-schema/tspconfig.yaml new file mode 100644 index 00000000000..efd0fcf3a83 --- /dev/null +++ b/packages/json-schema/tspconfig.yaml @@ -0,0 +1,7 @@ +# Opt this library into the experimental `auto-decorators` feature so that the +# metadata-only decorators declared as `auto dec` in lib/main.tsp are permitted +# in this library's own source. Per-package feature enablement is resolved from +# the owning package's config, so consumers do not need to enable it. +kind: project +features: + - auto-decorators