| 1 | import type * as Cause from "../../Cause.ts" |
| 2 | import * as Equal from "../../Equal.ts" |
| 3 | import * as Equivalence from "../../Equivalence.ts" |
| 4 | import { memoize } from "../../Function.ts" |
| 5 | import * as Predicate from "../../Predicate.ts" |
| 6 | import type * as Schema from "../../Schema.ts" |
| 7 | import * as SchemaAST from "../../SchemaAST.ts" |
| 8 | import * as SchemaParser from "../../SchemaParser.ts" |
| 9 | import * as InternalAnnotations from "./annotations.ts" |
| 10 | |
| 11 | /** @internal */ |
| 12 | export const toEquivalence = memoize((ast: SchemaAST.AST): Equivalence.Equivalence<any> => { |
| 13 | return recur(ast) |
| 14 | }) |
| 15 | |
| 16 | function recur(ast: SchemaAST.AST): Equivalence.Equivalence<any> { |
| 17 | // --------------------------------------------- |
| 18 | // handle annotations |
| 19 | // --------------------------------------------- |
| 20 | const annotation = InternalAnnotations.resolve(ast)?.["toEquivalence"] as |
| 21 | | Schema.Annotations.ToEquivalence.Declaration<any, ReadonlyArray<any>> |
| 22 | | undefined |
| 23 | if (annotation) { |
| 24 | return annotation(SchemaAST.isDeclaration(ast) ? ast.typeParameters.map(recur) : []) |
| 25 | } |
| 26 | switch (ast._tag) { |
| 27 | case "Never": |
| 28 | return Equivalence.strictEqual() |
| 29 | case "Declaration": |
| 30 | return declarationEquivalence(ast) |
| 31 | case "Null": |
| 32 | case "Undefined": |
| 33 | case "Void": |
| 34 | case "Unknown": |
| 35 | case "Any": |
| 36 | case "String": |
| 37 | case "Number": |
| 38 | case "Boolean": |
| 39 | case "BigInt": |
| 40 | case "Symbol": |
| 41 | case "Literal": |
| 42 | case "UniqueSymbol": |
| 43 | case "ObjectKeyword": |
| 44 | case "Enum": |
| 45 | case "TemplateLiteral": |
| 46 | return Equal.equals |
| 47 | case "Arrays": { |
| 48 | const elements = ast.elements.map(recur) |
| 49 | const rest = ast.rest.map(recur) |
| 50 | const [head, ...tail] = rest |
| 51 | const tailLength = tail.length |
| 52 | return Equivalence.make((a, b) => { |
| 53 | if (!Array.isArray(a) || !Array.isArray(b)) { |
| 54 | return false |
| 55 | } |
| 56 | const len = a.length |
| 57 | if (len !== b.length) { |
| 58 | return false |
| 59 | } |
| 60 | // --------------------------------------------- |
| 61 | // handle elements |
| 62 | // --------------------------------------------- |
| 63 | let i = 0 |
| 64 | for (; i < Math.min(len, ast.elements.length); i++) { |
| 65 | if (!elements[i](a[i], b[i])) { |
| 66 | return false |
| 67 | } |
| 68 | } |
| 69 | // --------------------------------------------- |
| 70 | // handle rest element |
| 71 | // --------------------------------------------- |
| 72 | if (rest.length > 0) { |
| 73 | for (; i < len - tailLength; i++) { |
| 74 | if (!head(a[i], b[i])) { |
| 75 | return false |
| 76 | } |
| 77 | } |
| 78 | // --------------------------------------------- |
| 79 | // handle post rest elements |
| 80 | // --------------------------------------------- |
| 81 | for (let j = 0; j < tailLength; j++) { |
| 82 | if (!tail[j](a[i + j], b[i + j])) { |
| 83 | return false |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | return true |
| 88 | }) |
| 89 | } |
| 90 | case "Objects": { |
| 91 | if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { |
| 92 | return Equal.equals |
| 93 | } |
| 94 | const propertySignatures = ast.propertySignatures.map((ps) => recur(ps.type)) |
| 95 | const indexSignatures = ast.indexSignatures.map((is) => recur(is.type)) |
| 96 | return Equivalence.make((a, b) => { |
| 97 | if (!Predicate.isObject(a) || !Predicate.isObject(b)) { |
| 98 | return false |
| 99 | } |
| 100 | // --------------------------------------------- |
| 101 | // handle property signatures |
| 102 | // --------------------------------------------- |
| 103 | for (let i = 0; i < propertySignatures.length; i++) { |
| 104 | const ps = ast.propertySignatures[i] |
| 105 | const name = ps.name |
| 106 | const aHas = Object.hasOwn(a, name) |
| 107 | const bHas = Object.hasOwn(b, name) |
| 108 | if (SchemaAST.isOptional(ps.type)) { |
| 109 | if (aHas !== bHas) { |
| 110 | return false |
| 111 | } |
| 112 | } |
| 113 | if (aHas && bHas && !propertySignatures[i](a[name], b[name])) { |
| 114 | return false |
| 115 | } |
| 116 | } |
| 117 | // --------------------------------------------- |
| 118 | // handle index signatures |
| 119 | // --------------------------------------------- |
| 120 | for (let i = 0; i < indexSignatures.length; i++) { |
| 121 | const is = ast.indexSignatures[i] |
| 122 | const aKeys = SchemaAST.getIndexSignatureKeys(a, is.parameter) |
| 123 | const bKeys = SchemaAST.getIndexSignatureKeys(b, is.parameter) |
| 124 | |
| 125 | if (aKeys.length !== bKeys.length) return false |
| 126 | |
| 127 | for (let j = 0; j < aKeys.length; j++) { |
| 128 | const key = aKeys[j] |
| 129 | if (!Object.hasOwn(b, key) || !indexSignatures[i](a[key], b[key])) { |
| 130 | return false |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | return true |
| 135 | }) |
| 136 | } |
| 137 | case "Union": { |
| 138 | const types = SchemaAST.toType(ast).types |
| 139 | const index = SchemaAST.getCandidateIndex(types) |
| 140 | const compiled = types.map((candidate, i) => [SchemaParser._is(candidate), recur(ast.types[i])] as const) |
| 141 | return Equivalence.make((a, b) => { |
| 142 | const candidates = index(a, false) |
| 143 | for (let i = 0; i < candidates.length; i++) { |
| 144 | const [is, equivalence] = compiled[candidates[i]] |
| 145 | if (is(a) && is(b)) { |
| 146 | return equivalence(a, b) |
| 147 | } |
| 148 | } |
| 149 | return false |
| 150 | }) |
| 151 | } |
| 152 | case "Suspend": { |
| 153 | let equivalence: Equivalence.Equivalence<any> |
| 154 | return Equivalence.make((a, b) => (equivalence ??= toEquivalence(ast.thunk()))(a, b)) |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | function declarationEquivalence(ast: SchemaAST.Declaration): Equivalence.Equivalence<any> { |
| 160 | const representation = (ast.annotations as Schema.Annotations.Declaration<any> | undefined)?.representation |
| 161 | if (representation === undefined) return Equal.equals |
| 162 | switch (representation.id) { |
| 163 | case "effect/schema/Option": { |
| 164 | const [value] = declarationTypeParameters(ast) |
| 165 | return (a, b) => a._tag === b._tag && (a._tag === "None" || value(a.value, b.value)) |
| 166 | } |
| 167 | case "effect/schema/Result": { |
| 168 | const [success, failure] = declarationTypeParameters(ast) |
| 169 | return (a, b) => |
| 170 | a._tag === b._tag && |
| 171 | (a._tag === "Success" ? success(a.success, b.success) : failure(a.failure, b.failure)) |
| 172 | } |
| 173 | case "effect/schema/CauseReason": { |
| 174 | const [error, defect] = declarationTypeParameters(ast) |
| 175 | return causeReasonEquivalence(error, defect) |
| 176 | } |
| 177 | case "effect/schema/Cause": { |
| 178 | const [error, defect] = declarationTypeParameters(ast) |
| 179 | return causeEquivalence(error, defect) |
| 180 | } |
| 181 | case "effect/schema/Exit": { |
| 182 | const [value, error, defect] = declarationTypeParameters(ast) |
| 183 | const cause = causeEquivalence(error, defect) |
| 184 | return (a, b) => a._tag === b._tag && (a._tag === "Success" ? value(a.value, b.value) : cause(a.cause, b.cause)) |
| 185 | } |
| 186 | case "effect/schema/ReadonlyMap": { |
| 187 | const [key, value] = declarationTypeParameters(ast) |
| 188 | return Equal.makeCompareMap(key, value) |
| 189 | } |
| 190 | case "effect/schema/ReadonlySet": |
| 191 | return Equal.makeCompareSet(declarationTypeParameters(ast)[0]) |
| 192 | case "effect/schema/RegExp": |
| 193 | return (a: globalThis.RegExp, b: globalThis.RegExp) => a.source === b.source && a.flags === b.flags |
| 194 | case "effect/schema/URL": |
| 195 | return (a: globalThis.URL, b: globalThis.URL) => a.toString() === b.toString() |
| 196 | default: |
| 197 | return Equal.equals |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | function declarationTypeParameters(ast: SchemaAST.Declaration) { |
| 202 | return ast.typeParameters.map(recur) |
| 203 | } |
| 204 | |
| 205 | function causeReasonEquivalence<E>( |
| 206 | error: Equivalence.Equivalence<E>, |
| 207 | defect: Equivalence.Equivalence<unknown> |
| 208 | ): Equivalence.Equivalence<Cause.Reason<E>> { |
| 209 | return (a, b) => { |
| 210 | if (a._tag !== b._tag) return false |
| 211 | switch (a._tag) { |
| 212 | case "Fail": |
| 213 | return error(a.error, (b as Cause.Fail<E>).error) |
| 214 | case "Die": |
| 215 | return defect(a.defect, (b as Cause.Die).defect) |
| 216 | case "Interrupt": |
| 217 | return a.fiberId === (b as Cause.Interrupt).fiberId |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | function causeEquivalence<E>( |
| 223 | error: Equivalence.Equivalence<E>, |
| 224 | defect: Equivalence.Equivalence<unknown> |
| 225 | ): Equivalence.Equivalence<Cause.Cause<E>> { |
| 226 | const reasons = Equivalence.Array(causeReasonEquivalence(error, defect)) |
| 227 | return (a, b) => reasons(a.reasons, b.reasons) |
| 228 | } |