From e9a65ca53d57c7c6e480cf1489355a71772df9dd Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 15 Oct 2024 12:12:26 -0700 Subject: [PATCH] Configure api extractor for json schema and add docs and refactor (#4732) Configure api extractor and add docs. Additionally refactored to use some newer pattern of decorator declaration. --- ...schema-api-extractor-2024-9-14-22-42-24.md | 8 + packages/json-schema/api-extractor.json | 4 + packages/json-schema/package.json | 5 +- packages/json-schema/src/decorators.ts | 365 +++++++++--------- packages/json-schema/src/index.ts | 64 ++- .../json-schema/src/json-schema-emitter.ts | 54 +-- packages/json-schema/src/lib.ts | 62 ++- packages/json-schema/src/on-emit.ts | 11 +- packages/json-schema/src/testing/index.ts | 2 +- packages/json-schema/src/tsp-index.ts | 4 +- packages/json-schema/src/utils.ts | 17 + packages/json-schema/test/extension.test.ts | 2 +- packages/json-schema/test/utils.ts | 4 +- packages/json-schema/tsconfig.json | 3 +- 14 files changed, 375 insertions(+), 230 deletions(-) create mode 100644 .chronus/changes/json-schema-api-extractor-2024-9-14-22-42-24.md create mode 100644 packages/json-schema/api-extractor.json create mode 100644 packages/json-schema/src/utils.ts diff --git a/.chronus/changes/json-schema-api-extractor-2024-9-14-22-42-24.md b/.chronus/changes/json-schema-api-extractor-2024-9-14-22-42-24.md new file mode 100644 index 0000000000..b598c64712 --- /dev/null +++ b/.chronus/changes/json-schema-api-extractor-2024-9-14-22-42-24.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/json-schema" +--- + +Document exported types diff --git a/packages/json-schema/api-extractor.json b/packages/json-schema/api-extractor.json new file mode 100644 index 0000000000..2069b8ac37 --- /dev/null +++ b/packages/json-schema/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../api-extractor.base.json" +} diff --git a/packages/json-schema/package.json b/packages/json-schema/package.json index 4d5eaeb5f6..277e7d288b 100644 --- a/packages/json-schema/package.json +++ b/packages/json-schema/package.json @@ -36,7 +36,7 @@ }, "scripts": { "clean": "rimraf ./dist ./temp", - "build": "npm run gen-extern-signature && tsc -p . && npm run lint-typespec-library", + "build": "npm run gen-extern-signature && tsc -p . && npm run lint-typespec-library && pnpm api-extractor", "watch": "tsc -p . --watch", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", @@ -45,7 +45,8 @@ "test:ci": "vitest run --coverage --reporter=junit --reporter=default", "lint": "eslint . --max-warnings=0", "lint:fix": "eslint . --fix", - "regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/emitters/json-schema/reference" + "regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/emitters/json-schema/reference", + "api-extractor": "api-extractor run --local --verbose" }, "files": [ "lib/*.tsp", diff --git a/packages/json-schema/src/decorators.ts b/packages/json-schema/src/decorators.ts index cb132efaec..df2c93bf65 100644 --- a/packages/json-schema/src/decorators.ts +++ b/packages/json-schema/src/decorators.ts @@ -1,22 +1,20 @@ import { - DecoratorContext, - Enum, - Model, - ModelProperty, - Namespace, - Numeric, - Program, - Scalar, - Tuple, - Type, - Union, + type DecoratorContext, + type Enum, isType, + type Model, + type Namespace, + type Program, + type Scalar, setTypeSpecNamespace, + type Tuple, + type Type, typespecTypeToJson, + type Union, } from "@typespec/compiler"; -import { ValidatesRawJsonDecorator } from "../generated-defs/TypeSpec.JsonSchema.Private.js"; -import { - BaseUriDecorator, +import { unsafe_useStateMap, unsafe_useStateSet } from "@typespec/compiler/experimental"; +import type { ValidatesRawJsonDecorator } from "../generated-defs/TypeSpec.JsonSchema.Private.js"; +import type { ContainsDecorator, ContentEncodingDecorator, ContentMediaTypeDecorator, @@ -33,18 +31,26 @@ import { PrefixItemsDecorator, UniqueItemsDecorator, } from "../generated-defs/TypeSpec.JsonSchema.js"; -import { createStateSymbol } from "./lib.js"; +import { JsonSchemaStateKeys } from "./lib.js"; +import { createDataDecorator } from "./utils.js"; +/** + * TypeSpec Types that can create a json schmea declaration + */ export type JsonSchemaDeclaration = Model | Union | Enum | Scalar; -const jsonSchemaKey = createStateSymbol("JsonSchema"); - +export const [ + /** Check if the given type is annotated with `@jsonSchema` */ + getJsonSchema, + markJsonSchema, +] = unsafe_useStateSet(JsonSchemaStateKeys.JsonSchema); +/** {@inheritdoc JsonSchemaDecorator} */ export const $jsonSchema: JsonSchemaDecorator = ( context: DecoratorContext, target: Type, baseUriOrId?: string, ) => { - context.program.stateSet(jsonSchemaKey).add(target); + markJsonSchema(context.program, target); if (baseUriOrId) { if (target.kind === "Namespace") { context.call($baseUri, target, baseUriOrId); @@ -54,19 +60,15 @@ export const $jsonSchema: JsonSchemaDecorator = ( } }; -const baseUriKey = createStateSymbol("JsonSchema.baseURI"); -export const $baseUri: BaseUriDecorator = ( - context: DecoratorContext, - target: Namespace, - baseUri: string, -) => { - context.program.stateMap(baseUriKey).set(target, baseUri); -}; - -export function getBaseUri(program: Program, target: Type) { - return program.stateMap(baseUriKey).get(target); -} +export const [ + /** Get base uri set via `@baseUri` decorator */ + getBaseUri, + setBaseUri, + /** {@inheritdoc BaseUriDecorator} */ + $baseUri, +] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.baseURI"]); +/** Find base uri for the given type. */ export function findBaseUri( program: Program, target: JsonSchemaDeclaration | Namespace, @@ -81,7 +83,12 @@ export function findBaseUri( return baseUrl; } -export function isJsonSchemaDeclaration(program: Program, target: JsonSchemaDeclaration) { +/** + * Check if the given type is annoted with `@jsonSchema` or within a container annoted with `@jsonSchema`. + * @param program TypeSpec program + * @param target Type + */ +export function isJsonSchemaDeclaration(program: Program, target: JsonSchemaDeclaration): boolean { let current: JsonSchemaDeclaration | Namespace | undefined = target; do { if (getJsonSchema(program, current)) { @@ -94,180 +101,157 @@ export function isJsonSchemaDeclaration(program: Program, target: JsonSchemaDecl return false; } +/** + * Returns types that are annotated with `@jsonSchema` or contained within a namespace that is annoted with `@jsonSchema`. + * @param program TypeSpec program + */ export function getJsonSchemaTypes(program: Program): (Namespace | Model)[] { - return [...(program.stateSet(jsonSchemaKey) || [])] as (Namespace | Model)[]; + return [...(program.stateSet(JsonSchemaStateKeys.JsonSchema) || [])] as (Namespace | Model)[]; } -export function getJsonSchema(program: Program, target: Type) { - return program.stateSet(jsonSchemaKey).has(target); -} +export const [ + /** Get value set by `@multipleOf` decorator as a `Numeric` type. */ + getMultipleOfAsNumeric, + setMultipleOf, + /** {@inheritdoc MultipleOfDecorator} */ -const multipleOfKey = createStateSymbol("JsonSchema.multipleOf"); -export const $multipleOf: MultipleOfDecorator = ( - context: DecoratorContext, - target: Scalar | ModelProperty, - value: Numeric, -) => { - context.program.stateMap(multipleOfKey).set(target, value); -}; + $multipleOf, +] = createDataDecorator(JsonSchemaStateKeys["JsonSchema.multipleOf"]); -export function getMultipleOfAsNumeric(program: Program, target: Type): Numeric | undefined { - return program.stateMap(multipleOfKey).get(target); -} +/** 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; } -const idKey = createStateSymbol("JsonSchema.id"); -export const $id: IdDecorator = (context: DecoratorContext, target: Type, value: string) => { - context.program.stateMap(idKey).set(target, value); -}; - -export function getId(program: Program, target: Type) { - return program.stateMap(idKey).get(target); -} - -const oneOfKey = createStateSymbol("JsonSchema.oneOf"); +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, +] = unsafe_useStateSet(JsonSchemaStateKeys["JsonSchema.oneOf"]); + +/** {@inheritdoc OneOfDecorator} */ export const $oneOf: OneOfDecorator = (context: DecoratorContext, target: Type) => { - context.program.stateMap(oneOfKey).set(target, true); -}; - -export function isOneOf(program: Program, target: Type) { - return program.stateMap(oneOfKey).has(target); -} - -const containsKey = createStateSymbol("JsonSchema.contains"); -export const $contains: ContainsDecorator = ( - context: DecoratorContext, - target: Type, - value: Type, -) => { - context.program.stateMap(containsKey).set(target, value); -}; - -export function getContains(program: Program, target: Type) { - return program.stateMap(containsKey).get(target); -} - -const minContainsKey = createStateSymbol("JsonSchema.minContains"); -export const $minContains: MinContainsDecorator = ( - context: DecoratorContext, - target: Type, - value: number, -) => { - context.program.stateMap(minContainsKey).set(target, value); + markOneOf(context.program, target); }; -export function getMinContains(program: Program, target: Type) { - return program.stateMap(minContainsKey).get(target); -} - -const maxContainsKey = createStateSymbol("JsonSchema.maxContains"); -export const $maxContains: MaxContainsDecorator = ( - context: DecoratorContext, - target: Type, - value: number, -) => { - context.program.stateMap(maxContainsKey).set(target, value); -}; - -export function getMaxContains(program: Program, target: Type) { - return program.stateMap(maxContainsKey).get(target); -} - -const uniqueItemsKey = createStateSymbol("JsonSchema.uniqueItems"); -export const $uniqueItems: UniqueItemsDecorator = (context: DecoratorContext, target: Type) => { - context.program.stateMap(uniqueItemsKey).set(target, true); -}; - -export function getUniqueItems(program: Program, target: Type) { - return program.stateMap(uniqueItemsKey).get(target); -} - -const minPropertiesKey = createStateSymbol("JsonSchema.minProperties"); -export const $minProperties: MinPropertiesDecorator = ( - context: DecoratorContext, - target: Type, - value: number, -) => { - context.program.stateMap(minPropertiesKey).set(target, value); -}; - -export function getMinProperties(program: Program, target: Type) { - return program.stateMap(minPropertiesKey).get(target); -} - -const maxPropertiesKey = createStateSymbol("JsonSchema.maxProperties"); -export const $maxProperties: MaxPropertiesDecorator = ( - context: DecoratorContext, - target: Type, - value: number, -) => { - context.program.stateMap(maxPropertiesKey).set(target, value); -}; - -export function getMaxProperties(program: Program, target: Type) { - return program.stateMap(maxPropertiesKey).get(target); -} - -const contentEncodingKey = createStateSymbol("JsonSchema.contentEncoding"); -export const $contentEncoding: ContentEncodingDecorator = ( - context: DecoratorContext, - target: Scalar | ModelProperty, - value: string, -) => { - context.program.stateMap(contentEncodingKey).set(target, value); -}; - -export function getContentEncoding(program: Program, target: Type): string { - return program.stateMap(contentEncodingKey).get(target); -} - -const contentMediaType = createStateSymbol("JsonSchema.contentMediaType"); -export const $contentMediaType: ContentMediaTypeDecorator = ( - context: DecoratorContext, - target: Scalar | ModelProperty, - value: string, -) => { - context.program.stateMap(contentMediaType).set(target, value); -}; - -export function getContentMediaType(program: Program, target: Type): string { - return program.stateMap(contentMediaType).get(target); -} - -const contentSchemaKey = createStateSymbol("JsonSchema.contentSchema"); -export const $contentSchema: ContentSchemaDecorator = ( - context: DecoratorContext, - target: Scalar | ModelProperty, - value: Type, -) => { - context.program.stateMap(contentSchemaKey).set(target, value); -}; - -export function getContentSchema(program: Program, target: Type) { - return program.stateMap(contentSchemaKey).get(target); -} - -const prefixItemsKey = createStateSymbol("JsonSchema.prefixItems"); +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, +] = unsafe_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"]); + +export const [ + /** Get content encoding as configured by `@contentEncoding` decorator. */ + getContentEncoding, + setContentEncoding, + /** {@inheritdoc ContentEncodingDecorator} */ + $contentEncoding, +] = createDataDecorator( + JsonSchemaStateKeys["JsonSchema.contentEncoding"], +); + +export const [ + /** Get content media type as configured by `@contentMediaType` decorator. */ + getContentMediaType, + setContentMediaType, + /** {@inheritdoc ContentMediaTypeDecorator} */ + $contentMediaType, +] = createDataDecorator( + JsonSchemaStateKeys["JsonSchema.contentMediaType"], +); + +export const [ + /** Get content schema set with `@contentSchema` decorator */ + getContentSchema, + setContentSchema, + /** {@inheritdoc ContentSchemaDecorator} */ + $contentSchema, +] = createDataDecorator( + JsonSchemaStateKeys["JsonSchema.contentSchema"], +); + +export const [ + /** Get prefix items set with `@prefixItems` decorator */ + getPrefixItems, + setPrefixItems, +] = unsafe_useStateMap(JsonSchemaStateKeys["JsonSchema.prefixItems"]); + +/** {@inheritdoc PrefixItemsDecorator} */ export const $prefixItems: PrefixItemsDecorator = ( context: DecoratorContext, target: Type, value: Type, ) => { - context.program.stateMap(prefixItemsKey).set(target, value); + setPrefixItems(context.program, target, value as Tuple); // This cast is incorrect and would cause a crash https://github.com/microsoft/typespec/issues/4742 }; -export function getPrefixItems(program: Program, target: Type): Tuple | undefined { - return program.stateMap(prefixItemsKey).get(target); -} - +/** + * Data type containing information about an extension. + */ export interface ExtensionRecord { + /** Extension key */ key: string; + /** Extension value */ value: Type | unknown; } -const extensionsKey = createStateSymbol("JsonSchema.extension"); +const [getExtensionsInternal, _, getExtensionsStateMap] = unsafe_useStateMap< + Type, + ExtensionRecord[] +>(JsonSchemaStateKeys["JsonSchema.extension"]); +/** {@inheritdoc ExtensionDecorator} */ export const $extension: ExtensionDecorator = ( context: DecoratorContext, target: Type, @@ -277,12 +261,24 @@ export const $extension: ExtensionDecorator = ( setExtension(context.program, target, key, value); }; +/** + * Get extensions set via the `@extension` decorator on the given type + * @param program TypeSpec program + * @param target Type + */ export function getExtensions(program: Program, target: Type): ExtensionRecord[] { - return program.stateMap(extensionsKey).get(target) ?? []; + return getExtensionsInternal(program, target) ?? []; } - +/** + * Set extension on the given type(Same as calling `@extension` decorator) + * @param program TypeSpec program + * @param target Type + * @param key Extension key + * @param value Extension value + */ export function setExtension(program: Program, target: Type, key: string, value: unknown) { - const stateMap = program.stateMap(extensionsKey) as Map; + const stateMap = getExtensionsStateMap(program); + const extensions = stateMap.has(target) ? stateMap.get(target)! : stateMap.set(target, []).get(target)!; @@ -311,6 +307,7 @@ function isJsonTemplateType( ); } +/** @internal */ export const $validatesRawJson: ValidatesRawJsonDecorator = ( context: DecoratorContext, target: Model, diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index ac6a098380..bd181361ae 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -1,10 +1,70 @@ +export type { + BaseUriDecorator, + ContainsDecorator, + ContentEncodingDecorator, + ContentMediaTypeDecorator, + ContentSchemaDecorator, + ExtensionDecorator, + IdDecorator, + JsonSchemaDecorator, + MaxContainsDecorator, + MaxPropertiesDecorator, + MinContainsDecorator, + MinPropertiesDecorator, + MultipleOfDecorator, + OneOfDecorator, + PrefixItemsDecorator, + UniqueItemsDecorator, +} from "../generated-defs/TypeSpec.JsonSchema.js"; + +/** @internal */ export { JsonSchemaEmitter } from "./json-schema-emitter.js"; -export { $flags, $lib, EmitterOptionsSchema, JSONSchemaEmitterOptions } from "./lib.js"; +export { $flags, $lib, EmitterOptionsSchema } from "./lib.js"; +export type { JSONSchemaEmitterOptions } from "./lib.js"; /** @internal */ export const namespace = "TypeSpec.JsonSchema"; -export * from "./decorators.js"; +export { + $baseUri, + $contains, + $contentEncoding, + $contentMediaType, + $contentSchema, + $extension, + $id, + $jsonSchema, + $maxContains, + $maxProperties, + $minContains, + $minProperties, + $multipleOf, + $oneOf, + $prefixItems, + $uniqueItems, + findBaseUri, + getBaseUri, + getContains, + getContentEncoding, + getContentMediaType, + getContentSchema, + getExtensions, + getId, + getJsonSchema, + getJsonSchemaTypes, + getMaxContains, + getMaxProperties, + getMinContains, + getMinProperties, + getMultipleOf, + getMultipleOfAsNumeric, + getPrefixItems, + getUniqueItems, + isJsonSchemaDeclaration, + isOneOf, + setExtension, +} from "./decorators.js"; +export type { ExtensionRecord, JsonSchemaDeclaration } from "./decorators.js"; export { $onEmit } from "./on-emit.js"; /** @internal */ export { $decorators } from "./tsp-index.js"; diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index 2486b53cd3..94c19813b7 100644 --- a/packages/json-schema/src/json-schema-emitter.ts +++ b/packages/json-schema/src/json-schema-emitter.ts @@ -1,20 +1,20 @@ import { - BooleanLiteral, - DiagnosticTarget, - Enum, - EnumMember, - IntrinsicType, - Model, - ModelProperty, - NumericLiteral, - Program, - Scalar, - StringLiteral, - StringTemplate, - Tuple, - Type, - Union, - UnionVariant, + type BooleanLiteral, + type DiagnosticTarget, + type Enum, + type EnumMember, + type IntrinsicType, + type Model, + type ModelProperty, + type NumericLiteral, + type Program, + type Scalar, + type StringLiteral, + type StringTemplate, + type Tuple, + type Type, + type Union, + type UnionVariant, compilerAssert, emitFile, explainStringTemplateNotSerializable, @@ -42,22 +42,22 @@ import { } from "@typespec/compiler"; import { ArrayBuilder, - Context, + type Context, Declaration, - EmitEntity, - EmittedSourceFile, - EmitterOutput, + type EmitEntity, + type EmittedSourceFile, + type EmitterOutput, ObjectBuilder, Placeholder, - Scope, - SourceFile, - SourceFileScope, + type Scope, + type SourceFile, + type SourceFileScope, TypeEmitter, } from "@typespec/compiler/emitter-framework"; import { DuplicateTracker } from "@typespec/compiler/utils"; import { stringify } from "yaml"; import { - JsonSchemaDeclaration, + type JsonSchemaDeclaration, findBaseUri, getContains, getContentEncoding, @@ -75,7 +75,9 @@ import { isJsonSchemaDeclaration, isOneOf, } from "./index.js"; -import { JSONSchemaEmitterOptions, reportDiagnostic } from "./lib.js"; +import { type JSONSchemaEmitterOptions, reportDiagnostic } from "./lib.js"; + +/** @internal */ export class JsonSchemaEmitter extends TypeEmitter, JSONSchemaEmitterOptions> { #idDuplicateTracker = new DuplicateTracker(); #typeForSourceFile = new Map, JsonSchemaDeclaration>(); @@ -565,7 +567,7 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche } }; - const applyTypeConstraint = (fn: (p: Program, t: Type) => Type, key: string) => { + const applyTypeConstraint = (fn: (p: Program, t: Type) => Type | undefined, key: string) => { const constraintType = fn(this.emitter.getProgram(), type); if (constraintType) { const ref = this.emitter.emitTypeReference(constraintType); diff --git a/packages/json-schema/src/lib.ts b/packages/json-schema/src/lib.ts index b810a538b8..24159c4bc2 100644 --- a/packages/json-schema/src/lib.ts +++ b/packages/json-schema/src/lib.ts @@ -1,17 +1,30 @@ import { createTypeSpecLibrary, definePackageFlags, - JSONSchemaType, + type JSONSchemaType, paramMessage, } from "@typespec/compiler"; +/** + * File type + */ export type FileType = "yaml" | "json"; + +/** + * Strategy for handling the int64 type in the resulting json schema. + * - string: As a string + * - number: As a number (In JavaScript, int64 cannot be accurately represented as number) + * + */ export type Int64Strategy = "string" | "number"; +/** + * Json schema emitter options + */ export interface JSONSchemaEmitterOptions { /** * Serialize the schema as either yaml or json. - * @default yaml, it not specified infer from the `output-file` extension + * @defaultValue yaml it not specified infer from the `output-file` extension */ "file-type"?: FileType; @@ -32,7 +45,7 @@ export interface JSONSchemaEmitterOptions { /** * When true, emit all model declarations to JSON Schema without requiring - * the @jsonSchema decorator. + * the `@jsonSchema` decorator. */ emitAllModels?: boolean; @@ -44,6 +57,9 @@ export interface JSONSchemaEmitterOptions { emitAllRefs?: boolean; } +/** + * Internal: Json Schema emitter options schema + */ export const EmitterOptionsSchema: JSONSchemaType = { type: "object", additionalProperties: false, @@ -85,6 +101,7 @@ export const EmitterOptionsSchema: JSONSchemaType = { required: [], }; +/** Internal: TypeSpec library definition */ export const $lib = createTypeSpecLibrary({ name: "@typespec/json-schema", diagnostics: { @@ -110,10 +127,47 @@ export const $lib = createTypeSpecLibrary({ emitter: { options: EmitterOptionsSchema as JSONSchemaType, }, + 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); +/** Internal: TypeSpec flags */ export const $flags = definePackageFlags({}); -export const { reportDiagnostic, createStateSymbol } = $lib; +export const { reportDiagnostic, createStateSymbol, stateKeys: JsonSchemaStateKeys } = $lib; export type JsonSchemaLibrary = typeof $lib; diff --git a/packages/json-schema/src/on-emit.ts b/packages/json-schema/src/on-emit.ts index c2e0321eca..33fc8137e5 100644 --- a/packages/json-schema/src/on-emit.ts +++ b/packages/json-schema/src/on-emit.ts @@ -1,15 +1,16 @@ -import { EmitContext, Enum, Model, Scalar, Union } from "@typespec/compiler"; +import type { EmitContext, Enum, Model, Scalar, Union } from "@typespec/compiler"; import { createAssetEmitter } from "@typespec/compiler/emitter-framework"; import { getJsonSchemaTypes } from "./decorators.js"; import { JsonSchemaEmitter } from "./json-schema-emitter.js"; -import { JSONSchemaEmitterOptions } from "./lib.js"; - -export { JsonSchemaEmitter } from "./json-schema-emitter.js"; -export { $flags, $lib, EmitterOptionsSchema, JSONSchemaEmitterOptions } from "./lib.js"; +import type { JSONSchemaEmitterOptions } from "./lib.js"; +export { $flags, $lib, EmitterOptionsSchema, type JSONSchemaEmitterOptions } from "./lib.js"; export const namespace = "TypeSpec.JsonSchema"; export type JsonSchemaDeclaration = Model | Union | Enum | Scalar; +/** + * Internal: TypeSpec emitter entry point + */ export async function $onEmit(context: EmitContext) { const emitter = createAssetEmitter(context.program, JsonSchemaEmitter as any, context); diff --git a/packages/json-schema/src/testing/index.ts b/packages/json-schema/src/testing/index.ts index b59565f2f5..78267d6095 100644 --- a/packages/json-schema/src/testing/index.ts +++ b/packages/json-schema/src/testing/index.ts @@ -1,7 +1,7 @@ import { createTestLibrary, findTestPackageRoot, - TypeSpecTestLibrary, + type TypeSpecTestLibrary, } from "@typespec/compiler/testing"; export const JsonSchemaTestLibrary: TypeSpecTestLibrary = createTestLibrary({ diff --git a/packages/json-schema/src/tsp-index.ts b/packages/json-schema/src/tsp-index.ts index ac065b5744..6d96dd80ad 100644 --- a/packages/json-schema/src/tsp-index.ts +++ b/packages/json-schema/src/tsp-index.ts @@ -1,5 +1,5 @@ -import { TypeSpecJsonSchemaDecorators } from "../generated-defs/TypeSpec.JsonSchema.js"; -import { TypeSpecJsonSchemaPrivateDecorators } from "../generated-defs/TypeSpec.JsonSchema.Private.js"; +import type { TypeSpecJsonSchemaDecorators } from "../generated-defs/TypeSpec.JsonSchema.js"; +import type { TypeSpecJsonSchemaPrivateDecorators } from "../generated-defs/TypeSpec.JsonSchema.Private.js"; import { $baseUri, $contains, diff --git a/packages/json-schema/src/utils.ts b/packages/json-schema/src/utils.ts new file mode 100644 index 0000000000..463957f4c6 --- /dev/null +++ b/packages/json-schema/src/utils.ts @@ -0,0 +1,17 @@ +import type { DecoratorFunction, Type } from "@typespec/compiler"; +import { unsafe_useStateMap } from "@typespec/compiler/experimental"; + +export function createDataDecorator< + T extends DecoratorFunction, + Target extends Type = Parameters[1], +>(key: symbol, validate?: (...args: Parameters) => boolean) { + const [getData, setData] = unsafe_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; +} diff --git a/packages/json-schema/test/extension.test.ts b/packages/json-schema/test/extension.test.ts index c650873784..1a021ffe32 100644 --- a/packages/json-schema/test/extension.test.ts +++ b/packages/json-schema/test/extension.test.ts @@ -1,4 +1,4 @@ -import { DecoratorContext, Type } from "@typespec/compiler"; +import type { DecoratorContext, Type } from "@typespec/compiler"; import { expectDiagnosticEmpty } from "@typespec/compiler/testing"; import assert from "assert"; import { describe, it } from "vitest"; diff --git a/packages/json-schema/test/utils.ts b/packages/json-schema/test/utils.ts index b94c80bee6..17c91ede9e 100644 --- a/packages/json-schema/test/utils.ts +++ b/packages/json-schema/test/utils.ts @@ -1,9 +1,9 @@ -import { Diagnostic } from "@typespec/compiler"; +import type { Diagnostic } from "@typespec/compiler"; import { createAssetEmitter } from "@typespec/compiler/emitter-framework"; import { createTestHost, expectDiagnosticEmpty } from "@typespec/compiler/testing"; import { parse } from "yaml"; import { JsonSchemaEmitter } from "../src/json-schema-emitter.js"; -import { JSONSchemaEmitterOptions } from "../src/lib.js"; +import type { JSONSchemaEmitterOptions } from "../src/lib.js"; import { JsonSchemaTestLibrary } from "../src/testing/index.js"; export async function getHostForCadlFile(contents: string, decorators?: Record) { diff --git a/packages/json-schema/tsconfig.json b/packages/json-schema/tsconfig.json index 284b90bcdc..19de0b3a83 100644 --- a/packages/json-schema/tsconfig.json +++ b/packages/json-schema/tsconfig.json @@ -4,7 +4,8 @@ "compilerOptions": { "outDir": "dist", "rootDir": ".", - "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo" + "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", + "verbatimModuleSyntax": true }, "include": ["src/**/*.ts", "generated-defs/**/*.ts", "test/**/*.ts"] }