| /** | |
| * Plain data structures for describing schemas in a serializable form. A | |
| * `Representation` is not the original `Schema` object; it is a JSON-friendly | |
| * description of the schema's types, fields, unions, checks, annotations, and | |
| * references. | |
| * | |
| * This module defines the representation node types, document types, and | |
| * codecs used to validate those documents. It can build representation | |
| * documents from schema ASTs, turn representation documents back into schemas, | |
| * convert them to and from JSON Schema documents, and generate TypeScript code | |
| * artifacts for schema definitions. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Arr from "./Array.ts" | |
| import { format, formatPropertyKey } from "./Formatter.ts" | |
| import { collectBrands } from "./internal/schema/annotations.ts" | |
| import * as InternalRepresentation from "./internal/schema/representation.ts" | |
| import { unescapeToken } from "./JsonPointer.ts" | |
| import type * as JsonSchema from "./JsonSchema.ts" | |
| import * as Option from "./Option.ts" | |
| import * as Predicate from "./Predicate.ts" | |
| import * as Rec from "./Record.ts" | |
| import * as Schema from "./Schema.ts" | |
| import type * as SchemaAST from "./SchemaAST.ts" | |
| import * as SchemaGetter from "./SchemaGetter.ts" | |
| // ----------------------------------------------------------------------------- | |
| // specification | |
| // ----------------------------------------------------------------------------- | |
| /** | |
| * A custom type declaration, such as `Date`, `Option`, or `ReadonlySet`. | |
| * | |
| * **When to use** | |
| * | |
| * Use when inspecting or transforming non-primitive schema types. | |
| * | |
| * **Details** | |
| * | |
| * `typeParameters` holds the inner type arguments, such as the `A` in | |
| * `Option<A>`. `encodedSchema` is the fallback representation when no | |
| * {@link Reviver} recognizes this declaration. `annotations.typeConstructor` | |
| * identifies the declaration kind, such as `{ _tag: "effect/Option" }`. | |
| * | |
| * @see {@link Reviver} | |
| * @see {@link toSchemaDefaultReviver} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Declaration { | |
| readonly _tag: "Declaration" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly typeParameters: ReadonlyArray<Representation> | |
| readonly checks: ReadonlyArray<Check<DeclarationMeta>> | |
| readonly encodedSchema: Representation | |
| } | |
| /** | |
| * A lazily resolved representation used for recursive schemas. | |
| * | |
| * **Details** | |
| * | |
| * `thunk` points to the actual representation, possibly via a | |
| * {@link Reference}. `checks` is always empty on `Suspend` nodes. | |
| * | |
| * @see {@link Reference} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Suspend { | |
| readonly _tag: "Suspend" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly checks: readonly [] | |
| readonly thunk: Representation | |
| } | |
| /** | |
| * A named reference to a definition in the {@link References} map. | |
| * | |
| * **When to use** | |
| * | |
| * Use when a representation should point to a named definition instead of | |
| * embedding the definition inline. | |
| * | |
| * **Details** | |
| * | |
| * `$ref` is the key into `Document.references` or `MultiDocument.references`. | |
| * References are resolved lazily by {@link toSchema} and | |
| * {@link toCodeDocument}. | |
| * | |
| * **Gotchas** | |
| * | |
| * Resolution throws at runtime if the key is not found in the references map. | |
| * | |
| * @see {@link References} | |
| * @see {@link Document} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Reference { | |
| readonly _tag: "Reference" | |
| readonly $ref: string | |
| } | |
| /** | |
| * The `null` type. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Null { | |
| readonly _tag: "Null" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * The `undefined` type. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Undefined { | |
| readonly _tag: "Undefined" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * The `void` type. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Void { | |
| readonly _tag: "Void" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * The `never` type (no valid values). | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Never { | |
| readonly _tag: "Never" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * The `unknown` type (any value accepted). | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Unknown { | |
| readonly _tag: "Unknown" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * The `any` type. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Any { | |
| readonly _tag: "Any" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * The `string` type with optional validation checks. | |
| * | |
| * **Details** | |
| * | |
| * `checks` holds string-specific constraints, such as min/max length, pattern, | |
| * and UUID checks. `contentMediaType` and `contentSchema` indicate that the | |
| * string contains encoded data, such as `"application/json"` with a nested | |
| * schema. | |
| * | |
| * @see {@link StringMeta} | |
| * @see {@link Check} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface String { | |
| readonly _tag: "String" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly checks: ReadonlyArray<Check<StringMeta>> | |
| readonly contentMediaType?: string | undefined | |
| readonly contentSchema?: Representation | undefined | |
| } | |
| /** | |
| * The `number` type with optional validation checks. | |
| * | |
| * **Details** | |
| * | |
| * `checks` holds number-specific constraints, such as int, finite, min, max, | |
| * multipleOf, and between checks. | |
| * | |
| * @see {@link NumberMeta} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Number { | |
| readonly _tag: "Number" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly checks: ReadonlyArray<Check<NumberMeta>> | |
| } | |
| /** | |
| * The `boolean` type. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Boolean { | |
| readonly _tag: "Boolean" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * The `bigint` type with optional validation checks. | |
| * | |
| * @see {@link BigIntMeta} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface BigInt { | |
| readonly _tag: "BigInt" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly checks: ReadonlyArray<Check<BigIntMeta>> | |
| } | |
| /** | |
| * The `symbol` type. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Symbol { | |
| readonly _tag: "Symbol" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * A specific literal value (`string`, `number`, `boolean`, or `bigint`). | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Literal { | |
| readonly _tag: "Literal" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly literal: string | number | boolean | bigint | |
| } | |
| /** | |
| * A specific unique `symbol` value. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface UniqueSymbol { | |
| readonly _tag: "UniqueSymbol" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly symbol: symbol | |
| } | |
| /** | |
| * The `object` keyword type (matches any non-primitive). | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface ObjectKeyword { | |
| readonly _tag: "ObjectKeyword" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * A TypeScript-style enum. Each entry is a `[name, value]` pair. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Enum { | |
| readonly _tag: "Enum" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly enums: ReadonlyArray<readonly [string, string | number]> | |
| } | |
| /** | |
| * A template literal type composed of a sequence of parts (literals, strings, | |
| * numbers, etc.). | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface TemplateLiteral { | |
| readonly _tag: "TemplateLiteral" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly parts: ReadonlyArray<Representation> | |
| } | |
| /** | |
| * An array or tuple type. | |
| * | |
| * **Details** | |
| * | |
| * `elements` are the fixed positional elements, or tuple prefix, and each may | |
| * be optional. `rest` contains the variadic tail types; a single-element | |
| * `rest` with no `elements` produces a plain `Array<T>`. `checks` holds | |
| * array-specific constraints, such as minLength, maxLength, and unique checks. | |
| * | |
| * @see {@link Element} | |
| * @see {@link ArraysMeta} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Arrays { | |
| readonly _tag: "Arrays" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly elements: ReadonlyArray<Element> | |
| readonly rest: ReadonlyArray<Representation> | |
| readonly checks: ReadonlyArray<Check<ArraysMeta>> | |
| } | |
| /** | |
| * A positional element within an {@link Arrays} tuple. | |
| * | |
| * **Details** | |
| * | |
| * `isOptional` indicates whether this element can be absent. `type` is the | |
| * schema representation for this element's value. | |
| * | |
| * @see {@link Arrays} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Element { | |
| readonly isOptional: boolean | |
| readonly type: Representation | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * An object/struct type with named properties and optional index signatures. | |
| * | |
| * **Details** | |
| * | |
| * `propertySignatures` are the explicitly named fields. `indexSignatures` | |
| * define catch-all key/value types, such as `Record<string, T>`. `checks` | |
| * holds object-specific constraints, such as minProperties and maxProperties. | |
| * | |
| * @see {@link PropertySignature} | |
| * @see {@link IndexSignature} | |
| * @see {@link ObjectsMeta} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Objects { | |
| readonly _tag: "Objects" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly propertySignatures: ReadonlyArray<PropertySignature> | |
| readonly indexSignatures: ReadonlyArray<IndexSignature> | |
| readonly checks: ReadonlyArray<Check<ObjectsMeta>> | |
| } | |
| /** | |
| * A named property within an {@link Objects} representation. | |
| * | |
| * **Details** | |
| * | |
| * `name` is the property key, which can be a string, number, or symbol. | |
| * `isOptional` indicates whether the key can be absent. `isMutable` indicates | |
| * whether the property is mutable rather than readonly. | |
| * | |
| * @see {@link Objects} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface PropertySignature { | |
| readonly name: PropertyKey | |
| readonly type: Representation | |
| readonly isOptional: boolean | |
| readonly isMutable: boolean | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| } | |
| /** | |
| * An index signature, such as `[key: string]: number`, within an | |
| * {@link Objects}. | |
| * | |
| * **Details** | |
| * | |
| * `parameter` is the key type representation. `type` is the value type | |
| * representation. | |
| * | |
| * @see {@link Objects} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface IndexSignature { | |
| readonly parameter: Representation | |
| readonly type: Representation | |
| } | |
| /** | |
| * A union of multiple representations. | |
| * | |
| * **Details** | |
| * | |
| * `types` are the union members. `mode` controls JSON Schema output as either | |
| * `"anyOf"` (the default) or mutually exclusive `"oneOf"`. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Union { | |
| readonly _tag: "Union" | |
| readonly annotations?: Schema.Annotations.Annotations | undefined | |
| readonly types: ReadonlyArray<Representation> | |
| readonly mode: "anyOf" | "oneOf" | |
| } | |
| /** | |
| * The core tagged union of all supported schema shapes. | |
| * | |
| * **Details** | |
| * | |
| * Each variant has a `_tag` discriminator. Switch on `_tag` to handle each | |
| * shape. Most variants carry optional `annotations` and some carry `checks` | |
| * for validation constraints. | |
| * | |
| * @see {@link Document} | |
| * @see {@link fromAST} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type Representation = | |
| | Declaration | |
| | Reference | |
| | Suspend | |
| | Null | |
| | Undefined | |
| | Void | |
| | Never | |
| | Unknown | |
| | Any | |
| | String | |
| | Number | |
| | Boolean | |
| | BigInt | |
| | Symbol | |
| | Literal | |
| | UniqueSymbol | |
| | ObjectKeyword | |
| | Enum | |
| | TemplateLiteral | |
| | Arrays | |
| | Objects | |
| | Union | |
| /** | |
| * A validation constraint attached to a type. Either a single {@link Filter} | |
| * or a {@link FilterGroup} combining multiple checks. | |
| * | |
| * @see {@link Filter} | |
| * @see {@link FilterGroup} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type Check<M> = Filter<M> | FilterGroup<M> | |
| /** | |
| * A single validation constraint with typed metadata describing the check | |
| * (e.g. `{ _tag: "isMinLength", minLength: 3 }`). | |
| * | |
| * @see {@link Check} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Filter<M> { | |
| readonly _tag: "Filter" | |
| readonly annotations?: Schema.Annotations.Filter | undefined | |
| readonly meta: M | |
| } | |
| /** | |
| * A group of validation constraints that are logically combined. Contains | |
| * at least one {@link Check}. | |
| * | |
| * @see {@link Check} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface FilterGroup<M> { | |
| readonly _tag: "FilterGroup" | |
| readonly annotations?: Schema.Annotations.Filter | undefined | |
| readonly checks: readonly [Check<M>, ...Array<Check<M>>] | |
| } | |
| /** | |
| * Metadata union for string-specific validation checks (minLength, maxLength, | |
| * pattern, UUID, trimmed, etc.). | |
| * | |
| * @see {@link String} | |
| * @see {@link Check} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type StringMeta = Schema.Annotations.BuiltInMetaDefinitions[ | |
| | "isStringFinite" | |
| | "isStringBigInt" | |
| | "isStringSymbol" | |
| | "isMinLength" | |
| | "isMaxLength" | |
| | "isPattern" | |
| | "isLengthBetween" | |
| | "isTrimmed" | |
| | "isUUID" | |
| | "isGUID" | |
| | "isULID" | |
| | "isBase64" | |
| | "isBase64Url" | |
| | "isStartsWith" | |
| | "isEndsWith" | |
| | "isIncludes" | |
| | "isUppercased" | |
| | "isLowercased" | |
| | "isCapitalized" | |
| | "isUncapitalized" | |
| ] | |
| /** | |
| * Metadata union for number-specific validation checks (int, finite, | |
| * min, max, multipleOf, between). | |
| * | |
| * @see {@link Number} | |
| * @see {@link Check} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type NumberMeta = Schema.Annotations.BuiltInMetaDefinitions[ | |
| | "isInt" | |
| | "isFinite" | |
| | "isMultipleOf" | |
| | "isGreaterThanOrEqualTo" | |
| | "isLessThanOrEqualTo" | |
| | "isGreaterThan" | |
| | "isLessThan" | |
| | "isBetween" | |
| ] | |
| /** | |
| * Metadata union for bigint-specific validation checks (min, max, between). | |
| * | |
| * @see {@link BigInt} | |
| * @see {@link Check} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type BigIntMeta = Schema.Annotations.BuiltInMetaDefinitions[ | |
| | "isGreaterThanOrEqualToBigInt" | |
| | "isLessThanOrEqualToBigInt" | |
| | "isGreaterThanBigInt" | |
| | "isLessThanBigInt" | |
| | "isBetweenBigInt" | |
| ] | |
| /** | |
| * Metadata union for array-specific validation checks (minLength, maxLength, | |
| * length, unique). | |
| * | |
| * @see {@link Arrays} | |
| * @see {@link Check} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type ArraysMeta = Schema.Annotations.BuiltInMetaDefinitions[ | |
| | "isMinLength" | |
| | "isMaxLength" | |
| | "isLengthBetween" | |
| | "isUnique" | |
| ] | |
| /** | |
| * Metadata union for object-specific validation checks (minProperties, | |
| * maxProperties, propertiesLength, propertyNames). | |
| * | |
| * @see {@link Objects} | |
| * @see {@link Check} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type ObjectsMeta = | |
| | Schema.Annotations.BuiltInMetaDefinitions[ | |
| | "isMinProperties" | |
| | "isMaxProperties" | |
| | "isPropertiesLengthBetween" | |
| ] | |
| | { readonly _tag: "isPropertyNames"; readonly propertyNames: Representation } | |
| /** | |
| * Metadata union for Date-specific validation checks (valid, min, max, between). | |
| * | |
| * @see {@link Declaration} | |
| * @see {@link DeclarationMeta} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type DateMeta = Schema.Annotations.BuiltInMetaDefinitions[ | |
| | "isDateValid" | |
| | "isGreaterThanDate" | |
| | "isGreaterThanOrEqualToDate" | |
| | "isLessThanDate" | |
| | "isLessThanOrEqualToDate" | |
| | "isBetweenDate" | |
| ] | |
| /** | |
| * Metadata union for size-based validation checks (minSize, maxSize, size). | |
| * Used for collection types like `Set`, `Map`. | |
| * | |
| * @see {@link Declaration} | |
| * @see {@link DeclarationMeta} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type SizeMeta = Schema.Annotations.BuiltInMetaDefinitions[ | |
| | "isMinSize" | |
| | "isMaxSize" | |
| | "isSizeBetween" | |
| ] | |
| /** | |
| * Metadata union for {@link Declaration} checks — either {@link DateMeta} | |
| * or {@link SizeMeta}. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type DeclarationMeta = DateMeta | SizeMeta | |
| /** @internal */ | |
| export type Meta = StringMeta | NumberMeta | BigIntMeta | ArraysMeta | ObjectsMeta | DeclarationMeta | |
| /** | |
| * A string-keyed map of named {@link Representation} definitions. Used by | |
| * {@link Document} and {@link MultiDocument} for `$ref` resolution (analogous | |
| * to JSON Schema `$defs`). | |
| * | |
| * @see {@link Reference} | |
| * @see {@link Document} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface References { | |
| readonly [$ref: string]: Representation | |
| } | |
| /** | |
| * A single {@link Representation} together with its named {@link References}. | |
| * | |
| * **When to use** | |
| * | |
| * Use when representing a single Schema AST together with its named references | |
| * before reconstructing a runtime Schema, converting to JSON Schema, or | |
| * wrapping it as a {@link MultiDocument}. | |
| * | |
| * @see {@link MultiDocument} | |
| * @see {@link fromAST} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type Document = { | |
| readonly representation: Representation | |
| readonly references: References | |
| } | |
| /** | |
| * One or more {@link Representation}s sharing a common {@link References} map. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you use {@link fromASTs} to create this from multiple Schema ASTs, | |
| * {@link toCodeDocument} to generate TypeScript code, and | |
| * {@link toJsonSchemaMultiDocument} to convert to JSON Schema. | |
| * | |
| * @see {@link Document} | |
| * @see {@link fromASTs} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type MultiDocument = { | |
| readonly representations: readonly [Representation, ...Array<Representation>] | |
| readonly references: References | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // schemas | |
| // ----------------------------------------------------------------------------- | |
| const Representation$ref = Schema.suspend(() => $Representation) | |
| const toJsonAnnotationsBlacklist: Set<string> = new Set([ | |
| ...InternalRepresentation.fromASTBlacklist, | |
| "expected", | |
| "contentMediaType", | |
| "contentSchema" | |
| ]) | |
| /** | |
| * A tree of primitive values used to serialize annotations to JSON. | |
| * | |
| * @category Tree | |
| * @since 4.0.0 | |
| */ | |
| export type PrimitiveTree = Schema.Tree<null | number | boolean | bigint | symbol | string> | |
| /** | |
| * Schema for {@link PrimitiveTree}. | |
| * | |
| * **When to use** | |
| * | |
| * Use to validate recursive annotation metadata trees whose leaves are `null`, | |
| * `number`, `boolean`, `bigint`, `symbol`, or `string`. | |
| * | |
| * @see {@link PrimitiveTree} for the recursive tree type accepted by this codec | |
| * @see {@link $Annotations} for the annotation codec that filters values through this codec | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $PrimitiveTree: Schema.Codec<PrimitiveTree> = Schema.Tree( | |
| Schema.Union([ | |
| Schema.Null, | |
| Schema.Number, // allows NaN, Infinity, -Infinity | |
| Schema.Boolean, | |
| Schema.BigInt, | |
| Schema.Symbol, | |
| Schema.String | |
| ]) | |
| ) | |
| const isPrimitiveTree = Schema.is($PrimitiveTree) | |
| /** | |
| * Schema for serializing public `Schema.Annotations.Annotations` values. It | |
| * filters out internal annotation keys and non-primitive values during | |
| * encoding. | |
| * | |
| * **When to use** | |
| * | |
| * Use to serialize schema annotations in representation schemas while retaining | |
| * only primitive-tree metadata. | |
| * | |
| * **Details** | |
| * | |
| * Decoding is passthrough. Encoding removes internal annotation keys and values | |
| * that are not accepted by `$PrimitiveTree`. | |
| * | |
| * @see {@link $PrimitiveTree} for the codec used to filter annotation values | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Annotations = Schema.Record(Schema.String, Schema.Unknown).pipe( | |
| Schema.encodeTo(Schema.Record(Schema.String, $PrimitiveTree), { | |
| decode: SchemaGetter.passthrough(), | |
| encode: SchemaGetter.transformOptional(Option.flatMap((r) => { | |
| const out: Record<string, typeof $PrimitiveTree["Type"]> = {} | |
| for (const [k, v] of Object.entries(r)) { | |
| if (!toJsonAnnotationsBlacklist.has(k) && isPrimitiveTree(v)) { | |
| out[k] = v | |
| } | |
| } | |
| return Rec.isEmptyRecord(out) ? Option.none() : Option.some(out) | |
| })) | |
| }) | |
| ).annotate({ identifier: "Annotations" }) | |
| /** | |
| * Schema for the {@link Null} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Null = Schema.Struct({ | |
| _tag: Schema.tag("Null"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Null" }) | |
| /** | |
| * Schema for the {@link Undefined} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Undefined = Schema.Struct({ | |
| _tag: Schema.tag("Undefined"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Undefined" }) | |
| /** | |
| * Schema for the {@link Void} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Void = Schema.Struct({ | |
| _tag: Schema.tag("Void"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Void" }) | |
| /** | |
| * Schema for the {@link Never} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Never = Schema.Struct({ | |
| _tag: Schema.tag("Never"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Never" }) | |
| /** | |
| * Schema for the {@link Unknown} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Unknown = Schema.Struct({ | |
| _tag: Schema.tag("Unknown"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Unknown" }) | |
| /** | |
| * Schema for the {@link Any} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Any = Schema.Struct({ | |
| _tag: Schema.tag("Any"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Any" }) | |
| const $IsStringFinite = Schema.Struct({ | |
| _tag: Schema.tag("isStringFinite"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsStringFinite" }) | |
| const $IsStringBigInt = Schema.Struct({ | |
| _tag: Schema.tag("isStringBigInt"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsStringBigInt" }) | |
| const $IsStringSymbol = Schema.Struct({ | |
| _tag: Schema.tag("isStringSymbol"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsStringSymbol" }) | |
| const $IsTrimmed = Schema.Struct({ | |
| _tag: Schema.tag("isTrimmed"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsTrimmed" }) | |
| const $IsUUID = Schema.Struct({ | |
| _tag: Schema.tag("isUUID"), | |
| regExp: Schema.RegExp, | |
| version: Schema.UndefinedOr(Schema.Literals([1, 2, 3, 4, 5, 6, 7, 8])) | |
| }).annotate({ identifier: "IsUUID" }) | |
| const $IsGUID = Schema.Struct({ | |
| _tag: Schema.tag("isGUID"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsGUID" }) | |
| const $IsULID = Schema.Struct({ | |
| _tag: Schema.tag("isULID"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsULID" }) | |
| const $IsBase64 = Schema.Struct({ | |
| _tag: Schema.tag("isBase64"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsBase64" }) | |
| const $IsBase64Url = Schema.Struct({ | |
| _tag: Schema.tag("isBase64Url"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsBase64Url" }) | |
| const $IsStartsWith = Schema.Struct({ | |
| _tag: Schema.tag("isStartsWith"), | |
| startsWith: Schema.String, | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsStartsWith" }) | |
| const $IsEndsWith = Schema.Struct({ | |
| _tag: Schema.tag("isEndsWith"), | |
| endsWith: Schema.String, | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsEndsWith" }) | |
| const $IsIncludes = Schema.Struct({ | |
| _tag: Schema.tag("isIncludes"), | |
| includes: Schema.String, | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsIncludes" }) | |
| const $IsUppercased = Schema.Struct({ | |
| _tag: Schema.tag("isUppercased"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsUppercased" }) | |
| const $IsLowercased = Schema.Struct({ | |
| _tag: Schema.tag("isLowercased"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsLowercased" }) | |
| const $IsCapitalized = Schema.Struct({ | |
| _tag: Schema.tag("isCapitalized"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsCapitalized" }) | |
| const $IsUncapitalized = Schema.Struct({ | |
| _tag: Schema.tag("isUncapitalized"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsUncapitalized" }) | |
| const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) | |
| const $IsMinLength = Schema.Struct({ | |
| _tag: Schema.tag("isMinLength"), | |
| minLength: NonNegativeInt | |
| }).annotate({ identifier: "IsMinLength" }) | |
| const $IsMaxLength = Schema.Struct({ | |
| _tag: Schema.tag("isMaxLength"), | |
| maxLength: NonNegativeInt | |
| }).annotate({ identifier: "IsMaxLength" }) | |
| const $IsLengthBetween = Schema.Struct({ | |
| _tag: Schema.tag("isLengthBetween"), | |
| minimum: NonNegativeInt, | |
| maximum: NonNegativeInt | |
| }).annotate({ identifier: "IsLengthBetween" }) | |
| const $IsPattern = Schema.Struct({ | |
| _tag: Schema.tag("isPattern"), | |
| regExp: Schema.RegExp | |
| }).annotate({ identifier: "IsPattern" }) | |
| /** | |
| * Schema for {@link StringMeta}. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $StringMeta = Schema.Union([ | |
| $IsStringFinite, | |
| $IsStringBigInt, | |
| $IsStringSymbol, | |
| $IsTrimmed, | |
| $IsUUID, | |
| $IsGUID, | |
| $IsULID, | |
| $IsBase64, | |
| $IsBase64Url, | |
| $IsStartsWith, | |
| $IsEndsWith, | |
| $IsIncludes, | |
| $IsUppercased, | |
| $IsLowercased, | |
| $IsCapitalized, | |
| $IsUncapitalized, | |
| $IsMinLength, | |
| $IsMaxLength, | |
| $IsPattern, | |
| $IsLengthBetween | |
| ]).annotate({ identifier: "StringMeta" }) | |
| function makeCheck<T>(meta: Schema.Codec<T>, identifier: string) { | |
| const Check$ref = Schema.suspend(() => Check) | |
| const Check: Schema.Codec<Check<T>> = Schema.Union([ | |
| Schema.Struct({ | |
| _tag: Schema.tag("Filter"), | |
| annotations: Schema.optional($Annotations), | |
| meta | |
| }).annotate({ identifier: `${identifier}Filter` }), | |
| Schema.Struct({ | |
| _tag: Schema.tag("FilterGroup"), | |
| annotations: Schema.optional($Annotations), | |
| checks: Schema.NonEmptyArray(Check$ref) | |
| }).annotate({ identifier: `${identifier}FilterGroup` }) | |
| ]).annotate({ identifier: `${identifier}Check` }) | |
| return Check | |
| } | |
| /** | |
| * Schema for the {@link String} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $String = Schema.Struct({ | |
| _tag: Schema.tag("String"), | |
| annotations: Schema.optional($Annotations), | |
| checks: Schema.Array(makeCheck($StringMeta, "String")), | |
| contentMediaType: Schema.optional(Schema.String), | |
| contentSchema: Schema.optional(Representation$ref) | |
| }).annotate({ identifier: "String" }) | |
| const $IsInt = Schema.Struct({ | |
| _tag: Schema.tag("isInt") | |
| }).annotate({ identifier: "IsInt" }) | |
| const $IsMultipleOf = Schema.Struct({ | |
| _tag: Schema.tag("isMultipleOf"), | |
| divisor: Schema.Finite | |
| }).annotate({ identifier: "IsMultipleOf" }) | |
| const $IsFinite = Schema.Struct({ | |
| _tag: Schema.tag("isFinite") | |
| }).annotate({ identifier: "IsFinite" }) | |
| const $IsGreaterThan = Schema.Struct({ | |
| _tag: Schema.tag("isGreaterThan"), | |
| exclusiveMinimum: Schema.Finite | |
| }).annotate({ identifier: "IsGreaterThan" }) | |
| const $IsGreaterThanOrEqualTo = Schema.Struct({ | |
| _tag: Schema.tag("isGreaterThanOrEqualTo"), | |
| minimum: Schema.Finite | |
| }).annotate({ identifier: "IsGreaterThanOrEqualTo" }) | |
| const $IsLessThan = Schema.Struct({ | |
| _tag: Schema.tag("isLessThan"), | |
| exclusiveMaximum: Schema.Finite | |
| }).annotate({ identifier: "IsLessThan" }) | |
| const $IsLessThanOrEqualTo = Schema.Struct({ | |
| _tag: Schema.tag("isLessThanOrEqualTo"), | |
| maximum: Schema.Finite | |
| }).annotate({ identifier: "IsLessThanOrEqualTo" }) | |
| const $IsBetween = Schema.Struct({ | |
| _tag: Schema.tag("isBetween"), | |
| minimum: Schema.Finite, | |
| maximum: Schema.Finite, | |
| exclusiveMinimum: Schema.optional(Schema.Boolean), | |
| exclusiveMaximum: Schema.optional(Schema.Boolean) | |
| }).annotate({ identifier: "IsBetween" }) | |
| /** | |
| * Schema for {@link NumberMeta}. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $NumberMeta = Schema.Union([ | |
| $IsInt, | |
| $IsMultipleOf, | |
| $IsFinite, | |
| $IsGreaterThan, | |
| $IsGreaterThanOrEqualTo, | |
| $IsLessThan, | |
| $IsLessThanOrEqualTo, | |
| $IsBetween | |
| ]).annotate({ identifier: "NumberMeta" }) | |
| /** | |
| * Schema for the {@link Number} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Number = Schema.Struct({ | |
| _tag: Schema.tag("Number"), | |
| annotations: Schema.optional($Annotations), | |
| checks: Schema.Array(makeCheck($NumberMeta, "Number")) | |
| }).annotate({ identifier: "Number" }) | |
| /** | |
| * Schema for the {@link Boolean} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Boolean = Schema.Struct({ | |
| _tag: Schema.tag("Boolean"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Boolean" }) | |
| const $IsGreaterThanBigInt = Schema.Struct({ | |
| _tag: Schema.tag("isGreaterThanBigInt"), | |
| exclusiveMinimum: Schema.BigInt | |
| }).annotate({ identifier: "IsGreaterThanBigInt" }) | |
| const $IsGreaterThanOrEqualToBigInt = Schema.Struct({ | |
| _tag: Schema.tag("isGreaterThanOrEqualToBigInt"), | |
| minimum: Schema.BigInt | |
| }).annotate({ identifier: "IsGreaterThanOrEqualToBigInt" }) | |
| const $IsLessThanBigInt = Schema.Struct({ | |
| _tag: Schema.tag("isLessThanBigInt"), | |
| exclusiveMaximum: Schema.BigInt | |
| }).annotate({ identifier: "IsLessThanBigInt" }) | |
| const $IsLessThanOrEqualToBigInt = Schema.Struct({ | |
| _tag: Schema.tag("isLessThanOrEqualToBigInt"), | |
| maximum: Schema.BigInt | |
| }).annotate({ identifier: "IsLessThanOrEqualToBigInt" }) | |
| const $IsBetweenBigInt = Schema.Struct({ | |
| _tag: Schema.tag("isBetweenBigInt"), | |
| minimum: Schema.BigInt, | |
| maximum: Schema.BigInt, | |
| exclusiveMinimum: Schema.optional(Schema.Boolean), | |
| exclusiveMaximum: Schema.optional(Schema.Boolean) | |
| }).annotate({ identifier: "IsBetweenBigInt" }) | |
| const $BigIntMeta = Schema.Union([ | |
| $IsGreaterThanBigInt, | |
| $IsGreaterThanOrEqualToBigInt, | |
| $IsLessThanBigInt, | |
| $IsLessThanOrEqualToBigInt, | |
| $IsBetweenBigInt | |
| ]).annotate({ identifier: "BigIntMeta" }) | |
| /** | |
| * Schema for the {@link BigInt} representation node. | |
| * | |
| * **When to use** | |
| * | |
| * Use to encode, decode, or validate serialized `BigInt` representation nodes, | |
| * not application `bigint` values. | |
| * | |
| * **Details** | |
| * | |
| * Accepts representation nodes with `_tag: "BigInt"`, optional annotations, | |
| * and bigint-specific validation metadata in `checks`. | |
| * | |
| * @see {@link BigIntMeta} for the metadata accepted by the `checks` array | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $BigInt = Schema.Struct({ | |
| _tag: Schema.tag("BigInt"), | |
| annotations: Schema.optional($Annotations), | |
| checks: Schema.Array(makeCheck($BigIntMeta, "BigInt")) | |
| }).annotate({ identifier: "BigInt" }) | |
| /** | |
| * Schema for the {@link Symbol} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Symbol = Schema.Struct({ | |
| _tag: Schema.tag("Symbol"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Symbol" }) | |
| /** | |
| * Schema for the literal value types allowed in a {@link Literal} node | |
| * (string, finite number, boolean, or bigint). | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $LiteralValue = Schema.Union([ | |
| Schema.String, | |
| Schema.Finite, | |
| Schema.Boolean, | |
| Schema.BigInt | |
| ]).annotate({ identifier: "LiteralValue" }) | |
| /** | |
| * Schema for the {@link Literal} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Literal = Schema.Struct({ | |
| _tag: Schema.tag("Literal"), | |
| annotations: Schema.optional($Annotations), | |
| literal: $LiteralValue | |
| }).annotate({ identifier: "Literal" }) | |
| /** | |
| * Schema for the {@link UniqueSymbol} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $UniqueSymbol = Schema.Struct({ | |
| _tag: Schema.tag("UniqueSymbol"), | |
| annotations: Schema.optional($Annotations), | |
| symbol: Schema.Symbol | |
| }).annotate({ identifier: "UniqueSymbol" }) | |
| /** | |
| * Schema for the {@link ObjectKeyword} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $ObjectKeyword = Schema.Struct({ | |
| _tag: Schema.tag("ObjectKeyword"), | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "ObjectKeyword" }) | |
| /** | |
| * Schema for the {@link Enum} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Enum = Schema.Struct({ | |
| _tag: Schema.tag("Enum"), | |
| annotations: Schema.optional($Annotations), | |
| enums: Schema.Array( | |
| Schema.Tuple([ | |
| Schema.String, | |
| Schema.Union([ | |
| Schema.String, | |
| Schema.Number // NaN, Infinity, -Infinity are allowed enum values | |
| ]) | |
| ]) | |
| ) | |
| }).annotate({ identifier: "Enum" }) | |
| /** | |
| * Schema for the {@link TemplateLiteral} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $TemplateLiteral = Schema.Struct({ | |
| _tag: Schema.tag("TemplateLiteral"), | |
| annotations: Schema.optional($Annotations), | |
| parts: Schema.Array(Representation$ref) | |
| }).annotate({ identifier: "TemplateLiteral" }) | |
| /** | |
| * Schema for the {@link Element} type (positional tuple element). | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Element = Schema.Struct({ | |
| isOptional: Schema.Boolean, | |
| type: Representation$ref, | |
| annotations: Schema.optional($Annotations) | |
| }).annotate({ identifier: "Element" }) | |
| const $IsUnique = Schema.Struct({ | |
| _tag: Schema.tag("isUnique") | |
| }).annotate({ identifier: "IsUnique" }) | |
| const $ArraysMeta = Schema.Union([ | |
| $IsMinLength, | |
| $IsMaxLength, | |
| $IsLengthBetween, | |
| $IsUnique | |
| ]).annotate({ identifier: "ArraysMeta" }) | |
| /** | |
| * Schema for the {@link Arrays} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Arrays = Schema.Struct({ | |
| _tag: Schema.tag("Arrays"), | |
| annotations: Schema.optional($Annotations), | |
| elements: Schema.Array($Element), | |
| rest: Schema.Array(Representation$ref), | |
| checks: Schema.Array(makeCheck($ArraysMeta, "Arrays")) | |
| }).annotate({ identifier: "Arrays" }) | |
| /** | |
| * Schema for the {@link PropertySignature} type. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $PropertySignature = Schema.Struct({ | |
| annotations: Schema.optional($Annotations), | |
| name: Schema.PropertyKey, | |
| type: Representation$ref, | |
| isOptional: Schema.Boolean, | |
| isMutable: Schema.Boolean | |
| }).annotate({ identifier: "PropertySignature" }) | |
| /** | |
| * Schema for the {@link IndexSignature} type. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $IndexSignature = Schema.Struct({ | |
| parameter: Representation$ref, | |
| type: Representation$ref | |
| }).annotate({ identifier: "IndexSignature" }) | |
| const $IsMinProperties = Schema.Struct({ | |
| _tag: Schema.tag("isMinProperties"), | |
| minProperties: NonNegativeInt | |
| }).annotate({ identifier: "IsMinProperties" }) | |
| const $IsMaxProperties = Schema.Struct({ | |
| _tag: Schema.tag("isMaxProperties"), | |
| maxProperties: NonNegativeInt | |
| }).annotate({ identifier: "IsMaxProperties" }) | |
| const $IsPropertiesLengthBetween = Schema.Struct({ | |
| _tag: Schema.tag("isPropertiesLengthBetween"), | |
| minimum: NonNegativeInt, | |
| maximum: NonNegativeInt | |
| }).annotate({ identifier: "IsPropertiesLengthBetween" }) | |
| const $IsPropertyNames = Schema.Struct({ | |
| _tag: Schema.tag("isPropertyNames"), | |
| propertyNames: Representation$ref | |
| }).annotate({ identifier: "IsPropertyNames" }) | |
| /** | |
| * Schema for {@link ObjectsMeta}. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $ObjectsMeta = Schema.Union([ | |
| $IsMinProperties, | |
| $IsMaxProperties, | |
| $IsPropertiesLengthBetween, | |
| $IsPropertyNames | |
| ]).annotate({ identifier: "ObjectsMeta" }) | |
| /** | |
| * Schema for the {@link Objects} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Objects = Schema.Struct({ | |
| _tag: Schema.tag("Objects"), | |
| annotations: Schema.optional($Annotations), | |
| propertySignatures: Schema.Array($PropertySignature), | |
| indexSignatures: Schema.Array($IndexSignature), | |
| checks: Schema.Array(makeCheck($ObjectsMeta, "Objects")) | |
| }).annotate({ identifier: "Objects" }) | |
| /** | |
| * Schema for the {@link Union} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Union = Schema.Struct({ | |
| _tag: Schema.tag("Union"), | |
| annotations: Schema.optional($Annotations), | |
| types: Schema.Array(Representation$ref), | |
| mode: Schema.Literals(["anyOf", "oneOf"]) | |
| }).annotate({ identifier: "Union" }) | |
| /** | |
| * Schema for the {@link Reference} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Reference = Schema.Struct({ | |
| _tag: Schema.tag("Reference"), | |
| $ref: Schema.String | |
| }).annotate({ identifier: "Reference" }) | |
| const $IsDateValid = Schema.Struct({ | |
| _tag: Schema.tag("isDateValid") | |
| }).annotate({ identifier: "IsDateValid" }) | |
| const $IsGreaterThanDate = Schema.Struct({ | |
| _tag: Schema.tag("isGreaterThanDate"), | |
| exclusiveMinimum: Schema.Date | |
| }).annotate({ identifier: "IsGreaterThanDate" }) | |
| const $IsGreaterThanOrEqualToDate = Schema.Struct({ | |
| _tag: Schema.tag("isGreaterThanOrEqualToDate"), | |
| minimum: Schema.Date | |
| }).annotate({ identifier: "IsGreaterThanOrEqualToDate" }) | |
| const $IsLessThanDate = Schema.Struct({ | |
| _tag: Schema.tag("isLessThanDate"), | |
| exclusiveMaximum: Schema.Date | |
| }).annotate({ identifier: "IsLessThanDate" }) | |
| const $IsLessThanOrEqualToDate = Schema.Struct({ | |
| _tag: Schema.tag("isLessThanOrEqualToDate"), | |
| maximum: Schema.Date | |
| }).annotate({ identifier: "IsLessThanOrEqualToDate" }) | |
| const $IsBetweenDate = Schema.Struct({ | |
| _tag: Schema.tag("isBetweenDate"), | |
| minimum: Schema.Date, | |
| maximum: Schema.Date, | |
| exclusiveMinimum: Schema.optional(Schema.Boolean), | |
| exclusiveMaximum: Schema.optional(Schema.Boolean) | |
| }).annotate({ identifier: "IsBetweenDate" }) | |
| /** | |
| * Schema for {@link DateMeta}. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $DateMeta = Schema.Union([ | |
| $IsDateValid, | |
| $IsGreaterThanDate, | |
| $IsGreaterThanOrEqualToDate, | |
| $IsLessThanDate, | |
| $IsLessThanOrEqualToDate, | |
| $IsBetweenDate | |
| ]).annotate({ identifier: "DateMeta" }) | |
| const $IsMinSize = Schema.Struct({ | |
| _tag: Schema.tag("isMinSize"), | |
| minSize: NonNegativeInt | |
| }).annotate({ identifier: "IsMinSize" }) | |
| const $IsMaxSize = Schema.Struct({ | |
| _tag: Schema.tag("isMaxSize"), | |
| maxSize: NonNegativeInt | |
| }).annotate({ identifier: "IsMaxSize" }) | |
| const $IsSizeBetween = Schema.Struct({ | |
| _tag: Schema.tag("isSizeBetween"), | |
| minimum: NonNegativeInt, | |
| maximum: NonNegativeInt | |
| }).annotate({ identifier: "IsSizeBetween" }) | |
| /** | |
| * Schema for {@link SizeMeta}. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $SizeMeta = Schema.Union([ | |
| $IsMinSize, | |
| $IsMaxSize, | |
| $IsSizeBetween | |
| ]).annotate({ identifier: "SizeMeta" }) | |
| /** | |
| * Schema for {@link DeclarationMeta}. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $DeclarationMeta = Schema.Union([ | |
| $DateMeta, | |
| $SizeMeta | |
| ]).annotate({ identifier: "DeclarationMeta" }) | |
| /** | |
| * Schema for the {@link Declaration} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Declaration = Schema.Struct({ | |
| _tag: Schema.tag("Declaration"), | |
| annotations: Schema.optional($Annotations), | |
| typeParameters: Schema.Array(Representation$ref), | |
| checks: Schema.Array(makeCheck($DeclarationMeta, "Declaration")), | |
| encodedSchema: Representation$ref | |
| }).annotate({ identifier: "Declaration" }) | |
| /** | |
| * Schema for the {@link Suspend} representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Suspend = Schema.Struct({ | |
| _tag: Schema.tag("Suspend"), | |
| annotations: Schema.optional($Annotations), | |
| checks: Schema.Tuple([]), | |
| thunk: Representation$ref | |
| }).annotate({ identifier: "Suspend" }) | |
| /** | |
| * Type-level helper for the recursive {@link $Representation} codec. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export interface $Representation extends Schema.Codec<Representation> {} | |
| /** | |
| * Schema for the full {@link Representation} union. It recursively validates | |
| * and encodes any representation node. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Representation: $Representation = Schema.Union([ | |
| $Null, | |
| $Undefined, | |
| $Void, | |
| $Never, | |
| $Unknown, | |
| $Any, | |
| $String, | |
| $Number, | |
| $Boolean, | |
| $BigInt, | |
| $Symbol, | |
| $Literal, | |
| $UniqueSymbol, | |
| $ObjectKeyword, | |
| $Enum, | |
| $TemplateLiteral, | |
| $Arrays, | |
| $Objects, | |
| $Union, | |
| $Reference, | |
| $Declaration, | |
| $Suspend | |
| ]).annotate({ identifier: "Schema" }) | |
| /** | |
| * Schema for {@link Document}. | |
| * | |
| * **When to use** | |
| * | |
| * Use to validate or serialize a single schema representation document with | |
| * `Schema.decodeUnknownSync` or `Schema.encodeSync`. | |
| * | |
| * **Gotchas** | |
| * | |
| * This codec validates document structure but does not resolve `$ref` keys | |
| * against `references`. | |
| * | |
| * @see {@link DocumentFromJson} for the JSON-string codec wrapper | |
| * @see {@link $MultiDocument} for validating documents with multiple root representations | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $Document = Schema.Struct({ | |
| representation: $Representation, | |
| references: Schema.Record(Schema.String, $Representation) | |
| }).annotate({ identifier: "Document" }) | |
| /** | |
| * Schema for {@link MultiDocument}. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const $MultiDocument = Schema.Struct({ | |
| representations: Schema.NonEmptyArray($Representation), | |
| references: Schema.Record(Schema.String, $Representation) | |
| }).annotate({ identifier: "MultiDocument" }) | |
| // ----------------------------------------------------------------------------- | |
| // APIs | |
| // ----------------------------------------------------------------------------- | |
| /** | |
| * Converts a Schema AST into a {@link Document}. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you have a single Schema AST and need a schema representation | |
| * document. | |
| * | |
| * **Details** | |
| * | |
| * Shared/recursive sub-schemas are extracted into the `references` map. | |
| * | |
| * **Example** (Converting a Schema to a Document) | |
| * | |
| * ```ts | |
| * import { Schema, SchemaRepresentation } from "effect" | |
| * | |
| * const Person = Schema.Struct({ | |
| * name: Schema.String, | |
| * age: Schema.Number | |
| * }) | |
| * | |
| * const doc = SchemaRepresentation.fromAST(Person.ast) | |
| * console.log(doc.representation._tag) | |
| * // "Objects" | |
| * ``` | |
| * | |
| * @see {@link Document} | |
| * @see {@link fromASTs} | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const fromAST: (ast: SchemaAST.AST) => Document = InternalRepresentation.fromAST | |
| /** | |
| * Converts one or more Schema ASTs into a {@link MultiDocument}. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you have multiple Schema ASTs and need one schema representation | |
| * `MultiDocument` with shared references. | |
| * | |
| * **Details** | |
| * | |
| * All schemas share a single `references` map. | |
| * | |
| * @see {@link MultiDocument} | |
| * @see {@link fromAST} | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const fromASTs: (asts: readonly [SchemaAST.AST, ...Array<SchemaAST.AST>]) => MultiDocument = | |
| InternalRepresentation.fromASTs | |
| /** | |
| * Schema that decodes a {@link Document} from JSON and encodes it back. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need a JSON codec for schema representation documents with | |
| * `Schema.decodeUnknownSync` or `Schema.encodeSync`. | |
| * | |
| * **Example** (Round-tripping a Document through JSON) | |
| * | |
| * ```ts | |
| * import { Schema, SchemaRepresentation } from "effect" | |
| * | |
| * const doc = SchemaRepresentation.fromAST(Schema.String.ast) | |
| * const json = Schema.encodeSync(SchemaRepresentation.DocumentFromJson)(doc) | |
| * const back = Schema.decodeUnknownSync(SchemaRepresentation.DocumentFromJson)(json) | |
| * ``` | |
| * | |
| * @see {@link $Document} | |
| * @see {@link MultiDocumentFromJson} | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const DocumentFromJson: Schema.Codec<Document, Schema.Json> = Schema.toCodecJson($Document) | |
| /** | |
| * Schema for `MultiDocument` values encoded as JSON. | |
| * | |
| * @see {@link $MultiDocument} | |
| * @see {@link DocumentFromJson} | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const MultiDocumentFromJson: Schema.Codec<MultiDocument, Schema.Json> = Schema.toCodecJson($MultiDocument) | |
| /** | |
| * Wraps a single {@link Document} as a {@link MultiDocument} with one | |
| * representation. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need to pass a single schema representation `Document` where an | |
| * API expects a `MultiDocument`. | |
| * | |
| * @see {@link Document} | |
| * @see {@link MultiDocument} | |
| * | |
| * @category transforming | |
| * @since 4.0.0 | |
| */ | |
| export function toMultiDocument(document: Document): MultiDocument { | |
| return { | |
| representations: [document.representation], | |
| references: document.references | |
| } | |
| } | |
| /** | |
| * A callback that handles {@link Declaration} nodes during reconstruction | |
| * ({@link toSchema}) or code generation ({@link toCodeDocument}). | |
| * | |
| * **Details** | |
| * | |
| * Return a value to handle the declaration. Return `undefined` to fall back to | |
| * default behavior, which uses `encodedSchema` for `toSchema` or the | |
| * `generation` annotation for `toCodeDocument`. `recur` processes child | |
| * representations recursively. | |
| * | |
| * @see {@link toSchema} | |
| * @see {@link toSchemaDefaultReviver} | |
| * @see {@link toCodeDocument} | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type Reviver<T> = (declaration: Declaration, recur: (representation: Representation) => T) => T | undefined | |
| /** | |
| * Default {@link Reviver} for {@link toSchema} that handles built-in Effect | |
| * types, including Option, Result, Redacted, Cause, Exit, ReadonlyMap, HashMap, | |
| * ReadonlySet, Date, Duration, URL, and RegExp. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need the default `options.reviver` for {@link toSchema} to | |
| * reconstruct runtime schemas for built-in Effect declarations. | |
| * | |
| * **Details** | |
| * | |
| * The reviver returns `undefined` for unrecognized declarations, causing | |
| * fallback to `encodedSchema`. | |
| * | |
| * @see {@link toSchema} | |
| * @see {@link Reviver} | |
| * | |
| * @category transforming | |
| * @since 4.0.0 | |
| */ | |
| export const toSchemaDefaultReviver: Reviver<Schema.Top> = (s, recur) => { | |
| const typeConstructor = s.annotations?.typeConstructor | |
| if (Predicate.isObject(typeConstructor) && typeof typeConstructor._tag === "string") { | |
| const typeParameters = s.typeParameters.map(recur) | |
| switch (typeConstructor._tag) { | |
| // built-in types | |
| case "Date": | |
| return Schema.Date | |
| case "Error": | |
| return Schema.Error(typeConstructor.options as Schema.ErrorOptions | undefined) | |
| case "File": | |
| return Schema.File | |
| case "FormData": | |
| return Schema.FormData | |
| case "ReadonlyMap": | |
| return Schema.ReadonlyMap(typeParameters[0], typeParameters[1]) | |
| case "ReadonlySet": | |
| return Schema.ReadonlySet(typeParameters[0]) | |
| case "RegExp": | |
| return Schema.RegExp | |
| case "Uint8Array": | |
| return Schema.Uint8Array | |
| case "URL": | |
| return Schema.URL | |
| case "URLSearchParams": | |
| return Schema.URLSearchParams | |
| // effect types | |
| case "effect/Option": | |
| return Schema.Option(typeParameters[0]) | |
| case "effect/Result": | |
| return Schema.Result(typeParameters[0], typeParameters[1]) | |
| case "effect/Redacted": | |
| return Schema.Redacted(typeParameters[0], typeConstructor.options as any) | |
| case "effect/DateTime.TimeZone": | |
| return Schema.TimeZone | |
| case "effect/DateTime.TimeZone.Named": | |
| return Schema.TimeZoneNamed | |
| case "effect/DateTime.TimeZone.Offset": | |
| return Schema.TimeZoneOffset | |
| case "effect/DateTime.Utc": | |
| return Schema.DateTimeUtc | |
| case "effect/DateTime.Zoned": | |
| return Schema.DateTimeZoned | |
| case "effect/BigDecimal": | |
| return Schema.BigDecimal | |
| case "effect/Chunk": | |
| return Schema.Chunk(typeParameters[0]) | |
| case "effect/Cause": | |
| return Schema.Cause(typeParameters[0], typeParameters[1]) | |
| case "effect/Cause/Failure": | |
| return Schema.CauseReason(typeParameters[0], typeParameters[1]) | |
| case "effect/Duration": | |
| return Schema.Duration | |
| case "effect/Exit": | |
| return Schema.Exit(typeParameters[0], typeParameters[1], typeParameters[2]) | |
| case "effect/Json": | |
| return Schema.Json | |
| case "effect/MutableJson": | |
| return Schema.MutableJson | |
| case "effect/HashMap": | |
| return Schema.HashMap(typeParameters[0], typeParameters[1]) | |
| case "effect/HashSet": | |
| return Schema.HashSet(typeParameters[0]) | |
| } | |
| } | |
| } | |
| /** | |
| * Creates a runtime Schema from a {@link Document}. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you have a serialized or computed schema representation document and | |
| * need a runtime Schema for decoding/encoding. | |
| * | |
| * **Details** | |
| * | |
| * Pass `options.reviver`, such as {@link toSchemaDefaultReviver}, to handle | |
| * {@link Declaration} nodes for types like `Date` and `Option`. Without a | |
| * reviver, declarations fall back to their `encodedSchema`. Circular references | |
| * are handled via lazy `Schema.suspend`. | |
| * | |
| * **Gotchas** | |
| * | |
| * This throws if a `$ref` is not found in `document.references`. | |
| * | |
| * **Example** (Reconstructing a Schema) | |
| * | |
| * ```ts | |
| * import { Schema, SchemaRepresentation } from "effect" | |
| * | |
| * const doc = SchemaRepresentation.fromAST( | |
| * Schema.Struct({ name: Schema.String }).ast | |
| * ) | |
| * | |
| * const schema = SchemaRepresentation.toSchema(doc) | |
| * console.log(JSON.stringify(Schema.toJsonSchemaDocument(schema), null, 2)) | |
| * ``` | |
| * | |
| * @see {@link Document} | |
| * @see {@link Reviver} | |
| * @see {@link toSchemaDefaultReviver} | |
| * | |
| * @category Runtime Generation | |
| * @since 4.0.0 | |
| */ | |
| export function toSchema<S extends Schema.Top = Schema.Top>(document: Document, options?: { | |
| readonly reviver?: Reviver<Schema.Top> | undefined | |
| }): S { | |
| type Slot = { | |
| // 0 = not started, 1 = building, 2 = done | |
| state: 0 | 1 | 2 | |
| value: Schema.Top | undefined | |
| ref: Schema.Top | |
| } | |
| const slots = new Map<string, Slot>() | |
| return recur(document.representation) as S | |
| function recur(r: Representation): Schema.Top { | |
| let out = on(r) | |
| if ("annotations" in r && r.annotations) out = out.annotate(r.annotations) | |
| out = toSchemaChecks(out, r) | |
| return out | |
| } | |
| function getSlot(identifier: string): Slot { | |
| const existing = slots.get(identifier) | |
| if (existing) return existing | |
| // Create the slot *before* resolving, so self-references can see it. | |
| const slot: Slot = { | |
| state: 0, | |
| value: undefined, | |
| ref: Schema.suspend(() => { | |
| if (slot.value === undefined) { | |
| return Schema.Unknown | |
| } | |
| return slot.value | |
| }) | |
| } | |
| slots.set(identifier, slot) | |
| return slot | |
| } | |
| function resolveReference($ref: string): Schema.Top { | |
| const definition = document.references[$ref] | |
| if (definition === undefined) { | |
| throw new Error(`Reference ${$ref} not found`) | |
| } | |
| const slot = getSlot($ref) | |
| if (slot.state === 2) { | |
| // Already built: return the built schema directly | |
| return slot.value! | |
| } | |
| if (slot.state === 1) { | |
| // Circular: we're currently building this identifier. | |
| return slot.ref | |
| } | |
| // First time: build it. | |
| slot.state = 1 | |
| try { | |
| slot.value = recur(definition) | |
| slot.state = 2 | |
| return slot.value | |
| } catch (e) { | |
| // Leave the slot in a safe state so future thunks don't silently succeed. | |
| slot.state = 0 | |
| slot.value = undefined | |
| throw e | |
| } | |
| } | |
| function on(r: Representation): Schema.Top { | |
| switch (r._tag) { | |
| case "Declaration": | |
| return options?.reviver?.(r, recur) ?? recur(r.encodedSchema) | |
| case "Reference": | |
| return resolveReference(r.$ref) | |
| case "Suspend": | |
| return recur(r.thunk) | |
| case "Null": | |
| return Schema.Null | |
| case "Undefined": | |
| return Schema.Undefined | |
| case "Void": | |
| return Schema.Void | |
| case "Never": | |
| return Schema.Never | |
| case "Unknown": | |
| return Schema.Unknown | |
| case "Any": | |
| return Schema.Any | |
| case "String": { | |
| const contentMediaType = r.contentMediaType | |
| const contentSchema = r.contentSchema | |
| if (contentMediaType === "application/json" && contentSchema !== undefined) { | |
| return Schema.fromJsonString(recur(contentSchema)) | |
| } | |
| return Schema.String | |
| } | |
| case "Number": | |
| return Schema.Number | |
| case "Boolean": | |
| return Schema.Boolean | |
| case "BigInt": | |
| return Schema.BigInt | |
| case "Symbol": | |
| return Schema.Symbol | |
| case "Literal": | |
| return Schema.Literal(r.literal) | |
| case "UniqueSymbol": | |
| return Schema.UniqueSymbol(r.symbol) | |
| case "ObjectKeyword": | |
| return Schema.ObjectKeyword | |
| case "Enum": | |
| return Schema.Enum(Object.fromEntries(r.enums)) | |
| case "TemplateLiteral": { | |
| const parts = r.parts.map(recur) as Schema.TemplateLiteral.Parts | |
| return Schema.TemplateLiteral(parts) | |
| } | |
| case "Arrays": { | |
| const elements = r.elements.map((e) => { | |
| const s = recur(e.type) | |
| return e.isOptional ? Schema.optionalKey(s) : s | |
| }) | |
| const rest = r.rest.map(recur) | |
| if (Arr.isArrayNonEmpty(rest)) { | |
| if (r.elements.length === 0 && r.rest.length === 1) { | |
| return Schema.Array(rest[0]) | |
| } | |
| return Schema.TupleWithRest(Schema.Tuple(elements), rest) | |
| } | |
| return Schema.Tuple(elements) | |
| } | |
| case "Objects": { | |
| const fields: Record<PropertyKey, Schema.Top> = {} | |
| for (const ps of r.propertySignatures) { | |
| const s = recur(ps.type) | |
| const withOptional = ps.isOptional ? Schema.optionalKey(s) : s | |
| fields[ps.name] = ps.isMutable ? Schema.mutableKey(withOptional) : withOptional | |
| } | |
| const indexSignatures = r.indexSignatures.map((is) => | |
| Schema.Record(recur(is.parameter) as Schema.Record.Key, recur(is.type)) | |
| ) | |
| if (Arr.isArrayNonEmpty(indexSignatures)) { | |
| if (r.propertySignatures.length === 0 && indexSignatures.length === 1) { | |
| return indexSignatures[0] | |
| } | |
| return Schema.StructWithRest(Schema.Struct(fields), indexSignatures) | |
| } | |
| return Schema.Struct(fields) | |
| } | |
| case "Union": { | |
| if (r.types.length === 0) return Schema.Never | |
| if (r.types.every((t) => t._tag === "Literal")) { | |
| if (r.types.length === 1) { | |
| return Schema.Literal(r.types[0].literal) | |
| } | |
| return Schema.Literals(r.types.map((t) => t.literal)) | |
| } | |
| return Schema.Union(r.types.map(recur), { mode: r.mode }) | |
| } | |
| } | |
| } | |
| function toSchemaChecks(top: Schema.Top, schema: Representation): Schema.Top { | |
| switch (schema._tag) { | |
| default: | |
| return top | |
| case "String": | |
| case "Number": | |
| case "BigInt": | |
| case "Arrays": | |
| case "Objects": | |
| case "Declaration": { | |
| const checks = schema.checks.map(toSchemaCheck) | |
| return Arr.isArrayNonEmpty(checks) ? top.check(...checks) : top | |
| } | |
| } | |
| } | |
| function toSchemaCheck(check: Check<Meta>): SchemaAST.Check<any> { | |
| switch (check._tag) { | |
| case "Filter": | |
| return toSchemaFilter(check) | |
| case "FilterGroup": { | |
| return Schema.makeFilterGroup(Arr.map(check.checks, toSchemaCheck), check.annotations) | |
| } | |
| } | |
| } | |
| function toSchemaFilter(filter: Filter<Meta>): SchemaAST.Check<any> { | |
| const a = filter.annotations | |
| switch (filter.meta._tag) { | |
| // String Meta | |
| case "isStringFinite": | |
| return Schema.isStringFinite(a) | |
| case "isStringBigInt": | |
| return Schema.isStringBigInt(a) | |
| case "isStringSymbol": | |
| return Schema.isStringSymbol(a) | |
| case "isMinLength": | |
| return Schema.isMinLength(filter.meta.minLength, a) | |
| case "isMaxLength": | |
| return Schema.isMaxLength(filter.meta.maxLength, a) | |
| case "isLengthBetween": | |
| return Schema.isLengthBetween(filter.meta.minimum, filter.meta.maximum, a) | |
| case "isPattern": | |
| return Schema.isPattern(filter.meta.regExp, a) | |
| case "isTrimmed": | |
| return Schema.isTrimmed(a) | |
| case "isUUID": | |
| return Schema.isUUID(filter.meta.version, a) | |
| case "isGUID": | |
| return Schema.isGUID(a) | |
| case "isULID": | |
| return Schema.isULID(a) | |
| case "isBase64": | |
| return Schema.isBase64(a) | |
| case "isBase64Url": | |
| return Schema.isBase64Url(a) | |
| case "isStartsWith": | |
| return Schema.isStartsWith(filter.meta.startsWith, a) | |
| case "isEndsWith": | |
| return Schema.isEndsWith(filter.meta.endsWith, a) | |
| case "isIncludes": | |
| return Schema.isIncludes(filter.meta.includes, a) | |
| case "isUppercased": | |
| return Schema.isUppercased(a) | |
| case "isLowercased": | |
| return Schema.isLowercased(a) | |
| case "isCapitalized": | |
| return Schema.isCapitalized(a) | |
| case "isUncapitalized": | |
| return Schema.isUncapitalized(a) | |
| // Number Meta | |
| case "isFinite": | |
| return Schema.isFinite(a) | |
| case "isInt": | |
| return Schema.isInt(a) | |
| case "isMultipleOf": | |
| return Schema.isMultipleOf(filter.meta.divisor, a) | |
| case "isGreaterThan": | |
| return Schema.isGreaterThan(filter.meta.exclusiveMinimum, a) | |
| case "isGreaterThanOrEqualTo": | |
| return Schema.isGreaterThanOrEqualTo(filter.meta.minimum, a) | |
| case "isLessThan": | |
| return Schema.isLessThan(filter.meta.exclusiveMaximum, a) | |
| case "isLessThanOrEqualTo": | |
| return Schema.isLessThanOrEqualTo(filter.meta.maximum, a) | |
| case "isBetween": | |
| return Schema.isBetween(filter.meta, a) | |
| // BigInt Meta | |
| case "isGreaterThanBigInt": | |
| return Schema.isGreaterThanBigInt(filter.meta.exclusiveMinimum, a) | |
| case "isGreaterThanOrEqualToBigInt": | |
| return Schema.isGreaterThanOrEqualToBigInt(filter.meta.minimum, a) | |
| case "isLessThanBigInt": | |
| return Schema.isLessThanBigInt(filter.meta.exclusiveMaximum, a) | |
| case "isLessThanOrEqualToBigInt": | |
| return Schema.isLessThanOrEqualToBigInt(filter.meta.maximum, a) | |
| case "isBetweenBigInt": | |
| return Schema.isBetweenBigInt(filter.meta, a) | |
| // Object Meta | |
| case "isMinProperties": | |
| return Schema.isMinProperties(filter.meta.minProperties, a) | |
| case "isMaxProperties": | |
| return Schema.isMaxProperties(filter.meta.maxProperties, a) | |
| case "isPropertiesLengthBetween": | |
| return Schema.isPropertiesLengthBetween(filter.meta.minimum, filter.meta.maximum, a) | |
| case "isPropertyNames": | |
| return Schema.isPropertyNames(recur(filter.meta.propertyNames) as Schema.Record.Key, a) | |
| // Arrays Meta | |
| case "isUnique": | |
| return Schema.isUnique(a) | |
| // Date Meta | |
| case "isDateValid": | |
| return Schema.isDateValid(a) | |
| case "isGreaterThanDate": | |
| return Schema.isGreaterThanDate(filter.meta.exclusiveMinimum, a) | |
| case "isGreaterThanOrEqualToDate": | |
| return Schema.isGreaterThanOrEqualToDate(filter.meta.minimum, a) | |
| case "isLessThanDate": | |
| return Schema.isLessThanDate(filter.meta.exclusiveMaximum, a) | |
| case "isLessThanOrEqualToDate": | |
| return Schema.isLessThanOrEqualToDate(filter.meta.maximum, a) | |
| case "isBetweenDate": | |
| return Schema.isBetweenDate(filter.meta, a) | |
| // Size Meta | |
| case "isMinSize": | |
| return Schema.isMinSize(filter.meta.minSize, a) | |
| case "isMaxSize": | |
| return Schema.isMaxSize(filter.meta.maxSize, a) | |
| case "isSizeBetween": | |
| return Schema.isSizeBetween(filter.meta.minimum, filter.meta.maximum, a) | |
| } | |
| } | |
| } | |
| /** | |
| * Converts a {@link Document} to a Draft 2020-12 JSON Schema document. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need to produce a standard JSON Schema document from a schema | |
| * representation `Document`. | |
| * | |
| * **Gotchas** | |
| * | |
| * JSON Schema generation is best-effort. Some Effect schema representation | |
| * semantics cannot be represented exactly in JSON Schema, and importing an | |
| * emitted JSON Schema may produce an equivalent approximation rather than the | |
| * original representation shape. | |
| * | |
| * **Example** (Generating JSON Schema) | |
| * | |
| * ```ts | |
| * import { Schema, SchemaRepresentation } from "effect" | |
| * | |
| * const doc = SchemaRepresentation.fromAST(Schema.String.ast) | |
| * const jsonSchema = SchemaRepresentation.toJsonSchemaDocument(doc) | |
| * console.log(jsonSchema.schema.type) | |
| * // "string" | |
| * ``` | |
| * | |
| * @see {@link Document} | |
| * @see {@link toJsonSchemaMultiDocument} | |
| * @see {@link fromJsonSchemaDocument} | |
| * | |
| * @category transforming | |
| * @since 4.0.0 | |
| */ | |
| export const toJsonSchemaDocument: ( | |
| document: Document, | |
| options?: Schema.ToJsonSchemaOptions | |
| ) => JsonSchema.Document<"draft-2020-12"> = InternalRepresentation.toJsonSchemaDocument | |
| /** | |
| * Converts a {@link MultiDocument} to a Draft 2020-12 JSON Schema | |
| * multi-document. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need to export related schema representation documents together | |
| * so shared definitions stay in multi-document JSON Schema form. | |
| * | |
| * **Gotchas** | |
| * | |
| * JSON Schema generation is best-effort. Some Effect schema representation | |
| * semantics cannot be represented exactly in JSON Schema, and importing an | |
| * emitted JSON Schema may produce equivalent approximations rather than the | |
| * original representation shapes. | |
| * | |
| * @see {@link MultiDocument} | |
| * @see {@link toJsonSchemaDocument} | |
| * @see {@link fromJsonSchemaMultiDocument} | |
| * | |
| * @category transforming | |
| * @since 4.0.0 | |
| */ | |
| export const toJsonSchemaMultiDocument: ( | |
| document: MultiDocument, | |
| options?: Schema.ToJsonSchemaOptions | |
| ) => JsonSchema.MultiDocument<"draft-2020-12"> = InternalRepresentation.toJsonSchemaMultiDocument | |
| /** | |
| * A pair of TypeScript source strings for a schema: `runtime` is the | |
| * executable Schema expression, `Type` is the corresponding TypeScript type. | |
| * | |
| * @see {@link makeCode} | |
| * @see {@link CodeDocument} | |
| * | |
| * @category Code Generation | |
| * @since 4.0.0 | |
| */ | |
| export type Code = { | |
| readonly runtime: string | |
| readonly Type: string | |
| } | |
| /** | |
| * Constructs a {@link Code} value from a runtime expression string and a | |
| * TypeScript type string. | |
| * | |
| * @see {@link Code} | |
| * | |
| * @category Code Generation | |
| * @since 4.0.0 | |
| */ | |
| export function makeCode(runtime: string, Type: string): Code { | |
| return { runtime, Type } | |
| } | |
| /** | |
| * An auxiliary code artifact produced during code generation — a symbol | |
| * declaration, an enum declaration, or an import statement. | |
| * | |
| * @see {@link CodeDocument} | |
| * @see {@link toCodeDocument} | |
| * | |
| * @category Code Generation | |
| * @since 4.0.0 | |
| */ | |
| export type Artifact = | |
| | { | |
| readonly _tag: "Symbol" | |
| readonly identifier: string | |
| readonly generation: Code | |
| } | |
| | { | |
| readonly _tag: "Enum" | |
| readonly identifier: string | |
| readonly generation: Code | |
| } | |
| | { | |
| readonly _tag: "Import" | |
| readonly importDeclaration: string | |
| } | |
| /** | |
| * The output of {@link toCodeDocument}: generated TypeScript code for one or | |
| * more schemas plus their shared references and auxiliary artifacts. | |
| * | |
| * **Details** | |
| * | |
| * `codes` contains one {@link Code} per input representation. | |
| * `references.nonRecursives` contains topologically sorted non-recursive | |
| * definitions. `references.recursives` contains definitions involved in cycles. | |
| * `artifacts` contains symbols, enums, and import statements needed by the | |
| * code. | |
| * | |
| * @see {@link toCodeDocument} | |
| * @see {@link Code} | |
| * @see {@link Artifact} | |
| * | |
| * @category Code Generation | |
| * @since 4.0.0 | |
| */ | |
| export type CodeDocument = { | |
| readonly codes: ReadonlyArray<Code> | |
| readonly references: { | |
| readonly nonRecursives: ReadonlyArray<{ | |
| readonly $ref: string | |
| readonly code: Code | |
| }> | |
| readonly recursives: { | |
| readonly [$ref: string]: Code | |
| } | |
| } | |
| readonly artifacts: ReadonlyArray<Artifact> | |
| } | |
| /** | |
| * Generates TypeScript code strings from a {@link MultiDocument}. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need to produce source code for Effect Schema definitions from a | |
| * schema representation `MultiDocument`. | |
| * | |
| * **Details** | |
| * | |
| * `options.reviver` can customize code generation for {@link Declaration} | |
| * nodes. Return `undefined` to fall back to the default logic, which uses | |
| * `generation` annotations or the encoded schema. References are | |
| * topologically sorted so non-recursive definitions are emitted before their | |
| * dependents. `$ref` keys are converted to sanitized JavaScript identifiers. | |
| * | |
| * **Example** (Generating TypeScript code) | |
| * | |
| * ```ts | |
| * import { Schema, SchemaRepresentation } from "effect" | |
| * | |
| * const Person = Schema.Struct({ | |
| * name: Schema.String, | |
| * age: Schema.Int | |
| * }) | |
| * | |
| * const multi = SchemaRepresentation.toMultiDocument( | |
| * SchemaRepresentation.fromAST(Person.ast) | |
| * ) | |
| * const codeDoc = SchemaRepresentation.toCodeDocument(multi) | |
| * console.log(codeDoc.codes[0].runtime) | |
| * // Schema.Struct({ ... }) | |
| * ``` | |
| * | |
| * @see {@link CodeDocument} | |
| * @see {@link MultiDocument} | |
| * @see {@link Reviver} | |
| * | |
| * @category Code Generation | |
| * @since 4.0.0 | |
| */ | |
| export function toCodeDocument(multiDocument: MultiDocument, options?: { | |
| /** | |
| * The reviver can return `undefined` to indicate that the generation should be generated by the default logic | |
| */ | |
| readonly reviver?: Reviver<Code> | undefined | |
| }): CodeDocument { | |
| const artifacts: Array<Artifact> = [] | |
| const ts = topologicalSort(multiDocument.references) | |
| // Phase 1: Build sanitization map with collision handling | |
| const sanitizedReferenceMap = new Map<string, string>() | |
| const uniqueSanitizedReferences = new Set<string>() | |
| const referenceCount = new Map<string, number>() | |
| // Process all references first to build the map | |
| const allRefs = [ | |
| ...ts.nonRecursives.map(({ $ref }) => $ref), | |
| ...Object.keys(ts.recursives) | |
| ] | |
| for (const ref of allRefs) { | |
| ensureUniqueSanitized(ref) | |
| } | |
| // Phase 2: Use the map when processing references | |
| const nonRecursives = ts.nonRecursives.map(({ $ref, representation }) => ({ | |
| $ref: sanitizedReferenceMap.get($ref)!, | |
| code: recur(representation) | |
| })) | |
| const recursives = Rec.mapEntries(ts.recursives, (representation, $ref) => [ | |
| sanitizedReferenceMap.get($ref)!, | |
| recur(representation) | |
| ]) | |
| const codes = multiDocument.representations.map(recur) | |
| return { | |
| codes, | |
| references: { | |
| nonRecursives: nonRecursives.filter(({ $ref }) => (referenceCount.get($ref) ?? 0) > 0), | |
| recursives: Rec.filter(recursives, (_, $ref) => (referenceCount.get($ref) ?? 0) > 0) | |
| }, | |
| artifacts | |
| } | |
| function ensureUniqueSanitized(originalRef: string): string { | |
| // Check if already mapped (consistency) | |
| const sanitized = sanitizedReferenceMap.get(originalRef) | |
| if (sanitized !== undefined) { | |
| return sanitized | |
| } | |
| // Find unique sanitized name | |
| const seed = sanitizeJavaScriptIdentifier(originalRef) | |
| let candidate = seed | |
| let suffix = 0 | |
| while (uniqueSanitizedReferences.has(candidate)) { | |
| candidate = `${seed}${++suffix}` | |
| } | |
| uniqueSanitizedReferences.add(candidate) | |
| sanitizedReferenceMap.set(originalRef, candidate) | |
| return candidate | |
| } | |
| function addSymbol(s: symbol): string { | |
| const identifier = ensureUniqueSanitized("_symbol") | |
| const key = globalThis.Symbol.keyFor(s) | |
| const description = s.description | |
| const generation = key === undefined | |
| ? makeCode(`Symbol(${description === undefined ? "" : format(description)})`, `typeof ${identifier}`) | |
| : makeCode(`Symbol.for(${format(key)})`, `typeof ${identifier}`) | |
| artifacts.push({ _tag: "Symbol", identifier, generation }) | |
| return identifier | |
| } | |
| function addEnum(s: Enum): string { | |
| const identifier = ensureUniqueSanitized("_Enum") | |
| artifacts.push({ | |
| _tag: "Enum", | |
| identifier, | |
| generation: makeCode( | |
| `enum ${identifier} { ${s.enums.map(([name, value]) => `${format(name)}: ${format(value)}`).join(", ")} }`, | |
| `typeof ${identifier}` | |
| ) | |
| }) | |
| return identifier | |
| } | |
| function addImport(importDeclaration: string) { | |
| if (!artifacts.some((a) => a._tag === "Import" && a.importDeclaration === importDeclaration)) { | |
| artifacts.push({ _tag: "Import", importDeclaration }) | |
| } | |
| } | |
| function recur(s: Representation): Code { | |
| const g = on(s) | |
| switch (s._tag) { | |
| default: | |
| return makeCode( | |
| g.runtime + toRuntimeAnnotate(s.annotations) + toRuntimeBrand(s.annotations), | |
| g.Type + toTypeBrand(s.annotations) | |
| ) | |
| case "Reference": | |
| return g | |
| case "Declaration": | |
| case "String": | |
| case "Number": | |
| case "BigInt": | |
| case "Arrays": | |
| case "Objects": | |
| case "Suspend": | |
| return makeCode( | |
| g.runtime + toRuntimeAnnotate(s.annotations) + toRuntimeBrand(s.annotations) + toRuntimeChecks(s.checks), | |
| g.Type + toTypeBrand(s.annotations) + toTypeChecks(s.checks) | |
| ) | |
| } | |
| } | |
| function on(s: Representation): Code { | |
| switch (s._tag) { | |
| case "Declaration": { | |
| // if there is a reviver, use it to generate the generation | |
| if (options?.reviver !== undefined) { | |
| // the reviver can return `undefined` to indicate that the generation should be generated by the default logic | |
| const out = options.reviver(s, recur) | |
| if (out !== undefined) { | |
| return out | |
| } | |
| } | |
| // otherwise, use the generation from the annotations | |
| const generation = s.annotations?.generation | |
| if ( | |
| Predicate.isObject(generation) && typeof generation.runtime === "string" && | |
| typeof generation.Type === "string" | |
| ) { | |
| const typeParameters = s.typeParameters.map(recur) | |
| if (typeof generation.importDeclaration === "string") { | |
| addImport(generation.importDeclaration) | |
| } | |
| return makeCode( | |
| replacePlaceholders(generation.runtime, typeParameters.map((p) => p.runtime)), | |
| replacePlaceholders(generation.Type, typeParameters.map((p) => p.Type)) | |
| ) | |
| } | |
| // otherwise, use the generation from the encoded schema | |
| return recur(s.encodedSchema) | |
| } | |
| case "Reference": { | |
| const sanitized = ensureUniqueSanitized(s.$ref) | |
| referenceCount.set(sanitized, (referenceCount.get(sanitized) ?? 0) + 1) | |
| return makeCode(sanitized, sanitized) | |
| } | |
| case "Suspend": { | |
| const thunk = recur(s.thunk) | |
| return makeCode( | |
| `Schema.suspend((): Schema.Codec<${thunk.Type}> => ${thunk.runtime})`, | |
| thunk.Type | |
| ) | |
| } | |
| case "Null": | |
| return makeCode(`Schema.Null`, "null") | |
| case "Undefined": | |
| return makeCode(`Schema.Undefined`, "undefined") | |
| case "Void": | |
| return makeCode(`Schema.Void`, "void") | |
| case "Never": | |
| return makeCode(`Schema.Never`, "never") | |
| case "Unknown": | |
| return makeCode(`Schema.Unknown`, "unknown") | |
| case "Any": | |
| return makeCode(`Schema.Any`, "any") | |
| case "Number": | |
| return makeCode(`Schema.Number`, "number") | |
| case "Boolean": | |
| return makeCode(`Schema.Boolean`, "boolean") | |
| case "BigInt": | |
| return makeCode(`Schema.BigInt`, "bigint") | |
| case "Symbol": | |
| return makeCode(`Schema.Symbol`, "symbol") | |
| case "String": { | |
| const contentMediaType = s.contentMediaType | |
| const contentSchema = s.contentSchema | |
| if (contentMediaType === "application/json" && contentSchema !== undefined) { | |
| return makeCode(`Schema.fromJsonString(${recur(contentSchema)})`, "string") | |
| } else { | |
| return makeCode(`Schema.String`, "string") | |
| } | |
| } | |
| case "Literal": { | |
| const literal = format(s.literal) | |
| return makeCode(`Schema.Literal(${literal})`, literal) | |
| } | |
| case "UniqueSymbol": { | |
| const identifier = addSymbol(s.symbol) | |
| return makeCode(`Schema.UniqueSymbol(${identifier})`, `typeof ${identifier}`) | |
| } | |
| case "ObjectKeyword": | |
| return makeCode(`Schema.ObjectKeyword`, "object") | |
| case "Enum": { | |
| const identifier = addEnum(s) | |
| return makeCode(`Schema.Enum(${identifier})`, `typeof ${identifier}`) | |
| } | |
| case "TemplateLiteral": { | |
| const parts = s.parts.map(recur) | |
| const type = toTypeParts(s.parts).map((p) => "`" + p + "`").join(" | ") | |
| return makeCode(`Schema.TemplateLiteral([${parts.map((p) => p.runtime).join(", ")}])`, type) | |
| } | |
| case "Arrays": { | |
| const elements = s.elements.map((e) => { | |
| return { | |
| isOptional: e.isOptional, | |
| type: recur(e.type), | |
| annotations: e.annotations | |
| } | |
| }) | |
| const rest = s.rest.map(recur) | |
| if (Arr.isArrayNonEmpty(rest)) { | |
| const item = rest[0] | |
| if (elements.length === 0 && rest.length === 1) { | |
| return makeCode( | |
| `Schema.Array(${item.runtime})`, | |
| `ReadonlyArray<${item.Type}>` | |
| ) | |
| } | |
| const post = rest.slice(1) | |
| return makeCode( | |
| `Schema.TupleWithRest(Schema.Tuple([${ | |
| elements.map((e) => | |
| toRuntimeIsOptional(e.isOptional, e.type.runtime) + toRuntimeAnnotateKey(e.annotations) | |
| ).join(", ") | |
| }]), [${rest.map((r) => r.runtime).join(", ")}])`, | |
| `readonly [${ | |
| elements.map((e) => toTypeIsOptional(e.isOptional, e.type.Type)).join(", ") | |
| }, ...Array<${item.Type}>${post.length > 0 ? `, ${post.map((p) => p.Type).join(", ")}` : ""}]` | |
| ) | |
| } | |
| return makeCode( | |
| `Schema.Tuple([${ | |
| elements.map((e) => toRuntimeIsOptional(e.isOptional, e.type.runtime) + toRuntimeAnnotateKey(e.annotations)) | |
| .join(", ") | |
| }])`, | |
| `readonly [${elements.map((e) => toTypeIsOptional(e.isOptional, e.type.Type)).join(", ")}]` | |
| ) | |
| } | |
| case "Objects": { | |
| const pss = s.propertySignatures.map((p) => { | |
| const isSymbol = typeof p.name === "symbol" | |
| const name = isSymbol ? addSymbol(p.name) : formatPropertyKey(p.name) | |
| const nameType = toTypeIsOptional( | |
| p.isOptional, | |
| toTypeIsMutable(p.isMutable, isSymbol ? `[typeof ${name}]` : name) | |
| ) | |
| const type = recur(p.type) | |
| return makeCode( | |
| `${isSymbol ? `[${name}]` : name}: ${ | |
| toRuntimeIsOptional(p.isOptional, toRuntimeIsMutable(p.isMutable, type.runtime)) | |
| }` + | |
| toRuntimeAnnotateKey(p.annotations), | |
| `${nameType}: ${type.Type}` | |
| ) | |
| }) | |
| const iss = s.indexSignatures.map((is) => { | |
| return { | |
| parameter: recur(is.parameter), | |
| type: recur(is.type) | |
| } | |
| }) | |
| if (iss.length === 0) { | |
| // 1) Only properties -> Struct | |
| return makeCode( | |
| `Schema.Struct({ ${pss.map((p) => p.runtime).join(", ")} })`, | |
| `{ ${pss.map((p) => p.Type).join(", ")} }` | |
| ) | |
| } else if (pss.length === 0 && iss.length === 1) { | |
| // 2) Only one index signature and no properties -> Record | |
| return makeCode( | |
| `Schema.Record(${iss[0].parameter.runtime}, ${iss[0].type.runtime})`, | |
| `{ readonly [x: ${iss[0].parameter.Type}]: ${iss[0].type.Type} }` | |
| ) | |
| } else { | |
| // 3) Properties + index signatures -> StructWithRest | |
| return makeCode( | |
| `Schema.StructWithRest(Schema.Struct({ ${pss.map((p) => p.runtime).join(", ")} }), [${ | |
| iss.map((is) => `Schema.Record(${is.parameter.runtime}, ${is.type.runtime})`).join(", ") | |
| }])`, | |
| `{ ${pss.map((p) => p.Type).join(", ")}, ${ | |
| iss.map((is) => `readonly [x: ${is.parameter.Type}]: ${is.type.Type}`).join(", ") | |
| } }` | |
| ) | |
| } | |
| } | |
| case "Union": { | |
| if (s.types.length === 0) { | |
| return makeCode("Schema.Never", "never") | |
| } | |
| if (s.types.every((t) => t._tag === "Literal")) { | |
| const literals = s.types.map((l) => format(l.literal)) | |
| if (literals.length === 1) { | |
| return makeCode(`Schema.Literal(${literals[0]})`, literals[0]) | |
| } | |
| return makeCode(`Schema.Literals([${literals.join(", ")}])`, literals.join(" | ")) | |
| } | |
| const mode = s.mode === "anyOf" ? "" : `, { mode: "oneOf" }` | |
| const types = s.types.map((t) => recur(t)) | |
| return makeCode( | |
| `Schema.Union([${types.map((t) => t.runtime).join(", ")}]${mode})`, | |
| types.map((t) => t.Type).join(" | ") | |
| ) | |
| } | |
| } | |
| } | |
| function toTypeBrand(annotations: Schema.Annotations.Annotations | undefined): string { | |
| const brands = collectBrands(annotations) | |
| if (brands.length === 0) return "" | |
| addImport(`import type * as Brand from "effect/Brand"`) | |
| return brands.map((b) => ` & Brand.Brand<${format(b)}>`).join("") | |
| } | |
| function toTypeChecks(checks: ReadonlyArray<Check<Meta>>): string { | |
| return checks.map((c) => toTypeCheck(c)).join("") | |
| } | |
| function toTypeCheck(check: Check<Meta>): string { | |
| switch (check._tag) { | |
| case "Filter": | |
| return toTypeBrand(check.annotations) | |
| case "FilterGroup": { | |
| return toTypeChecks(check.checks) | |
| } | |
| } | |
| } | |
| function toRuntimeChecks(checks: ReadonlyArray<Check<Meta>>): string { | |
| return checks.map((c) => `.check(${toRuntimeCheck(c)})` + toRuntimeBrand(c.annotations)).join("") | |
| } | |
| function toRuntimeCheck(check: Check<Meta>): string { | |
| switch (check._tag) { | |
| case "Filter": | |
| return toRuntimeFilter(check) | |
| case "FilterGroup": { | |
| const a = toRuntimeAnnotations(check.annotations) | |
| const ca = a === "" ? "" : `, ${a}` | |
| return `Schema.makeFilterGroup([${check.checks.map((c) => toRuntimeCheck(c)).join(", ")}]${ca})` | |
| } | |
| } | |
| } | |
| function toRuntimeFilter(filter: Filter<Meta>): string { | |
| const a = toRuntimeAnnotations(filter.annotations) | |
| const ca = a === "" ? "" : `, ${a}` | |
| switch (filter.meta._tag) { | |
| case "isTrimmed": | |
| case "isGUID": | |
| case "isULID": | |
| case "isBase64": | |
| case "isBase64Url": | |
| case "isUppercased": | |
| case "isLowercased": | |
| case "isCapitalized": | |
| case "isUncapitalized": | |
| case "isFinite": | |
| case "isInt": | |
| case "isUnique": | |
| case "isDateValid": | |
| return `Schema.${filter.meta._tag}(${a})` | |
| case "isStringFinite": | |
| case "isStringBigInt": | |
| case "isStringSymbol": | |
| case "isPattern": | |
| return `Schema.${filter.meta._tag}(${toRuntimeRegExp(filter.meta.regExp)}${ca})` | |
| case "isMinLength": | |
| return `Schema.isMinLength(${filter.meta.minLength}${ca})` | |
| case "isMaxLength": | |
| return `Schema.isMaxLength(${filter.meta.maxLength}${ca})` | |
| case "isLengthBetween": | |
| return `Schema.isLengthBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` | |
| case "isUUID": | |
| return `Schema.isUUID(${filter.meta.version}${ca})` | |
| case "isStartsWith": | |
| return `Schema.isStartsWith(${format(filter.meta.startsWith)}${ca})` | |
| case "isEndsWith": | |
| return `Schema.isEndsWith(${format(filter.meta.endsWith)}${ca})` | |
| case "isIncludes": | |
| return `Schema.isIncludes(${format(filter.meta.includes)}${ca})` | |
| case "isGreaterThan": | |
| case "isGreaterThanBigInt": | |
| case "isGreaterThanDate": | |
| return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.exclusiveMinimum)}${ca})` | |
| case "isGreaterThanOrEqualTo": | |
| case "isGreaterThanOrEqualToBigInt": | |
| case "isGreaterThanOrEqualToDate": | |
| return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.minimum)}${ca})` | |
| case "isLessThan": | |
| case "isLessThanBigInt": | |
| case "isLessThanDate": | |
| return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.exclusiveMaximum)}${ca})` | |
| case "isLessThanOrEqualTo": | |
| case "isLessThanOrEqualToBigInt": | |
| case "isLessThanOrEqualToDate": | |
| return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.maximum)}${ca})` | |
| case "isBetween": | |
| case "isBetweenBigInt": | |
| case "isBetweenDate": | |
| return `Schema.${filter.meta._tag}({ minimum: ${toRuntimeValue(filter.meta.minimum)}, maximum: ${ | |
| toRuntimeValue(filter.meta.maximum) | |
| }, exclusiveMinimum: ${toRuntimeValue(filter.meta.exclusiveMinimum)}, exclusiveMaximum: ${ | |
| toRuntimeValue(filter.meta.exclusiveMaximum) | |
| }${ca})` | |
| case "isMultipleOf": | |
| return `Schema.isMultipleOf(${filter.meta.divisor}${ca})` | |
| case "isMinProperties": | |
| return `Schema.isMinProperties(${filter.meta.minProperties}${ca})` | |
| case "isMaxProperties": | |
| return `Schema.isMaxProperties(${filter.meta.maxProperties}${ca})` | |
| case "isPropertiesLengthBetween": | |
| return `Schema.isPropertiesLengthBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` | |
| case "isPropertyNames": | |
| return `Schema.isPropertyNames(${recur(filter.meta.propertyNames).runtime}${ca})` | |
| case "isMinSize": | |
| return `Schema.isMinSize(${filter.meta.minSize}${ca})` | |
| case "isMaxSize": | |
| return `Schema.isMaxSize(${filter.meta.maxSize}${ca})` | |
| case "isSizeBetween": | |
| return `Schema.isSizeBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` | |
| } | |
| } | |
| } | |
| const VALID_ASCII_UPPER_JAVASCRIPT_IDENTIFIER_REGEXP = /^[A-Z_$][A-Za-z0-9_$]*$/ | |
| /** | |
| * Converts an arbitrary string into a valid (ASCII) JavaScript identifier | |
| * starting with an uppercase letter, `$`, or `_`. | |
| * | |
| * - Replaces invalid identifier characters with `_` | |
| * - Uppercases a leading ASCII letter | |
| * - If the first character is a digit, prefixes `_` | |
| * - Empty input becomes `_` | |
| * | |
| * @internal | |
| */ | |
| export function sanitizeJavaScriptIdentifier(s: string): string { | |
| if (s.length === 0) return "_" | |
| if (VALID_ASCII_UPPER_JAVASCRIPT_IDENTIFIER_REGEXP.test(s)) return s | |
| const out: Array<string> = [] | |
| let needsPrefix = false | |
| let i = 0 | |
| for (const ch of s) { | |
| if (i === 0) { | |
| if (ch === "_" || ch === "$" || (ch >= "A" && ch <= "Z")) { | |
| out.push(ch) | |
| } else if (ch >= "a" && ch <= "z") { | |
| out.push(ch.toUpperCase()) | |
| } else if (ch >= "0" && ch <= "9") { | |
| out.push(ch) | |
| needsPrefix = true | |
| } else { | |
| out.push("_") | |
| } | |
| } else { | |
| out.push(isAsciiIdPart(ch) ? ch : "_") | |
| } | |
| i++ | |
| } | |
| return needsPrefix ? "_" + out.join("") : out.join("") | |
| } | |
| function isAsciiIdStart(ch: string): boolean { | |
| return ( | |
| ch === "_" || | |
| ch === "$" || | |
| (ch >= "A" && ch <= "Z") || | |
| (ch >= "a" && ch <= "z") | |
| ) | |
| } | |
| function isAsciiIdPart(ch: string): boolean { | |
| return isAsciiIdStart(ch) || (ch >= "0" && ch <= "9") | |
| } | |
| function replacePlaceholders(template: string, items: ReadonlyArray<string>) { | |
| let i = 0 | |
| return template.replace(/\?/g, () => items[i++]); | |
| } | |
| function toTypeParts(parts: ReadonlyArray<Representation>): ReadonlyArray<string> { | |
| if (parts.length === 0) { | |
| return [""] | |
| } | |
| const [first, ...rest] = parts | |
| const restPatterns = toTypeParts(rest) | |
| return toTypePart(first).flatMap((f) => restPatterns.map((r) => f + r)) | |
| } | |
| function toTypePart(r: Representation): ReadonlyArray<string> { | |
| switch (r._tag) { | |
| case "Literal": | |
| return [globalThis.String(r.literal)] | |
| case "String": | |
| return ["${string}"] | |
| case "Number": | |
| return ["${number}"] | |
| case "BigInt": | |
| return ["${bigint}"] | |
| case "TemplateLiteral": | |
| return toTypeParts(r.parts) | |
| case "Union": | |
| return r.types.flatMap(toTypePart) | |
| default: | |
| return [] | |
| } | |
| } | |
| const toCodeAnnotationsBlacklist: Set<string> = new Set([ | |
| ...toJsonAnnotationsBlacklist, | |
| "typeConstructor", | |
| "generation", | |
| "brands" | |
| ]) | |
| function toRuntimeAnnotations(annotations: Schema.Annotations.Annotations | undefined): string { | |
| if (!annotations) return "" | |
| const entries: Array<string> = [] | |
| for (const [key, value] of Object.entries(annotations)) { | |
| if (toCodeAnnotationsBlacklist.has(key)) continue | |
| entries.push(`${formatPropertyKey(key)}: ${format(value)}`) | |
| } | |
| if (entries.length === 0) return "" | |
| return `{ ${entries.join(", ")} }` | |
| } | |
| function toRuntimeBrand(annotations: Schema.Annotations.Annotations | undefined): string { | |
| const brands = collectBrands(annotations) | |
| return brands.length > 0 ? `.pipe(${brands.map((b) => `Schema.brand(${format(b)})`).join(", ")})` : "" | |
| } | |
| function toRuntimeAnnotate(annotations: Schema.Annotations.Annotations | undefined): string { | |
| const s = toRuntimeAnnotations(annotations) | |
| return s === "" ? "" : `.annotate(${s})` | |
| } | |
| function toRuntimeAnnotateKey(annotations: Schema.Annotations.Annotations | undefined): string { | |
| const s = toRuntimeAnnotations(annotations) | |
| return s === "" ? "" : `.annotateKey(${s})` | |
| } | |
| function toRuntimeIsOptional(isOptional: boolean, runtime: string): string { | |
| return isOptional ? `Schema.optionalKey(${runtime})` : runtime | |
| } | |
| function toTypeIsOptional(isOptional: boolean, type: string): string { | |
| return isOptional ? `${type}?` : type | |
| } | |
| function toRuntimeIsMutable(isMutable: boolean, runtime: string): string { | |
| return isMutable ? `Schema.mutableKey(${runtime})` : runtime | |
| } | |
| function toTypeIsMutable(isMutable: boolean, type: string): string { | |
| return isMutable ? type : `readonly ${type}` | |
| } | |
| function toRuntimeValue(value: undefined | number | boolean | bigint | Date): string { | |
| if (value instanceof Date) { | |
| return `new Date(${value.getTime()})` | |
| } | |
| return format(value) | |
| } | |
| function toRuntimeRegExp(regExp: RegExp): string { | |
| const args = [format(regExp.source)] | |
| const flags = regExp.flags.trim() | |
| if (flags !== "") { | |
| args.push(format(flags)) | |
| } | |
| return `new RegExp(${args.join(", ")})` | |
| } | |
| /** | |
| * Parses a Draft 2020-12 JSON Schema document into a {@link Document}. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need to import a Draft 2020-12 JSON Schema document into the | |
| * Effect schema representation system. | |
| * | |
| * **Details** | |
| * | |
| * `options.onEnter` is an optional hook called on each JSON Schema node before | |
| * processing, allowing pre-transformation. | |
| * | |
| * **Gotchas** | |
| * | |
| * JSON Schema import is best-effort. Some JSON Schema constructs do not map | |
| * exactly to Effect schema representations, and importing a schema previously | |
| * emitted by `toJsonSchemaDocument` may produce an equivalent approximation | |
| * rather than the original representation shape. | |
| * | |
| * This throws if a `$ref` cannot be resolved within the document's definitions. | |
| * Circular `$ref`s are detected and cause an error. | |
| * | |
| * @see {@link Document} | |
| * @see {@link toJsonSchemaDocument} | |
| * @see {@link fromJsonSchemaMultiDocument} | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export function fromJsonSchemaDocument(document: JsonSchema.Document<"draft-2020-12">, options?: { | |
| readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined | |
| }): Document { | |
| const { references, representations: schemas } = fromJsonSchemaMultiDocument({ | |
| dialect: document.dialect, | |
| schemas: [document.schema], | |
| definitions: document.definitions | |
| }, options) | |
| return { | |
| representation: schemas[0], | |
| references | |
| } | |
| } | |
| /** | |
| * Parses a Draft 2020-12 JSON Schema multi-document into a | |
| * {@link MultiDocument}. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you need to import a Draft 2020-12 JSON Schema multi-document whose | |
| * schemas share definitions. | |
| * | |
| * **Details** | |
| * | |
| * `options.onEnter` is an optional hook called on each JSON Schema node before | |
| * processing. | |
| * | |
| * **Gotchas** | |
| * | |
| * JSON Schema import is best-effort. Some JSON Schema constructs do not map | |
| * exactly to Effect schema representations, and importing schemas previously | |
| * emitted by `toJsonSchemaMultiDocument` may produce equivalent approximations | |
| * rather than the original representation shapes. | |
| * | |
| * This throws if a `$ref` cannot be resolved. | |
| * | |
| * @see {@link MultiDocument} | |
| * @see {@link toJsonSchemaMultiDocument} | |
| * @see {@link fromJsonSchemaDocument} | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export function fromJsonSchemaMultiDocument(document: JsonSchema.MultiDocument<"draft-2020-12">, options?: { | |
| readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined | |
| }): MultiDocument { | |
| let visited: Set<string> | |
| const references: Record<string, Representation> = {} | |
| type Slot = { | |
| // 0 = not started, 1 = building, 2 = done | |
| state: 0 | 1 | 2 | |
| value: Exclude<Representation, { _tag: "Reference" }> | undefined | |
| } | |
| const slots = new Map<string, Slot>() | |
| function getSlot(identifier: string): Slot { | |
| const existing = slots.get(identifier) | |
| if (existing) return existing | |
| // Create the slot *before* resolving, so self-references can see it. | |
| const slot: Slot = { | |
| state: 0, | |
| value: undefined | |
| } | |
| slots.set(identifier, slot) | |
| return slot | |
| } | |
| function resolveReference($ref: string): Exclude<Representation, { _tag: "Reference" }> { | |
| const definition = document.definitions[$ref] | |
| if (definition === undefined) { | |
| throw new Error(`Reference ${$ref} not found`) | |
| } | |
| const slot = getSlot($ref) | |
| if (slot.state === 2) { | |
| // Already built: return the built schema directly | |
| return slot.value! | |
| } | |
| if (slot.state === 1) { | |
| // Circular: we're currently building this identifier. | |
| throw new Error(`Circular reference detected: ${$ref}`) | |
| } | |
| // First time: build it. | |
| slot.state = 1 | |
| const value = recur(definition) | |
| slot.value = value._tag === "Reference" ? resolveReference(value.$ref) : value | |
| slot.state = 2 | |
| return slot.value | |
| } | |
| Object.entries(document.definitions).forEach(([identifier, definition]) => { | |
| visited = new Set<string>([identifier]) | |
| references[identifier] = recur(definition) | |
| }) | |
| visited = new Set<string>() | |
| const representations = Arr.map(document.schemas, recur) | |
| return { | |
| representations, | |
| references | |
| } | |
| function recur(u: unknown): Representation { | |
| if (u === false) return never | |
| if (!Predicate.isObject(u)) return unknown | |
| let js: JsonSchema.JsonSchema = options?.onEnter?.(u) ?? u | |
| if (Array.isArray(js.type)) { | |
| if (js.type.every(isType)) { | |
| const { type, ...rest } = js | |
| js = { | |
| anyOf: type.map((type) => ({ type })), | |
| ...rest | |
| } | |
| } else { | |
| js = {} | |
| } | |
| } | |
| let out = on(js) | |
| const annotations = collectAnnotations(js) | |
| if (annotations !== undefined) { | |
| out = combine(out, { _tag: "Unknown", annotations }) | |
| } | |
| if (Array.isArray(js.allOf)) { | |
| out = js.allOf.reduce((acc, curr) => combine(acc, recur(curr)), out) | |
| } | |
| if (Array.isArray(js.anyOf)) { | |
| out = combine({ _tag: "Union", types: js.anyOf.map((type) => recur(type)), mode: "anyOf" }, out) | |
| } | |
| if (Array.isArray(js.oneOf)) { | |
| out = combine({ _tag: "Union", types: js.oneOf.map((type) => recur(type)), mode: "oneOf" }, out) | |
| } | |
| return out | |
| } | |
| function on(js: JsonSchema.JsonSchema): Representation { | |
| if (typeof js.$ref === "string") { | |
| const $ref = js.$ref.slice(2).split("/").at(-1) | |
| if ($ref !== undefined) { | |
| const reference: Reference = { _tag: "Reference", $ref: unescapeToken($ref) } | |
| if (visited.has($ref)) { | |
| return { _tag: "Suspend", thunk: reference, checks: [] } | |
| } else { | |
| return reference | |
| } | |
| } | |
| } else if ("const" in js) { | |
| if (isLiteralValue(js.const)) { | |
| return { _tag: "Literal", literal: js.const } | |
| } else if (js.const === null) { | |
| return null_ | |
| } | |
| } else if (Array.isArray(js.enum)) { | |
| const types: Array<Representation> = [] | |
| for (const e of js.enum) { | |
| if (isLiteralValue(e)) { | |
| types.push({ _tag: "Literal", literal: e }) | |
| } else if (e === null) { | |
| types.push(null_) | |
| } else { | |
| types.push(recur(e)) | |
| } | |
| } | |
| if (types.length === 1) { | |
| return types[0] | |
| } else { | |
| return { _tag: "Union", types, mode: "anyOf" } | |
| } | |
| } | |
| const type = isType(js.type) ? js.type : getType(js) | |
| if (type !== undefined) { | |
| switch (type) { | |
| case "null": | |
| return null_ | |
| case "string": { | |
| const checks = collectStringChecks(js) | |
| if (checks.length > 0) { | |
| return { ...string, checks } | |
| } | |
| return string | |
| } | |
| case "number": | |
| return { | |
| _tag: "Number", | |
| checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }, ...collectNumberChecks(js)] | |
| } | |
| case "integer": | |
| return { | |
| _tag: "Number", | |
| checks: [{ _tag: "Filter", meta: { _tag: "isInt" } }, ...collectNumberChecks(js)] | |
| } | |
| case "boolean": | |
| return boolean | |
| case "array": { | |
| const minItems = typeof js.minItems === "number" ? js.minItems : 0 | |
| const elements: Array<Element> = (Array.isArray(js.prefixItems) ? js.prefixItems : []).map((e, i) => ({ | |
| isOptional: i + 1 > minItems, | |
| type: recur(e) | |
| })) | |
| const rest: Array<Representation> = js.items !== undefined ? | |
| [recur(js.items)] | |
| : js.prefixItems !== undefined && typeof js.maxItems === "number" | |
| ? [] | |
| : [unknown] | |
| return { _tag: "Arrays", elements, rest, checks: collectArraysChecks(js) } | |
| } | |
| case "object": { | |
| return { | |
| _tag: "Objects", | |
| propertySignatures: collectProperties(js), | |
| indexSignatures: collectIndexSignatures(js), | |
| checks: collectObjectsChecks(js) | |
| } | |
| } | |
| } | |
| } | |
| return { _tag: "Unknown" } | |
| } | |
| function collectObjectsChecks(js: JsonSchema.JsonSchema): Array<Check<ObjectsMeta>> { | |
| const checks: Array<Check<ObjectsMeta>> = [] | |
| if (typeof js.minProperties === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isMinProperties", minProperties: js.minProperties } }) | |
| } | |
| if (typeof js.maxProperties === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isMaxProperties", maxProperties: js.maxProperties } }) | |
| } | |
| if (js.propertyNames !== undefined) { | |
| const propertyNames = recur(js.propertyNames) | |
| checks.push({ _tag: "Filter", meta: { _tag: "isPropertyNames", propertyNames } }) | |
| } | |
| return checks | |
| } | |
| function combine(a: Representation, b: Representation): Representation { | |
| switch (a._tag) { | |
| default: | |
| return never | |
| case "Reference": | |
| return combine(resolveReference(a.$ref), b) | |
| case "Never": | |
| return a | |
| case "Unknown": | |
| switch (b._tag) { | |
| case "Reference": | |
| return combine(a, resolveReference(b.$ref)) | |
| default: | |
| return { ...b, ...combineAnnotations(a.annotations, b.annotations) } | |
| } | |
| case "Null": | |
| switch (b._tag) { | |
| case "Unknown": | |
| case "Null": | |
| return { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| case "Union": | |
| return combine(b, a) | |
| case "Reference": | |
| return combine(a, resolveReference(b.$ref)) | |
| default: | |
| return never | |
| } | |
| case "String": | |
| switch (b._tag) { | |
| case "Unknown": | |
| return { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| case "String": { | |
| const checks = combineChecks(a.checks, b.checks, b.annotations) | |
| return { | |
| _tag: "String", | |
| checks: checks ?? a.checks, | |
| ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) | |
| } | |
| } | |
| case "Literal": | |
| return typeof b.literal === "string" ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } : never | |
| case "Union": | |
| return combine(b, a) | |
| case "Reference": | |
| return combine(a, resolveReference(b.$ref)) | |
| default: | |
| return never | |
| } | |
| case "Number": | |
| switch (b._tag) { | |
| case "Unknown": | |
| return { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| case "Number": { | |
| const checks = combineNumberChecks(a.checks, b.checks, b.annotations) | |
| return { | |
| _tag: "Number", | |
| checks: checks ?? a.checks, | |
| ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) | |
| } | |
| } | |
| case "Literal": | |
| return typeof b.literal === "number" ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } : never | |
| case "Union": | |
| return combine(b, a) | |
| case "Reference": | |
| return combine(a, resolveReference(b.$ref)) | |
| default: | |
| return never | |
| } | |
| case "Boolean": | |
| switch (b._tag) { | |
| case "Unknown": | |
| return { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| case "Boolean": | |
| return { _tag: "Boolean", ...combineAnnotations(a.annotations, b.annotations) } | |
| case "Literal": | |
| return typeof b.literal === "boolean" | |
| ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } | |
| : never | |
| case "Union": | |
| return combine(b, a) | |
| case "Reference": | |
| return combine(a, resolveReference(b.$ref)) | |
| default: | |
| return never | |
| } | |
| case "Literal": | |
| switch (b._tag) { | |
| case "Unknown": | |
| return { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| case "Literal": | |
| return a.literal === b.literal | |
| ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| : never | |
| case "String": | |
| return typeof a.literal === "string" ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } : never | |
| case "Number": | |
| return typeof a.literal === "number" ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } : never | |
| case "Boolean": | |
| return typeof a.literal === "boolean" | |
| ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| : never | |
| case "Union": | |
| return combine(b, a) | |
| case "Reference": | |
| return combine(a, resolveReference(b.$ref)) | |
| default: | |
| return never | |
| } | |
| case "Arrays": | |
| switch (b._tag) { | |
| case "Unknown": | |
| return { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| case "Arrays": { | |
| const checks = combineArraysChecks(a.checks, b.checks, b.annotations) | |
| return { | |
| _tag: "Arrays", | |
| elements: combineElements(a.elements, b.elements), | |
| rest: combineRest(a.rest, b.rest), | |
| checks: checks ?? a.checks, | |
| ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) | |
| } | |
| } | |
| case "Union": | |
| return combine(b, a) | |
| case "Reference": | |
| return combine(a, resolveReference(b.$ref)) | |
| default: | |
| return never | |
| } | |
| case "Objects": | |
| switch (b._tag) { | |
| case "Unknown": | |
| return { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| case "Objects": { | |
| const checks = combineChecks(a.checks, b.checks, b.annotations) | |
| return { | |
| _tag: "Objects", | |
| propertySignatures: combinePropertySignatures(a.propertySignatures, b.propertySignatures), | |
| indexSignatures: combineIndexSignatures(a.indexSignatures, b.indexSignatures), | |
| checks: checks ?? a.checks, | |
| ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) | |
| } | |
| } | |
| case "Union": | |
| return combine(b, a) | |
| case "Reference": | |
| return combine(a, resolveReference(b.$ref)) | |
| default: | |
| return never | |
| } | |
| case "Union": { | |
| switch (b._tag) { | |
| case "Unknown": | |
| return { ...a, ...combineAnnotations(a.annotations, b.annotations) } | |
| default: { | |
| const types = a.types.map((s) => combine(s, b)).filter((s) => s !== never) | |
| if (types.length === 0) return never | |
| return { | |
| _tag: "Union", | |
| types, | |
| mode: a.mode, | |
| ...makeAnnotations(a.annotations) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function collectProperties(js: JsonSchema.JsonSchema): Array<PropertySignature> { | |
| const properties: Record<string, unknown> = Predicate.isObject(js.properties) ? js.properties : {} | |
| const required = Array.isArray(js.required) ? js.required : [] | |
| required.forEach((key) => { | |
| if (!Object.hasOwn(properties, key)) { | |
| properties[key] = {} | |
| } | |
| }) | |
| return Object.entries(properties).map(([key, v]) => ({ | |
| name: key, | |
| type: recur(v), | |
| isOptional: !required.includes(key), | |
| isMutable: false | |
| })) | |
| } | |
| function collectIndexSignatures(js: JsonSchema.JsonSchema): Array<IndexSignature> { | |
| const out: Array<IndexSignature> = [] | |
| if (Predicate.isObject(js.patternProperties)) { | |
| for (const [pattern, value] of Object.entries(js.patternProperties)) { | |
| out.push({ parameter: recur({ pattern }), type: recur(value) }) | |
| } | |
| } | |
| if (js.additionalProperties === undefined || js.additionalProperties === true) { | |
| out.push({ parameter: string, type: unknown }) | |
| } else if (Predicate.isObject(js.additionalProperties)) { | |
| out.push({ parameter: string, type: recur(js.additionalProperties) }) | |
| } | |
| return out | |
| } | |
| function combineElements(a: ReadonlyArray<Element>, b: ReadonlyArray<Element>): Array<Element> { | |
| const len = Math.max(a.length, b.length) | |
| let out: Array<Element> = [] | |
| for (let i = 0; i < len; i++) { | |
| out.push({ | |
| isOptional: a[i].isOptional && b[i].isOptional, | |
| type: combine(a[i].type, b[i].type) | |
| }) | |
| } | |
| if (a.length > len) { | |
| out = [...out, ...a.slice(len)] | |
| } else if (b.length > len) { | |
| out = [...out, ...b.slice(len)] | |
| } | |
| return out | |
| } | |
| function combineRest(a: ReadonlyArray<Representation>, b: ReadonlyArray<Representation>): Array<Representation> { | |
| const len = Math.max(a.length, b.length) | |
| let out: Array<Representation> = [] | |
| for (let i = 0; i < len; i++) { | |
| out.push(combine(a[i], b[i])) | |
| } | |
| if (a.length > len) { | |
| out = [...out, ...a.slice(len)] | |
| } else if (b.length > len) { | |
| out = [...out, ...b.slice(len)] | |
| } | |
| return out | |
| } | |
| function combinePropertySignatures( | |
| a: ReadonlyArray<PropertySignature>, | |
| b: ReadonlyArray<PropertySignature> | |
| ): Array<PropertySignature> { | |
| const propertySignatures: Array<PropertySignature> = [] | |
| const thatPropertiesMap: Record<PropertyKey, PropertySignature> = {} | |
| for (const p of b) { | |
| thatPropertiesMap[p.name] = p | |
| } | |
| const keys = new Set<PropertyKey>() | |
| for (const p of a) { | |
| keys.add(p.name) | |
| const thatp = thatPropertiesMap[p.name] | |
| if (thatp) { | |
| propertySignatures.push( | |
| { | |
| name: p.name, | |
| type: combine(p.type, thatp.type), | |
| isOptional: p.isOptional && thatp.isOptional, | |
| isMutable: p.isMutable | |
| } | |
| ) | |
| } else { | |
| propertySignatures.push(p) | |
| } | |
| } | |
| for (const p of b) { | |
| if (!keys.has(p.name)) propertySignatures.push(p) | |
| } | |
| return propertySignatures | |
| } | |
| function combineIndexSignatures( | |
| a: ReadonlyArray<IndexSignature>, | |
| b: ReadonlyArray<IndexSignature> | |
| ): Array<IndexSignature> { | |
| if (a.length === 0 || b.length === 0) return [] | |
| const out: Array<IndexSignature> = [...a] | |
| for (const is of b) { | |
| if (is.parameter === string) { | |
| const i = a.findIndex((is) => is.parameter === string) | |
| if (i !== -1) { | |
| out[i] = { parameter: string, type: combine(a[i].type, is.type) } | |
| } else { | |
| out.push(is) | |
| } | |
| } else { | |
| out.push(is) | |
| } | |
| } | |
| return out | |
| } | |
| } | |
| function asChecks<M>( | |
| checks: ReadonlyArray<Check<M>>, | |
| annotations: Schema.Annotations.Annotations | undefined | |
| ): ReadonlyArray<Check<M>> | undefined { | |
| if (Arr.isReadonlyArrayNonEmpty(checks)) { | |
| if (annotations !== undefined) { | |
| if (checks.length === 1) { | |
| const check = checks[0] | |
| if (check.annotations === undefined) { | |
| return [{ ...check, annotations }] | |
| } else { | |
| return [{ _tag: "FilterGroup", checks, annotations }] | |
| } | |
| } else { | |
| return [{ _tag: "FilterGroup", checks, annotations }] | |
| } | |
| } | |
| return checks | |
| } | |
| } | |
| function combineChecks<M>( | |
| a: ReadonlyArray<Check<M>>, | |
| b: ReadonlyArray<Check<M>>, | |
| annotations: Schema.Annotations.Annotations | undefined | |
| ): Array<Check<M>> | undefined { | |
| const checks = asChecks(b, annotations) | |
| if (checks) { | |
| return [...a, ...checks] | |
| } | |
| } | |
| function combineNumberChecks( | |
| a: ReadonlyArray<Check<NumberMeta>>, | |
| b: ReadonlyArray<Check<NumberMeta>>, | |
| annotations: Schema.Annotations.Annotations | undefined | |
| ): Array<Check<NumberMeta>> | undefined { | |
| if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isFinite")) { | |
| b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isFinite") | |
| } | |
| if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isInt")) { | |
| b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isInt") | |
| } | |
| return combineChecks(a, b, annotations) | |
| } | |
| function combineArraysChecks( | |
| a: ReadonlyArray<Check<ArraysMeta>>, | |
| b: ReadonlyArray<Check<ArraysMeta>>, | |
| annotations: Schema.Annotations.Annotations | undefined | |
| ): Array<Check<ArraysMeta>> | undefined { | |
| if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isUnique")) { | |
| b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isUnique") | |
| } | |
| return combineChecks(a, b, annotations) | |
| } | |
| function makeAnnotations( | |
| annotations: Schema.Annotations.Annotations | undefined | |
| ): { annotations: Schema.Annotations.Annotations } | undefined { | |
| return annotations ? { annotations } : undefined | |
| } | |
| function combineAnnotations( | |
| a: Schema.Annotations.Annotations | undefined, | |
| b: Schema.Annotations.Annotations | undefined | |
| ): { annotations: Schema.Annotations.Annotations } | undefined { | |
| if (a === undefined) return makeAnnotations(b) | |
| if (b === undefined) return makeAnnotations(a) | |
| return { annotations: { ...a, ...b } } // TODO: better merge | |
| } | |
| function collectStringChecks(js: JsonSchema.JsonSchema): Array<Check<StringMeta>> { | |
| const checks: Array<Check<StringMeta>> = [] | |
| if (typeof js.minLength === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isMinLength", minLength: js.minLength } }) | |
| } | |
| if (typeof js.maxLength === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: js.maxLength } }) | |
| } | |
| if (typeof js.pattern === "string") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp(js.pattern) } }) | |
| } | |
| return checks | |
| } | |
| function collectNumberChecks(js: JsonSchema.JsonSchema): Array<Check<NumberMeta>> { | |
| const checks: Array<Check<NumberMeta>> = [] | |
| if (typeof js.minimum === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isGreaterThanOrEqualTo", minimum: js.minimum } }) | |
| } | |
| if (typeof js.maximum === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isLessThanOrEqualTo", maximum: js.maximum } }) | |
| } | |
| if (typeof js.exclusiveMinimum === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isGreaterThan", exclusiveMinimum: js.exclusiveMinimum } }) | |
| } | |
| if (typeof js.exclusiveMaximum === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isLessThan", exclusiveMaximum: js.exclusiveMaximum } }) | |
| } | |
| if (typeof js.multipleOf === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isMultipleOf", divisor: js.multipleOf } }) | |
| } | |
| return checks | |
| } | |
| function collectArraysChecks(js: JsonSchema.JsonSchema): Array<Check<ArraysMeta>> { | |
| const checks: Array<Check<ArraysMeta>> = [] | |
| if (js.prefixItems === undefined) { | |
| if (typeof js.minItems === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isMinLength", minLength: js.minItems } }) | |
| } | |
| if (typeof js.maxItems === "number") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: js.maxItems } }) | |
| } | |
| } | |
| if (typeof js.uniqueItems === "boolean") { | |
| checks.push({ _tag: "Filter", meta: { _tag: "isUnique" } }) | |
| } | |
| return checks | |
| } | |
| const unknown: Unknown = { _tag: "Unknown" } | |
| const never: Never = { _tag: "Never" } | |
| const null_: Null = { _tag: "Null" } | |
| const string: String = { _tag: "String", checks: [] } | |
| const boolean: Boolean = { _tag: "Boolean" } | |
| function collectAnnotations( | |
| schema: JsonSchema.JsonSchema | |
| ): Schema.Annotations.Annotations | undefined { | |
| const as: Record<string, unknown> = {} | |
| if (typeof schema.title === "string") as.title = schema.title | |
| if (typeof schema.description === "string") as.description = schema.description | |
| if (schema.default !== undefined) as.default = schema.default | |
| if (Array.isArray(schema.examples)) as.examples = schema.examples | |
| if (typeof schema.readOnly === "boolean") as.readOnly = schema.readOnly | |
| if (typeof schema.writeOnly === "boolean") as.writeOnly = schema.writeOnly | |
| if (typeof schema.format === "string") as.format = schema.format | |
| if (typeof schema.contentEncoding === "string") as.contentEncoding = schema.contentEncoding | |
| if (typeof schema.contentMediaType === "string") as.contentMediaType = schema.contentMediaType | |
| return Rec.isEmptyRecord(as) ? undefined : as | |
| } | |
| function isLiteralValue(value: unknown): value is SchemaAST.LiteralValue { | |
| return typeof value === "string" || typeof value === "number" || typeof value === "boolean" | |
| } | |
| const stringKeys = ["minLength", "maxLength", "pattern", "format", "contentMediaType", "contentSchema"] | |
| const numberKeys = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"] | |
| const objectKeys = [ | |
| "properties", | |
| "required", | |
| "additionalProperties", | |
| "patternProperties", | |
| "propertyNames", | |
| "minProperties", | |
| "maxProperties" | |
| ] | |
| const arrayKeys = ["items", "prefixItems", "additionalItems", "minItems", "maxItems", "uniqueItems"] | |
| function getType(js: JsonSchema.JsonSchema): JsonSchema.Type | undefined { | |
| if (stringKeys.some((key) => js[key] !== undefined)) { | |
| return "string" | |
| } | |
| if (numberKeys.some((key) => js[key] !== undefined)) { | |
| return "number" | |
| } | |
| if (objectKeys.some((key) => js[key] !== undefined)) { | |
| return "object" | |
| } | |
| if (arrayKeys.some((key) => js[key] !== undefined)) { | |
| return "array" | |
| } | |
| } | |
| const types = ["null", "string", "number", "integer", "boolean", "object", "array"] | |
| function isType(type: unknown): type is JsonSchema.Type { | |
| return typeof type === "string" && types.includes(type) | |
| } | |
| /** @internal */ | |
| export type TopologicalSort = { | |
| /** | |
| * The definitions that are not recursive. | |
| * The definitions that depends on other definitions are placed after the definitions they depend on | |
| */ | |
| readonly nonRecursives: ReadonlyArray<{ | |
| readonly $ref: string | |
| readonly representation: Representation | |
| }> | |
| /** | |
| * The recursive definitions (with no particular order). | |
| */ | |
| readonly recursives: { | |
| readonly [$ref: string]: Representation | |
| } | |
| } | |
| /** @internal */ | |
| export function topologicalSort(references: References): TopologicalSort { | |
| const identifiers = Object.keys(references) | |
| const identifierSet = new Set(identifiers) | |
| const collectRefs = (root: Representation): Set<string> => { | |
| const refs = new Set<string>() | |
| const visited = new WeakSet<object>() | |
| const stack: Array<Representation> = [root] | |
| while (stack.length > 0) { | |
| const r = stack.pop()! | |
| if (visited.has(r)) continue | |
| visited.add(r) | |
| if (r._tag === "Reference") { | |
| if (identifierSet.has(r.$ref)) { | |
| refs.add(r.$ref) | |
| } | |
| } | |
| // Push nested Representation schemas onto the stack | |
| switch (r._tag) { | |
| case "Declaration": | |
| for (const typeParam of r.typeParameters) stack.push(typeParam) | |
| stack.push(r.encodedSchema) | |
| break | |
| case "Suspend": | |
| stack.push(r.thunk) | |
| break | |
| case "String": | |
| if (r.contentSchema !== undefined) stack.push(r.contentSchema) | |
| break | |
| case "TemplateLiteral": | |
| for (const part of r.parts) stack.push(part) | |
| break | |
| case "Arrays": | |
| for (const element of r.elements) stack.push(element.type) | |
| for (const rest of r.rest) stack.push(rest) | |
| break | |
| case "Objects": | |
| for (const propertySignature of r.propertySignatures) stack.push(propertySignature.type) | |
| for (const indexSignature of r.indexSignatures) { | |
| stack.push(indexSignature.parameter) | |
| stack.push(indexSignature.type) | |
| } | |
| break | |
| case "Union": | |
| for (const type of r.types) stack.push(type) | |
| break | |
| } | |
| } | |
| return refs | |
| } | |
| // identifier -> internal identifiers it depends on | |
| const dependencies = new Map<string, Set<string>>( | |
| identifiers.map((id) => [id, collectRefs(references[id])]) | |
| ) | |
| // Mark only nodes that are part of cycles | |
| const recursive = new Set<string>() | |
| const state = new Map<string, 0 | 1 | 2>() // 0 = new, 1 = visiting, 2 = done | |
| const stack: Array<string> = [] | |
| const indexInStack = new Map<string, number>() | |
| const dfs = (id: string): void => { | |
| const s = state.get(id) ?? 0 | |
| if (s === 1) { | |
| const start = indexInStack.get(id) | |
| if (start !== undefined) { | |
| for (let i = start; i < stack.length; i++) { | |
| recursive.add(stack[i]) | |
| } | |
| } | |
| return | |
| } | |
| if (s === 2) return | |
| state.set(id, 1) | |
| indexInStack.set(id, stack.length) | |
| stack.push(id) | |
| for (const dep of dependencies.get(id) ?? []) { | |
| dfs(dep) | |
| } | |
| stack.pop() | |
| indexInStack.delete(id) | |
| state.set(id, 2) | |
| } | |
| for (const id of identifiers) dfs(id) | |
| // Topologically sort the non-recursive nodes (ignoring edges to recursive nodes) | |
| const inDegree = new Map<string, number>() | |
| const dependents = new Map<string, Set<string>>() // dep -> nodes that depend on it | |
| for (const id of identifiers) { | |
| if (!recursive.has(id)) { | |
| inDegree.set(id, 0) | |
| dependents.set(id, new Set()) | |
| } | |
| } | |
| for (const [id, deps] of dependencies) { | |
| if (recursive.has(id)) continue | |
| for (const dep of deps) { | |
| if (recursive.has(dep)) continue | |
| inDegree.set(id, (inDegree.get(id) ?? 0) + 1) | |
| dependents.get(dep)?.add(id) | |
| } | |
| } | |
| const queue: Array<string> = [] | |
| for (const [id, deg] of inDegree) { | |
| if (deg === 0) queue.push(id) | |
| } | |
| const nonRecursives: Array<{ readonly $ref: string; readonly representation: Representation }> = [] | |
| for (let i = 0; i < queue.length; i++) { | |
| const $ref = queue[i] | |
| nonRecursives.push({ $ref, representation: references[$ref] }) | |
| for (const next of dependents.get($ref) ?? []) { | |
| const deg = (inDegree.get(next) ?? 0) - 1 | |
| inDegree.set(next, deg) | |
| if (deg === 0) queue.push(next) | |
| } | |
| } | |
| const recursives: Record<string, Representation> = {} | |
| for (const $ref of recursive) { | |
| recursives[$ref] = references[$ref] | |
| } | |
| return { nonRecursives, recursives } | |
| } | |
Xet Storage Details
- Size:
- 115 kB
- Xet hash:
- d651dc8a53bccb061d3421fc63a589e1adbf3e8f6ba1a8aab711d626174277f6
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.