import type { Literal, ParseOptions, TemplateLiteral, TypeLiteral } from "../AST.js"; import type { Brand, Validation } from "@fncts/base/data/Branded"; import { show } from "@fncts/base/data/Showable"; import { getParameter } from "../AST.js"; import { ASTTag, concrete, TemplateLiteralSpan } from "../AST.js"; import { ownKeys } from "../utils.js"; /** * @tsplus static fncts.schema.SchemaOps fromAST */ export function make(ast: AST): Schema { return new Schema(ast); } /** * @tsplus pipeable fncts.schema.Schema annotate */ export function annotate(annotation: ASTAnnotation, value: V) { return (self: Schema): Schema => { return Schema.fromAST(self.ast.clone({ annotations: self.ast.annotations.annotate(annotation, value) })); }; } /** * @tsplus static fncts.schema.SchemaOps declaration */ export function declaration( typeParameters: Vector>, decode: (...typeParameters: ReadonlyArray>) => Parser, encode: (...typeParameters: ReadonlyArray>) => Parser, annotations?: ASTAnnotationMap, ): Schema { return Schema.fromAST( AST.createDeclaration( typeParameters.map((tp) => tp.ast), (...typeParameters) => decode(...typeParameters.map(Schema.fromAST)), (...typeParameters) => encode(...typeParameters.map(Schema.fromAST)), annotations, ), ); } /** * @tsplus pipeable fncts.schema.Schema filter */ export function filter(refinement: Refinement): (self: Schema) => Schema; export function filter(predicate: Predicate): (self: Schema) => Schema; export function filter(predicate: Predicate) { return (self: Schema): Schema => { const ast: AST = AST.createRefinement(self.ast, predicate); return Schema.fromAST(ast); }; } /** * @tsplus pipeable fncts.schema.Schema brand */ export function brand(validation: Validation) { return (self: Schema): Schema> => { const ast = AST.createRefinement( self.ast, validation.validate, self.ast.annotations.annotate(ASTAnnotation.Brand, Vector(validation)), ); return Schema.fromAST(ast); }; } function makeLiteral(value: Literal): Schema { return Schema.fromAST(AST.createLiteral(value)); } /** * @tsplus static fncts.schema.SchemaOps literal */ export function literal>(...literals: Literals): Schema { return Schema.union(...literals.map(makeLiteral)); } /** * @tsplus static fncts.schema.SchemaOps never * @tsplus implicit */ export const never: Schema = Schema.fromAST(AST.neverKeyword); /** * @tsplus static fncts.schema.SchemaOps unknown * @tsplus implicit */ export const unknown: Schema = Schema.fromAST(AST.unknownKeyword); /** * @tsplus static fncts.schema.SchemaOps any */ export const any: Schema = Schema.fromAST(AST.anyKeyword); /** * @tsplus static fncts.schema.SchemaOps undefined * @tsplus implicit */ export const _undefined: Schema = Schema.fromAST(AST.undefinedKeyword); export { _undefined as undefined }; /** * @tsplus static fncts.schema.SchemaOps null * @tsplus implicit */ export const _null: Schema = Schema.fromAST(AST.createLiteral(null)); export { _null as null }; /** * @tsplus static fncts.schema.SchemaOps void * @tsplus implicit */ export const _void: Schema = Schema.fromAST(AST.voidKeyword); export { _void as void }; /** * @tsplus static fncts.schema.SchemaOps string * @tsplus implicit */ export const string: Schema = Schema.fromAST(AST.stringKeyword); /** * @tsplus static fncts.schema.SchemaOps number * @tsplus implicit */ export const number: Schema = Schema.fromAST(AST.numberKeyword); /** * @tsplus static fncts.schema.SchemaOps boolean * @tsplus implicit */ export const boolean: Schema = Schema.fromAST(AST.booleanKeyword); /** * @tsplus static fncts.schema.SchemaOps bigint * @tsplus implicit */ export const bigint: Schema = Schema.fromAST(AST.bigIntKeyword); /** * @tsplus static fncts.schema.SchemaOps symbol * @tsplus implicit */ export const symbol: Schema = Schema.fromAST(AST.symbolKeyword); /** * @tsplus static fncts.schema.SchemaOps object * @tsplus implicit */ export const object: Schema = Schema.fromAST(AST.objectKeyword); /** * @tsplus static fncts.schema.SchemaOps date */ export const date: Schema = Schema.object.instanceOf(Date); /** * @tsplus implicit */ export const implicitDate: Schema = Schema.unknown.transformOrFail( Schema.date, (input, options) => { if (typeof input === "string" || typeof input === "number") { return ParseResult.succeed(new Date(input)); } else { return Schema.date.decode(input, options); } }, (input) => ParseResult.succeed(input), ); /** * @tsplus derive fncts.schema.Schema<|> 30 * @tsplus static fncts.schema.SchemaOps union */ export function union>( ...members: { [K in keyof A]: Schema; } ): Schema { return Schema.fromAST(AST.createUnion(Vector.from(members.map((m) => m.ast)))); } /** * @tsplus getter fncts.schema.Schema nullable */ export function nullable(self: Schema): Schema { return Schema.union(Schema.null, self); } /** * @tsplus static fncts.schema.SchemaOps uniqueSymbol */ export function uniqueSymbol(symbol: S, annotations?: ASTAnnotationMap): Schema { return Schema.fromAST(AST.createUniqueSymbol(symbol, annotations)); } /** * @tsplus getter fncts.schema.Schema optional */ export function optional(self: Schema): OptionalSchema { return Schema.fromAST( self.ast.clone({ annotations: self.ast.annotations.annotate(ASTAnnotation.Optional, true) }), ) as OptionalSchema; } /** * @tsplus fluent fncts.schema.Schema isOptional */ export function isOptional(self: Schema): self is OptionalSchema { return self.ast.annotations.get(ASTAnnotation.Optional).getOrElse(false); } export type OptionalSchemaKeys = { [K in keyof T]: T[K] extends OptionalSchema ? K : never }[keyof T]; /** * @tsplus getter fncts.schema.Schema parseOptional */ export function parseOptional(self: Schema): Schema> { return Schema.fromAST( self.ast.clone({ annotations: self.ast.annotations.annotate(ASTAnnotation.ParseOptional, true) }), ); } /** * @tsplus fluent fncts.schema.Schema isParseOptional */ export function isParseOptional(self: Schema): boolean { return self.ast.annotations.get(ASTAnnotation.ParseOptional).getOrElse(false); } export type Spread = { [K in keyof A]: A[K]; } extends infer B ? B : never; /** * @tsplus static fncts.schema.SchemaOps struct */ export function struct>>( fields: Fields, ): Schema< Spread< { readonly [K in Exclude>]: Schema.Infer } & { readonly [K in OptionalSchemaKeys]?: Schema.Infer; } > > { const parseOptionalKeys: Vector = ownKeys(fields).filter((key) => isParseOptional(fields[key]!)); const struct = Schema.fromAST( AST.createTypeLiteral( ownKeys(fields).map((key) => AST.createPropertySignature(key, fields[key]!.ast, isOptional(fields[key]!), true)), Vector.empty(), ), ); if (parseOptionalKeys.isEmpty()) { return struct as Schema; } const propertySignatures = (struct.ast as TypeLiteral).propertySignatures; const from = Schema.fromAST( AST.createTypeLiteral( propertySignatures.map((p) => parseOptionalKeys.includes(p.name) ? AST.createPropertySignature( p.name, AST.createUnion(Vector(AST.undefinedKeyword, AST.createLiteral(null), p.type)), true, p.isReadonly, ) : p, ), Vector.empty(), ), ); const to = Schema.fromAST( AST.createTypeLiteral( propertySignatures.map((p) => { if (parseOptionalKeys.includes(p.name)) { if (fields[p.name]!.ast.isLazy()) { return AST.createPropertySignature( p.name, AST.createLazy(() => Schema.maybe(fields[p.name]!).ast), true, p.isReadonly, ); } return AST.createPropertySignature(p.name, Schema.maybe(fields[p.name]!).ast, true, p.isReadonly); } return p; }), Vector.empty(), ), ); return from.transform( to, (input) => { const output = { ...input }; for (const key of parseOptionalKeys) { output[key] = Maybe.fromNullable(input[key]); } return output; }, (input) => { const output = { ...input }; for (const key of parseOptionalKeys) { const value: Maybe = input[key]; if (value.isNothing()) { delete output[key]; continue; } output[key] = value.value; } return output; }, ); } /** * @tsplus static fncts.schema.SchemaOps tuple */ export function tuple>>( ...elements: Elements ): Schema<{ readonly [K in keyof Elements]: Schema.Infer }> { return Schema.fromAST( AST.createTuple(Vector.from(elements.map((schema) => AST.createElement(schema.ast, false))), Nothing(), true), ); } /** * @tsplus static fncts.schema.SchemaOps lazy */ export function lazy(f: () => Schema, annotations?: ASTAnnotationMap): Schema { return Schema.fromAST(AST.createLazy(() => f().ast, annotations)); } /** * @tsplus static fncts.schema.SchemaOps array * @tsplus getter fncts.schema.Schema array */ export function array(item: Schema): Schema> { return Schema.fromAST(AST.createTuple(Vector.empty(), Just(Vector(item.ast)), true)); } /** * @tsplus static fncts.schema.SchemaOps mutableArray * @tsplus getter fncts.schema.Schema mutableArray */ export function mutableArray(item: Schema): Schema> { return Schema.fromAST(AST.createTuple(Vector.empty(), Just(Vector(item.ast)), false)); } /** * @tsplus static fncts.schema.SchemaOps record */ export function record( key: Schema, value: Schema, ): Schema<{ readonly [k in K]: V }> { return Schema.fromAST(AST.createRecord(key.ast, value.ast, true)); } /** * @tsplus static fncts.schema.SchemaOps enum */ export function enum_(enums: A): Schema { return Schema.fromAST( AST.createEnum( Vector.from( Object.keys(enums) .filter((key) => typeof enums[enums[key]!] !== "number") .map((key) => [key, enums[key]!]), ), ), ); } export { enum_ as enum }; type Join = T extends [infer Head, ...infer Tail] ? `${Head & (string | number | bigint | boolean | null | undefined)}${Tail extends [] ? "" : Join}` : never; function getTemplateLiterals(ast: AST): Vector { concrete(ast); switch (ast._tag) { case ASTTag.Literal: return Vector(ast); case ASTTag.NumberKeyword: case ASTTag.StringKeyword: return Vector(AST.createTemplateLiteral("", Vector(new TemplateLiteralSpan(ast, "")))); case ASTTag.Union: return ast.types.flatMap(getTemplateLiterals); default: throw new Error(`Unsupported template literal span ${show(ast)}`); } } function combineTemplateLiterals( a: TemplateLiteral | Literal, b: TemplateLiteral | Literal, ): TemplateLiteral | Literal { if (a.isLiteral()) { return b.isLiteral() ? AST.createLiteral(String(a.literal) + String(b.literal)) : AST.createTemplateLiteral(String(a.literal) + b.head, b.spans); } if (b.isLiteral()) { if (!a.spans.isNonEmpty()) { throw new Error("Invalid template literal"); } const last = a.spans.unsafeLast!; return AST.createTemplateLiteral( a.head, a.spans.slice(0, -1).append(new TemplateLiteralSpan(last.type, last.literal + String(b.literal))), ); } if (!a.spans.isNonEmpty()) { throw new Error("Invalid template literal"); } const last = a.spans.unsafeLast!; return AST.createTemplateLiteral( a.head, a.spans .slice(0, -1) .append(new TemplateLiteralSpan(last.type, last.literal + String(b.head))) .concat(b.spans), ); } /** * @tsplus static fncts.schema.SchemaOps templateLiteral */ export function templateLiteral, ...Array>]>( ...[head, ...tail]: T ): Schema }>> { let types: Vector = getTemplateLiterals(head.ast); for (const span of tail) { types = types.flatMap((a) => getTemplateLiterals(span.ast).map((b) => combineTemplateLiterals(a, b))); } return Schema.fromAST(AST.createUnion(types)); } /** * @tsplus static fncts.schema.SchemaOps keyof * @tsplus getter fncts.schema.Schema keyof */ export function keyof(self: Schema): Schema { return Schema.fromAST(self.ast.keyof); } function isOverlappingPropertySignatures(x: TypeLiteral, y: TypeLiteral): boolean { return x.propertySignatures.some((px) => y.propertySignatures.some((py) => px.name === py.name)); } function isOverlappingIndexSignatures(x: TypeLiteral, y: TypeLiteral): boolean { return x.indexSignatures.some((ix) => y.indexSignatures.some((iy) => { const bx = getParameter(ix.parameter); const by = getParameter(iy.parameter); return (bx.isStringKeyword() && by.isStringKeyword()) || (bx.isSymbolKeyword() && by.isSymbolKeyword()); }), ); } /** * @tsplus pipeable fncts.schema.Schema extend */ export function extend(that: Schema) { return (self: Schema): Schema> => { const selfTypes = self.ast.isUnion() ? self.ast.types : Vector(self.ast); const thatTypes = that.ast.isUnion() ? that.ast.types : Vector(that.ast); if (selfTypes.every(AST.isTypeLiteral) && thatTypes.every(AST.isTypeLiteral)) { return Schema.fromAST( AST.createUnion( selfTypes.flatMap((x) => thatTypes.map((y) => { if (isOverlappingPropertySignatures(x, y)) { throw new Error("`extend` cannot handle overlapping property signatures"); } if (isOverlappingIndexSignatures(x, y)) { throw new Error("`extend` cannot handle overlapping index signatures"); } return AST.createTypeLiteral( x.propertySignatures.concat(y.propertySignatures), x.indexSignatures.concat(y.indexSignatures), ); }), ), ), ); } throw new Error("`extend can only handle type literals or unions of type literals`"); }; } /** * @tsplus pipeable fncts.schema.Schema instanceOf */ export function instanceOf any>(constructor: A) { return (self: Schema): Schema> => { return self .filter((value): value is InstanceType => value instanceof constructor) .annotate(ASTAnnotation.Description, `an instance of ${constructor.name}`); }; } /** * @tsplus pipeable fncts.schema.Schema transformOrFail */ export function transformOrFail( to: Schema, decode: (input: A, options?: ParseOptions) => ParseResult, encode: (input: B, options?: ParseOptions) => ParseResult, ) { return (from: Schema): Schema => { return Schema.fromAST(AST.createTransform(from.ast, to.ast, decode, encode)); }; } /** * @tsplus pipeable fncts.schema.Schema transform */ export function transform( to: Schema, decode: (input: A, options?: ParseOptions) => B, encode: (input: B, options?: ParseOptions) => A, ) { return (from: Schema): Schema => { return from.transformOrFail( to, (input, options) => ParseResult.succeed(decode(input, options)), (input, options) => ParseResult.succeed(encode(input, options)), ); }; } /** * @tsplus pipeable fncts.schema.Schema pick */ export function pick>(...keys: Keys) { return (self: Schema): Schema> => { return Schema.fromAST(self.ast.pick(Vector.from(keys))); }; } /** * @tsplus pipeable fncts.schema.Schema omit */ export function omit>(...keys: Keys) { return (self: Schema): Schema> => { return Schema.fromAST(self.ast.omit(Vector.from(keys))); }; }