1import { Equivalence, Number, pipe, Schema, String, String as Str, Struct, UndefinedOr } from "effect"
2import { TestSchema } from "effect/testing"
3import { describe, it } from "vitest"
4import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "./utils/assert.ts"
5
6const equals = TestSchema.Asserts.ast.fields.equals
7
8describe("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})