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 */
9import * as Effect from "../../Effect.ts"
10import { effectIsExit, resolveConcurrency } from "../../internal/effect.ts"
11import { lazyParser, type Resolve, resolve, setCompiler, withDecode } from "../../internal/schema/compilerRegistry.ts"
12import * as Interpreter from "../../internal/schema/interpreter.ts"
13import * as InternalParser from "../../internal/schema/parser.ts"
14import * as SchemaAST from "../../SchemaAST.ts"
15import * as SchemaIssue from "../../SchemaIssue.ts"
16import type { Compiler } from "../../SchemaParser.ts"
17import { type Decode, invalid } from "../SchemaCompiler.ts"
18
19type SchemaIssueParser = ReturnType<typeof Interpreter.compile>
20type ObjectParserState = Parameters<typeof SchemaAST.stepProperty>[0]
21type ParsedProperty = Parameters<typeof SchemaAST.stepProperty>[1]
22type ArrayParserState = Parameters<typeof SchemaAST.stepArray>[0]
23type 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
34type 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
45const 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
84const 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
139const 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
169const 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
185const 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
197const 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
208const 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
226const invalidType = (ast: SchemaAST.AST, input: unknown, options: SchemaAST.ParseOptions) =>
227 Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
228
229const 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
240const 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 */
258export 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}