| 1 | /** |
| 2 | * Runtime helpers used by generated schema modules. This module contains no |
| 3 | * source generator or dynamic function construction. Generated modules must |
| 4 | * use the same Effect version as their generator. |
| 5 | * |
| 6 | * @stability unstable |
| 7 | * @since 4.0.0 |
| 8 | */ |
| 9 | import * as Effect from "../../Effect.ts" |
| 10 | import { effectIsExit, resolveConcurrency } from "../../internal/effect.ts" |
| 11 | import { lazyParser, type Resolve, resolve, setCompiler, withDecode } from "../../internal/schema/compilerRegistry.ts" |
| 12 | import * as Interpreter from "../../internal/schema/interpreter.ts" |
| 13 | import * as InternalParser from "../../internal/schema/parser.ts" |
| 14 | import * as SchemaAST from "../../SchemaAST.ts" |
| 15 | import * as SchemaIssue from "../../SchemaIssue.ts" |
| 16 | import type { Compiler } from "../../SchemaParser.ts" |
| 17 | import { type Decode, invalid } from "../SchemaCompiler.ts" |
| 18 | |
| 19 | type SchemaIssueParser = ReturnType<typeof Interpreter.compile> |
| 20 | type ObjectParserState = Parameters<typeof SchemaAST.stepProperty>[0] |
| 21 | type ParsedProperty = Parameters<typeof SchemaAST.stepProperty>[1] |
| 22 | type ArrayParserState = Parameters<typeof SchemaAST.stepArray>[0] |
| 23 | type GenerateObject = (context: { |
| 24 | readonly ast: SchemaAST.Objects |
| 25 | readonly getProperties: () => ReadonlyArray<ParsedProperty> |
| 26 | readonly fallback: SchemaIssueParser |
| 27 | readonly resume: ( |
| 28 | state: ObjectParserState, |
| 29 | index: number, |
| 30 | pending: Effect.Effect<unknown, SchemaIssue.Issue, any> |
| 31 | ) => Effect.Effect<unknown, SchemaIssue.Issue, any> |
| 32 | readonly step: typeof SchemaAST.stepProperty |
| 33 | }) => SchemaIssueParser |
| 34 | type GenerateArray = (context: { |
| 35 | readonly getElement: () => SchemaIssueParser |
| 36 | readonly step: typeof SchemaAST.stepArray |
| 37 | readonly resume: ( |
| 38 | state: ArrayParserState, |
| 39 | item: unknown, |
| 40 | index: number, |
| 41 | pending: Effect.Effect<unknown, SchemaIssue.Issue, any>, |
| 42 | end: number |
| 43 | ) => Effect.Effect<void, SchemaIssue.Issue, any> |
| 44 | }) => typeof SchemaAST.parseArray |
| 45 | const makeObjectBase = ( |
| 46 | ast: SchemaAST.Objects, |
| 47 | compile: Compiler, |
| 48 | compileField: Compiler, |
| 49 | generate: GenerateObject |
| 50 | ): SchemaIssueParser => { |
| 51 | let properties: Array<ParsedProperty> | undefined |
| 52 | const getProperties = (): Array<ParsedProperty> => { |
| 53 | if (properties !== undefined) return properties |
| 54 | const parsers = new Map<SchemaAST.AST, SchemaIssueParser>() |
| 55 | return properties = ast.propertySignatures.map((property) => { |
| 56 | let parser = parsers.get(property.type) |
| 57 | if (parser === undefined) { |
| 58 | parser = compileField(property.type) |
| 59 | parsers.set(property.type, parser) |
| 60 | } |
| 61 | return { parser, name: property.name, type: property.type } |
| 62 | }) |
| 63 | } |
| 64 | let fallback: SchemaIssueParser | undefined |
| 65 | const runFallback: SchemaIssueParser = (input, options) => |
| 66 | (fallback ??= ast.getParser(compile, compileField))(input, options) |
| 67 | const resume = ( |
| 68 | state: ObjectParserState, |
| 69 | index: number, |
| 70 | pending: Effect.Effect<unknown, SchemaIssue.Issue, any> |
| 71 | ): Effect.Effect<unknown, SchemaIssue.Issue, any> => { |
| 72 | const property = properties![index] |
| 73 | return Effect.flatMap(Effect.exit(pending), (exit) => { |
| 74 | const terminal = SchemaAST.stepProperty(state, property, exit) |
| 75 | if (terminal) return terminal |
| 76 | const done = () => InternalParser.succeed(state.out) |
| 77 | const effect = SchemaAST.parseProperties(state, properties!.slice(index + 1)) |
| 78 | return effect ? Effect.flatMapEager(effect, done) : done() |
| 79 | }) |
| 80 | } |
| 81 | return generate({ ast, getProperties, fallback: runFallback, resume, step: SchemaAST.stepProperty }) |
| 82 | } |
| 83 | |
| 84 | const makeArrayBase = ( |
| 85 | ast: SchemaAST.Arrays, |
| 86 | compile: Compiler, |
| 87 | compileField: Compiler, |
| 88 | generate: GenerateArray |
| 89 | ): SchemaIssueParser => { |
| 90 | let element: { readonly ast: SchemaAST.AST; readonly parser: SchemaIssueParser } | undefined |
| 91 | const getElement = () => (element ??= { |
| 92 | ast: ast.rest[0], |
| 93 | parser: compileField(ast.rest[0]) |
| 94 | }) |
| 95 | let fallback: SchemaIssueParser | undefined |
| 96 | const runFallback: SchemaIssueParser = (input, options) => |
| 97 | (fallback ??= ast.getParser(compile, compileField))(input, options) |
| 98 | const run = generate({ |
| 99 | getElement: () => getElement().parser, |
| 100 | step: SchemaAST.stepArray, |
| 101 | resume: (state, item, index, pending, end) => |
| 102 | Effect.flatMap( |
| 103 | Effect.exit(pending), |
| 104 | (exit) => |
| 105 | SchemaAST.stepArray(state, item, exit, index) ?? |
| 106 | SchemaAST.parseArray(state, state.input, index + 1, end) ?? Effect.void |
| 107 | ) |
| 108 | }) |
| 109 | const specialized = Effect.fnUntracedEager(function*(input: unknown, options: SchemaAST.ParseOptions) { |
| 110 | if (input === InternalParser.missing) return InternalParser.missing |
| 111 | if (!Array.isArray(input)) { |
| 112 | return yield* Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) |
| 113 | } |
| 114 | const descriptor = getElement() |
| 115 | const len = input.length |
| 116 | const state: ArrayParserState = { |
| 117 | ast, |
| 118 | getParser: () => descriptor, |
| 119 | input, |
| 120 | len, |
| 121 | tailThreshold: len, |
| 122 | output: new globalThis.Array(len), |
| 123 | issues: undefined, |
| 124 | options |
| 125 | } |
| 126 | const effect = run(state, input, 0, len) |
| 127 | if (effect) yield* effect |
| 128 | if (state.issues) { |
| 129 | return yield* Effect.fail(new SchemaIssue.Composite(ast, state.issues, input, options)) |
| 130 | } |
| 131 | return state.output |
| 132 | }) |
| 133 | return (input, options) => |
| 134 | options.concurrency !== undefined && resolveConcurrency(options.concurrency) !== 1 |
| 135 | ? runFallback(input, options) |
| 136 | : specialized(input, options) |
| 137 | } |
| 138 | |
| 139 | const decode = ( |
| 140 | ast: SchemaAST.AST, |
| 141 | resolve: Resolve, |
| 142 | generate?: GenerateObject, |
| 143 | detailed = false, |
| 144 | makeDecode?: () => Decode, |
| 145 | generateArray?: GenerateArray |
| 146 | ): SchemaIssueParser => { |
| 147 | const child = (ast: SchemaAST.AST) => lazyParser(resolve, ast, detailed ? "decodeEffect" : "parser") |
| 148 | const localChild = makeDecode === undefined |
| 149 | ? child |
| 150 | : (ast: SchemaAST.AST) => lazyParser(resolve, ast, "decodeEffect") |
| 151 | const base = ast._tag === "Objects" && generate !== undefined ? |
| 152 | makeObjectBase(ast, localChild, localChild, generate) |
| 153 | : ast._tag === "Arrays" && generateArray !== undefined |
| 154 | ? makeArrayBase(ast, localChild, localChild, generateArray) |
| 155 | : makeDecode !== undefined |
| 156 | ? ast.getParser(localChild) |
| 157 | : undefined |
| 158 | const specialize = makeDecode === undefined ? undefined : (local: SchemaIssueParser): SchemaIssueParser => { |
| 159 | try { |
| 160 | return withDecode(makeDecode(), () => local) |
| 161 | } catch { |
| 162 | // Initialization failure selects the local interpreter, without parsing again. |
| 163 | return local |
| 164 | } |
| 165 | } |
| 166 | return Interpreter.compile(ast, child, undefined, base, specialize) |
| 167 | } |
| 168 | |
| 169 | const make = ( |
| 170 | ast: SchemaAST.AST, |
| 171 | resolve: Resolve, |
| 172 | generate?: GenerateObject, |
| 173 | generateArray?: GenerateArray |
| 174 | ): SchemaIssueParser => { |
| 175 | const child = (ast: SchemaAST.AST) => lazyParser(resolve, ast, "makeEffect") |
| 176 | const field = (ast: SchemaAST.AST) => Interpreter.compileField(ast, child) |
| 177 | const base = generate !== undefined && ast._tag === "Objects" ? |
| 178 | makeObjectBase(ast, child, field, generate) |
| 179 | : generateArray !== undefined && ast._tag === "Arrays" |
| 180 | ? makeArrayBase(ast, child, field, generateArray) |
| 181 | : undefined |
| 182 | return Interpreter.compile(ast, child, field, base) |
| 183 | } |
| 184 | |
| 185 | const getCheckIssues = ( |
| 186 | ast: SchemaAST.AST, |
| 187 | value: unknown, |
| 188 | encoded: boolean, |
| 189 | options: SchemaAST.ParseOptions |
| 190 | ): ReturnType<typeof SchemaAST.collectIssues> => { |
| 191 | const checks = encoded ? "encodingChecks" in ast ? ast.encodingChecks : undefined : ast.checks |
| 192 | return !options.disableChecks && checks !== undefined |
| 193 | ? SchemaAST.collectIssues(checks, value, undefined, ast, options) |
| 194 | : undefined |
| 195 | } |
| 196 | |
| 197 | const check = ( |
| 198 | ast: SchemaAST.AST, |
| 199 | value: unknown, |
| 200 | options: SchemaAST.ParseOptions |
| 201 | ): Effect.Effect<unknown, SchemaIssue.Issue> => { |
| 202 | const issues = getCheckIssues(ast, value, false, options) |
| 203 | return issues === undefined |
| 204 | ? InternalParser.succeed(value) |
| 205 | : Effect.fail(new SchemaIssue.Composite(ast, issues, value, options)) |
| 206 | } |
| 207 | |
| 208 | const hasExcessProperties = ( |
| 209 | ast: SchemaAST.Objects, |
| 210 | input: Record<PropertyKey, unknown>, |
| 211 | indexKeys?: ReadonlyArray<ReadonlyArray<PropertyKey>> |
| 212 | ): boolean => { |
| 213 | const covered = new Set<PropertyKey>( |
| 214 | ast.propertySignatures.map((p) => typeof p.name === "number" ? String(p.name) : p.name) |
| 215 | ) |
| 216 | if (indexKeys) { |
| 217 | for (const keys of indexKeys) { |
| 218 | for (const key of keys) covered.add(key) |
| 219 | } |
| 220 | } |
| 221 | return Reflect.ownKeys(input).some((key) => |
| 222 | !covered.has(key) && Object.prototype.propertyIsEnumerable.call(input, key) |
| 223 | ) |
| 224 | } |
| 225 | |
| 226 | const invalidType = (ast: SchemaAST.AST, input: unknown, options: SchemaAST.ParseOptions) => |
| 227 | Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) |
| 228 | |
| 229 | const invalidEncoding = ( |
| 230 | ast: SchemaAST.AST & { readonly encoding: SchemaAST.Encoding }, |
| 231 | index: number, |
| 232 | input: unknown, |
| 233 | value: unknown, |
| 234 | options: SchemaAST.ParseOptions |
| 235 | ) => |
| 236 | index === 0 |
| 237 | ? invalidType(ast, value, options) |
| 238 | : Interpreter.wrapEncoding(ast, input, options, invalidType(ast.encoding[index - 1].to, value, options)) |
| 239 | |
| 240 | const invalidEncodingChecks = ( |
| 241 | ast: SchemaAST.AST & { readonly encoding: SchemaAST.Encoding }, |
| 242 | input: unknown, |
| 243 | issues: readonly [SchemaIssue.Issue, ...Array<SchemaIssue.Issue>], |
| 244 | options: SchemaAST.ParseOptions |
| 245 | ) => { |
| 246 | const source = ast.encoding[ast.encoding.length - 1].to |
| 247 | return Interpreter.wrapEncoding( |
| 248 | ast, |
| 249 | input, |
| 250 | options, |
| 251 | Effect.fail(new SchemaIssue.Composite(source, issues, input, options)) |
| 252 | ) |
| 253 | } |
| 254 | |
| 255 | /** |
| 256 | * @internal |
| 257 | */ |
| 258 | export const runtime = { |
| 259 | decode, |
| 260 | make, |
| 261 | resolve, |
| 262 | setCompiler, |
| 263 | invalid, |
| 264 | missing: InternalParser.missing, |
| 265 | missingExit: InternalParser.missingExit, |
| 266 | sameExit: InternalParser.sameExit, |
| 267 | args: InternalParser.args, |
| 268 | succeed: InternalParser.succeed, |
| 269 | effectIsExit, |
| 270 | die: Effect.die, |
| 271 | invalidType, |
| 272 | invalidEncoding, |
| 273 | invalidEncodingChecks, |
| 274 | getCheckIssues, |
| 275 | check, |
| 276 | getExpectedKeys: (ast: SchemaAST.Objects) => |
| 277 | ast.propertySignatures.map((p) => typeof p.name === "number" ? String(p.name) : p.name), |
| 278 | hasExcessProperties, |
| 279 | matchesTemplateLiteral: (ast: SchemaAST.TemplateLiteral, input: unknown, options: SchemaAST.ParseOptions) => |
| 280 | typeof input === "string" && ast.matchPart(input, options) !== undefined, |
| 281 | getCandidateIndex: SchemaAST.getCandidateIndex, |
| 282 | getIndexSignatureKeys: SchemaAST.getIndexSignatureKeys, |
| 283 | parameterFromPropertyKey: SchemaAST.parameterFromPropertyKey, |
| 284 | getConstructorDescriptor: SchemaAST.getConstructorDescriptor, |
| 285 | defaultParseOptions: SchemaAST.defaultParseOptions |
| 286 | } |