1import * as Arr from "../../Array.ts"
2import type * as Schema from "../../Schema.ts"
3import * as SchemaAST from "../../SchemaAST.ts"
4import type * as SchemaRepresentation from "../../SchemaRepresentation.ts"
5import * as InternalRecord from "../record.ts"
6import * as InternalAnnotations from "./annotations.ts"
7
8const defaultReferencePolicy: SchemaRepresentation.ReferencePolicy = ({ identifier }) => identifier
9
10type CheckRepresentationAnnotation = SchemaRepresentation.CheckRepresentationAnnotation<
11 SchemaRepresentation.Representation
12>
13
14interface ReferenceCandidate {
15 readonly ast: SchemaAST.AST
16 readonly identifier: string | undefined
17 readonly fallback: string | undefined
18 occurrences: number
19 isRecursive: boolean
20 reference: string | undefined
21}
22
23function annotationsField<A>(annotations: A | undefined): { readonly annotations: A } | undefined {
24 return annotations === undefined ? undefined : { annotations }
25}
26
27/** @internal */
28export function toRepresentation(
29 ast: SchemaAST.AST,
30 options?: SchemaRepresentation.ToRepresentationOptions
31): SchemaRepresentation.Document {
32 const { references, representations } = toRepresentations([ast], options)
33 return { representation: representations[0], references }
34}
35
36/** @internal */
37export function toRepresentations(
38 asts: readonly [SchemaAST.AST, ...Array<SchemaAST.AST>],
39 options?: SchemaRepresentation.ToRepresentationOptions
40): SchemaRepresentation.MultiDocument {
41 const references: Record<string, SchemaRepresentation.Representation> = {}
42 const referenceOwners = new Map<string, ReferenceCandidate>()
43 const buildingReferences = new Set<string>()
44 const candidates = new Map<SchemaAST.AST, Map<string | undefined, ReferenceCandidate>>()
45 const visitingCandidates = new Set<ReferenceCandidate>()
46
47 for (const ast of asts) visit(ast)
48
49 const referencePolicy = options?.referencePolicy ?? defaultReferencePolicy
50 for (const candidatesByIdentifier of candidates.values()) {
51 for (const candidate of candidatesByIdentifier.values()) {
52 const requestedReference = referencePolicy({
53 ast: candidate.ast,
54 occurrences: candidate.occurrences,
55 identifier: candidate.identifier
56 })
57 if (requestedReference !== undefined) {
58 const separator = requestedReference === candidate.identifier ||
59 !requestedReference.endsWith("_")
60 ? "_"
61 : ""
62 candidate.reference = getReference(requestedReference, candidate, separator)
63 } else if (candidate.isRecursive) {
64 candidate.reference = getReference(`${candidate.ast._tag}_`, candidate, "")
65 }
66 }
67 }
68
69 const representations = Arr.map(asts, (ast) => recur(ast))
70
71 return { representations, references }
72
73 function getReference(prefix: string, owner: ReferenceCandidate, separator = "_"): string {
74 let candidate = prefix
75 let suffix = 0
76 while (referenceOwners.has(candidate)) {
77 if (referenceOwners.get(candidate) === owner) return candidate
78 candidate = `${prefix}${separator}${++suffix}`
79 }
80 referenceOwners.set(candidate, owner)
81 return candidate
82 }
83
84 function annotateReference(
85 ast: SchemaAST.AST,
86 candidate: ReferenceCandidate,
87 reference: string
88 ): SchemaAST.AST {
89 const fallback = candidate.fallback
90 if (fallback !== undefined) {
91 return InternalAnnotations.resolveIdentifierFallback(ast) === fallback
92 ? ast
93 : SchemaAST.annotate(ast, {
94 [InternalAnnotations.IDENTIFIER_FALLBACK_KEY]: fallback
95 })
96 }
97 return reference === candidate.identifier
98 ? ast
99 : SchemaAST.annotate(ast, { identifier: reference })
100 }
101
102 function makeReference(reference: string, ast: SchemaAST.AST): SchemaRepresentation.Reference {
103 if (!Object.hasOwn(references, reference) && !buildingReferences.has(reference)) {
104 buildingReferences.add(reference)
105 const representation = on(ast)
106 buildingReferences.delete(reference)
107 InternalRecord.assignProperty(references, reference, representation)
108 }
109 return { _tag: "Reference", $ref: reference }
110 }
111
112 function getCandidate(input: SchemaAST.AST): ReferenceCandidate {
113 const ast = SchemaAST.getLastEncoding(input)
114 const owner = SchemaAST.getContextOwner(ast)
115 let identifier = InternalAnnotations.resolveIdentifier(ast)
116 const fallback = identifier === undefined
117 ? (ast !== input ? InternalAnnotations.resolveIdentifier(input) : undefined) ??
118 InternalAnnotations.resolveIdentifierFallback(ast)
119 : undefined
120 if (fallback !== undefined) identifier = `${fallback}Encoded`
121 let candidatesByIdentifier = candidates.get(owner)
122 if (candidatesByIdentifier === undefined) {
123 candidatesByIdentifier = new Map()
124 candidates.set(owner, candidatesByIdentifier)
125 }
126 let candidate = candidatesByIdentifier.get(identifier)
127 if (candidate === undefined) {
128 candidate = {
129 ast: owner,
130 identifier,
131 fallback,
132 occurrences: 0,
133 isRecursive: false,
134 reference: undefined
135 }
136 candidatesByIdentifier.set(identifier, candidate)
137 }
138 return candidate
139 }
140
141 function visit(input: SchemaAST.AST): void {
142 const candidate = getCandidate(input)
143 const ast = candidate.ast
144 candidate.occurrences++
145 if (visitingCandidates.has(candidate)) {
146 candidate.isRecursive = true
147 return
148 }
149 if (candidate.occurrences > 1) return
150 visitingCandidates.add(candidate)
151 visitChecks(ast.checks)
152 switch (ast._tag) {
153 case "Declaration":
154 case "Arrays":
155 case "Objects":
156 case "Union":
157 ast.recur((child) => {
158 visit(child)
159 return child
160 })
161 break
162 case "TemplateLiteral":
163 ast.parts.forEach(visit)
164 break
165 case "Suspend":
166 visit(ast.thunk())
167 break
168 }
169 visitingCandidates.delete(candidate)
170 }
171
172 function visitChecks(checks: SchemaAST.Checks | undefined): void {
173 checks?.forEach((check) => {
174 check.annotations?.representation?.schemas?.forEach((schema) => visit(SchemaAST.toType(schema)))
175 if (check._tag === "FilterGroup") visitChecks(check.checks)
176 })
177 }
178
179 function recur(input: SchemaAST.AST): SchemaRepresentation.Representation {
180 const candidate = getCandidate(input)
181 const ast = candidate.ast
182 const reference = candidate.reference
183 if (reference !== undefined) {
184 const annotated = candidate.identifier === undefined ? ast : annotateReference(ast, candidate, reference)
185 return makeReference(reference, annotated)
186 }
187 return on(ast)
188 }
189
190 function on(ast: SchemaAST.AST): SchemaRepresentation.Representation {
191 const checks = fromChecks(ast.checks)
192 switch (ast._tag) {
193 case "Declaration":
194 return {
195 _tag: "Declaration",
196 typeParameters: ast.typeParameters.map((ast) => recur(ast)),
197 checks,
198 ...fromDeclarationAnnotations(ast.annotations)
199 }
200 case "Null":
201 case "Undefined":
202 case "Void":
203 case "Never":
204 case "Unknown":
205 case "Any":
206 case "String":
207 case "Boolean":
208 case "Number":
209 case "BigInt":
210 case "Symbol":
211 case "ObjectKeyword":
212 return {
213 _tag: ast._tag,
214 checks,
215 ...annotationsField(ast.annotations)
216 }
217 case "Literal":
218 return {
219 _tag: "Literal",
220 literal: ast.literal,
221 checks,
222 ...annotationsField(ast.annotations)
223 }
224 case "UniqueSymbol":
225 return {
226 _tag: "UniqueSymbol",
227 symbol: ast.symbol,
228 checks,
229 ...annotationsField(ast.annotations)
230 }
231 case "Enum":
232 return {
233 _tag: "Enum",
234 enums: ast.enums,
235 checks,
236 ...annotationsField(ast.annotations)
237 }
238 case "TemplateLiteral":
239 return {
240 _tag: "TemplateLiteral",
241 parts: ast.parts.map((ast) => recur(ast)),
242 checks,
243 ...annotationsField(ast.annotations)
244 }
245 case "Arrays":
246 return {
247 _tag: "Arrays",
248 elements: ast.elements.map((element) => {
249 const projected = SchemaAST.getLastEncoding(element)
250 const annotations = projected.context?.annotations
251 return {
252 isOptional: SchemaAST.isOptional(projected),
253 type: recur(element),
254 ...annotationsField(annotations)
255 }
256 }),
257 rest: ast.rest.map((ast) => recur(ast)),
258 checks,
259 ...annotationsField(ast.annotations)
260 }
261 case "Objects":
262 return {
263 _tag: "Objects",
264 propertySignatures: ast.propertySignatures.map((property) => {
265 const projected = SchemaAST.getLastEncoding(property.type)
266 const annotations = projected.context?.annotations
267 return {
268 name: property.name,
269 type: recur(property.type),
270 isOptional: SchemaAST.isOptional(projected),
271 isMutable: SchemaAST.isMutable(projected),
272 ...annotationsField(annotations)
273 }
274 }),
275 indexSignatures: ast.indexSignatures.map((index) => ({
276 parameter: recur(index.parameter),
277 type: recur(index.type)
278 })),
279 checks,
280 ...annotationsField(ast.annotations)
281 }
282 case "Union":
283 return {
284 _tag: "Union",
285 types: ast.types.map((ast) => recur(ast)),
286 ...(ast.options === undefined ? {} : { options: ast.options }),
287 checks,
288 ...annotationsField(ast.annotations)
289 }
290 case "Suspend":
291 return {
292 _tag: "Suspend",
293 checks: [],
294 thunk: recur(ast.thunk()),
295 ...annotationsField(ast.annotations)
296 }
297 }
298 }
299
300 function fromChecks(
301 checks: readonly [SchemaAST.Check<any>, ...Array<SchemaAST.Check<any>>] | undefined
302 ): Array<SchemaRepresentation.Check> {
303 return checks?.map(fromCheck) ?? []
304 }
305
306 function fromCheck(
307 check: SchemaAST.Check<any>
308 ): SchemaRepresentation.Check {
309 switch (check._tag) {
310 case "Filter":
311 return {
312 _tag: "Filter",
313 aborted: check.aborted,
314 ...fromCheckAnnotations(check.annotations)
315 }
316 case "FilterGroup":
317 return {
318 _tag: "FilterGroup",
319 checks: Arr.map(check.checks, fromCheck),
320 ...fromCheckAnnotations(check.annotations)
321 }
322 }
323 }
324
325 function fromDeclarationAnnotations<
326 A extends Schema.Annotations.Annotations & {
327 readonly representation?: SchemaRepresentation.RepresentationAnnotation | undefined
328 }
329 >(annotations: A | undefined): {
330 readonly representation?: SchemaRepresentation.RepresentationAnnotation | undefined
331 readonly annotations?: Omit<A, "representation"> | undefined
332 } | undefined {
333 if (annotations === undefined) return undefined
334 const { representation, ...ordinary } = annotations
335 return {
336 ...(representation === undefined ? undefined : { representation }),
337 ...(Object.keys(ordinary).length === 0 ? undefined : { annotations: ordinary })
338 }
339 }
340
341 function fromCheckAnnotations<
342 A extends Schema.Annotations.Annotations & {
343 readonly representation?: SchemaRepresentation.CheckRepresentationAnnotation<SchemaAST.AST> | undefined
344 }
345 >(annotations: A | undefined): {
346 readonly representation?: CheckRepresentationAnnotation | undefined
347 readonly annotations?: Omit<A, "representation"> | undefined
348 } | undefined {
349 if (annotations === undefined) return undefined
350 const { representation, ...ordinary } = annotations
351 const projected = representation === undefined
352 ? undefined
353 : representation.schemas === undefined
354 ? representation as CheckRepresentationAnnotation
355 : { ...representation, schemas: representation.schemas.map((schema) => recur(SchemaAST.toType(schema))) }
356 return {
357 ...(projected === undefined ? undefined : { representation: projected }),
358 ...(Object.keys(ordinary).length === 0 ? undefined : { annotations: ordinary })
359 }
360 }
361}