| 1 | import { Equivalence, Number, pipe, Schema, String, String as Str, Struct, UndefinedOr } from "effect" |
| 2 | import { TestSchema } from "effect/testing" |
| 3 | import { describe, it } from "vitest" |
| 4 | import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "./utils/assert.ts" |
| 5 | |
| 6 | const equals = TestSchema.Asserts.ast.fields.equals |
| 7 | |
| 8 | describe("Struct", () => { |
| 9 | it("get", () => { |
| 10 | strictEqual(pipe({ a: "a", b: 1 }, Struct.get("a")), "a") |
| 11 | strictEqual(pipe({ a: "a", b: 1 }, Struct.get("b")), 1) |
| 12 | |
| 13 | strictEqual(Struct.get({ a: "a", b: 1 }, "a"), "a") |
| 14 | strictEqual(Struct.get({ a: "a", b: 1 }, "b"), 1) |
| 15 | }) |
| 16 | |
| 17 | it("keys", () => { |
| 18 | const aSym = Symbol.for("a") |
| 19 | deepStrictEqual(pipe({ a: 1, b: 2, [aSym]: 3 }, Struct.keys), ["a", "b"]) |
| 20 | deepStrictEqual(Struct.keys({ a: 1, b: 2, [aSym]: 3 }), ["a", "b"]) |
| 21 | }) |
| 22 | |
| 23 | describe("pick", () => { |
| 24 | it("defined properties", () => { |
| 25 | const s: { a: string; b: number; c: boolean } = { a: "a", b: 1, c: true } |
| 26 | deepStrictEqual(pipe(s, Struct.pick(["a", "b"])), { a: "a", b: 1 }) |
| 27 | deepStrictEqual(Struct.pick(s, ["a", "b"]), { a: "a", b: 1 }) |
| 28 | }) |
| 29 | |
| 30 | it("omitted properties", () => { |
| 31 | const s: { a?: string; b?: number; c?: boolean } = { b: 1, c: true } |
| 32 | deepStrictEqual(pipe(s, Struct.pick(["a", "b"])), { b: 1 }) |
| 33 | deepStrictEqual(Struct.pick(s, ["a", "b"]), { b: 1 }) |
| 34 | }) |
| 35 | |
| 36 | it("numeric properties", () => { |
| 37 | const s: { 1: string; 2: number } = { 1: "a", 2: 1 } |
| 38 | deepStrictEqual(pipe(s, Struct.pick([1])), { 1: "a" }) |
| 39 | deepStrictEqual(Struct.pick(s, [1]), { 1: "a" }) |
| 40 | }) |
| 41 | }) |
| 42 | |
| 43 | describe("omit", () => { |
| 44 | it("defined properties", () => { |
| 45 | const s: { a: string; b: number; c: boolean } = { a: "a", b: 1, c: true } |
| 46 | deepStrictEqual(pipe(s, Struct.omit(["c"])), { a: "a", b: 1 }) |
| 47 | deepStrictEqual(Struct.omit(s, ["c"]), { a: "a", b: 1 }) |
| 48 | }) |
| 49 | |
| 50 | it("omitted properties", () => { |
| 51 | const s: { a?: string; b?: number; c?: boolean } = { b: 1, c: true } |
| 52 | deepStrictEqual(pipe(s, Struct.omit(["c"])), { b: 1 }) |
| 53 | deepStrictEqual(Struct.omit(s, ["c"]), { b: 1 }) |
| 54 | }) |
| 55 | |
| 56 | it("numeric properties", () => { |
| 57 | const s: { 1: string; 2: number } = { 1: "a", 2: 1 } |
| 58 | deepStrictEqual(pipe(s, Struct.omit([2])), { 1: "a" }) |
| 59 | deepStrictEqual(Struct.omit(s, [2]), { 1: "a" }) |
| 60 | }) |
| 61 | |
| 62 | it("ignores non-enumerable properties", () => { |
| 63 | const hidden = Symbol.for("hidden") |
| 64 | const s = { a: "a" } |
| 65 | Object.defineProperty(s, "b", { value: 1, enumerable: false }) |
| 66 | Object.defineProperty(s, hidden, { value: true, enumerable: false }) |
| 67 | |
| 68 | deepStrictEqual(Struct.omit(s, []), { a: "a" }) |
| 69 | }) |
| 70 | }) |
| 71 | |
| 72 | describe("evolve", () => { |
| 73 | it("partial required fields", () => { |
| 74 | const s = { a: "a", b: 1 } |
| 75 | deepStrictEqual(pipe(s, Struct.evolve({ a: (s) => s.length })), { a: 1, b: 1 }) |
| 76 | deepStrictEqual(Struct.evolve(s, { a: (s) => s.length }), { a: 1, b: 1 }) |
| 77 | }) |
| 78 | |
| 79 | it("all required fields", () => { |
| 80 | const s = { a: "a", b: 1 } |
| 81 | deepStrictEqual(pipe(s, Struct.evolve({ a: (s) => s.length, b: (b) => b > 0 })), { a: 1, b: true }) |
| 82 | deepStrictEqual(Struct.evolve(s, { a: (s) => s.length, b: (b) => b > 0 }), { a: 1, b: true }) |
| 83 | }) |
| 84 | }) |
| 85 | |
| 86 | it("evolveKeys", () => { |
| 87 | deepStrictEqual(pipe({ a: "a", b: 2 }, Struct.evolveKeys({ a: (k) => Str.toUpperCase(k) })), { A: "a", b: 2 }) |
| 88 | deepStrictEqual(Struct.evolveKeys({ a: "a", b: 2 }, { a: (k) => Str.toUpperCase(k) }), { A: "a", b: 2 }) |
| 89 | }) |
| 90 | |
| 91 | it("renameKeys", () => { |
| 92 | deepStrictEqual(pipe({ a: "a", b: 1, c: true }, Struct.renameKeys({ a: "A", b: "B" })), { A: "a", B: 1, c: true }) |
| 93 | deepStrictEqual(Struct.renameKeys({ a: "a", b: 1, c: true }, { a: "A", b: "B" }), { A: "a", B: 1, c: true }) |
| 94 | |
| 95 | const from = Symbol.for("from") |
| 96 | const to = Symbol.for("to") |
| 97 | deepStrictEqual(pipe({ [from]: "a", b: 1 }, Struct.renameKeys({ [from]: to })), { [to]: "a", b: 1 }) |
| 98 | deepStrictEqual(Struct.renameKeys({ a: "a", b: 1 }, { a: to }), { [to]: "a", b: 1 }) |
| 99 | }) |
| 100 | |
| 101 | it("evolveEntries transforms a selected key and value together", () => { |
| 102 | deepStrictEqual( |
| 103 | pipe({ a: "a", b: 2 }, Struct.evolveEntries({ a: (k, v) => [Str.toUpperCase(k), v.length] })), |
| 104 | { A: 1, b: 2 } |
| 105 | ) |
| 106 | deepStrictEqual(Struct.evolveEntries({ a: "a", b: 2 }, { a: (k, v) => [Str.toUpperCase(k), v.length] }), { |
| 107 | A: 1, |
| 108 | b: 2 |
| 109 | }) |
| 110 | }) |
| 111 | |
| 112 | it("map", () => { |
| 113 | equals(pipe({ a: Schema.String, b: Schema.Number }, Struct.map(Schema.NullOr)), { |
| 114 | a: Schema.NullOr(Schema.String), |
| 115 | b: Schema.NullOr(Schema.Number) |
| 116 | }) |
| 117 | equals(Struct.map({ a: Schema.String, b: Schema.Number }, Schema.NullOr), { |
| 118 | a: Schema.NullOr(Schema.String), |
| 119 | b: Schema.NullOr(Schema.Number) |
| 120 | }) |
| 121 | }) |
| 122 | |
| 123 | it("mapPick", () => { |
| 124 | equals( |
| 125 | pipe({ a: Schema.String, b: Schema.Number }, Struct.mapPick(["a"], Schema.NullOr)), |
| 126 | { |
| 127 | a: Schema.NullOr(Schema.String), |
| 128 | b: Schema.Number |
| 129 | } |
| 130 | ) |
| 131 | equals( |
| 132 | Struct.mapPick({ a: Schema.String, b: Schema.Number }, ["a"], Schema.NullOr), |
| 133 | { |
| 134 | a: Schema.NullOr(Schema.String), |
| 135 | b: Schema.Number |
| 136 | } |
| 137 | ) |
| 138 | }) |
| 139 | |
| 140 | it("mapPick numeric properties", () => { |
| 141 | equals( |
| 142 | Struct.mapPick({ 1: Schema.String, 2: Schema.Number }, [1], Schema.NullOr), |
| 143 | { |
| 144 | 1: Schema.NullOr(Schema.String), |
| 145 | 2: Schema.Number |
| 146 | } |
| 147 | ) |
| 148 | }) |
| 149 | |
| 150 | it("mapOmit", () => { |
| 151 | equals( |
| 152 | pipe({ a: Schema.String, b: Schema.Number }, Struct.mapOmit(["b"], Schema.NullOr)), |
| 153 | { |
| 154 | a: Schema.NullOr(Schema.String), |
| 155 | b: Schema.Number |
| 156 | } |
| 157 | ) |
| 158 | equals( |
| 159 | Struct.mapOmit({ a: Schema.String, b: Schema.Number }, ["b"], Schema.NullOr), |
| 160 | { |
| 161 | a: Schema.NullOr(Schema.String), |
| 162 | b: Schema.Number |
| 163 | } |
| 164 | ) |
| 165 | }) |
| 166 | |
| 167 | it("mapOmit numeric properties", () => { |
| 168 | equals( |
| 169 | Struct.mapOmit({ 1: Schema.String, 2: Schema.Number }, [2], Schema.NullOr), |
| 170 | { |
| 171 | 1: Schema.NullOr(Schema.String), |
| 172 | 2: Schema.Number |
| 173 | } |
| 174 | ) |
| 175 | }) |
| 176 | |
| 177 | it("makeEquivalence compares structs by configured fields", () => { |
| 178 | const PersonEquivalence = Struct.makeEquivalence({ |
| 179 | a: Equivalence.strictEqual<string>(), |
| 180 | b: Equivalence.strictEqual<number>() |
| 181 | }) |
| 182 | |
| 183 | assertTrue(PersonEquivalence({ a: "a", b: 1 }, { a: "a", b: 1 })) |
| 184 | assertFalse(PersonEquivalence({ a: "a", b: 1 }, { a: "a", b: 2 })) |
| 185 | }) |
| 186 | |
| 187 | describe("makeCombiner", () => { |
| 188 | it("default omitKeyWhen (never omit)", () => { |
| 189 | const C = Struct.makeCombiner({ |
| 190 | n: Number.ReducerSum, |
| 191 | s: String.ReducerConcat |
| 192 | }) |
| 193 | |
| 194 | deepStrictEqual(C.combine({ n: 1, s: "a" }, { n: 2, s: "b" }), { n: 3, s: "ab" }) |
| 195 | }) |
| 196 | |
| 197 | it("custom omitKeyWhen", () => { |
| 198 | const C = Struct.makeCombiner<{ n?: number | undefined; s?: string | undefined }>( |
| 199 | { |
| 200 | n: UndefinedOr.makeReducer(Number.ReducerSum), |
| 201 | s: UndefinedOr.makeReducer(String.ReducerConcat) |
| 202 | }, |
| 203 | { omitKeyWhen: (v) => v === undefined } |
| 204 | ) |
| 205 | |
| 206 | // merged values equal to undefined should be omitted |
| 207 | deepStrictEqual(C.combine({ n: undefined, s: "a" }, { n: undefined, s: "b" }), { s: "ab" }) |
| 208 | deepStrictEqual(C.combine({ s: undefined }, { s: "b" }), { s: "b" }) |
| 209 | deepStrictEqual(C.combine({ s: "a" }, { s: undefined }), { s: "a" }) |
| 210 | deepStrictEqual(C.combine({ s: "a" }, { s: "b" }), { s: "ab" }) |
| 211 | }) |
| 212 | }) |
| 213 | |
| 214 | describe("Record", () => { |
| 215 | it("string keys", () => { |
| 216 | deepStrictEqual(Struct.Record(["a", "b"], "value"), { a: "value", b: "value" }) |
| 217 | }) |
| 218 | |
| 219 | it("symbol keys", () => { |
| 220 | const s1 = Symbol.for("s1") |
| 221 | const s2 = Symbol.for("s2") |
| 222 | deepStrictEqual(Struct.Record([s1, s2], 42), { [s1]: 42, [s2]: 42 }) |
| 223 | }) |
| 224 | |
| 225 | it("mixed keys", () => { |
| 226 | const sym = Symbol.for("sym") |
| 227 | deepStrictEqual(Struct.Record(["a", sym], true), { a: true, [sym]: true }) |
| 228 | }) |
| 229 | |
| 230 | it("empty keys", () => { |
| 231 | deepStrictEqual(Struct.Record([], "value"), {}) |
| 232 | }) |
| 233 | }) |
| 234 | |
| 235 | describe("makeReducer", () => { |
| 236 | it("custom omitKeyWhen", () => { |
| 237 | const R = Struct.makeReducer<{ n: number; s?: string | undefined }>( |
| 238 | { |
| 239 | n: Number.ReducerSum, |
| 240 | s: UndefinedOr.makeReducer(String.ReducerConcat) |
| 241 | }, |
| 242 | { omitKeyWhen: (v) => v === undefined } |
| 243 | ) |
| 244 | |
| 245 | deepStrictEqual(R.initialValue, { n: 0 }) |
| 246 | }) |
| 247 | }) |
| 248 | }) |