1import * as Arr from "../../Array.ts"
2import * as Result from "../../Result.ts"
3import * as Schema from "../../Schema.ts"
4import type * as SchemaAST from "../../SchemaAST.ts"
5import * as SchemaParser from "../../SchemaParser.ts"
6import type * as SchemaRepresentation from "../../SchemaRepresentation.ts"
7import { errorWithPath } from "../errors.ts"
8import * as InternalRecord from "../record.ts"
9import * as InternalToCodec from "./toCodec.ts"
10
11type Path = ReadonlyArray<string | number>
12
13/** @internal */
14export function fromRepresentations(
15 document: SchemaRepresentation.MultiDocument,
16 revivers: ReadonlyArray<SchemaRepresentation.AnyReviver>
17): readonly [Schema.Top, ...Array<Schema.Top>] {
18 return revivePersisted(document.representations, document.references, makeReviverMap(revivers), false)
19}
20
21class ReferenceSlot {
22 body: Schema.Top | undefined
23 resolving = false
24 readonly wrapper: Schema.Top
25
26 constructor(key: string) {
27 this.wrapper = Schema.suspend(() => {
28 if (this.body === undefined) {
29 throw new Error(`Reference ${key} was evaluated before it was resolved`)
30 }
31 return this.body
32 })
33 }
34}
35
36function makeReviverMap(
37 revivers: ReadonlyArray<SchemaRepresentation.AnyReviver>
38): Map<string, SchemaRepresentation.AnyReviver> {
39 const out = new Map<string, SchemaRepresentation.AnyReviver>()
40
41 for (let index = 0; index < revivers.length; index++) {
42 const reviver = revivers[index]
43 if (out.has(reviver.id)) {
44 throw errorWithPath(`Duplicate reviver for ${reviver.id}`, ["revivers", index, "id"])
45 }
46 out.set(reviver.id, {
47 ...reviver,
48 payloadSchema: InternalToCodec.toCodecJson(reviver.payloadSchema)
49 })
50 }
51
52 return out
53}
54
55function revivePersisted(
56 representations: readonly [
57 SchemaRepresentation.Representation,
58 ...Array<SchemaRepresentation.Representation>
59 ],
60 references: SchemaRepresentation.References,
61 reviverMap: ReadonlyMap<string, SchemaRepresentation.AnyReviver>,
62 singleRoot: boolean
63): readonly [Schema.Top, ...Array<Schema.Top>] {
64 const slots = new Map<string, ReferenceSlot>()
65
66 function resolveReference(key: string, path: Path): Schema.Top {
67 let slot = slots.get(key)
68 if (slot === undefined) {
69 if (!Object.hasOwn(references, key)) {
70 throw errorWithPath(`Invalid reference ${key}`, [...path, "$ref"])
71 }
72 slot = new ReferenceSlot(key)
73 slots.set(key, slot)
74 }
75 if (slot.body !== undefined) {
76 return slot.body
77 }
78 if (slot.resolving) {
79 return slot.wrapper
80 }
81
82 slot.resolving = true
83 try {
84 slot.body = annotate(recur(references[key], ["references", key]), { identifier: key })
85 return slot.body
86 } finally {
87 slot.resolving = false
88 }
89 }
90
91 function resolveReviver<R extends SchemaRepresentation.AnyReviver>(
92 representation: SchemaRepresentation.RepresentationAnnotation,
93 path: Path
94 ): R {
95 const reviver = reviverMap.get(representation.id)
96 if (reviver === undefined) {
97 throw errorWithPath(`Missing reviver for ${representation.id}`, path)
98 }
99 return reviver as R
100 }
101
102 function decodePayload(
103 representation: SchemaRepresentation.RepresentationAnnotation,
104 reviver: SchemaRepresentation.AnyReviver,
105 path: Path
106 ): any {
107 const decoded = SchemaParser.decodeUnknownResult(reviver.payloadSchema)(representation.payload)
108 if (Result.isFailure(decoded)) {
109 throw errorWithPath(`Invalid representation payload for ${representation.id}`, path)
110 }
111 return decoded.success
112 }
113
114 function reviveSchemas(
115 representations: ReadonlyArray<SchemaRepresentation.Representation>,
116 path: Path
117 ): ReadonlyArray<Schema.Top> {
118 return representations.map((representation, index) => recur(representation, [...path, index]))
119 }
120
121 function reviveDeclaration(
122 declaration: SchemaRepresentation.Declaration,
123 path: Path
124 ): Schema.Top {
125 const representationPath = [...path, "representation"]
126 const representation = declaration.representation
127 if (representation === undefined) {
128 throw errorWithPath("Missing representation annotation", representationPath)
129 }
130 const reviver = resolveReviver<SchemaRepresentation.DeclarationReviver<any>>(representation, representationPath)
131 const payload = decodePayload(representation, reviver, [...representationPath, "payload"])
132 const typeParameters = reviveSchemas(declaration.typeParameters, [...path, "typeParameters"])
133 const schema = reviver.revive({ payload, typeParameters, annotations: declaration.annotations })
134 return appendChecks(schema, declaration.checks, [...path, "checks"])
135 }
136
137 function reviveFilter(
138 filter: SchemaRepresentation.Filter,
139 path: Path
140 ): SchemaAST.Filter<any> {
141 const representationPath = [...path, "representation"]
142 const representation = filter.representation
143 if (representation === undefined) {
144 throw errorWithPath("Missing representation annotation", representationPath)
145 }
146 const reviver = resolveReviver<SchemaRepresentation.FilterReviver<any>>(representation, representationPath)
147 const payload = decodePayload(representation, reviver, [...representationPath, "payload"])
148 const schemas = reviveSchemas(representation.schemas ?? [], [...representationPath, "schemas"])
149 const check = reviver.revive({ payload, schemas, annotations: filter.annotations })
150 return filter.aborted ? check.abort() : check
151 }
152
153 function reviveFilterGroup(
154 group: SchemaRepresentation.FilterGroup,
155 path: Path
156 ): SchemaAST.FilterGroup<any> {
157 const representationPath = [...path, "representation"]
158 const representation = group.representation
159 if (representation === undefined) {
160 const checks = group.checks.map((check, index) => reviveCheck(check, [...path, "checks", index]))
161 return Schema.makeFilterGroup(
162 checks as [SchemaAST.Check<any>, ...Array<SchemaAST.Check<any>>],
163 group.annotations as Schema.Annotations.Filter | undefined
164 )
165 }
166
167 const reviver = resolveReviver<SchemaRepresentation.FilterGroupReviver<any>>(representation, representationPath)
168 const payload = decodePayload(representation, reviver, [...representationPath, "payload"])
169 const schemas = reviveSchemas(representation.schemas ?? [], [...representationPath, "schemas"])
170 return reviver.revive({ payload, schemas, annotations: group.annotations })
171 }
172
173 function reviveCheck(
174 check: SchemaRepresentation.Check,
175 path: Path
176 ): SchemaAST.Check<any> {
177 return check._tag === "Filter"
178 ? reviveFilter(check, path)
179 : reviveFilterGroup(check, path)
180 }
181
182 function appendChecks<S extends Schema.Top>(
183 schema: S,
184 checks: ReadonlyArray<SchemaRepresentation.Check>,
185 path: Path
186 ): S["Rebuild"] {
187 const revived = checks.map((check, index) => reviveCheck(check, [...path, index]))
188 return Arr.isArrayNonEmpty(revived) ? schema.check(...revived) : schema as S["Rebuild"]
189 }
190
191 function annotate(
192 schema: Schema.Top,
193 annotations: Schema.Annotations.Annotations | undefined
194 ): Schema.Top {
195 return annotations === undefined ? schema : schema.annotate(annotations)
196 }
197
198 function finishStructural(
199 schema: Schema.Top,
200 representation: Exclude<SchemaRepresentation.Representation, SchemaRepresentation.Reference>,
201 path: Path
202 ): Schema.Top {
203 return appendChecks(
204 annotate(schema, representation.annotations),
205 representation.checks,
206 [...path, "checks"]
207 )
208 }
209
210 function recur(
211 representation: SchemaRepresentation.Representation,
212 path: Path
213 ): Schema.Top {
214 switch (representation._tag) {
215 case "Reference":
216 return resolveReference(representation.$ref, path)
217 case "Declaration":
218 return reviveDeclaration(representation, path)
219 case "Suspend": {
220 const thunkPath = [...path, "thunk"]
221 if (representation.thunk._tag === "Reference") {
222 const key = representation.thunk.$ref
223 resolveReference(key, thunkPath)
224 const slot = slots.get(key)!
225 return annotate(slot.wrapper, representation.annotations)
226 }
227 const thunk = recur(representation.thunk, thunkPath)
228 return annotate(Schema.suspend(() => thunk), representation.annotations)
229 }
230 case "Null":
231 return finishStructural(Schema.Null, representation, path)
232 case "Undefined":
233 return finishStructural(Schema.Undefined, representation, path)
234 case "Void":
235 return finishStructural(Schema.Void, representation, path)
236 case "Never":
237 return finishStructural(Schema.Never, representation, path)
238 case "Unknown":
239 return finishStructural(Schema.Unknown, representation, path)
240 case "Any":
241 return finishStructural(Schema.Any, representation, path)
242 case "String":
243 return finishStructural(Schema.String, representation, path)
244 case "Number":
245 return finishStructural(Schema.Number, representation, path)
246 case "Boolean":
247 return finishStructural(Schema.Boolean, representation, path)
248 case "BigInt":
249 return finishStructural(Schema.BigInt, representation, path)
250 case "Symbol":
251 return finishStructural(Schema.Symbol, representation, path)
252 case "Literal":
253 return finishStructural(Schema.Literal(representation.literal), representation, path)
254 case "UniqueSymbol":
255 return finishStructural(Schema.UniqueSymbol(representation.symbol), representation, path)
256 case "ObjectKeyword":
257 return finishStructural(Schema.ObjectKeyword, representation, path)
258 case "Enum":
259 return finishStructural(
260 Schema.Enum(Object.fromEntries(representation.enums)),
261 representation,
262 path
263 )
264 case "TemplateLiteral": {
265 const parts = representation.parts.map((part, index) => recur(part, [...path, "parts", index]))
266 return finishStructural(
267 Schema.TemplateLiteral(parts as unknown as Schema.TemplateLiteral.Parts),
268 representation,
269 path
270 )
271 }
272 case "Arrays": {
273 const elements = representation.elements.map((element, index) => {
274 let schema = recur(element.type, [...path, "elements", index, "type"])
275 if (element.annotations !== undefined) {
276 schema = schema.annotateKey(element.annotations as Schema.Annotations.Key<unknown>)
277 }
278 return element.isOptional ? Schema.optionalKey(schema) : schema
279 })
280 const rest = representation.rest.map((item, index) => recur(item, [...path, "rest", index]))
281 const schema = Arr.isArrayNonEmpty(rest)
282 ? elements.length === 0 && rest.length === 1
283 ? Schema.Array(rest[0])
284 : Schema.TupleWithRest(Schema.Tuple(elements), rest)
285 : Schema.Tuple(elements)
286 return finishStructural(schema, representation, path)
287 }
288 case "Objects": {
289 const fields: Record<PropertyKey, Schema.Top> = {}
290 for (let index = 0; index < representation.propertySignatures.length; index++) {
291 const property = representation.propertySignatures[index]
292 let schema = recur(property.type, [...path, "propertySignatures", index, "type"])
293 if (property.annotations !== undefined) {
294 schema = schema.annotateKey(property.annotations as Schema.Annotations.Key<unknown>)
295 }
296 if (property.isOptional) {
297 schema = Schema.optionalKey(schema)
298 }
299 if (property.isMutable) {
300 schema = Schema.mutableKey(schema)
301 }
302 InternalRecord.assignProperty(fields, property.name, schema)
303 }
304 const records = representation.indexSignatures.map((indexSignature, index) =>
305 Schema.Record(
306 recur(indexSignature.parameter, [...path, "indexSignatures", index, "parameter"]) as Schema.Record.Key,
307 recur(indexSignature.type, [...path, "indexSignatures", index, "type"])
308 )
309 )
310 const schema = Arr.isArrayNonEmpty(records)
311 ? representation.propertySignatures.length === 0 && records.length === 1
312 ? records[0]
313 : Schema.StructWithRest(Schema.Struct(fields), records)
314 : Schema.Struct(fields)
315 return finishStructural(schema, representation, path)
316 }
317 case "Union": {
318 const members = representation.types.map((member, index) => recur(member, [...path, "types", index]))
319 return finishStructural(Schema.Union(members, representation.options), representation, path)
320 }
321 }
322 }
323
324 const schemas = representations.map((representation, index) =>
325 recur(representation, singleRoot ? ["representation"] : ["representations", index])
326 ) as [Schema.Top, ...Array<Schema.Top>]
327 return schemas
328}
329
330/** @internal */
331export function fromRepresentation(
332 document: SchemaRepresentation.Document,
333 revivers: ReadonlyArray<SchemaRepresentation.AnyReviver>
334): Schema.Top {
335 return revivePersisted(
336 [document.representation],
337 document.references,
338 makeReviverMap(revivers),
339 true
340 )[0]
341}