UNPKG

effect

Version:

The missing standard library for TypeScript, for writing production-grade software.

2,136 lines (1,960 loc) 79.3 kB
/** * @since 3.10.0 */ import * as Arr from "./Array.js" import type { Effect } from "./Effect.js" import type { Equivalence } from "./Equivalence.js" import { dual, identity } from "./Function.js" import { globalValue } from "./GlobalValue.js" import * as Inspectable from "./Inspectable.js" import * as errors_ from "./internal/schema/errors.js" import * as util_ from "./internal/schema/util.js" import * as Number from "./Number.js" import * as Option from "./Option.js" import * as Order from "./Order.js" import type { ParseIssue } from "./ParseResult.js" import * as Predicate from "./Predicate.js" import * as regexp from "./RegExp.js" import type { Concurrency } from "./Types.js" /** * @category model * @since 3.10.0 */ export type AST = | Declaration | Literal | UniqueSymbol | UndefinedKeyword | VoidKeyword | NeverKeyword | UnknownKeyword | AnyKeyword | StringKeyword | NumberKeyword | BooleanKeyword | BigIntKeyword | SymbolKeyword | ObjectKeyword | Enums | TemplateLiteral // possible transformations | Refinement | TupleType | TypeLiteral | Union | Suspend // transformations | Transformation // ------------------------------------------------------------------------------------- // annotations // ------------------------------------------------------------------------------------- /** * @category annotations * @since 3.19.0 * @experimental */ export type TypeConstructorAnnotation = { readonly _tag: string [key: PropertyKey]: unknown } /** * @category annotations * @since 3.19.0 * @experimental */ export const TypeConstructorAnnotationId: unique symbol = Symbol.for("effect/annotation/TypeConstructor") /** * @category annotations * @since 3.10.0 */ export type BrandAnnotation = Arr.NonEmptyReadonlyArray<string | symbol> /** * @category annotations * @since 3.10.0 */ export const BrandAnnotationId: unique symbol = Symbol.for("effect/annotation/Brand") /** * @category annotations * @since 3.10.0 */ export type SchemaIdAnnotation = string | symbol /** * @category annotations * @since 3.10.0 */ export const SchemaIdAnnotationId: unique symbol = Symbol.for("effect/annotation/SchemaId") /** * @category annotations * @since 3.10.0 */ export type MessageAnnotation = (issue: ParseIssue) => string | Effect<string> | { readonly message: string | Effect<string> readonly override: boolean } /** * @category annotations * @since 3.10.0 */ export const MessageAnnotationId: unique symbol = Symbol.for("effect/annotation/Message") /** * @category annotations * @since 3.10.0 */ export type MissingMessageAnnotation = () => string | Effect<string> /** * @category annotations * @since 3.10.0 */ export const MissingMessageAnnotationId: unique symbol = Symbol.for("effect/annotation/MissingMessage") /** * @category annotations * @since 3.10.0 */ export type IdentifierAnnotation = string /** * @category annotations * @since 3.10.0 */ export const IdentifierAnnotationId: unique symbol = Symbol.for("effect/annotation/Identifier") /** * @category annotations * @since 3.10.0 */ export type TitleAnnotation = string /** * @category annotations * @since 3.10.0 */ export const TitleAnnotationId: unique symbol = Symbol.for("effect/annotation/Title") /** @internal */ export const AutoTitleAnnotationId: unique symbol = Symbol.for("effect/annotation/AutoTitle") /** * @category annotations * @since 3.10.0 */ export type DescriptionAnnotation = string /** * @category annotations * @since 3.10.0 */ export const DescriptionAnnotationId: unique symbol = Symbol.for("effect/annotation/Description") /** * @category annotations * @since 3.10.0 */ export type ExamplesAnnotation<A> = Arr.NonEmptyReadonlyArray<A> /** * @category annotations * @since 3.10.0 */ export const ExamplesAnnotationId: unique symbol = Symbol.for("effect/annotation/Examples") /** * @category annotations * @since 3.10.0 */ export type DefaultAnnotation<A> = A /** * @category annotations * @since 3.10.0 */ export const DefaultAnnotationId: unique symbol = Symbol.for("effect/annotation/Default") /** * @category annotations * @since 3.10.0 */ export type JSONSchemaAnnotation = object /** * @category annotations * @since 3.10.0 */ export const JSONSchemaAnnotationId: unique symbol = Symbol.for("effect/annotation/JSONSchema") /** * @category annotations * @since 3.10.0 */ export const ArbitraryAnnotationId: unique symbol = Symbol.for("effect/annotation/Arbitrary") /** * @category annotations * @since 3.10.0 */ export const PrettyAnnotationId: unique symbol = Symbol.for("effect/annotation/Pretty") /** * @category annotations * @since 3.10.0 */ export type EquivalenceAnnotation<A, TypeParameters extends ReadonlyArray<any> = readonly []> = ( ...equivalences: { readonly [K in keyof TypeParameters]: Equivalence<TypeParameters[K]> } ) => Equivalence<A> /** * @category annotations * @since 3.10.0 */ export const EquivalenceAnnotationId: unique symbol = Symbol.for("effect/annotation/Equivalence") /** * @category annotations * @since 3.10.0 */ export type DocumentationAnnotation = string /** * @category annotations * @since 3.10.0 */ export const DocumentationAnnotationId: unique symbol = Symbol.for("effect/annotation/Documentation") /** * @category annotations * @since 3.10.0 */ export type ConcurrencyAnnotation = Concurrency | undefined /** * @category annotations * @since 3.10.0 */ export const ConcurrencyAnnotationId: unique symbol = Symbol.for("effect/annotation/Concurrency") /** * @category annotations * @since 3.10.0 */ export type BatchingAnnotation = boolean | "inherit" | undefined /** * @category annotations * @since 3.10.0 */ export const BatchingAnnotationId: unique symbol = Symbol.for("effect/annotation/Batching") /** * @category annotations * @since 3.10.0 */ export type ParseIssueTitleAnnotation = (issue: ParseIssue) => string | undefined /** * @category annotations * @since 3.10.0 */ export const ParseIssueTitleAnnotationId: unique symbol = Symbol.for("effect/annotation/ParseIssueTitle") /** * @category annotations * @since 3.10.0 */ export const ParseOptionsAnnotationId: unique symbol = Symbol.for("effect/annotation/ParseOptions") /** * @category annotations * @since 3.10.0 */ export type DecodingFallbackAnnotation<A> = (issue: ParseIssue) => Effect<A, ParseIssue> /** * @category annotations * @since 3.10.0 */ export const DecodingFallbackAnnotationId: unique symbol = Symbol.for("effect/annotation/DecodingFallback") /** * @category annotations * @since 3.10.0 */ export const SurrogateAnnotationId: unique symbol = Symbol.for("effect/annotation/Surrogate") /** * @category annotations * @since 3.10.0 */ export type SurrogateAnnotation = AST /** @internal */ export const StableFilterAnnotationId: unique symbol = Symbol.for("effect/annotation/StableFilter") /** * A stable filter consistently applies fixed validation rules, such as * 'minItems', 'maxItems', and 'itemsCount', to ensure array length complies * with set criteria regardless of the input data's content. * * @internal */ export type StableFilterAnnotation = boolean /** * @category annotations * @since 3.10.0 */ export interface Annotations { readonly [_: string]: unknown readonly [_: symbol]: unknown } /** * @category annotations * @since 3.10.0 */ export interface Annotated { readonly annotations: Annotations } /** * @category annotations * @since 3.10.0 */ export const getAnnotation: { /** * @category annotations * @since 3.10.0 */ <A>(key: symbol): (annotated: Annotated) => Option.Option<A> /** * @category annotations * @since 3.10.0 */ <A>(annotated: Annotated, key: symbol): Option.Option<A> } = dual( 2, <A>(annotated: Annotated, key: symbol): Option.Option<A> => Object.prototype.hasOwnProperty.call(annotated.annotations, key) ? Option.some(annotated.annotations[key] as any) : Option.none() ) /** * @category annotations * @since 3.19.0 * @experimental */ export const getTypeConstructorAnnotation = getAnnotation<TypeConstructorAnnotation>(TypeConstructorAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getBrandAnnotation = getAnnotation<BrandAnnotation>(BrandAnnotationId) /** * @category annotations * @since 3.14.2 */ export const getSchemaIdAnnotation = getAnnotation<SchemaIdAnnotation>(SchemaIdAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getMessageAnnotation = getAnnotation<MessageAnnotation>(MessageAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getMissingMessageAnnotation = getAnnotation<MissingMessageAnnotation>(MissingMessageAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getTitleAnnotation = getAnnotation<TitleAnnotation>(TitleAnnotationId) /** @internal */ export const getAutoTitleAnnotation = getAnnotation<TitleAnnotation>(AutoTitleAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getIdentifierAnnotation = getAnnotation<IdentifierAnnotation>(IdentifierAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getDescriptionAnnotation = getAnnotation<DescriptionAnnotation>(DescriptionAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getExamplesAnnotation = getAnnotation<ExamplesAnnotation<unknown>>(ExamplesAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getDefaultAnnotation = getAnnotation<DefaultAnnotation<unknown>>(DefaultAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getJSONSchemaAnnotation = getAnnotation<JSONSchemaAnnotation>(JSONSchemaAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getDocumentationAnnotation = getAnnotation<DocumentationAnnotation>(DocumentationAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getConcurrencyAnnotation = getAnnotation<ConcurrencyAnnotation>(ConcurrencyAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getBatchingAnnotation = getAnnotation<BatchingAnnotation>(BatchingAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getParseIssueTitleAnnotation = getAnnotation<ParseIssueTitleAnnotation>(ParseIssueTitleAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getParseOptionsAnnotation = getAnnotation<ParseOptions>(ParseOptionsAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getDecodingFallbackAnnotation = getAnnotation<DecodingFallbackAnnotation<unknown>>( DecodingFallbackAnnotationId ) /** * @category annotations * @since 3.10.0 */ export const getSurrogateAnnotation = getAnnotation<SurrogateAnnotation>(SurrogateAnnotationId) const getStableFilterAnnotation = getAnnotation<StableFilterAnnotation>(StableFilterAnnotationId) /** @internal */ export const hasStableFilter = (annotated: Annotated) => Option.exists(getStableFilterAnnotation(annotated), (b) => b === true) /** * @category annotations * @since 3.10.0 */ export const JSONIdentifierAnnotationId: unique symbol = Symbol.for("effect/annotation/JSONIdentifier") /** * @category annotations * @since 3.10.0 */ export const getJSONIdentifierAnnotation = getAnnotation<IdentifierAnnotation>(JSONIdentifierAnnotationId) /** * @category annotations * @since 3.10.0 */ export const getJSONIdentifier = (annotated: Annotated) => Option.orElse(getJSONIdentifierAnnotation(annotated), () => getIdentifierAnnotation(annotated)) // ------------------------------------------------------------------------------------- // schema ids // ------------------------------------------------------------------------------------- /** * @category schema id * @since 3.10.0 */ export const ParseJsonSchemaId: unique symbol = Symbol.for("effect/schema/ParseJson") /** * @category model * @since 3.10.0 */ export class Declaration implements Annotated { /** * @since 3.10.0 */ readonly _tag = "Declaration" constructor( readonly typeParameters: ReadonlyArray<AST>, readonly decodeUnknown: ( ...typeParameters: ReadonlyArray<AST> ) => (input: unknown, options: ParseOptions, self: Declaration) => Effect<any, ParseIssue, any>, readonly encodeUnknown: ( ...typeParameters: ReadonlyArray<AST> ) => (input: unknown, options: ParseOptions, self: Declaration) => Effect<any, ParseIssue, any>, readonly annotations: Annotations = {} ) {} /** * @since 3.10.0 */ toString() { return Option.getOrElse(getExpected(this), () => "<declaration schema>") } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, typeParameters: this.typeParameters.map((ast) => ast.toJSON()), annotations: toJSONAnnotations(this.annotations) } } } const createASTGuard = <T extends AST["_tag"]>(tag: T) => (ast: AST): ast is Extract<AST, { _tag: T }> => ast._tag === tag /** * @category guards * @since 3.10.0 */ export const isDeclaration: (ast: AST) => ast is Declaration = createASTGuard("Declaration") /** * @category model * @since 3.10.0 */ export type LiteralValue = string | number | boolean | null | bigint /** * @category model * @since 3.10.0 */ export class Literal implements Annotated { /** * @since 3.10.0 */ readonly _tag = "Literal" constructor(readonly literal: LiteralValue, readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return Option.getOrElse(getExpected(this), () => Inspectable.formatUnknown(this.literal)) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, literal: Predicate.isBigInt(this.literal) ? String(this.literal) : this.literal, annotations: toJSONAnnotations(this.annotations) } } } /** * @category guards * @since 3.10.0 */ export const isLiteral: (ast: AST) => ast is Literal = createASTGuard("Literal") const $null = new Literal(null) export { /** * @category constructors * @since 3.10.0 */ $null as null } /** * @category model * @since 3.10.0 */ export class UniqueSymbol implements Annotated { /** * @since 3.10.0 */ readonly _tag = "UniqueSymbol" constructor(readonly symbol: symbol, readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return Option.getOrElse(getExpected(this), () => Inspectable.formatUnknown(this.symbol)) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, symbol: String(this.symbol), annotations: toJSONAnnotations(this.annotations) } } } /** * @category guards * @since 3.10.0 */ export const isUniqueSymbol: (ast: AST) => ast is UniqueSymbol = createASTGuard("UniqueSymbol") /** * @category model * @since 3.10.0 */ export class UndefinedKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "UndefinedKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const undefinedKeyword: UndefinedKeyword = new UndefinedKeyword({ [TitleAnnotationId]: "undefined" }) /** * @category guards * @since 3.10.0 */ export const isUndefinedKeyword: (ast: AST) => ast is UndefinedKeyword = createASTGuard("UndefinedKeyword") /** * @category model * @since 3.10.0 */ export class VoidKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "VoidKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const voidKeyword: VoidKeyword = new VoidKeyword({ [TitleAnnotationId]: "void" }) /** * @category guards * @since 3.10.0 */ export const isVoidKeyword: (ast: AST) => ast is VoidKeyword = createASTGuard("VoidKeyword") /** * @category model * @since 3.10.0 */ export class NeverKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "NeverKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const neverKeyword: NeverKeyword = new NeverKeyword({ [TitleAnnotationId]: "never" }) /** * @category guards * @since 3.10.0 */ export const isNeverKeyword: (ast: AST) => ast is NeverKeyword = createASTGuard("NeverKeyword") /** * @category model * @since 3.10.0 */ export class UnknownKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "UnknownKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const unknownKeyword: UnknownKeyword = new UnknownKeyword({ [TitleAnnotationId]: "unknown" }) /** * @category guards * @since 3.10.0 */ export const isUnknownKeyword: (ast: AST) => ast is UnknownKeyword = createASTGuard("UnknownKeyword") /** * @category model * @since 3.10.0 */ export class AnyKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "AnyKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const anyKeyword: AnyKeyword = new AnyKeyword({ [TitleAnnotationId]: "any" }) /** * @category guards * @since 3.10.0 */ export const isAnyKeyword: (ast: AST) => ast is AnyKeyword = createASTGuard("AnyKeyword") /** * @category model * @since 3.10.0 */ export class StringKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "StringKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const stringKeyword: StringKeyword = new StringKeyword({ [TitleAnnotationId]: "string", [DescriptionAnnotationId]: "a string" }) /** * @category guards * @since 3.10.0 */ export const isStringKeyword: (ast: AST) => ast is StringKeyword = createASTGuard("StringKeyword") /** * @category model * @since 3.10.0 */ export class NumberKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "NumberKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const numberKeyword: NumberKeyword = new NumberKeyword({ [TitleAnnotationId]: "number", [DescriptionAnnotationId]: "a number" }) /** * @category guards * @since 3.10.0 */ export const isNumberKeyword: (ast: AST) => ast is NumberKeyword = createASTGuard("NumberKeyword") /** * @category model * @since 3.10.0 */ export class BooleanKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "BooleanKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const booleanKeyword: BooleanKeyword = new BooleanKeyword({ [TitleAnnotationId]: "boolean", [DescriptionAnnotationId]: "a boolean" }) /** * @category guards * @since 3.10.0 */ export const isBooleanKeyword: (ast: AST) => ast is BooleanKeyword = createASTGuard("BooleanKeyword") /** * @category model * @since 3.10.0 */ export class BigIntKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "BigIntKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const bigIntKeyword: BigIntKeyword = new BigIntKeyword({ [TitleAnnotationId]: "bigint", [DescriptionAnnotationId]: "a bigint" }) /** * @category guards * @since 3.10.0 */ export const isBigIntKeyword: (ast: AST) => ast is BigIntKeyword = createASTGuard("BigIntKeyword") /** * @category model * @since 3.10.0 */ export class SymbolKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "SymbolKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const symbolKeyword: SymbolKeyword = new SymbolKeyword({ [TitleAnnotationId]: "symbol", [DescriptionAnnotationId]: "a symbol" }) /** * @category guards * @since 3.10.0 */ export const isSymbolKeyword: (ast: AST) => ast is SymbolKeyword = createASTGuard("SymbolKeyword") /** * @category model * @since 3.10.0 */ export class ObjectKeyword implements Annotated { /** * @since 3.10.0 */ readonly _tag = "ObjectKeyword" constructor(readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return formatKeyword(this) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, annotations: toJSONAnnotations(this.annotations) } } } /** * @category constructors * @since 3.10.0 */ export const objectKeyword: ObjectKeyword = new ObjectKeyword({ [TitleAnnotationId]: "object", [DescriptionAnnotationId]: "an object in the TypeScript meaning, i.e. the `object` type" }) /** * @category guards * @since 3.10.0 */ export const isObjectKeyword: (ast: AST) => ast is ObjectKeyword = createASTGuard("ObjectKeyword") /** * @category model * @since 3.10.0 */ export class Enums implements Annotated { /** * @since 3.10.0 */ readonly _tag = "Enums" constructor( readonly enums: ReadonlyArray<readonly [string, string | number]>, readonly annotations: Annotations = {} ) {} /** * @since 3.10.0 */ toString() { return Option.getOrElse( getExpected(this), () => `<enum ${this.enums.length} value(s): ${this.enums.map(([_, value]) => JSON.stringify(value)).join(" | ")}>` ) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, enums: this.enums, annotations: toJSONAnnotations(this.annotations) } } } /** * @category guards * @since 3.10.0 */ export const isEnums: (ast: AST) => ast is Enums = createASTGuard("Enums") type TemplateLiteralSpanBaseType = StringKeyword | NumberKeyword | Literal | TemplateLiteral type TemplateLiteralSpanType = TemplateLiteralSpanBaseType | Union<TemplateLiteralSpanType> const isTemplateLiteralSpanType = (ast: AST): ast is TemplateLiteralSpanType => { switch (ast._tag) { case "Literal": case "NumberKeyword": case "StringKeyword": case "TemplateLiteral": return true case "Union": return ast.types.every(isTemplateLiteralSpanType) } return false } const templateLiteralSpanUnionTypeToString = (type: TemplateLiteralSpanType): string => { switch (type._tag) { case "Literal": return JSON.stringify(String(type.literal)) case "StringKeyword": return "string" case "NumberKeyword": return "number" case "TemplateLiteral": return String(type) case "Union": return type.types.map(templateLiteralSpanUnionTypeToString).join(" | ") } } const templateLiteralSpanTypeToString = (type: TemplateLiteralSpanType): string => { switch (type._tag) { case "Literal": return String(type.literal) case "StringKeyword": return "${string}" case "NumberKeyword": return "${number}" case "TemplateLiteral": return "${" + String(type) + "}" case "Union": return "${" + type.types.map(templateLiteralSpanUnionTypeToString).join(" | ") + "}" } } /** * @category model * @since 3.10.0 */ export class TemplateLiteralSpan { /** * @since 3.10.0 */ readonly type: TemplateLiteralSpanType constructor(type: AST, readonly literal: string) { if (isTemplateLiteralSpanType(type)) { this.type = type } else { throw new Error(errors_.getSchemaUnsupportedLiteralSpanErrorMessage(type)) } } /** * @since 3.10.0 */ toString() { return templateLiteralSpanTypeToString(this.type) + this.literal } /** * @since 3.10.0 */ toJSON(): object { return { type: this.type.toJSON(), literal: this.literal } } } /** * @category model * @since 3.10.0 */ export class TemplateLiteral implements Annotated { /** * @since 3.10.0 */ readonly _tag = "TemplateLiteral" constructor( readonly head: string, readonly spans: Arr.NonEmptyReadonlyArray<TemplateLiteralSpan>, readonly annotations: Annotations = {} ) {} /** * @since 3.10.0 */ toString() { return Option.getOrElse(getExpected(this), () => formatTemplateLiteral(this)) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, head: this.head, spans: this.spans.map((span) => span.toJSON()), annotations: toJSONAnnotations(this.annotations) } } } const formatTemplateLiteral = (ast: TemplateLiteral): string => "`" + ast.head + ast.spans.map(String).join("") + "`" /** * @category guards * @since 3.10.0 */ export const isTemplateLiteral: (ast: AST) => ast is TemplateLiteral = createASTGuard("TemplateLiteral") /** * @category model * @since 3.10.0 */ export class Type implements Annotated { constructor( readonly type: AST, readonly annotations: Annotations = {} ) {} /** * @since 3.10.0 */ toJSON(): object { return { type: this.type.toJSON(), annotations: toJSONAnnotations(this.annotations) } } /** * @since 3.10.0 */ toString() { return String(this.type) } } /** * @category model * @since 3.10.0 */ export class OptionalType extends Type { constructor( type: AST, readonly isOptional: boolean, annotations: Annotations = {} ) { super(type, annotations) } /** * @since 3.10.0 */ toJSON(): object { return { type: this.type.toJSON(), isOptional: this.isOptional, annotations: toJSONAnnotations(this.annotations) } } /** * @since 3.10.0 */ toString() { return String(this.type) + (this.isOptional ? "?" : "") } } const getRestASTs = (rest: ReadonlyArray<Type>): ReadonlyArray<AST> => rest.map((annotatedAST) => annotatedAST.type) /** * @category model * @since 3.10.0 */ export class TupleType implements Annotated { /** * @since 3.10.0 */ readonly _tag = "TupleType" constructor( readonly elements: ReadonlyArray<OptionalType>, readonly rest: ReadonlyArray<Type>, readonly isReadonly: boolean, readonly annotations: Annotations = {} ) { let hasOptionalElement = false let hasIllegalRequiredElement = false for (const e of elements) { if (e.isOptional) { hasOptionalElement = true } else if (hasOptionalElement) { hasIllegalRequiredElement = true break } } if (hasIllegalRequiredElement || (hasOptionalElement && rest.length > 1)) { throw new Error(errors_.getASTRequiredElementFollowinAnOptionalElementErrorMessage) } } /** * @since 3.10.0 */ toString() { return Option.getOrElse(getExpected(this), () => formatTuple(this)) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, elements: this.elements.map((e) => e.toJSON()), rest: this.rest.map((ast) => ast.toJSON()), isReadonly: this.isReadonly, annotations: toJSONAnnotations(this.annotations) } } } const formatTuple = (ast: TupleType): string => { const formattedElements = ast.elements.map(String) .join(", ") return Arr.matchLeft(ast.rest, { onEmpty: () => `readonly [${formattedElements}]`, onNonEmpty: (head, tail) => { const formattedHead = String(head) const wrappedHead = formattedHead.includes(" | ") ? `(${formattedHead})` : formattedHead if (tail.length > 0) { const formattedTail = tail.map(String).join(", ") if (ast.elements.length > 0) { return `readonly [${formattedElements}, ...${wrappedHead}[], ${formattedTail}]` } else { return `readonly [...${wrappedHead}[], ${formattedTail}]` } } else { if (ast.elements.length > 0) { return `readonly [${formattedElements}, ...${wrappedHead}[]]` } else { return `ReadonlyArray<${formattedHead}>` } } } }) } /** * @category guards * @since 3.10.0 */ export const isTupleType: (ast: AST) => ast is TupleType = createASTGuard("TupleType") /** * @category model * @since 3.10.0 */ export class PropertySignature extends OptionalType { constructor( readonly name: PropertyKey, type: AST, isOptional: boolean, readonly isReadonly: boolean, annotations?: Annotations ) { super(type, isOptional, annotations) } /** * @since 3.10.0 */ toString(): string { return (this.isReadonly ? "readonly " : "") + String(this.name) + (this.isOptional ? "?" : "") + ": " + this.type } /** * @since 3.10.0 */ toJSON(): object { return { name: String(this.name), type: this.type.toJSON(), isOptional: this.isOptional, isReadonly: this.isReadonly, annotations: toJSONAnnotations(this.annotations) } } } /** * @since 3.10.0 */ export type Parameter = StringKeyword | SymbolKeyword | TemplateLiteral | Refinement<Parameter> /** * @since 3.10.0 */ export const isParameter = (ast: AST): ast is Parameter => { switch (ast._tag) { case "StringKeyword": case "SymbolKeyword": case "TemplateLiteral": return true case "Refinement": return isParameter(ast.from) } return false } /** * @category model * @since 3.10.0 */ export class IndexSignature { /** * @since 3.10.0 */ readonly parameter: Parameter constructor( parameter: AST, readonly type: AST, readonly isReadonly: boolean ) { if (isParameter(parameter)) { this.parameter = parameter } else { throw new Error(errors_.getASTIndexSignatureParameterErrorMessage) } } /** * @since 3.10.0 */ toString(): string { return (this.isReadonly ? "readonly " : "") + `[x: ${this.parameter}]: ${this.type}` } /** * @since 3.10.0 */ toJSON(): object { return { parameter: this.parameter.toJSON(), type: this.type.toJSON(), isReadonly: this.isReadonly } } } /** * @category model * @since 3.10.0 */ export class TypeLiteral implements Annotated { /** * @since 3.10.0 */ readonly _tag = "TypeLiteral" /** * @since 3.10.0 */ readonly propertySignatures: ReadonlyArray<PropertySignature> /** * @since 3.10.0 */ readonly indexSignatures: ReadonlyArray<IndexSignature> constructor( propertySignatures: ReadonlyArray<PropertySignature>, indexSignatures: ReadonlyArray<IndexSignature>, readonly annotations: Annotations = {} ) { // check for duplicate property signatures const keys: Record<PropertyKey, null> = {} for (let i = 0; i < propertySignatures.length; i++) { const name = propertySignatures[i].name if (Object.prototype.hasOwnProperty.call(keys, name)) { throw new Error(errors_.getASTDuplicatePropertySignatureErrorMessage(name)) } keys[name] = null } // check for duplicate index signatures const parameters = { string: false, symbol: false } for (let i = 0; i < indexSignatures.length; i++) { const encodedParameter = getEncodedParameter(indexSignatures[i].parameter) if (isStringKeyword(encodedParameter)) { if (parameters.string) { throw new Error(errors_.getASTDuplicateIndexSignatureErrorMessage("string")) } parameters.string = true } else if (isSymbolKeyword(encodedParameter)) { if (parameters.symbol) { throw new Error(errors_.getASTDuplicateIndexSignatureErrorMessage("symbol")) } parameters.symbol = true } } this.propertySignatures = propertySignatures this.indexSignatures = indexSignatures } /** * @since 3.10.0 */ toString() { return Option.getOrElse(getExpected(this), () => formatTypeLiteral(this)) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, propertySignatures: this.propertySignatures.map((ps) => ps.toJSON()), indexSignatures: this.indexSignatures.map((ps) => ps.toJSON()), annotations: toJSONAnnotations(this.annotations) } } } const formatIndexSignatures = (iss: ReadonlyArray<IndexSignature>): string => iss.map(String).join("; ") const formatTypeLiteral = (ast: TypeLiteral): string => { if (ast.propertySignatures.length > 0) { const pss = ast.propertySignatures.map(String).join("; ") if (ast.indexSignatures.length > 0) { return `{ ${pss}; ${formatIndexSignatures(ast.indexSignatures)} }` } else { return `{ ${pss} }` } } else { if (ast.indexSignatures.length > 0) { return `{ ${formatIndexSignatures(ast.indexSignatures)} }` } else { return "{}" } } } /** * @category guards * @since 3.10.0 */ export const isTypeLiteral: (ast: AST) => ast is TypeLiteral = createASTGuard("TypeLiteral") /** * @since 3.10.0 */ export type Members<A> = readonly [A, A, ...Array<A>] const sortCandidates = Arr.sort( Order.mapInput(Number.Order, (ast: AST) => { switch (ast._tag) { case "AnyKeyword": return 0 case "UnknownKeyword": return 1 case "ObjectKeyword": return 2 case "StringKeyword": case "NumberKeyword": case "BooleanKeyword": case "BigIntKeyword": case "SymbolKeyword": return 3 } return 4 }) ) const literalMap = { string: "StringKeyword", number: "NumberKeyword", boolean: "BooleanKeyword", bigint: "BigIntKeyword" } as const /** @internal */ export const flatten = (candidates: ReadonlyArray<AST>): Array<AST> => Arr.flatMap(candidates, (ast) => isUnion(ast) ? flatten(ast.types) : [ast]) /** @internal */ export const unify = (candidates: ReadonlyArray<AST>): Array<AST> => { const cs = sortCandidates(candidates) const out: Array<AST> = [] const uniques: { [K in AST["_tag"] | "{}"]?: AST } = {} const literals: Array<LiteralValue | symbol> = [] for (const ast of cs) { switch (ast._tag) { case "NeverKeyword": break case "AnyKeyword": return [anyKeyword] case "UnknownKeyword": return [unknownKeyword] // uniques case "ObjectKeyword": case "UndefinedKeyword": case "VoidKeyword": case "StringKeyword": case "NumberKeyword": case "BooleanKeyword": case "BigIntKeyword": case "SymbolKeyword": { if (!uniques[ast._tag]) { uniques[ast._tag] = ast out.push(ast) } break } case "Literal": { const type = typeof ast.literal switch (type) { case "string": case "number": case "bigint": case "boolean": { const _tag = literalMap[type] if (!uniques[_tag] && !literals.includes(ast.literal)) { literals.push(ast.literal) out.push(ast) } break } // null case "object": { if (!literals.includes(ast.literal)) { literals.push(ast.literal) out.push(ast) } break } } break } case "UniqueSymbol": { if (!uniques["SymbolKeyword"] && !literals.includes(ast.symbol)) { literals.push(ast.symbol) out.push(ast) } break } case "TupleType": { if (!uniques["ObjectKeyword"]) { out.push(ast) } break } case "TypeLiteral": { if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { if (!uniques["{}"]) { uniques["{}"] = ast out.push(ast) } } else if (!uniques["ObjectKeyword"]) { out.push(ast) } break } default: out.push(ast) } } return out } /** * @category model * @since 3.10.0 */ export class Union<M extends AST = AST> implements Annotated { static make = (types: ReadonlyArray<AST>, annotations?: Annotations): AST => { return isMembers(types) ? new Union(types, annotations) : types.length === 1 ? types[0] : neverKeyword } /** @internal */ static unify = (candidates: ReadonlyArray<AST>, annotations?: Annotations): AST => { return Union.make(unify(flatten(candidates)), annotations) } /** * @since 3.10.0 */ readonly _tag = "Union" private constructor(readonly types: Members<M>, readonly annotations: Annotations = {}) {} /** * @since 3.10.0 */ toString() { return Option.getOrElse(getExpected(this), () => this.types.map(String).join(" | ")) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, types: this.types.map((ast) => ast.toJSON()), annotations: toJSONAnnotations(this.annotations) } } } /** @internal */ export const mapMembers = <A, B>(members: Members<A>, f: (a: A) => B): Members<B> => members.map(f) as any /** @internal */ export const isMembers = <A>(as: ReadonlyArray<A>): as is Members<A> => as.length > 1 /** * @category guards * @since 3.10.0 */ export const isUnion: (ast: AST) => ast is Union = createASTGuard("Union") const toJSONMemoMap = globalValue( Symbol.for("effect/Schema/AST/toJSONMemoMap"), () => new WeakMap<AST, object>() ) /** * @category model * @since 3.10.0 */ export class Suspend implements Annotated { /** * @since 3.10.0 */ readonly _tag = "Suspend" constructor(readonly f: () => AST, readonly annotations: Annotations = {}) { this.f = util_.memoizeThunk(f) } /** * @since 3.10.0 */ toString() { return getExpected(this).pipe( Option.orElse(() => Option.flatMap( Option.liftThrowable(this.f)(), (ast) => getExpected(ast) ) ), Option.getOrElse(() => "<suspended schema>") ) } /** * @since 3.10.0 */ toJSON(): object { const ast = this.f() let out = toJSONMemoMap.get(ast) if (out) { return out } toJSONMemoMap.set(ast, { _tag: this._tag }) out = { _tag: this._tag, ast: ast.toJSON(), annotations: toJSONAnnotations(this.annotations) } toJSONMemoMap.set(ast, out) return out } } /** * @category guards * @since 3.10.0 */ export const isSuspend: (ast: AST) => ast is Suspend = createASTGuard("Suspend") /** * @category model * @since 3.10.0 */ export class Refinement<From extends AST = AST> implements Annotated { /** * @since 3.10.0 */ readonly _tag = "Refinement" constructor( readonly from: From, readonly filter: ( input: any, options: ParseOptions, self: Refinement ) => Option.Option<ParseIssue>, readonly annotations: Annotations = {} ) {} /** * @since 3.10.0 */ toString() { return getIdentifierAnnotation(this).pipe(Option.getOrElse(() => Option.match(getOrElseExpected(this), { onNone: () => `{ ${this.from} | filter }`, onSome: (expected) => isRefinement(this.from) ? String(this.from) + " & " + expected : expected }) )) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, from: this.from.toJSON(), annotations: toJSONAnnotations(this.annotations) } } } /** * @category guards * @since 3.10.0 */ export const isRefinement: (ast: AST) => ast is Refinement<AST> = createASTGuard("Refinement") /** * @category model * @since 3.10.0 */ export interface ParseOptions { /** * The `errors` option allows you to receive all parsing errors when * attempting to parse a value using a schema. By default only the first error * is returned, but by setting the `errors` option to `"all"`, you can receive * all errors that occurred during the parsing process. This can be useful for * debugging or for providing more comprehensive error messages to the user. * * default: "first" * * @since 3.10.0 */ readonly errors?: "first" | "all" | undefined /** * When using a `Schema` to parse a value, by default any properties that are * not specified in the `Schema` will be stripped out from the output. This is * because the `Schema` is expecting a specific shape for the parsed value, * and any excess properties do not conform to that shape. * * However, you can use the `onExcessProperty` option (default value: * `"ignore"`) to trigger a parsing error. This can be particularly useful in * cases where you need to detect and handle potential errors or unexpected * values. * * If you want to allow excess properties to remain, you can use * `onExcessProperty` set to `"preserve"`. * * default: "ignore" * * @since 3.10.0 */ readonly onExcessProperty?: "ignore" | "error" | "preserve" | undefined /** * The `propertyOrder` option provides control over the order of object fields * in the output. This feature is particularly useful when the sequence of * keys is important for the consuming processes or when maintaining the input * order enhances readability and usability. * * By default, the `propertyOrder` option is set to `"none"`. This means that * the internal system decides the order of keys to optimize parsing speed. * The order of keys in this mode should not be considered stable, and it's * recommended not to rely on key ordering as it may change in future updates * without notice. * * Setting `propertyOrder` to `"original"` ensures that the keys are ordered * as they appear in the input during the decoding/encoding process. * * default: "none" * * @since 3.10.0 */ readonly propertyOrder?: "none" | "original" | undefined /** * Handles missing properties in data structures. By default, missing * properties are treated as if present with an `undefined` value. To treat * missing properties as errors, set the `exact` option to `true`. This * setting is already enabled by default for `is` and `asserts` functions, * treating absent properties strictly unless overridden. * * default: false * * @since 3.10.0 */ readonly exact?: boolean | undefined } /** * @since 3.10.0 */ export const defaultParseOption: ParseOptions = {} /** * @category model * @since 3.10.0 */ export class Transformation implements Annotated { /** * @since 3.10.0 */ readonly _tag = "Transformation" constructor( readonly from: AST, readonly to: AST, readonly transformation: TransformationKind, readonly annotations: Annotations = {} ) {} /** * @since 3.10.0 */ toString() { return Option.getOrElse( getExpected(this), () => `(${String(this.from)} <-> ${String(this.to)})` ) } /** * @since 3.10.0 */ toJSON(): object { return { _tag: this._tag, from: this.from.toJSON(), to: this.to.toJSON(), annotations: toJSONAnnotations(this.annotations) } } } /** * @category guards * @since 3.10.0 */ export const isTransformation: (ast: AST) => ast is Transformation = createASTGuard("Transformation") /** * @category model * @since 3.10.0 */ export type TransformationKind = | FinalTransformation | ComposeTransformation | TypeLiteralTransformation /** * @category model * @since 3.10.0 */ export class FinalTransformation { /** * @since 3.10.0 */ readonly _tag = "FinalTransformation" constructor( readonly decode: ( fromA: any, options: ParseOptions, self: Transformation, fromI: any ) => Effect<any, ParseIssue, any>, readonly encode: (toI: any, options: ParseOptions, self: Transformation, toA: any) => Effect<any, ParseIssue, any> ) {} } const createTransformationGuard = <T extends TransformationKind["_tag"]>(tag: T) => (ast: TransformationKind): ast is Extract<TransformationKind, { _tag: T }> => ast._tag === tag /** * @category guards * @since 3.10.0 */ export const isFinalTransformation: (ast: TransformationKind) => ast is FinalTransformation = createTransformationGuard( "FinalTransformation" ) /** * @category model * @since 3.10.0 */ export class ComposeTransformation { /** * @since 3.10.0 */ readonly _tag = "ComposeTransformation" } /** * @category constructors * @since 3.10.0 */ export const composeTransformation: ComposeTransformation = new ComposeTransformation() /** * @category guards * @since 3.10.0 */ export const isComposeTransformation: (ast: TransformationKind) => ast is ComposeTransformation = createTransformationGuard( "ComposeTransformation" ) /** * Represents a `PropertySignature -> PropertySignature` transformation * * The semantic of `decode` is: * - `none()` represents the absence of the key/value pair * - `some(value)` represents the presence of the key/value pair * * The semantic of `encode` is: * - `none()` you don't want to output the key/value pair * - `some(value)` you want to output the key/value pair * * @category model * @since 3.10.0 */ export class PropertySignatureTransformation { constructor( readonly from: PropertyKey, readonly to: PropertyKey, readonly decode: (o: Option.Option<any>) => Option.Option<any>, readonly encode: (o: Option.Option<any>) => Option.Option<any> ) {} } const isRenamingPropertySignatureTransformation = (t: PropertySignatureTransformation) => t.decode === identity && t.encode === identity /** * @category model * @since 3.10.0 */ export class TypeLiteralTransformation { /** * @since 3.10.0 */ readonly _tag = "TypeLiteralTransformation" constructor( readonly propertySignatureTransformations: ReadonlyArray< PropertySignatureTransformation > ) { // check for duplicate property signature transformations const fromKeys: Record<PropertyKey, true> = {} const toKeys: Record<PropertyKey, true> = {} for (const pst of propertySignatureTransformations) { const from = pst.from if (fromKeys[from]) { throw new Error(errors_.getASTDuplicatePropertySignatureTransformationErrorMessage(from)) } fromKeys[from] = true const to = pst.to if (toKeys[to]) { throw new Error(errors_.getASTDuplicatePropertySignatureTransformationErrorMessage(to)) } toKeys[to] = true } } } /** * @category guards * @since 3.10.0 */ export const isTypeLiteralTransformation: (ast: TransformationKind) => ast is TypeLiteralTransformation = createTransformationGuard("TypeLiteralTransformation") // ------------------------------------------------------------------------------------- // API // ------------------------------------------------------------------------------------- /** * Merges a set of new annotations with existing ones, potentially overwriting * any duplicates. * * Any previously existing identifier annotations are deleted. * * @since 3.10.0 */ export const annotations = (ast: AST, overrides: Annotations): AST => { const d = Object.getOwnPropertyDescriptors(ast) const base: any = { ...ast.annotations } delete base[IdentifierAnnotationId] const value = { ...base, ...overrides } const surrogate = getSurrogateAnnotation(ast) if (Option.isSome(surrogate)) { value[SurrogateAnnotationId] = annotations(surrogate.value, overrides) } d.annotations.value = value return Object.create(Object.getPrototypeOf(ast), d) } /** * Equivalent at runtime to the TypeScript type-level `keyof` operator. * * @since 3.10.0 */ export const keyof = (ast: AST): AST => Union.unify(_keyof(a