1import { memoize } from "../../Function.ts"
2import * as Predicate from "../../Predicate.ts"
3import type * as PublicSchema from "../../Schema.ts"
4import * as SchemaAST from "../../SchemaAST.ts"
5import * as SchemaGetter from "../../SchemaGetter.ts"
6import * as InternalTransformation from "../../SchemaTransformation.ts"
7import * as InternalMake from "./make.ts"
8
9/** @internal */
10export function toCodecJson<S extends PublicSchema.Constraint>(schema: S): PublicSchema.toCodecJson<S> {
11 return InternalMake.make(toCodecJsonAST(schema.ast), { schema })
12}
13
14/** @internal */
15export const toCodecJsonAST = SchemaAST.applyToSelfOrLastLinkEncodingIdempotent((ast) => {
16 const out = toCodecJsonASTStep(ast, toCodecJsonAST)
17 const context = ast.context
18 if (out === ast || context === undefined) return out
19 return SchemaAST.replaceContextLastLink(out, withoutConstructorDefault(context))
20})
21
22function withoutConstructorDefault(context: SchemaAST.Context): SchemaAST.Context {
23 return context.constructorDefault === undefined ?
24 context :
25 new SchemaAST.Context(context.isOptional, context.isMutable, undefined, context.annotations)
26}
27
28function validateCanonicalObjectPropertyNames(ast: SchemaAST.Objects): void {
29 if (ast.propertySignatures.some((ps) => typeof ps.name !== "string")) {
30 throw new globalThis.Error("Objects property names must be strings", { cause: ast })
31 }
32}
33
34function makeReorder(getPriority: (ast: SchemaAST.AST) => number) {
35 return (types: ReadonlyArray<SchemaAST.AST>): ReadonlyArray<SchemaAST.AST> => {
36 const sortedTypes = [...types].sort((a, b) =>
37 getPriority(SchemaAST.toEncoded(a)) - getPriority(SchemaAST.toEncoded(b))
38 )
39
40 // Check if order changed by comparing arrays
41 const orderChanged = sortedTypes.some((ast, index) => ast !== types[index])
42
43 if (!orderChanged) return types
44 return sortedTypes
45 }
46}
47
48const toCodecJsonReorder = makeReorder((ast: SchemaAST.AST) => {
49 switch (ast._tag) {
50 case "BigInt":
51 case "Symbol":
52 case "UniqueSymbol":
53 return 0
54 default:
55 return 1
56 }
57})
58
59function toCodecJsonASTStep(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST {
60 switch (ast._tag) {
61 case "Declaration": {
62 const getLink = ast.annotations?.toCodecJson ?? ast.annotations?.toCodec
63 if (!Predicate.isFunction(getLink)) {
64 return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToJson])
65 }
66 const typeParameters = ast.typeParameters.map((tp) => InternalMake.make(SchemaAST.toEncoded(tp)))
67 const link = getLink(typeParameters)
68 return link === undefined ? ast : SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, recur)])
69 }
70 case "Unknown":
71 return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToJson])
72 case "ObjectKeyword":
73 return SchemaAST.replaceEncoding(ast, [SchemaAST.objectKeywordToJson])
74 case "Undefined":
75 case "Void":
76 case "Literal":
77 case "Number":
78 return ast.toCodecJson()
79 case "UniqueSymbol":
80 case "Symbol":
81 case "BigInt":
82 return ast.toCodecStringTree()
83 case "Objects": {
84 validateCanonicalObjectPropertyNames(ast)
85 return ast.recur(recur, SchemaAST.parameterFromString)
86 }
87 case "Union": {
88 const sortedTypes = toCodecJsonReorder(ast.types)
89 if (sortedTypes !== ast.types) {
90 return new SchemaAST.Union(
91 sortedTypes,
92 ast.options,
93 ast.annotations,
94 ast.checks,
95 ast.encoding,
96 ast.context,
97 ast.encodingChecks
98 ).recur(recur)
99 }
100 return ast.recur(recur)
101 }
102 case "Arrays":
103 case "Suspend":
104 return ast.recur(recur)
105 }
106 // `Schema.Any` is used as an escape hatch
107 return ast
108}
109
110/** @internal */
111export function toCodecIso<S extends PublicSchema.Constraint>(
112 schema: S
113): PublicSchema.Codec<S["Type"], S["Iso"]> {
114 return InternalMake.make(toCodecIsoAST(SchemaAST.toType(schema.ast)))
115}
116
117const toCodecIsoAST = memoize((ast: SchemaAST.AST): SchemaAST.AST => {
118 const out = toCodecIsoASTStep(ast, toCodecIsoAST)
119 return out !== ast && ast.context !== undefined ?
120 SchemaAST.replaceContextLastLink(out, withoutConstructorDefault(ast.context)) :
121 out
122})
123
124function toCodecIsoASTStep(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST {
125 switch (ast._tag) {
126 case "Declaration": {
127 const getLink = ast.annotations?.toCodecIso ?? ast.annotations?.toCodec
128 if (Predicate.isFunction(getLink)) {
129 const link = getLink(ast.typeParameters.map((tp) => InternalMake.make(tp)))
130 return SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, recur)])
131 }
132 return ast
133 }
134 case "Arrays":
135 case "Objects":
136 case "Union":
137 case "Suspend":
138 return ast.recur(recur)
139 }
140 return ast
141}
142
143/** @internal */
144export function toCodecStringTree<S extends PublicSchema.Constraint>(
145 schema: S
146): PublicSchema.toCodecStringTree<S> {
147 return InternalMake.make(toCodecStringTreeAST(schema.ast), { schema })
148}
149
150/** @internal */
151export function toCodecArrayFromSingle<S extends PublicSchema.Constraint>(
152 schema: S
153): PublicSchema.toCodecArrayFromSingle<S> {
154 return InternalMake.make(toCodecArrayFromSingleAST(schema.ast))
155}
156
157const toStringTreeReorder = makeReorder((ast: SchemaAST.AST) => {
158 switch (ast._tag) {
159 case "Null":
160 case "Boolean":
161 case "Number":
162 case "BigInt":
163 case "Symbol":
164 case "UniqueSymbol":
165 return 0
166 default:
167 return 1
168 }
169})
170
171function toCodecStringTreeASTStep(
172 ast: SchemaAST.AST,
173 recur: (ast: SchemaAST.AST) => SchemaAST.AST,
174 onMissingAnnotation: (ast: SchemaAST.AST) => SchemaAST.AST
175): SchemaAST.AST {
176 switch (ast._tag) {
177 case "Declaration": {
178 const typeParameters = ast.typeParameters.map((tp) => InternalMake.make(recur(SchemaAST.toEncoded(tp))))
179 const getStringTreeLink = ast.annotations?.toCodecStringTree
180 if (Predicate.isFunction(getStringTreeLink)) {
181 const link = getStringTreeLink(typeParameters)
182 if (link === undefined) return ast
183 return SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, recur)])
184 }
185 const getJsonLink = ast.annotations?.toCodecJson
186 const jsonLink = Predicate.isFunction(getJsonLink) ? getJsonLink(typeParameters) : undefined
187 const getLink = jsonLink === undefined ? ast.annotations?.toCodec : undefined
188 const link = jsonLink ?? (Predicate.isFunction(getLink) ? getLink(typeParameters) : undefined)
189 return link === undefined
190 ? onMissingAnnotation(ast)
191 : SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, recur)])
192 }
193 case "Null":
194 return SchemaAST.replaceEncoding(ast, [nullToString])
195 case "Boolean":
196 return SchemaAST.replaceEncoding(ast, [booleanToString])
197 case "Unknown":
198 case "ObjectKeyword":
199 return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToStringTree])
200 case "Enum":
201 case "Number":
202 case "Literal":
203 case "UniqueSymbol":
204 case "Symbol":
205 case "BigInt":
206 return ast.toCodecStringTree()
207 case "Objects": {
208 validateCanonicalObjectPropertyNames(ast)
209 return ast.recur(recur, SchemaAST.parameterFromString)
210 }
211 case "Union": {
212 const sortedTypes = toStringTreeReorder(ast.types)
213 if (sortedTypes !== ast.types) {
214 return new SchemaAST.Union(
215 sortedTypes,
216 ast.options,
217 ast.annotations,
218 ast.checks,
219 ast.encoding,
220 ast.context,
221 ast.encodingChecks
222 ).recur(recur)
223 }
224 return ast.recur(recur)
225 }
226 case "Arrays":
227 case "Suspend":
228 return ast.recur(recur)
229 }
230 // `Schema.Any` is used as an escape hatch
231 return ast
232}
233
234const nullToString = new SchemaAST.Link(
235 new SchemaAST.Literal("null"),
236 new InternalTransformation.Transformation(
237 SchemaGetter.transform(() => null),
238 SchemaGetter.transform(() => "null")
239 )
240)
241
242const booleanToString = new SchemaAST.Link(
243 new SchemaAST.Union([new SchemaAST.Literal("true"), new SchemaAST.Literal("false")]),
244 new InternalTransformation.Transformation(
245 SchemaGetter.transform((s) => s === "true"),
246 SchemaGetter.String()
247 )
248)
249
250const arrayFromSingleTransformation = new InternalTransformation.Transformation(
251 SchemaGetter.transform((input: ReadonlyArray<unknown> | string) => typeof input === "string" ? [input] : input),
252 SchemaGetter.passthrough()
253)
254
255const isCodecArrayFromSingleLink = (link: SchemaAST.Link): boolean =>
256 link.transformation === arrayFromSingleTransformation
257
258const toCodecStringTreeAST = SchemaAST.applyToSelfOrLastLinkEncodingIdempotent((ast) => {
259 const out = toCodecStringTreeASTStep(ast, toCodecStringTreeAST, (ast) => {
260 throw new globalThis.Error("Missing structural codec for StringTree", { cause: ast })
261 })
262 if (out !== ast && ast.context !== undefined) {
263 return SchemaAST.replaceContextLastLink(out, withoutConstructorDefault(ast.context))
264 }
265 return out
266}, { stopAt: isCodecArrayFromSingleLink })
267
268const toArrayFromSingleInputElement = (ast: SchemaAST.AST): SchemaAST.AST =>
269 SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(SchemaAST.unknown) : SchemaAST.unknown
270
271const toCodecArrayFromSingleAST = SchemaAST.applyToSelfOrLastLinkEncodingIdempotent((ast) => {
272 const out = toCodecArrayFromSingleASTStep(ast)
273 if (SchemaAST.isArrays(out)) {
274 const ensure = SchemaAST.decodeTo(
275 new SchemaAST.Union(
276 [
277 new SchemaAST.Arrays(
278 out.isMutable,
279 out.elements.map(toArrayFromSingleInputElement),
280 out.rest.map(toArrayFromSingleInputElement)
281 ),
282 SchemaAST.string
283 ],
284 { mode: "anyOf" }
285 ),
286 out,
287 arrayFromSingleTransformation
288 )
289 return SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(ensure) : ensure
290 }
291 return out
292}, { stopAt: isCodecArrayFromSingleLink })
293
294function toCodecArrayFromSingleASTStep(ast: SchemaAST.AST): SchemaAST.AST {
295 return ast._tag === "Declaration" || ast._tag === "Arrays" || ast._tag === "Objects" || ast._tag === "Union" ||
296 ast._tag === "Suspend"
297 ? ast.recur(toCodecArrayFromSingleAST)
298 : ast
299}