1import type * as Cause from "../../Cause.ts"
2import * as Chunk from "../../Chunk.ts"
3import type * as Exit from "../../Exit.ts"
4import type { Formatter } from "../../Formatter.ts"
5import { format, formatPropertyKey } from "../../Formatter.ts"
6import * as HashMap from "../../HashMap.ts"
7import * as HashSet from "../../HashSet.ts"
8import * as Option from "../../Option.ts"
9import * as Result from "../../Result.ts"
10import type * as Schema from "../../Schema.ts"
11import * as SchemaAST from "../../SchemaAST.ts"
12import * as SchemaParser from "../../SchemaParser.ts"
13import * as InternalAnnotations from "./annotations.ts"
14
15function causeReasonToFormatter<E>(error: Formatter<E>, defect: Formatter<unknown>) {
16 return (reason: Cause.Reason<E>) => {
17 switch (reason._tag) {
18 case "Fail":
19 return `Fail(${error(reason.error)})`
20 case "Die":
21 return `Die(${defect(reason.defect)})`
22 case "Interrupt":
23 return "Interrupt"
24 }
25 }
26}
27
28function causeToFormatter<E>(error: Formatter<E>, defect: Formatter<unknown>) {
29 const causeReason = causeReasonToFormatter(error, defect)
30 return (cause: Cause.Cause<E>) => `Cause([${cause.reasons.map(causeReason).join(", ")}])`
31}
32
33/** @internal */
34export function toFormatter<T>(ast: SchemaAST.AST, options?: {
35 readonly onBefore?:
36 | ((ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => Formatter<any>) => Formatter<any> | undefined)
37 | undefined
38}): Formatter<T> {
39 const compiled = new Map<SchemaAST.AST, Formatter<any>>()
40 return recur(ast)
41
42 function recur(ast: SchemaAST.AST): Formatter<any> {
43 let formatter = compiled.get(ast)
44 if (formatter === undefined) {
45 formatter = compile(ast)
46 compiled.set(ast, formatter)
47 }
48 return formatter
49 }
50
51 function compile(ast: SchemaAST.AST): Formatter<any> {
52 const annotation = InternalAnnotations.resolve(ast)?.["toFormatter"]
53 if (typeof annotation === "function") {
54 return annotation(SchemaAST.isDeclaration(ast) ? ast.typeParameters.map(recur) : [])
55 }
56 if (options?.onBefore) {
57 const onBefore = options.onBefore(ast, recur)
58 if (onBefore !== undefined) {
59 return onBefore
60 }
61 }
62 return on(ast)
63 }
64
65 function on(ast: SchemaAST.AST): Formatter<any> {
66 switch (ast._tag) {
67 default:
68 return format
69 case "Never":
70 return () => "never"
71 case "Void":
72 return () => "void"
73 case "Declaration": {
74 const representation = (ast.annotations as Schema.Annotations.Declaration<any> | undefined)?.representation
75 if (representation === undefined) return format
76 const typeParameters = ast.typeParameters.map(recur)
77 switch (representation.id) {
78 case "effect/schema/Option": {
79 const [value] = typeParameters
80 return Option.match({
81 onNone: () => "none()",
82 onSome: (value_) => `some(${value(value_)})`
83 })
84 }
85 case "effect/schema/Result": {
86 const [success, failure] = typeParameters
87 return Result.match({
88 onSuccess: (value) => `success(${success(value)})`,
89 onFailure: (error) => `failure(${failure(error)})`
90 })
91 }
92 case "effect/schema/CauseReason":
93 return causeReasonToFormatter(typeParameters[0], typeParameters[1])
94 case "effect/schema/Cause":
95 return causeToFormatter(typeParameters[0], typeParameters[1])
96 case "effect/schema/Exit": {
97 const [value, error, defect] = typeParameters
98 const cause = causeToFormatter(error, defect)
99 return (exit: Exit.Exit<unknown, unknown>) => {
100 switch (exit._tag) {
101 case "Success":
102 return `Exit.Success(${value(exit.value)})`
103 case "Failure":
104 return `Exit.Failure(${cause(exit.cause)})`
105 }
106 }
107 }
108 case "effect/schema/ReadonlyMap": {
109 const [key, value] = typeParameters
110 return (map: globalThis.ReadonlyMap<unknown, unknown>) => {
111 const size = map.size
112 if (size === 0) return "ReadonlyMap(0) {}"
113 const entries = globalThis.Array.from(map.entries()).sort().map(([key_, value_]) =>
114 `${key(key_)} => ${value(value_)}`
115 )
116 return `ReadonlyMap(${size}) { ${entries.join(", ")} }`
117 }
118 }
119 case "effect/schema/HashMap": {
120 const [key, value] = typeParameters
121 return (map: HashMap.HashMap<unknown, unknown>) => {
122 const size = HashMap.size(map)
123 if (size === 0) return "HashMap(0) {}"
124 const entries = HashMap.toEntries(map).sort().map(([key_, value_]) => `${key(key_)} => ${value(value_)}`)
125 return `HashMap(${size}) { ${entries.join(", ")} }`
126 }
127 }
128 case "effect/schema/ReadonlySet": {
129 const [value] = typeParameters
130 return (set: globalThis.ReadonlySet<unknown>) => {
131 const size = set.size
132 if (size === 0) return "ReadonlySet(0) {}"
133 const values = globalThis.Array.from(set.values()).sort().map((item) => `${value(item)}`)
134 return `ReadonlySet(${size}) { ${values.join(", ")} }`
135 }
136 }
137 case "effect/schema/HashSet": {
138 const [value] = typeParameters
139 return (set: HashSet.HashSet<unknown>) => {
140 const size = HashSet.size(set)
141 if (size === 0) return "HashSet(0) {}"
142 const values = globalThis.Array.from(set).sort().map((item) => `${value(item)}`)
143 return `HashSet(${size}) { ${values.join(", ")} }`
144 }
145 }
146 case "effect/schema/Chunk": {
147 const [value] = typeParameters
148 return (chunk: Chunk.Chunk<unknown>) => {
149 const size = Chunk.size(chunk)
150 if (size === 0) return "Chunk(0) {}"
151 const values = globalThis.Array.from(chunk).sort().map((item) => `${value(item)}`)
152 return `Chunk(${size}) { ${values.join(", ")} }`
153 }
154 }
155 default:
156 return format
157 }
158 }
159 case "Arrays": {
160 const elements = ast.elements.map((element) => recur(element))
161 const rest = ast.rest.map(recur)
162 const [head, ...tail] = rest
163 const tailLength = tail.length
164 return (value) => {
165 const out: Array<string> = []
166 let i = 0
167 for (; i < elements.length; i++) {
168 if (value.length < i + 1) {
169 if (SchemaAST.isOptional(ast.elements[i])) {
170 continue
171 }
172 } else {
173 out.push(elements[i](value[i]))
174 }
175 }
176 if (rest.length > 0) {
177 for (; i < value.length - tailLength; i++) {
178 out.push(head(value[i]))
179 }
180 for (let j = 0; j < tailLength; j++) {
181 out.push(tail[j](value[i + j]))
182 }
183 }
184 return "[" + out.join(", ") + "]"
185 }
186 }
187 case "Objects": {
188 const propertySignatures = ast.propertySignatures.map((signature) => recur(signature.type))
189 const indexSignatures = ast.indexSignatures.map((signature) => recur(signature.type))
190 if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
191 return format
192 }
193 return (value) => {
194 const out: Array<string> = []
195 const visited = new Set<PropertyKey>()
196 for (let i = 0; i < propertySignatures.length; i++) {
197 const signature = ast.propertySignatures[i]
198 const name = signature.name
199 visited.add(name)
200 if (SchemaAST.isOptional(signature.type) && !Object.hasOwn(value, name)) {
201 continue
202 }
203 out.push(`${formatPropertyKey(name)}: ${propertySignatures[i](value[name])}`)
204 }
205 for (let i = 0; i < indexSignatures.length; i++) {
206 const keys = SchemaAST.getIndexSignatureKeys(value, ast.indexSignatures[i].parameter)
207 for (const key of keys) {
208 if (visited.has(key)) {
209 continue
210 }
211 visited.add(key)
212 out.push(`${formatPropertyKey(key)}: ${indexSignatures[i](value[key])}`)
213 }
214 }
215 return out.length > 0 ? "{ " + out.join(", ") + " }" : "{}"
216 }
217 }
218 case "Union": {
219 const types = SchemaAST.toType(ast).types
220 const index = SchemaAST.getCandidateIndex(types)
221 const compiled = types.map((candidate, i) => [SchemaParser._is(candidate), recur(ast.types[i])] as const)
222 return (value) => {
223 const candidates = index(value, false)
224 for (let i = 0; i < candidates.length; i++) {
225 const [is, formatter] = compiled[candidates[i]]
226 if (is(value)) {
227 return formatter(value)
228 }
229 }
230 return format(value)
231 }
232 }
233 case "Suspend": {
234 let formatter: Formatter<any>
235 return (value) => (formatter ??= recur(ast.thunk()))(value)
236 }
237 }
238 }
239}