| 1 | import type * as Cause from "../../Cause.ts" |
| 2 | import * as Chunk from "../../Chunk.ts" |
| 3 | import type * as Exit from "../../Exit.ts" |
| 4 | import type { Formatter } from "../../Formatter.ts" |
| 5 | import { format, formatPropertyKey } from "../../Formatter.ts" |
| 6 | import * as HashMap from "../../HashMap.ts" |
| 7 | import * as HashSet from "../../HashSet.ts" |
| 8 | import * as Option from "../../Option.ts" |
| 9 | import * as Result from "../../Result.ts" |
| 10 | import type * as Schema from "../../Schema.ts" |
| 11 | import * as SchemaAST from "../../SchemaAST.ts" |
| 12 | import * as SchemaParser from "../../SchemaParser.ts" |
| 13 | import * as InternalAnnotations from "./annotations.ts" |
| 14 | |
| 15 | function 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 | |
| 28 | function 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 */ |
| 34 | export 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 | } |