diff --git a/.chronus/changes/graphql-regen-auto-accessor-docs-2026-9-2.md b/.chronus/changes/graphql-regen-auto-accessor-docs-2026-9-2.md new file mode 100644 index 00000000000..4e77fe62a66 --- /dev/null +++ b/.chronus/changes/graphql-regen-auto-accessor-docs-2026-9-2.md @@ -0,0 +1,8 @@ +--- +changeKind: internal +packages: + - "@typespec/graphql" +--- + +Regenerate the decorator signatures to pick up the doc comments now emitted for auto decorator +accessors. 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/.chronus/changes/tspd-auto-accessor-docs-2026-9-2.md b/.chronus/changes/tspd-auto-accessor-docs-2026-9-2.md new file mode 100644 index 00000000000..c444bc8cac2 --- /dev/null +++ b/.chronus/changes/tspd-auto-accessor-docs-2026-9-2.md @@ -0,0 +1,15 @@ +--- +changeKind: fix +packages: + - "@typespec/tspd" +--- + +Document the generated auto decorator accessors with the description of the decorator they read or +write, so libraries re-exporting them satisfy api-extractor's `ae-undocumented` rule. + +```ts +/** Mark a model as a GraphQL input type in the emitted schema. */ +export function isInputType(program: Program, target: Model): boolean { + return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target); +} +``` diff --git a/.chronus/changes/tspd-honor-library-config-2026-9-2.md b/.chronus/changes/tspd-honor-library-config-2026-9-2.md new file mode 100644 index 00000000000..e9b8ea3c1d8 --- /dev/null +++ b/.chronus/changes/tspd-honor-library-config-2026-9-2.md @@ -0,0 +1,9 @@ +--- +changeKind: fix +packages: + - "@typespec/tspd" +--- + +Honor the library's own `tspconfig.yaml` when generating signatures and reference documentation, so +libraries that opt into a compiler feature (such as `auto-decorators`) no longer report errors during +`gen-extern-signature` and `doc`. diff --git a/packages/graphql/generated-defs/TypeSpec.GraphQL.ts b/packages/graphql/generated-defs/TypeSpec.GraphQL.ts index c854d6aa9f0..45595962d2c 100644 --- a/packages/graphql/generated-defs/TypeSpec.GraphQL.ts +++ b/packages/graphql/generated-defs/TypeSpec.GraphQL.ts @@ -184,14 +184,34 @@ export type TypeSpecGraphQLDecorators = { specifiedBy: SpecifiedByDecorator; }; +/** + * Mark a model as a GraphQL input type in the emitted schema. + * + * This decorator is applied automatically by the mutation engine when it produces + * a model that is used in input position. The emitter uses this to emit the model + * as an `input` type rather than an object `type`. + */ export function isInputType(program: Program, target: Model): boolean { return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target); } +/** + * Mark a model as a GraphQL input type in the emitted schema. + * + * This decorator is applied automatically by the mutation engine when it produces + * a model that is used in input position. The emitter uses this to emit the model + * as an `input` type rather than an object `type`. + */ export function setInputType(program: Program, target: Model): void { setAutoDecorator(program, "TypeSpec.GraphQL.inputType", target); } +/** + * Mark a field, operation, or type as nullable in the emitted GraphQL schema. + * + * Applied automatically by the mutation engine when it strips `| null` from + * union types, and can also be applied directly in TypeSpec source. + */ export function isNullable( program: Program, target: ModelProperty | Operation | Union | Model, @@ -199,6 +219,12 @@ export function isNullable( return hasAutoDecorator(program, "TypeSpec.GraphQL.nullable", target); } +/** + * Mark a field, operation, or type as nullable in the emitted GraphQL schema. + * + * Applied automatically by the mutation engine when it strips `| null` from + * union types, and can also be applied directly in TypeSpec source. + */ export function setNullable( program: Program, target: ModelProperty | Operation | Union | Model, @@ -206,18 +232,44 @@ export function setNullable( setAutoDecorator(program, "TypeSpec.GraphQL.nullable", target); } +/** + * Mark a field or operation as having nullable array elements in the emitted GraphQL schema. + * + * Applied automatically by the mutation engine when it detects `Array` + * patterns. Causes the emitter to emit `[T]` instead of `[T!]`. + */ export function isNullableElements(program: Program, target: ModelProperty | Operation): boolean { return hasAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target); } +/** + * Mark a field or operation as having nullable array elements in the emitted GraphQL schema. + * + * Applied automatically by the mutation engine when it detects `Array` + * patterns. Causes the emitter to emit `[T]` instead of `[T!]`. + */ export function setNullableElements(program: Program, target: ModelProperty | Operation): void { setAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target); } +/** + * Mark a model as a `@oneOf` input object in the emitted GraphQL schema. + * + * This decorator is applied automatically by the mutation engine when it converts + * a union type in input context to a synthetic input object (since GraphQL unions + * are output-only). The emitter uses this to emit the `@oneOf` directive. + */ export function isOneOf(program: Program, target: Model): boolean { return hasAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target); } +/** + * Mark a model as a `@oneOf` input object in the emitted GraphQL schema. + * + * This decorator is applied automatically by the mutation engine when it converts + * a union type in input context to a synthetic input object (since GraphQL unions + * are output-only). The emitter uses this to emit the `@oneOf` directive. + */ export function setOneOf(program: Program, target: Model): void { setAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target); } diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts index cbf82755d92..0a380e25624 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 }); +} + +/** Specify that `oneOf` should be used instead of `anyOf` for that union. */ +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, +/** Specify that every item in the array must be unique. */ +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 diff --git a/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx index 37aa16099c9..9b84a6f0880 100644 --- a/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/auto-decorator-accessors.tsx @@ -1,5 +1,6 @@ import { code, For, List } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; +import type { Decorator } from "@typespec/compiler"; import { typespecCompiler } from "../external-packages/compiler.js"; import type { DecoratorSignature } from "../types.js"; import { ParameterTsType, TargetParameterTsType } from "./decorator-signature-type.js"; @@ -51,17 +52,20 @@ function AutoDecoratorReader(props: Readonly) { if (params.length === 0) { // No-arg auto decorator — generate `is*` function return ( - - {code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`} - + + + + {code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`} + + ); } @@ -99,17 +103,20 @@ function AutoDecoratorReader(props: Readonly) { } return ( - - {body} - + + + + {body} + + ); } @@ -161,13 +168,60 @@ function AutoDecoratorSetter(props: Readonly) { } return ( - - {body} - + + + + {body} + + ); } + +/** + * Render the decorator's own documentation as the accessor doc comment. + * + * Only the description is carried over: the `@param` tags of the decorator describe its TypeSpec + * parameters, which do not line up with the accessor signatures. + * + * The comment is rendered standalone rather than through the `doc` prop of + * `ts.FunctionDeclaration`, because that also emits `@param {Type}` tags whose type references count + * as value usages and would turn the type-only imports of this file into value imports. + */ +function AccessorDoc(props: Readonly<{ decorator: Decorator }>) { + const description = getDocDescription(props.decorator); + if (description === undefined) { + return null; + } + const lines = description.split("\n"); + const comment = + lines.length === 1 + ? `/** ${lines[0]} */` + : [`/**`, ...lines.map((line) => ` * ${line}`.trimEnd()), ` */`].join("\n"); + return <>{comment}; +} + +/** Get the description of a decorator, excluding any doc tag. */ +function getDocDescription(decorator: Decorator): string | undefined { + const docs = decorator.node?.docs; + if (docs === undefined || docs.length === 0) { + return undefined; + } + + const lines: string[] = []; + for (const doc of docs) { + for (const content of doc.content) { + for (const line of content.text.split("\n")) { + // Issue to escape @internal and other tsdoc tags https://github.com/microsoft/TypeScript/issues/47679 + lines.push(line.replaceAll("@internal", "@_internal")); + } + } + } + + const description = lines.join("\n").trim(); + return description === "" ? undefined : description; +} diff --git a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts index ef10780f977..e30af6a81ee 100644 --- a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts @@ -25,6 +25,7 @@ import { } from "@typespec/compiler"; import prettier from "prettier"; import { createDiagnostic } from "../ref-doc/lib.js"; +import { resolveLibraryCompilerOptions } from "../utils/library-config.js"; import { generateSignatures } from "./components/entity-signatures.js"; import type { DecoratorSignature, EntitySignature, FunctionSignature } from "./types.js"; @@ -155,6 +156,7 @@ export async function generateExternSignatureForExports( for (const entry of exports) { programs.push( await compile(host, entry.typespecEntrypoint, { + ...(await resolveLibraryCompilerOptions(host, entry.typespecEntrypoint)), parseOptions: { comments: true, docs: true }, }), ); diff --git a/packages/tspd/src/ref-doc/experimental.ts b/packages/tspd/src/ref-doc/experimental.ts index 58726dd8271..6cdefbd2236 100644 --- a/packages/tspd/src/ref-doc/experimental.ts +++ b/packages/tspd/src/ref-doc/experimental.ts @@ -2,6 +2,7 @@ import type { Diagnostic } from "@typespec/compiler"; import { compile, createDiagnosticCollector, joinPaths, NodeHost } from "@typespec/compiler"; import { mkdir, writeFile } from "fs/promises"; import prettier from "prettier"; +import { resolveLibraryCompilerOptions } from "../utils/library-config.js"; import { generateJsApiDocs } from "./api-docs.js"; import { renderReadme } from "./emitters/markdown.js"; import { renderToAstroStarlightMarkdown } from "./emitters/starlight.js"; @@ -74,6 +75,7 @@ export async function resolveLibraryRefDocsBase( if (pkgJson.tspMain) { const main = joinPaths(libraryPath, pkgJson.tspMain); const program = await compile(NodeHost, main, { + ...(await resolveLibraryCompilerOptions(NodeHost, main)), parseOptions: { comments: true, docs: true }, }); const refDoc = diagnostics.pipe(extractRefDocs(program, options)); diff --git a/packages/tspd/src/ref-doc/extractor.ts b/packages/tspd/src/ref-doc/extractor.ts index 9d5e7f2ec28..a1620b8d7f7 100644 --- a/packages/tspd/src/ref-doc/extractor.ts +++ b/packages/tspd/src/ref-doc/extractor.ts @@ -46,6 +46,7 @@ import { import { SyntaxKind, type DocUnknownTagNode } from "@typespec/compiler/ast"; import { readFile } from "fs/promises"; import { pathToFileURL } from "url"; +import { resolveLibraryCompilerOptions } from "../utils/library-config.js"; import { createDiagnostic, reportDiagnostic } from "./lib.js"; import type { DecoratorRefDoc, @@ -109,6 +110,7 @@ export async function extractLibraryRefDocs( if (tspMain) { const main = resolvePath(libraryPath, tspMain); const program = await compile(NodeHost, main, { + ...(await resolveLibraryCompilerOptions(NodeHost, main)), parseOptions: { comments: true, docs: true }, }); mainSourceFiles = new Set(program.sourceFiles.keys()); @@ -203,6 +205,7 @@ async function extractSubExports( const main = resolvePath(libraryPath, tspEntry); try { const program = await compile(NodeHost, main, { + ...(await resolveLibraryCompilerOptions(NodeHost, main)), parseOptions: { comments: true, docs: true }, }); const subRefDoc = diagnostics.pipe( diff --git a/packages/tspd/src/utils/library-config.ts b/packages/tspd/src/utils/library-config.ts new file mode 100644 index 00000000000..079ade2b722 --- /dev/null +++ b/packages/tspd/src/utils/library-config.ts @@ -0,0 +1,19 @@ +import { + resolveCompilerOptions, + type CompilerHost, + type CompilerOptions, +} from "@typespec/compiler"; + +/** + * Resolve the compiler options from the library's own `tspconfig.yaml`, so that features it opts + * into (such as `auto-decorators`) apply when tspd compiles it. + * + * tspd only ever inspects a library, so emitting is always disabled. + */ +export async function resolveLibraryCompilerOptions( + host: CompilerHost, + entrypoint: string, +): Promise { + const [options] = await resolveCompilerOptions(host, { cwd: process.cwd(), entrypoint }); + return { ...options, noEmit: true }; +} diff --git a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts index e9678c07a78..4dc9a33ad12 100644 --- a/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts +++ b/packages/tspd/test/gen-extern-signature/decorators-signatures.test.ts @@ -531,6 +531,49 @@ export function setMyMeta(program: Program, target: Model, value: { name: string }); }); + it("documents accessors with the decorator description, dropping doc tags", async () => { + await expectSignatures({ + code: ` + /** + * Specify the minimum number of instances the array must contain. + * + * @param value The minimum number of instances. + */ + auto dec myMin(target: Model, value: valueof int32); + `, + expected: ` +import { getAutoDecoratorValue, type Model, type Program, setAutoDecorator } from "@typespec/compiler"; + +/** Specify the minimum number of instances the array must contain. */ +export function getMyMin(program: Program, target: Model): number | undefined { + return getAutoDecoratorValue(program, "myMin", target)?.["value"] as any; +} + +/** Specify the minimum number of instances the array must contain. */ +export function setMyMin(program: Program, target: Model, value: number): void { + setAutoDecorator(program, "myMin", target, { value: value }); +} + `, + }); + }); + + it("renders a multi line description as a block comment", async () => { + const result = await generateDecoratorSignatures(` + /** + * First line. + * + * Second line. + */ + auto dec myFlag(target: Model); + `); + expect(result).toContain(`/** + * First line. + * + * Second line. + */ +export function isMyFlag`); + }); + it("generates accessor with fully-qualified name for namespaced auto decorator", async () => { const [{ program }] = await Tester.compileAndDiagnose( `