/** * Runtime helpers used by generated schema modules. This module contains no * source generator or dynamic function construction. Generated modules must * use the same Effect version as their generator. * * @stability unstable * @since 4.0.0 */ import * as Effect from "../../Effect.ts" import { effectIsExit, resolveConcurrency } from "../../internal/effect.ts" import { lazyParser, type Resolve, resolve, setCompiler, withDecode } from "../../internal/schema/compilerRegistry.ts" import * as Interpreter from "../../internal/schema/interpreter.ts" import * as InternalParser from "../../internal/schema/parser.ts" import * as SchemaAST from "../../SchemaAST.ts" import * as SchemaIssue from "../../SchemaIssue.ts" import type { Compiler } from "../../SchemaParser.ts" import { type Decode, invalid } from "../SchemaCompiler.ts" type SchemaIssueParser = ReturnType type ObjectParserState = Parameters[0] type ParsedProperty = Parameters[1] type ArrayParserState = Parameters[0] type GenerateObject = (context: { readonly ast: SchemaAST.Objects readonly getProperties: () => ReadonlyArray readonly fallback: SchemaIssueParser readonly resume: ( state: ObjectParserState, index: number, pending: Effect.Effect ) => Effect.Effect readonly step: typeof SchemaAST.stepProperty }) => SchemaIssueParser type GenerateArray = (context: { readonly getElement: () => SchemaIssueParser readonly step: typeof SchemaAST.stepArray readonly resume: ( state: ArrayParserState, item: unknown, index: number, pending: Effect.Effect, end: number ) => Effect.Effect }) => typeof SchemaAST.parseArray const makeObjectBase = ( ast: SchemaAST.Objects, compile: Compiler, compileField: Compiler, generate: GenerateObject ): SchemaIssueParser => { let properties: Array | undefined const getProperties = (): Array => { if (properties !== undefined) return properties const parsers = new Map() return properties = ast.propertySignatures.map((property) => { let parser = parsers.get(property.type) if (parser === undefined) { parser = compileField(property.type) parsers.set(property.type, parser) } return { parser, name: property.name, type: property.type } }) } let fallback: SchemaIssueParser | undefined const runFallback: SchemaIssueParser = (input, options) => (fallback ??= ast.getParser(compile, compileField))(input, options) const resume = ( snapshot: ObjectParserState, index: number, pending: Effect.Effect ): Effect.Effect => { const property = properties![index] return Effect.flatMap(Effect.exit(pending), (exit) => { // Each execution gets its own output; `issues` is still undefined here // because `errors: "all"` is routed to the fallback before suspending. const state: ObjectParserState = { ...snapshot, out: { ...snapshot.out } } const terminal = SchemaAST.stepProperty(state, property, exit) if (terminal) return terminal const done = () => InternalParser.succeed(state.out) const effect = SchemaAST.parseProperties(state, properties!.slice(index + 1)) return effect ? Effect.flatMapEager(effect, done) : done() }) } return generate({ ast, getProperties, fallback: runFallback, resume, step: SchemaAST.stepProperty }) } const makeArrayBase = ( ast: SchemaAST.Arrays, compile: Compiler, compileField: Compiler, generate: GenerateArray ): SchemaIssueParser => { let element: { readonly ast: SchemaAST.AST; readonly parser: SchemaIssueParser } | undefined const getElement = () => (element ??= { ast: ast.rest[0], parser: compileField(ast.rest[0]) }) let fallback: SchemaIssueParser | undefined const runFallback: SchemaIssueParser = (input, options) => (fallback ??= ast.getParser(compile, compileField))(input, options) const run = generate({ getElement: () => getElement().parser, step: SchemaAST.stepArray, resume: (state, item, index, pending, end) => Effect.flatMap( Effect.exit(pending), (exit) => SchemaAST.stepArray(state, item, exit, index) ?? SchemaAST.parseArray(state, state.input, index + 1, end) ?? Effect.void ) }) const specialized = Effect.fnUntracedEager(function*(input: unknown, options: SchemaAST.ParseOptions) { if (input === InternalParser.missing) return InternalParser.missing if (!Array.isArray(input)) { return yield* Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } const descriptor = getElement() const len = input.length const state: ArrayParserState = { ast, getParser: () => descriptor, input, len, tailThreshold: len, output: new globalThis.Array(len), issues: undefined, options } const effect = run(state, input, 0, len) if (effect) yield* effect if (state.issues) { return yield* Effect.fail(new SchemaIssue.Composite(ast, state.issues, input, options)) } return state.output }) return (input, options) => options.concurrency !== undefined && resolveConcurrency(options.concurrency) !== 1 ? runFallback(input, options) : specialized(input, options) } const decode = ( ast: SchemaAST.AST, resolve: Resolve, generate?: GenerateObject, detailed = false, makeDecode?: () => Decode, generateArray?: GenerateArray ): SchemaIssueParser => { const child = (ast: SchemaAST.AST) => lazyParser(resolve, ast, detailed ? "decodeEffect" : "parser") const localChild = makeDecode === undefined ? child : (ast: SchemaAST.AST) => lazyParser(resolve, ast, "decodeEffect") const base = ast._tag === "Objects" && generate !== undefined ? makeObjectBase(ast, localChild, localChild, generate) : ast._tag === "Arrays" && generateArray !== undefined ? makeArrayBase(ast, localChild, localChild, generateArray) : makeDecode !== undefined ? ast.getParser(localChild) : undefined const specialize = makeDecode === undefined ? undefined : (local: SchemaIssueParser): SchemaIssueParser => { try { return withDecode(makeDecode(), () => local) } catch { // Initialization failure selects the local interpreter, without parsing again. return local } } return Interpreter.compile(ast, child, undefined, base, specialize) } const make = ( ast: SchemaAST.AST, resolve: Resolve, generate?: GenerateObject, generateArray?: GenerateArray ): SchemaIssueParser => { const child = (ast: SchemaAST.AST) => lazyParser(resolve, ast, "makeEffect") const field = (ast: SchemaAST.AST) => Interpreter.compileField(ast, child) const base = generate !== undefined && ast._tag === "Objects" ? makeObjectBase(ast, child, field, generate) : generateArray !== undefined && ast._tag === "Arrays" ? makeArrayBase(ast, child, field, generateArray) : undefined return Interpreter.compile(ast, child, field, base) } const getCheckIssues = ( ast: SchemaAST.AST, value: unknown, encoded: boolean, options: SchemaAST.ParseOptions ): ReturnType => { const checks = encoded ? "encodingChecks" in ast ? ast.encodingChecks : undefined : ast.checks return !options.disableChecks && checks !== undefined ? SchemaAST.collectIssues(checks, value, undefined, ast, options) : undefined } const check = ( ast: SchemaAST.AST, value: unknown, options: SchemaAST.ParseOptions ): Effect.Effect => { const issues = getCheckIssues(ast, value, false, options) return issues === undefined ? InternalParser.succeed(value) : Effect.fail(new SchemaIssue.Composite(ast, issues, value, options)) } const hasExcessProperties = ( ast: SchemaAST.Objects, input: Record, indexKeys?: ReadonlyArray> ): boolean => { const covered = new Set( ast.propertySignatures.map((p) => typeof p.name === "number" ? String(p.name) : p.name) ) if (indexKeys) { for (const keys of indexKeys) { for (const key of keys) covered.add(key) } } return Reflect.ownKeys(input).some((key) => !covered.has(key) && Object.prototype.propertyIsEnumerable.call(input, key) ) } const invalidType = (ast: SchemaAST.AST, input: unknown, options: SchemaAST.ParseOptions) => Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) const invalidEncoding = ( ast: SchemaAST.AST & { readonly encoding: SchemaAST.Encoding }, index: number, input: unknown, value: unknown, options: SchemaAST.ParseOptions ) => index === 0 ? invalidType(ast, value, options) : Interpreter.wrapEncoding(ast, input, options, invalidType(ast.encoding[index - 1].to, value, options)) const invalidEncodingChecks = ( ast: SchemaAST.AST & { readonly encoding: SchemaAST.Encoding }, input: unknown, issues: readonly [SchemaIssue.Issue, ...Array], options: SchemaAST.ParseOptions ) => { const source = ast.encoding[ast.encoding.length - 1].to return Interpreter.wrapEncoding( ast, input, options, Effect.fail(new SchemaIssue.Composite(source, issues, input, options)) ) } /** * @internal */ export const runtime = { decode, make, resolve, setCompiler, invalid, missing: InternalParser.missing, missingExit: InternalParser.missingExit, sameExit: InternalParser.sameExit, args: InternalParser.args, succeed: InternalParser.succeed, effectIsExit, die: Effect.die, invalidType, invalidEncoding, invalidEncodingChecks, getCheckIssues, check, getExpectedKeys: (ast: SchemaAST.Objects) => ast.propertySignatures.map((p) => typeof p.name === "number" ? String(p.name) : p.name), hasExcessProperties, matchesTemplateLiteral: (ast: SchemaAST.TemplateLiteral, input: unknown, options: SchemaAST.ParseOptions) => typeof input === "string" && ast.matchPart(input, options) !== undefined, getCandidateIndex: SchemaAST.getCandidateIndex, getIndexSignatureKeys: SchemaAST.getIndexSignatureKeys, parameterFromPropertyKey: SchemaAST.parameterFromPropertyKey, getConstructorDescriptor: SchemaAST.getConstructorDescriptor, defaultParseOptions: SchemaAST.defaultParseOptions }