/** * @since 4.0.0 */ import * as Arr from "./Array.ts" import * as Cause from "./Cause.ts" import * as Effect from "./Effect.ts" import * as Exit from "./Exit.ts" import { identity, memoize } from "./Function.ts" import * as InternalAnnotations from "./internal/schema/annotations.ts" import * as Option from "./Option.ts" import * as Predicate from "./Predicate.ts" import * as Result from "./Result.ts" import type * as Schema from "./Schema.ts" import * as AST from "./SchemaAST.ts" import * as Issue from "./SchemaIssue.ts" const recurDefaults = memoize((ast: AST.AST): AST.AST => { switch (ast._tag) { case "Declaration": { const getLink = ast.annotations?.[AST.ClassTypeId] if (Predicate.isFunction(getLink)) { const link = getLink(ast.typeParameters) const to = recurDefaults(link.to) return AST.replaceEncoding(ast, to === link.to ? [link] : [new AST.Link(to, link.transformation)]) } return ast } case "Objects": case "Arrays": return ast.recur((ast) => { const defaultValue = ast.context?.defaultValue if (defaultValue) { return AST.replaceEncoding(recurDefaults(ast), defaultValue) } return recurDefaults(ast) }) case "Suspend": return ast.recur(recurDefaults) default: return ast } }) /** * @category Constructing * @since 4.0.0 */ export function makeEffect(schema: S) { const ast = recurDefaults(AST.toType(schema.ast)) const parser = run(ast) return (input: S["~type.make.in"], options?: Schema.MakeOptions): Effect.Effect => { return parser( input, options?.disableChecks ? options?.parseOptions ? { ...options.parseOptions, disableChecks: true } : { disableChecks: true } : options?.parseOptions ) } } /** * @category Constructing * @since 4.0.0 */ export function makeOption(schema: S) { const parser = makeEffect(schema) return (input: S["~type.make.in"], options?: Schema.MakeOptions): Option.Option => { return Exit.getSuccess(Effect.runSyncExit(parser(input, options) as any)) } } /** * @category Constructing * @since 4.0.0 */ export function makeUnsafe(schema: S) { const parser = makeEffect(schema) return (input: S["~type.make.in"], options?: Schema.MakeOptions): S["Type"] => { return Effect.runSync( Effect.mapErrorEager( parser(input, options), (issue) => new Error(issue.toString(), { cause: issue }) ) ) } } /** * @category Asserting * @since 4.0.0 */ export function is(schema: Schema.Schema): (input: I) => input is I & T { return _is(schema.ast) } /** @internal */ export function _is(ast: AST.AST) { const parser = asExit(run(AST.toType(ast))) return (input: I): input is I & T => { return Exit.isSuccess(parser(input, AST.defaultParseOptions)) } } /** @internal */ export function _issue(ast: AST.AST) { const parser = run(ast) return (input: unknown, options: AST.ParseOptions): Issue.Issue | undefined => { return Effect.runSync(Effect.matchEager(parser(input, options), { onSuccess: () => undefined, onFailure: identity })) } } /** * @category Asserting * @since 4.0.0 */ export function asserts(schema: Schema.Schema) { const parser = asExit(run(AST.toType(schema.ast))) return (input: I): asserts input is I & T => { const exit = parser(input, AST.defaultParseOptions) if (Exit.isFailure(exit)) { const issue = Cause.findError(exit.cause) if (Result.isFailure(issue)) { throw Cause.squash(issue.failure) } throw new Error(issue.success.toString(), { cause: issue.success }) } } } /** * @category Decoding * @since 4.0.0 */ export function decodeUnknownEffect( schema: S ): (input: unknown, options?: AST.ParseOptions) => Effect.Effect { return run(schema.ast) } /** * @category Decoding * @since 4.0.0 */ export const decodeEffect: ( schema: S ) => (input: S["Encoded"], options?: AST.ParseOptions) => Effect.Effect = decodeUnknownEffect /** * @category Decoding * @since 4.0.0 */ export function decodeUnknownPromise>( schema: S ): (input: unknown, options?: AST.ParseOptions) => Promise { return asPromise(decodeUnknownEffect(schema)) } /** * @category Decoding * @since 4.0.0 */ export function decodePromise>( schema: S ): (input: S["Encoded"], options?: AST.ParseOptions) => Promise { return asPromise(decodeEffect(schema)) } /** * @category Decoding * @since 4.0.0 */ export function decodeUnknownExit>( schema: S ): (input: unknown, options?: AST.ParseOptions) => Exit.Exit { return asExit(decodeUnknownEffect(schema)) } /** * @category Decoding * @since 4.0.0 */ export const decodeExit: >( schema: S ) => (input: S["Encoded"], options?: AST.ParseOptions) => Exit.Exit = decodeUnknownExit /** * @category Decoding * @since 4.0.0 */ export function decodeUnknownOption>( schema: S ): (input: unknown, options?: AST.ParseOptions) => Option.Option { return asOption(decodeUnknownEffect(schema)) } /** * @category Decoding * @since 4.0.0 */ export const decodeOption: >( schema: S ) => (input: S["Encoded"], options?: AST.ParseOptions) => Option.Option = decodeUnknownOption /** * @category Decoding * @since 4.0.0 */ export function decodeUnknownResult>( schema: S ): (input: unknown, options?: AST.ParseOptions) => Result.Result { return asResult(decodeUnknownEffect(schema)) } /** * @category Decoding * @since 4.0.0 */ export const decodeResult: >( schema: S ) => (input: S["Encoded"], options?: AST.ParseOptions) => Result.Result = decodeUnknownResult /** * @category Decoding * @since 4.0.0 */ export function decodeUnknownSync>( schema: S ): (input: unknown, options?: AST.ParseOptions) => S["Type"] { return asSync(decodeUnknownEffect(schema)) } /** * @category Decoding * @since 4.0.0 */ export const decodeSync: >( schema: S ) => (input: S["Encoded"], options?: AST.ParseOptions) => S["Type"] = decodeUnknownSync /** * @category Encoding * @since 4.0.0 */ export function encodeUnknownEffect( schema: S ): (input: unknown, options?: AST.ParseOptions) => Effect.Effect { return run(AST.flip(schema.ast)) } /** * @category Encoding * @since 4.0.0 */ export const encodeEffect: ( schema: S ) => (input: S["Type"], options?: AST.ParseOptions) => Effect.Effect = encodeUnknownEffect /** * @category Encoding * @since 4.0.0 */ export const encodeUnknownPromise = >( schema: S ): (input: unknown, options?: AST.ParseOptions) => Promise => asPromise(encodeUnknownEffect(schema)) /** * @category Encoding * @since 4.0.0 */ export const encodePromise: >( schema: S ) => (input: S["Type"], options?: AST.ParseOptions) => Promise = encodeUnknownPromise /** * @category Encoding * @since 4.0.0 */ export function encodeUnknownExit>( schema: S ): (input: unknown, options?: AST.ParseOptions) => Exit.Exit { return asExit(encodeUnknownEffect(schema)) } /** * @category Encoding * @since 4.0.0 */ export const encodeExit: >( schema: S ) => (input: S["Type"], options?: AST.ParseOptions) => Exit.Exit = encodeUnknownExit /** * @category Encoding * @since 4.0.0 */ export function encodeUnknownOption>( schema: S ): (input: unknown, options?: AST.ParseOptions) => Option.Option { return asOption(encodeUnknownEffect(schema)) } /** * @category Encoding * @since 4.0.0 */ export const encodeOption: >( schema: S ) => (input: S["Type"], options?: AST.ParseOptions) => Option.Option = encodeUnknownOption /** * @category Encoding * @since 4.0.0 */ export function encodeUnknownResult>( schema: S ): (input: unknown, options?: AST.ParseOptions) => Result.Result { return asResult(encodeUnknownEffect(schema)) } /** * @category Encoding * @since 4.0.0 */ export const encodeResult: >( schema: S ) => (input: S["Type"], options?: AST.ParseOptions) => Result.Result = encodeUnknownResult /** * @category Encoding * @since 4.0.0 */ export function encodeUnknownSync>( schema: S ): (input: unknown, options?: AST.ParseOptions) => S["Encoded"] { return asSync(encodeUnknownEffect(schema)) } /** * @category Encoding * @since 4.0.0 */ export const encodeSync: >( schema: S ) => (input: S["Type"], options?: AST.ParseOptions) => S["Encoded"] = encodeUnknownSync /** @internal */ export function run(ast: AST.AST) { const parser = recur(ast) return (input: unknown, options?: AST.ParseOptions): Effect.Effect => Effect.flatMapEager(parser(Option.some(input), options ?? AST.defaultParseOptions), (oa) => { if (oa._tag === "None") { return Effect.fail(new Issue.InvalidValue(oa)) } return Effect.succeed(oa.value as T) }) } function asPromise( parser: (input: E, options?: AST.ParseOptions) => Effect.Effect ): (input: E, options?: AST.ParseOptions) => Promise { return (input: E, options?: AST.ParseOptions) => Effect.runPromise(parser(input, options)) } function asExit( parser: (input: E, options?: AST.ParseOptions) => Effect.Effect ): (input: E, options?: AST.ParseOptions) => Exit.Exit { return (input: E, options?: AST.ParseOptions) => Effect.runSyncExit(parser(input, options) as any) } /** @internal */ export function asOption( parser: (input: E, options?: AST.ParseOptions) => Effect.Effect ): (input: E, options?: AST.ParseOptions) => Option.Option { const parserExit = asExit(parser) return (input: E, options?: AST.ParseOptions) => Exit.getSuccess(parserExit(input, options)) } function asResult( parser: (input: E, options?: AST.ParseOptions) => Effect.Effect ): (input: E, options?: AST.ParseOptions) => Result.Result { const parserExit = asExit(parser) return (input: E, options?: AST.ParseOptions) => { const exit = parserExit(input, options) if (Exit.isSuccess(exit)) { return Result.succeed(exit.value) } const error = Cause.findError(exit.cause) if (Result.isFailure(error)) { throw Cause.squash(error.failure) } return Result.fail(error.success) } } function asSync( parser: (input: E, options?: AST.ParseOptions) => Effect.Effect ): (input: E, options?: AST.ParseOptions) => T { return (input: E, options?: AST.ParseOptions) => Effect.runSync( Effect.mapErrorEager( parser(input, options), (issue) => new Error(issue.toString(), { cause: issue }) ) as any ) } /** @internal */ export interface Parser { (input: Option.Option, options: AST.ParseOptions): Effect.Effect, Issue.Issue, any> } const recur = memoize( (ast: AST.AST): Parser => { let parser: Parser const astOptions = InternalAnnotations.resolve(ast)?.["parseOptions"] if (!ast.context && !ast.encoding && !ast.checks) { return (ou, options) => { parser ??= ast.getParser(recur) if (astOptions) { options = { ...options, ...astOptions } } return parser(ou, options) } } const isStructural = AST.isArrays(ast) || AST.isObjects(ast) || (AST.isDeclaration(ast) && ast.typeParameters.length > 0) return (ou, options) => { if (astOptions) { options = { ...options, ...astOptions } } const encoding = ast.encoding let srou: Effect.Effect, Issue.Issue, unknown> | undefined if (encoding) { const links = encoding const len = links.length for (let i = len - 1; i >= 0; i--) { const link = links[i] const to = link.to const parser = recur(to) srou = srou ? Effect.flatMapEager(srou, (ou) => parser(ou, options)) : parser(ou, options) if (link.transformation._tag === "Transformation") { const getter = link.transformation.decode srou = Effect.flatMapEager(srou, (ou) => getter.run(ou, options)) } else { srou = link.transformation.decode(srou, options) } } srou = Effect.mapErrorEager(srou!, (issue) => new Issue.Encoding(ast, ou, issue)) } parser ??= ast.getParser(recur) let sroa = srou ? Effect.flatMapEager(srou, (ou) => parser(ou, options)) : parser(ou, options) if (ast.checks && !options?.disableChecks) { const checks = ast.checks if (options?.errors === "all" && isStructural && Option.isSome(ou)) { sroa = Effect.catchEager(sroa, (issue) => { const issues: Array = [] AST.collectIssues( checks.filter((check) => check.annotations?.[AST.STRUCTURAL_ANNOTATION_KEY]), ou.value, issues, ast, options ) const out: Issue.Issue = Arr.isArrayNonEmpty(issues) ? issue._tag === "Composite" && issue.ast === ast ? new Issue.Composite(ast, issue.actual, [...issue.issues, ...issues]) : new Issue.Composite(ast, ou, [issue, ...issues]) : issue return Effect.fail(out) }) } sroa = Effect.flatMapEager(sroa, (oa) => { if (Option.isSome(oa)) { const value = oa.value const issues: Array = [] AST.collectIssues(checks, value, issues, ast, options) if (Arr.isArrayNonEmpty(issues)) { return Effect.fail(new Issue.Composite(ast, oa, issues)) } } return Effect.succeed(oa) }) } return sroa } } )