1import * as Cause from "../../Cause.ts"
2import * as Effect from "../../Effect.ts"
3import type * as Exit from "../../Exit.ts"
4import type * as Option from "../../Option.ts"
5import * as SchemaAST from "../../SchemaAST.ts"
6import * as SchemaIssue from "../../SchemaIssue.ts"
7import type { Compiler, Parser } from "../../SchemaParser.ts"
8import { effectIsExit } from "../effect.ts"
9import * as InternalParser from "./parser.ts"
10
11type ApplyTransformation = (
12 result: Effect.Effect<unknown, SchemaIssue.Issue, unknown>,
13 current: unknown,
14 options: SchemaAST.ParseOptions
15) => Effect.Effect<unknown, SchemaIssue.Issue, unknown>
16
17const flatMapTransformation = (
18 result: Effect.Effect<unknown, SchemaIssue.Issue, unknown>,
19 current: unknown,
20 f: (value: unknown) => Effect.Effect<unknown, SchemaIssue.Issue, unknown>
21): Effect.Effect<unknown, SchemaIssue.Issue, unknown> =>
22 result === InternalParser.sameExit ? f(current) : Effect.flatMapEager(result, f)
23
24function compileTransformation(transformation: SchemaAST.Link["transformation"]): ApplyTransformation {
25 if (transformation._tag === "Middleware") {
26 return (result, current, options) => {
27 const transformed = result === InternalParser.sameExit
28 ? transformation.decode(InternalParser.succeed(InternalParser.toOption(current)), options)
29 : transformation.decode(Effect.mapEager(result, InternalParser.toOption), options)
30 return fromOptionalEffect(transformed)
31 }
32 }
33
34 const getter = transformation.decode
35 switch (getter._tag) {
36 case "Passthrough":
37 return (result, current) => result === InternalParser.sameExit ? InternalParser.succeed(current) : result
38 case "Transform": {
39 const transform = (value: unknown) =>
40 value === InternalParser.missing
41 ? InternalParser.missingExit
42 : InternalParser.succeed(getter.transform(value))
43 return (result, current) => flatMapTransformation(result, current, transform)
44 }
45 case "TransformOptional": {
46 const transform = (value: unknown) =>
47 InternalParser.fromOptionExit(getter.transform(InternalParser.toOption(value)))
48 return (result, current) => flatMapTransformation(result, current, transform)
49 }
50 case "TransformEffect":
51 return (result, current, options) =>
52 flatMapTransformation(result, current, (value) =>
53 value === InternalParser.missing
54 ? InternalParser.missingExit
55 : getter.transform(value, options))
56 case "TransformOptionalEffect":
57 return (result, current, options) =>
58 flatMapTransformation(
59 result,
60 current,
61 (value) => fromOptionalEffect(getter.transform(InternalParser.toOption(value), options))
62 )
63 }
64}
65
66const fromOptionalEffect = (
67 effect: Effect.Effect<Option.Option<unknown>, SchemaIssue.Issue, unknown>
68): Effect.Effect<unknown, SchemaIssue.Issue, unknown> => Effect.flatMapEager(effect, InternalParser.fromOptionExit)
69
70/** @internal */
71export const wrapEncoding = (
72 ast: SchemaAST.AST,
73 input: unknown,
74 options: SchemaAST.ParseOptions,
75 effect: Effect.Effect<unknown, SchemaIssue.Issue, unknown>
76): Effect.Effect<unknown, SchemaIssue.Issue, unknown> =>
77 Effect.catchCause(
78 effect,
79 (cause) =>
80 Effect.failCauseSync(() => Cause.map(cause, (issue) => new SchemaIssue.Encoding(ast, issue, input, options)))
81 )
82
83function makeConstructorParser(descriptor: SchemaAST.ConstructorDescriptor, compile: Compiler): Parser {
84 const transform = compileTransformation(descriptor.link.transformation)
85 let sourceParser: Parser
86 return (input, options) => {
87 if (input === InternalParser.missing) return InternalParser.missingExit
88 if (descriptor.isConstructed(input)) return InternalParser.sameExit
89 const result = (sourceParser ??= compile(descriptor.link.to))(input, options)
90 return transform(result, input, options)
91 }
92}
93
94function withDefault(ast: SchemaAST.AST, parser: Parser): Parser {
95 const defaultValue = ast.context!.constructorDefault!
96 return (input, options) => {
97 if (input !== InternalParser.missing && input !== undefined) return parser(input, options)
98 const result = defaultValue
99 if (effectIsExit(result) && result._tag === "Success") {
100 const local = parser((result as InternalParser.Success<unknown>)[InternalParser.args], options)
101 return local === InternalParser.sameExit ? result : local
102 }
103 return Effect.flatMapEager(
104 wrapEncoding(ast, input, options, result),
105 (value) => {
106 const local = parser(value, options)
107 return local === InternalParser.sameExit ? InternalParser.succeed(value) : local
108 }
109 )
110 }
111}
112
113/** @internal */
114export function compileField(ast: SchemaAST.AST, compile: Compiler): Parser {
115 const parser = compile(ast)
116 return ast.context?.constructorDefault === undefined ? parser : withDefault(ast, parser)
117}
118
119/** @internal */
120export function compile(
121 ast: SchemaAST.AST,
122 compile: Compiler,
123 compileField?: Compiler,
124 base?: Parser,
125 specialize?: (local: Parser) => Parser
126): Parser {
127 if (ast._tag === "Declaration") {
128 // Declaration callbacks can create public parsers for their type parameters.
129 // Register those ASTs with the same resolver before invoking the callback.
130 for (const parameter of ast.typeParameters) compile(parameter)
131 }
132 // Construction supplies compileField for parent-owned defaults. Its presence
133 // also selects the constructor semantics of declarations and Unions.
134 const descriptor = compileField ? SchemaAST.getConstructorDescriptor(ast) : undefined
135 const parser = descriptor
136 ? makeConstructorParser(descriptor, compile)
137 : base ?? ast.getParser(compile, compileField)
138 const checks = ast.checks
139 const links = ast.encoding
140 const transformations = links?.map((link) => compileTransformation(link.transformation))
141 const encodingChecks = (ast as any).encodingChecks
142 if (!links && !checks && !encodingChecks) {
143 return parser
144 }
145 let encodingParsers: ReadonlyArray<Parser> | undefined
146 const parseChecks = (
147 input: unknown,
148 options: SchemaAST.ParseOptions
149 ) => {
150 let result = parser(input, options)
151 if (encodingChecks && !options.disableChecks) {
152 if (effectIsExit(result)) {
153 if (result._tag === "Success") {
154 const output = result === InternalParser.sameExit
155 ? input
156 : (result as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args]
157 if (input !== InternalParser.missing && output !== InternalParser.missing) {
158 const issues = SchemaAST.collectIssues(encodingChecks, input, undefined, ast, options)
159 if (issues) {
160 result = Effect.fail(new SchemaIssue.Composite(ast, issues, input, options))
161 }
162 }
163 }
164 } else {
165 result = Effect.flatMap(result, (value) => {
166 if (input !== InternalParser.missing && value !== InternalParser.missing) {
167 const issues = SchemaAST.collectIssues(encodingChecks, input, undefined, ast, options)
168 if (issues) {
169 return Effect.fail(new SchemaIssue.Composite(ast, issues, input, options))
170 }
171 }
172 return Effect.succeed(value)
173 })
174 }
175 }
176
177 if (checks && !options.disableChecks) {
178 if (effectIsExit(result)) {
179 if (result._tag === "Success") {
180 const value = result === InternalParser.sameExit
181 ? input
182 : (result as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args]
183 if (value === InternalParser.missing) return result
184 const issues = SchemaAST.collectIssues(checks, value, undefined, ast, options)
185 if (issues) {
186 result = Effect.fail(new SchemaIssue.Composite(ast, issues, value, options))
187 }
188 }
189 } else {
190 result = Effect.flatMap(result, (value) => {
191 if (value !== InternalParser.missing) {
192 const issues = SchemaAST.collectIssues(checks, value, undefined, ast, options)
193 if (issues) {
194 return Effect.fail(new SchemaIssue.Composite(ast, issues, value, options))
195 }
196 }
197 return Effect.succeed(value)
198 })
199 }
200 }
201
202 return result
203 }
204 const parseLocal = specialize === undefined ? parseChecks : specialize(parseChecks)
205 if (!links) {
206 return parseLocal
207 }
208 return (
209 input: unknown,
210 options: SchemaAST.ParseOptions
211 ) => {
212 const parsers = encodingParsers ??= links.map((link) => compile(link.to))
213 let current = input
214 let result = parsers[parsers.length - 1](input, options)
215 for (let i = links.length - 1; i >= 0; i--) {
216 result = transformations![i](result, current, options)
217 if (i !== 0) {
218 const next = parsers[i - 1]
219 if ((result as Exit.Exit<unknown, unknown>)._tag === "Success") {
220 current = (result as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args]
221 result = next(current, options)
222 } else {
223 result = Effect.flatMapEager(result, (value) => {
224 const nextResult = next(value, options)
225 return nextResult === InternalParser.sameExit ? InternalParser.succeed(value) : nextResult
226 })
227 }
228 }
229 }
230 if ((result as Exit.Exit<unknown, unknown>)._tag === "Success") {
231 const value = (result as InternalParser.Success<unknown, SchemaIssue.Issue>)[InternalParser.args]
232 const local = parseLocal(value, options)
233 return local === InternalParser.sameExit ? result : local
234 }
235 result = wrapEncoding(ast, input, options, result)
236 return Effect.flatMapEager(result, (value) => {
237 const local = parseLocal(value, options)
238 return local === InternalParser.sameExit ? InternalParser.succeed(value) : local
239 })
240 }
241}