EdgeAIG/opencode / .opencode /node_modules /effect /src /SchemaRepresentation.ts
EdgeAIG's picture
download
raw
115 kB
/**
* 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.