@nunofyobiz/effect-extras 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +38 -3
  2. package/dist/ArrayX.d.ts +381 -0
  3. package/dist/ArrayX.d.ts.map +1 -0
  4. package/dist/ArrayX.js +493 -0
  5. package/dist/ArrayX.js.map +1 -0
  6. package/dist/BigIntX.d.ts +24 -0
  7. package/dist/BigIntX.d.ts.map +1 -0
  8. package/dist/BigIntX.js +30 -0
  9. package/dist/BigIntX.js.map +1 -0
  10. package/dist/BooleanX.d.ts +25 -0
  11. package/dist/BooleanX.d.ts.map +1 -0
  12. package/dist/BooleanX.js +25 -0
  13. package/dist/BooleanX.js.map +1 -0
  14. package/dist/DurationX.d.ts +73 -0
  15. package/dist/DurationX.d.ts.map +1 -0
  16. package/dist/DurationX.js +91 -0
  17. package/dist/DurationX.js.map +1 -0
  18. package/dist/EffectX.d.ts +120 -0
  19. package/dist/EffectX.d.ts.map +1 -0
  20. package/dist/EffectX.js +140 -0
  21. package/dist/EffectX.js.map +1 -0
  22. package/dist/FormDataX.d.ts +49 -0
  23. package/dist/FormDataX.d.ts.map +1 -0
  24. package/dist/FormDataX.js +42 -0
  25. package/dist/FormDataX.js.map +1 -0
  26. package/dist/InclusiveOr.d.ts +1123 -0
  27. package/dist/InclusiveOr.d.ts.map +1 -0
  28. package/dist/InclusiveOr.js +1074 -0
  29. package/dist/InclusiveOr.js.map +1 -0
  30. package/dist/MapX.d.ts +32 -0
  31. package/dist/MapX.d.ts.map +1 -0
  32. package/dist/MapX.js +49 -0
  33. package/dist/MapX.js.map +1 -0
  34. package/dist/NonNullableX.d.ts +174 -0
  35. package/dist/NonNullableX.d.ts.map +1 -0
  36. package/dist/NonNullableX.js +217 -0
  37. package/dist/NonNullableX.js.map +1 -0
  38. package/dist/NumberX.d.ts +178 -0
  39. package/dist/NumberX.d.ts.map +1 -0
  40. package/dist/NumberX.js +214 -0
  41. package/dist/NumberX.js.map +1 -0
  42. package/dist/OptionX.d.ts +187 -0
  43. package/dist/OptionX.d.ts.map +1 -0
  44. package/dist/OptionX.js +201 -0
  45. package/dist/OptionX.js.map +1 -0
  46. package/dist/OrderX.d.ts +32 -0
  47. package/dist/OrderX.d.ts.map +1 -0
  48. package/dist/OrderX.js +32 -0
  49. package/dist/OrderX.js.map +1 -0
  50. package/dist/PredicateX.d.ts +108 -0
  51. package/dist/PredicateX.d.ts.map +1 -0
  52. package/dist/PredicateX.js +111 -0
  53. package/dist/PredicateX.js.map +1 -0
  54. package/dist/PromiseX.d.ts +32 -0
  55. package/dist/PromiseX.d.ts.map +1 -0
  56. package/dist/PromiseX.js +32 -0
  57. package/dist/PromiseX.js.map +1 -0
  58. package/dist/RecordX.d.ts +450 -0
  59. package/dist/RecordX.d.ts.map +1 -0
  60. package/dist/RecordX.js +487 -0
  61. package/dist/RecordX.js.map +1 -0
  62. package/dist/ResultX.d.ts +50 -0
  63. package/dist/ResultX.d.ts.map +1 -0
  64. package/dist/ResultX.js +50 -0
  65. package/dist/ResultX.js.map +1 -0
  66. package/dist/SchemaX.d.ts +249 -0
  67. package/dist/SchemaX.d.ts.map +1 -0
  68. package/dist/SchemaX.js +243 -0
  69. package/dist/SchemaX.js.map +1 -0
  70. package/dist/SetX.d.ts +121 -0
  71. package/dist/SetX.d.ts.map +1 -0
  72. package/dist/SetX.js +137 -0
  73. package/dist/SetX.js.map +1 -0
  74. package/dist/StringX.d.ts +131 -0
  75. package/dist/StringX.d.ts.map +1 -0
  76. package/dist/StringX.js +149 -0
  77. package/dist/StringX.js.map +1 -0
  78. package/dist/StructX.d.ts +219 -0
  79. package/dist/StructX.d.ts.map +1 -0
  80. package/dist/StructX.js +173 -0
  81. package/dist/StructX.js.map +1 -0
  82. package/dist/WarnResult.d.ts +1191 -0
  83. package/dist/WarnResult.d.ts.map +1 -0
  84. package/dist/WarnResult.js +991 -0
  85. package/dist/WarnResult.js.map +1 -0
  86. package/dist/index.d.ts +23 -3772
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +22 -1011
  89. package/dist/index.js.map +1 -1
  90. package/package.json +18 -5
  91. package/src/{ArrayX/ArrayX.ts → ArrayX.ts} +6 -88
  92. package/src/{DurationX/DurationX.ts → DurationX.ts} +1 -1
  93. package/src/InclusiveOr.ts +1255 -0
  94. package/src/{NonNullableX/NonNullableX.ts → NonNullableX.ts} +5 -0
  95. package/src/{OptionX/OptionX.ts → OptionX.ts} +8 -2
  96. package/src/{PredicateX/PredicateX.ts → PredicateX.ts} +41 -0
  97. package/src/{RecordX/RecordX.ts → RecordX.ts} +184 -2
  98. package/src/StringX.ts +210 -0
  99. package/src/{WarnResult/WarnResult.ts → WarnResult.ts} +297 -227
  100. package/src/index.ts +22 -20
  101. package/src/ArrayX/index.ts +0 -1
  102. package/src/BigIntX/index.ts +0 -1
  103. package/src/BooleanX/index.ts +0 -1
  104. package/src/DurationX/index.ts +0 -1
  105. package/src/EffectX/index.ts +0 -1
  106. package/src/FormDataX/index.ts +0 -1
  107. package/src/MapX/index.ts +0 -1
  108. package/src/NonNullableX/index.ts +0 -2
  109. package/src/NumberX/index.ts +0 -1
  110. package/src/OptionX/index.ts +0 -1
  111. package/src/OrderX/index.ts +0 -1
  112. package/src/PredicateX/index.ts +0 -1
  113. package/src/PromiseX/index.ts +0 -1
  114. package/src/RecordX/index.ts +0 -1
  115. package/src/ResultX/index.ts +0 -1
  116. package/src/SchemaX/index.ts +0 -1
  117. package/src/SetX/index.ts +0 -1
  118. package/src/StringX/StringX.ts +0 -97
  119. package/src/StringX/index.ts +0 -1
  120. package/src/StructX/index.ts +0 -1
  121. package/src/WarnResult/index.ts +0 -1
  122. /package/src/{BigIntX/BigIntX.ts → BigIntX.ts} +0 -0
  123. /package/src/{BooleanX/BooleanX.ts → BooleanX.ts} +0 -0
  124. /package/src/{EffectX/EffectX.ts → EffectX.ts} +0 -0
  125. /package/src/{FormDataX/FormDataX.ts → FormDataX.ts} +0 -0
  126. /package/src/{MapX/MapX.ts → MapX.ts} +0 -0
  127. /package/src/{NumberX/NumberX.ts → NumberX.ts} +0 -0
  128. /package/src/{OrderX/OrderX.ts → OrderX.ts} +0 -0
  129. /package/src/{PromiseX/PromiseX.ts → PromiseX.ts} +0 -0
  130. /package/src/{ResultX/ResultX.ts → ResultX.ts} +0 -0
  131. /package/src/{SchemaX/SchemaX.ts → SchemaX.ts} +0 -0
  132. /package/src/{SetX/SetX.ts → SetX.ts} +0 -0
  133. /package/src/{StructX/StructX.ts → StructX.ts} +0 -0
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Record` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Array, Option, Order, Predicate, Record, Reducer, pipe } from "effect";
7
+ import { dual } from "effect/Function";
8
+ import * as ArrayX from "./ArrayX.js";
9
+ import * as PredicateX from "./PredicateX.js";
10
+ /**
11
+ * Returns `true` when `record` has at least one entry, narrowing it to a
12
+ * known-non-empty `Record`.
13
+ *
14
+ * The negation of `Record.isEmptyRecord`, packaged as a type guard so it reads
15
+ * naturally at call sites that want to branch on "this record has something in
16
+ * it" without a manual `!Record.isEmptyRecord(...)`.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { RecordX } from "@nunofyobiz/effect-extras"
21
+ *
22
+ * assert.deepStrictEqual(RecordX.isNonEmptyRecord({ a: 1 }), true)
23
+ * assert.deepStrictEqual(RecordX.isNonEmptyRecord({}), false)
24
+ * ```
25
+ *
26
+ * @category guards
27
+ * @since 0.0.0
28
+ */
29
+ export const isNonEmptyRecord = record => pipe(record, Predicate.not(Record.isEmptyRecord));
30
+ /**
31
+ * Modifies the value at `key` in `self` with `f`, leaving the record unchanged
32
+ * if the key doesn't exist.
33
+ *
34
+ * v4's `Record.modify` returns `Option<Record>` — `None` when the key is
35
+ * absent. This helper picks the "do nothing if absent" semantics that v3's
36
+ * `Record.modify` had implicitly, and that most call sites assume. The modifier
37
+ * is never invoked when the key is missing.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { pipe } from "effect"
42
+ * import { RecordX } from "@nunofyobiz/effect-extras"
43
+ *
44
+ * // data-first
45
+ * assert.deepStrictEqual(
46
+ * RecordX.modifyIfExists({ a: 1, b: 2 }, "b", (n) => n * 10),
47
+ * { a: 1, b: 20 },
48
+ * )
49
+ *
50
+ * // a missing key leaves the record untouched
51
+ * const counts: Record<string, number> = { a: 1, b: 2 }
52
+ * assert.deepStrictEqual(
53
+ * RecordX.modifyIfExists(counts, "missing", (n) => n + 1),
54
+ * { a: 1, b: 2 },
55
+ * )
56
+ *
57
+ * // data-last (pipeable)
58
+ * assert.deepStrictEqual(
59
+ * pipe({ a: 1, b: 2 }, RecordX.modifyIfExists("a", (n) => n * 10)),
60
+ * { a: 10, b: 2 },
61
+ * )
62
+ * ```
63
+ *
64
+ * @category mapping
65
+ * @since 0.0.0
66
+ */
67
+ export const modifyIfExists = /*#__PURE__*/dual(3, (self, key, f) => pipe(Record.modify(self, key, f), Option.getOrElse(() => self)));
68
+ /**
69
+ * Returns the smallest value of `record` (per `order`) that matches
70
+ * `predicate`, narrowed to the refined type `B`, or `Option.none()` if none
71
+ * match.
72
+ *
73
+ * The `Record` counterpart of `ArrayX.takeFirstWhere`: it considers only the
74
+ * record's values, keeps those satisfying `predicate`, and returns the minimum
75
+ * of them by `order`. Keys are ignored entirely.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { Option, Order, pipe } from "effect"
80
+ * import { RecordX } from "@nunofyobiz/effect-extras"
81
+ *
82
+ * const isEven = (n: number): n is number => n % 2 === 0
83
+ *
84
+ * // data-first
85
+ * assert.deepStrictEqual(
86
+ * RecordX.takeFirstWhere({ a: 3, b: 4, c: 2 }, isEven, Order.Number),
87
+ * Option.some(2),
88
+ * )
89
+ *
90
+ * // data-last (pipeable); no even values yields None
91
+ * assert.deepStrictEqual(
92
+ * pipe({ a: 1, b: 3 }, RecordX.takeFirstWhere(isEven, Order.Number)),
93
+ * Option.none(),
94
+ * )
95
+ * ```
96
+ *
97
+ * @category getters
98
+ * @since 0.0.0
99
+ */
100
+ export const takeFirstWhere = /*#__PURE__*/dual(3, (record, predicate, order) => ArrayX.takeFirstWhere(Record.values(record), predicate, order));
101
+ /**
102
+ * Returns the largest value of `record` (per `order`) that matches `predicate`,
103
+ * narrowed to the refined type `B`, or `Option.none()` if none match.
104
+ *
105
+ * The mirror of {@link takeFirstWhere}: it considers only the record's values,
106
+ * keeps those satisfying `predicate`, and returns the maximum of them by
107
+ * `order`. Keys are ignored entirely.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { Option, Order, pipe } from "effect"
112
+ * import { RecordX } from "@nunofyobiz/effect-extras"
113
+ *
114
+ * const isEven = (n: number): n is number => n % 2 === 0
115
+ *
116
+ * // data-first
117
+ * assert.deepStrictEqual(
118
+ * RecordX.takeLastWhere({ a: 3, b: 4, c: 2 }, isEven, Order.Number),
119
+ * Option.some(4),
120
+ * )
121
+ *
122
+ * // data-last (pipeable); no even values yields None
123
+ * assert.deepStrictEqual(
124
+ * pipe({ a: 1, b: 3 }, RecordX.takeLastWhere(isEven, Order.Number)),
125
+ * Option.none(),
126
+ * )
127
+ * ```
128
+ *
129
+ * @category getters
130
+ * @since 0.0.0
131
+ */
132
+ export const takeLastWhere = /*#__PURE__*/dual(3, (record, predicate, order) => ArrayX.takeLastWhere(Record.values(record), predicate, order));
133
+ /**
134
+ * Returns the largest value of `record` according to `order`, wrapped in an
135
+ * `Option` so empty records are handled safely.
136
+ *
137
+ * Sorts the record's values by `order` and takes the last one, yielding
138
+ * `Option.none()` when the record is empty and `Option.some(max)` otherwise.
139
+ * Keys are ignored — only values participate in the ordering.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * import { Option, Order, pipe } from "effect"
144
+ * import { RecordX } from "@nunofyobiz/effect-extras"
145
+ *
146
+ * // data-first
147
+ * assert.deepStrictEqual(
148
+ * RecordX.takeLast({ a: 3, b: 5, c: 1 }, Order.Number),
149
+ * Option.some(5),
150
+ * )
151
+ *
152
+ * // data-last (pipeable); empty record yields None
153
+ * assert.deepStrictEqual(
154
+ * pipe({}, RecordX.takeLast(Order.Number)),
155
+ * Option.none(),
156
+ * )
157
+ * ```
158
+ *
159
+ * @category getters
160
+ * @since 0.0.0
161
+ */
162
+ export const takeLast = /*#__PURE__*/dual(2, (record, order) => pipe(Record.values(record), Array.sort(order), Array.last));
163
+ /**
164
+ * Re-types the keys of a `Record` to a different key type `K2` without touching
165
+ * the runtime value.
166
+ *
167
+ * A purely type-level reinterpretation: the record is returned as-is, but the
168
+ * compiler now treats its keys as `K2` instead of the inferred `K1`. Use it at a
169
+ * boundary where you know the keys conform to a narrower branded or literal key
170
+ * type that TypeScript can't infer from the value alone. It performs no
171
+ * validation — the caller is responsible for the keys actually matching `K2`.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * import { RecordX } from "@nunofyobiz/effect-extras"
176
+ *
177
+ * type UserId = string & { readonly _brand: "UserId" }
178
+ *
179
+ * const byId: Record<string, number> = { u1: 10, u2: 20 }
180
+ * const branded = RecordX.keysAs<UserId>()(byId)
181
+ *
182
+ * // Same runtime value, keys now seen as `UserId`
183
+ * assert.deepStrictEqual(branded, { u1: 10, u2: 20 })
184
+ * ```
185
+ *
186
+ * @category conversions
187
+ * @since 0.0.0
188
+ */
189
+ export const keysAs = () => record => record;
190
+ /**
191
+ * Returns the value at `key` in `record`, throwing an `Error` if the key is
192
+ * absent.
193
+ *
194
+ * The unsafe, get-or-explode counterpart to `Record.get` (which returns an
195
+ * `Option`). The thrown error names the missing key and lists the record's
196
+ * existing keys to aid debugging. Reach for {@link getOrThrowWith} when you need
197
+ * a custom error; prefer `Record.get` whenever absence is a real possibility.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * import { pipe } from "effect"
202
+ * import { RecordX } from "@nunofyobiz/effect-extras"
203
+ *
204
+ * const record: Record<string, number> = { a: 1, b: 2 }
205
+ *
206
+ * // data-first
207
+ * assert.deepStrictEqual(RecordX.getOrThrow(record, "a"), 1)
208
+ *
209
+ * // data-last (pipeable)
210
+ * assert.deepStrictEqual(pipe(record, RecordX.getOrThrow("b")), 2)
211
+ *
212
+ * // missing key throws
213
+ * assert.throws(() => RecordX.getOrThrow(record, "missing"))
214
+ * ```
215
+ *
216
+ * @category unsafe
217
+ * @since 0.0.0
218
+ */
219
+ export const getOrThrow = /*#__PURE__*/dual(2, (record, key) => getOrThrowWith(record, key, key => new Error(`Key ${String(key)} not found in record\nExisting keys=${String(Record.keys(record))}`)));
220
+ /**
221
+ * Returns the value at `key` in `record`, throwing the result of `onNone(key)`
222
+ * if the key is absent.
223
+ *
224
+ * Like {@link getOrThrow}, but lets the caller supply the thrown value (an
225
+ * `Error`, a string, or any custom failure) computed from the missing key. Use
226
+ * it when the default "key not found" message isn't descriptive enough for the
227
+ * call site.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * import { pipe } from "effect"
232
+ * import { RecordX } from "@nunofyobiz/effect-extras"
233
+ *
234
+ * const record: Record<string, number> = { a: 1 }
235
+ * const onNone = (key: string) => new Error(`no ${key}`)
236
+ *
237
+ * // data-first
238
+ * assert.deepStrictEqual(RecordX.getOrThrowWith(record, "a", onNone), 1)
239
+ *
240
+ * // data-last (pipeable)
241
+ * assert.deepStrictEqual(pipe(record, RecordX.getOrThrowWith("a", onNone)), 1)
242
+ *
243
+ * // missing key throws the custom error
244
+ * assert.throws(() => RecordX.getOrThrowWith(record, "missing", onNone))
245
+ * ```
246
+ *
247
+ * @category unsafe
248
+ * @since 0.0.0
249
+ */
250
+ export const getOrThrowWith = /*#__PURE__*/dual(3, (record, key, onNone) => Record.get(record, key).pipe(Option.getOrThrowWith(() => onNone(key))));
251
+ /**
252
+ * Inserts or updates the value at `key`, deriving the new value from the current
253
+ * one via `upsert`.
254
+ *
255
+ * The `upsert` function receives an `Option` of the existing value —
256
+ * `Option.some(value)` when the key is present, `Option.none()` when it's
257
+ * absent — and returns the value to store. This unifies "insert if missing" and
258
+ * "update if present" into a single pass, with `Option.match` as the natural way
259
+ * to handle both cases.
260
+ *
261
+ * @example
262
+ * ```ts
263
+ * import { Option, pipe } from "effect"
264
+ * import { RecordX } from "@nunofyobiz/effect-extras"
265
+ *
266
+ * const bump = Option.match({
267
+ * onNone: () => 1,
268
+ * onSome: (n: number) => n + 1,
269
+ * })
270
+ *
271
+ * // data-first: updates an existing entry
272
+ * assert.deepStrictEqual(RecordX.upsert({ a: 1 }, "a", bump), { a: 2 })
273
+ *
274
+ * // data-last (pipeable): inserts a missing entry
275
+ * const counts: Record<string, number> = { a: 1 }
276
+ * assert.deepStrictEqual(pipe(counts, RecordX.upsert("b", bump)), {
277
+ * a: 1,
278
+ * b: 1,
279
+ * })
280
+ * ```
281
+ *
282
+ * @category mapping
283
+ * @since 0.0.0
284
+ */
285
+ export const upsert = /*#__PURE__*/dual(3, (record, key, upsert) => {
286
+ const existingValue = Record.get(record, key);
287
+ const updatedValue = upsert(existingValue);
288
+ return Record.set(record, key, updatedValue);
289
+ });
290
+ /**
291
+ * Indexes an iterable of values into a `Record`, keying each value by the result
292
+ * of `identify`.
293
+ *
294
+ * Builds a lookup table from a collection: every value is stored under the key
295
+ * `identify(value)`. When two values produce the same key the later one wins, so
296
+ * the result holds the last value seen per key. Useful for turning a list of
297
+ * records into a by-id map.
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * import { pipe } from "effect"
302
+ * import { RecordX } from "@nunofyobiz/effect-extras"
303
+ *
304
+ * const items = [
305
+ * { id: "a", n: 1 },
306
+ * { id: "b", n: 2 },
307
+ * { id: "a", n: 3 }, // wins over the earlier "a"
308
+ * ]
309
+ *
310
+ * // data-first
311
+ * assert.deepStrictEqual(
312
+ * RecordX.collectBy(items, (item) => item.id),
313
+ * { a: { id: "a", n: 3 }, b: { id: "b", n: 2 } },
314
+ * )
315
+ *
316
+ * // data-last (pipeable)
317
+ * assert.deepStrictEqual(
318
+ * pipe(items, RecordX.collectBy((item) => item.id)),
319
+ * { a: { id: "a", n: 3 }, b: { id: "b", n: 2 } },
320
+ * )
321
+ * ```
322
+ *
323
+ * @category constructors
324
+ * @since 0.0.0
325
+ */
326
+ export const collectBy = /*#__PURE__*/dual(2, (values, identify) => Array.reduce(values, {}, (accumulator, value) => Record.set(accumulator, identify(value), value)));
327
+ /**
328
+ * Deep-merges two JSON values, with `b` winning on conflicts.
329
+ *
330
+ * Plain objects (per `PredicateX.unsafeIsRecord`) are merged recursively,
331
+ * key-by-key; every other shape — primitives, arrays, and impostors like
332
+ * `Map`/`Date`/class instances — is replaced wholesale by `b`. If either side
333
+ * isn't a plain object, `b` is returned as-is. Neither input is mutated. Reach
334
+ * for it to layer partial JSON overrides on top of a base.
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * import { RecordX } from "@nunofyobiz/effect-extras"
339
+ * import { pipe } from "effect"
340
+ *
341
+ * // data-first — nested objects merge, b wins on leaf conflicts
342
+ * assert.deepStrictEqual(
343
+ * RecordX.deepMerge({ a: { x: 1 }, b: 2 }, { a: { y: 3 }, c: 4 }),
344
+ * { a: { x: 1, y: 3 }, b: 2, c: 4 },
345
+ * )
346
+ *
347
+ * // arrays are replaced wholesale, not concatenated
348
+ * assert.deepStrictEqual(RecordX.deepMerge({ a: [1, 2] }, { a: [3] }), { a: [3] })
349
+ *
350
+ * // data-last (pipeable) — merges the override onto the piped base
351
+ * assert.deepStrictEqual(pipe({ a: 1 }, RecordX.deepMerge({ b: 2 })), {
352
+ * a: 1,
353
+ * b: 2,
354
+ * })
355
+ * ```
356
+ *
357
+ * @category combining
358
+ * @since 0.0.0
359
+ */
360
+ export const deepMerge = /*#__PURE__*/dual(2, (a, b) => {
361
+ if (!PredicateX.unsafeIsRecord(a) || !PredicateX.unsafeIsRecord(b)) {
362
+ return b;
363
+ }
364
+ return Record.reduce(b, {
365
+ ...a
366
+ }, (accumulator, value, key) => ({
367
+ ...accumulator,
368
+ [key]: key in a ? deepMerge(a[key], value) : value
369
+ }));
370
+ });
371
+ /**
372
+ * {@link deepMerge} as a `Reducer` (monoid) with identity `{}`.
373
+ *
374
+ * `deepMergeReducer.combineAll(layers)` folds an iterable of object layers
375
+ * left-to-right via {@link deepMerge} — the universal "merge N JSON objects into
376
+ * one" fold, replacing a hand-rolled `Array.reduce(layers, {}, deepMerge)`. The
377
+ * `{}` identity is exact for the object-valued layers these folds carry: an empty
378
+ * list yields `{}`, and a single layer is returned unchanged.
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * import { RecordX } from "@nunofyobiz/effect-extras"
383
+ *
384
+ * assert.deepStrictEqual(
385
+ * RecordX.deepMergeReducer.combineAll([
386
+ * { a: { x: 1 } },
387
+ * { a: { y: 2 }, b: 3 },
388
+ * { b: 4 },
389
+ * ]),
390
+ * { a: { x: 1, y: 2 }, b: 4 },
391
+ * )
392
+ *
393
+ * assert.deepStrictEqual(RecordX.deepMergeReducer.combineAll([]), {})
394
+ * ```
395
+ *
396
+ * @category instances
397
+ * @since 0.0.0
398
+ */
399
+ export const deepMergeReducer = /*#__PURE__*/Reducer.make(deepMerge, {});
400
+ /**
401
+ * Canonicalizes a JSON value by recursively sorting object keys; arrays keep
402
+ * their order.
403
+ *
404
+ * Two structurally-equal values that differ only in key order canonicalize to
405
+ * the same shape, so `JSON.stringify(canonicalize(x))` is a stable structural
406
+ * key for comparison, deduping, or hashing. Plain objects (per
407
+ * `PredicateX.unsafeIsRecord`) have their keys sorted ascending and their values
408
+ * canonicalized recursively; arrays are canonicalized element-wise in place;
409
+ * everything else passes through unchanged.
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * import { RecordX } from "@nunofyobiz/effect-extras"
414
+ *
415
+ * assert.deepStrictEqual(
416
+ * JSON.stringify(RecordX.canonicalize({ b: 1, a: { d: 1, c: 2 } })),
417
+ * JSON.stringify({ a: { c: 2, d: 1 }, b: 1 }),
418
+ * )
419
+ *
420
+ * // arrays keep their order; primitives pass through
421
+ * assert.deepStrictEqual(RecordX.canonicalize([3, 1, 2]), [3, 1, 2])
422
+ * assert.deepStrictEqual(RecordX.canonicalize("x"), "x")
423
+ * ```
424
+ *
425
+ * @category transformations
426
+ * @since 0.0.0
427
+ */
428
+ export const canonicalize = value => {
429
+ if (Array.isArray(value)) {
430
+ return Array.map(value, canonicalize);
431
+ }
432
+ if (!PredicateX.unsafeIsRecord(value)) {
433
+ return value;
434
+ }
435
+ return pipe(Record.toEntries(value), Array.map(([key, entry]) => [key, canonicalize(entry)]), Array.sort(Order.mapInput(Order.String, ([key]) => key)), Record.fromEntries);
436
+ };
437
+ /**
438
+ * Immutably deletes the value at a `path` from a JSON object, pruning any parent
439
+ * objects left empty by the deletion.
440
+ *
441
+ * Walks `path` from the root; when it resolves to an existing key, that key is
442
+ * removed and every ancestor that becomes empty as a result is removed too.
443
+ * Returns `Some(newObject)` when the path existed (so callers can tell something
444
+ * changed), or `None` when it was absent or `object` isn't a plain object. The
445
+ * input is never mutated.
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * import { RecordX } from "@nunofyobiz/effect-extras"
450
+ * import { Option, pipe } from "effect"
451
+ *
452
+ * // deletes the leaf and prunes the now-empty parent
453
+ * assert.deepStrictEqual(
454
+ * RecordX.deleteByPath({ a: { b: 1 } }, ["a", "b"]),
455
+ * Option.some({}),
456
+ * )
457
+ *
458
+ * // a sibling key keeps the parent alive
459
+ * assert.deepStrictEqual(
460
+ * RecordX.deleteByPath({ a: { b: 1, c: 2 } }, ["a", "b"]),
461
+ * Option.some({ a: { c: 2 } }),
462
+ * )
463
+ *
464
+ * // absent path → None; data-last (pipeable) form
465
+ * assert.deepStrictEqual(pipe({ a: 1 }, RecordX.deleteByPath(["b"])), Option.none())
466
+ * ```
467
+ *
468
+ * @category combining
469
+ * @since 0.0.0
470
+ */
471
+ export const deleteByPath = /*#__PURE__*/dual(2, (object, path) => {
472
+ if (!PredicateX.unsafeIsRecord(object)) {
473
+ return Option.none();
474
+ }
475
+ const [head, ...rest] = path;
476
+ if (head === undefined) {
477
+ return Option.none();
478
+ }
479
+ if (rest.length === 0) {
480
+ return head in object ? Option.some(Record.remove(object, head)) : Option.none();
481
+ }
482
+ return Option.map(deleteByPath(object[head], rest), newChild => PredicateX.unsafeIsRecord(newChild) && Record.isEmptyRecord(newChild) ? Record.remove(object, head) : {
483
+ ...object,
484
+ [head]: newChild
485
+ });
486
+ });
487
+ //# sourceMappingURL=RecordX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordX.js","names":["Array","Option","Order","Predicate","Record","Reducer","pipe","dual","ArrayX","PredicateX","isNonEmptyRecord","record","not","isEmptyRecord","modifyIfExists","self","key","f","modify","getOrElse","takeFirstWhere","predicate","order","values","takeLastWhere","takeLast","sort","last","keysAs","getOrThrow","getOrThrowWith","Error","String","keys","onNone","get","upsert","existingValue","updatedValue","set","collectBy","identify","reduce","accumulator","value","deepMerge","a","b","unsafeIsRecord","deepMergeReducer","make","canonicalize","isArray","map","toEntries","entry","mapInput","fromEntries","deleteByPath","object","path","none","head","rest","undefined","length","some","remove","newChild"],"sources":["../src/RecordX.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Result` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Option, Result } from "effect";
7
+ /**
8
+ * Lifts an `Option` into a `Result` with a `void` failure: `Some(value)` becomes
9
+ * `Result.succeed(value)` and `None` becomes `Result.failVoid`.
10
+ *
11
+ * Useful in v4 where `Array.filterMap` and `Record.filterMap` expect
12
+ * `Result`-returning predicates (a `Success` keeps the value, a `Failure` drops
13
+ * it); in v3 those APIs accepted `Option`-returning predicates directly:
14
+ *
15
+ * ```ts
16
+ * import { Array } from "effect"
17
+ * import { ResultX } from "@nunofyobiz/effect-extras"
18
+ *
19
+ * declare const items: ReadonlyArray<number>
20
+ * declare const maybeTransform: (item: number) => import("effect").Option.Option<string>
21
+ *
22
+ * // v3: Array.filterMap(items, (item) => maybeTransform(item))
23
+ * // v4:
24
+ * Array.filterMap(items, (item) => ResultX.fromOption(maybeTransform(item)))
25
+ * ```
26
+ *
27
+ * Effect ships `Result.fromOption(option, onNone)` which requires a non-`void`
28
+ * failure value; this helper specializes to the common "drop the item, no error
29
+ * needed" case used by `filterMap`.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { Option, Result } from "effect"
34
+ * import { ResultX } from "@nunofyobiz/effect-extras"
35
+ *
36
+ * assert.deepStrictEqual(
37
+ * ResultX.fromOption(Option.some(1)),
38
+ * Result.succeed(1),
39
+ * )
40
+ * assert.deepStrictEqual(
41
+ * ResultX.fromOption(Option.none<number>()),
42
+ * Result.failVoid,
43
+ * )
44
+ * ```
45
+ *
46
+ * @category conversions
47
+ * @since 0.0.0
48
+ */
49
+ export declare const fromOption: <A>(option: Option.Option<A>) => Result.Result<A, void>;
50
+ //# sourceMappingURL=ResultX.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResultX.d.ts","sourceRoot":"","sources":["../src/ResultX.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAC1B,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KACvB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CACgD,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Result` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Option, Result } from "effect";
7
+ /**
8
+ * Lifts an `Option` into a `Result` with a `void` failure: `Some(value)` becomes
9
+ * `Result.succeed(value)` and `None` becomes `Result.failVoid`.
10
+ *
11
+ * Useful in v4 where `Array.filterMap` and `Record.filterMap` expect
12
+ * `Result`-returning predicates (a `Success` keeps the value, a `Failure` drops
13
+ * it); in v3 those APIs accepted `Option`-returning predicates directly:
14
+ *
15
+ * ```ts
16
+ * import { Array } from "effect"
17
+ * import { ResultX } from "@nunofyobiz/effect-extras"
18
+ *
19
+ * declare const items: ReadonlyArray<number>
20
+ * declare const maybeTransform: (item: number) => import("effect").Option.Option<string>
21
+ *
22
+ * // v3: Array.filterMap(items, (item) => maybeTransform(item))
23
+ * // v4:
24
+ * Array.filterMap(items, (item) => ResultX.fromOption(maybeTransform(item)))
25
+ * ```
26
+ *
27
+ * Effect ships `Result.fromOption(option, onNone)` which requires a non-`void`
28
+ * failure value; this helper specializes to the common "drop the item, no error
29
+ * needed" case used by `filterMap`.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { Option, Result } from "effect"
34
+ * import { ResultX } from "@nunofyobiz/effect-extras"
35
+ *
36
+ * assert.deepStrictEqual(
37
+ * ResultX.fromOption(Option.some(1)),
38
+ * Result.succeed(1),
39
+ * )
40
+ * assert.deepStrictEqual(
41
+ * ResultX.fromOption(Option.none<number>()),
42
+ * Result.failVoid,
43
+ * )
44
+ * ```
45
+ *
46
+ * @category conversions
47
+ * @since 0.0.0
48
+ */
49
+ export const fromOption = option => Option.isSome(option) ? Result.succeed(option.value) : Result.failVoid;
50
+ //# sourceMappingURL=ResultX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResultX.js","names":["Option","Result","fromOption","option","isSome","succeed","value","failVoid"],"sources":["../src/ResultX.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;AAKA,SAASA,MAAM,EAAEC,MAAM,QAAQ,QAAQ;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,OAAO,MAAMC,UAAU,GACrBC,MAAwB,IAExBH,MAAM,CAACI,MAAM,CAACD,MAAM,CAAC,GAAGF,MAAM,CAACI,OAAO,CAACF,MAAM,CAACG,KAAK,CAAC,GAAGL,MAAM,CAACM,QAAQ","ignoreList":[]}