@nunofyobiz/effect-extras 0.0.1

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/index.d.ts +3703 -0
  4. package/dist/index.js +1006 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +103 -0
  7. package/src/ArrayX/ArrayX.ts +818 -0
  8. package/src/ArrayX/index.ts +1 -0
  9. package/src/BigIntX/BigIntX.ts +35 -0
  10. package/src/BigIntX/index.ts +1 -0
  11. package/src/BooleanX/BooleanX.ts +24 -0
  12. package/src/BooleanX/index.ts +1 -0
  13. package/src/DurationX/DurationX.ts +178 -0
  14. package/src/DurationX/index.ts +1 -0
  15. package/src/EffectX/EffectX.ts +183 -0
  16. package/src/EffectX/index.ts +1 -0
  17. package/src/FormDataX/FormDataX.ts +57 -0
  18. package/src/FormDataX/index.ts +1 -0
  19. package/src/MapX/MapX.ts +54 -0
  20. package/src/MapX/index.ts +1 -0
  21. package/src/NonNullableX/NonNullableX.ts +290 -0
  22. package/src/NonNullableX/index.ts +2 -0
  23. package/src/NumberX/NumberX.ts +282 -0
  24. package/src/NumberX/index.ts +1 -0
  25. package/src/OptionX/OptionX.ts +234 -0
  26. package/src/OptionX/index.ts +1 -0
  27. package/src/OrderX/OrderX.ts +35 -0
  28. package/src/OrderX/index.ts +1 -0
  29. package/src/PredicateX/PredicateX.ts +98 -0
  30. package/src/PredicateX/index.ts +1 -0
  31. package/src/PromiseX/PromiseX.ts +32 -0
  32. package/src/PromiseX/index.ts +1 -0
  33. package/src/RecordX/RecordX.ts +478 -0
  34. package/src/RecordX/index.ts +1 -0
  35. package/src/ResultX/ResultX.ts +53 -0
  36. package/src/ResultX/index.ts +1 -0
  37. package/src/SchemaX/SchemaX.ts +324 -0
  38. package/src/SchemaX/index.ts +1 -0
  39. package/src/SetX/SetX.ts +160 -0
  40. package/src/SetX/index.ts +1 -0
  41. package/src/StringX/StringX.ts +97 -0
  42. package/src/StringX/index.ts +1 -0
  43. package/src/StructX/StructX.ts +310 -0
  44. package/src/StructX/index.ts +1 -0
  45. package/src/These/These.ts +1173 -0
  46. package/src/These/index.ts +1 -0
  47. package/src/index.ts +20 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Schema` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { BigInt, Schema, SchemaGetter, Struct } from "effect";
7
+
8
+ /**
9
+ * A `Schema` for a non-empty string that is trimmed on both decode and encode.
10
+ *
11
+ * Leading and trailing whitespace is stripped, and the resulting value must be
12
+ * non-empty — so a whitespace-only input (e.g. `" "`) fails the
13
+ * `NonEmptyString` refinement after trimming. Use it wherever user-supplied text
14
+ * should be normalized and guaranteed to carry content.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { Effect, Schema } from "effect"
19
+ * import { SchemaX } from "@nunofyobiz/effect-extras"
20
+ *
21
+ * const decoded = Effect.runSync(
22
+ * Schema.decodeEffect(SchemaX.TrimmedNonEmptyString)(" hello "),
23
+ * )
24
+ * assert.deepStrictEqual(decoded, "hello")
25
+ * ```
26
+ *
27
+ * @category constructors
28
+ * @since 0.0.0
29
+ */
30
+ export const TrimmedNonEmptyString = Schema.NonEmptyString.pipe(
31
+ Schema.decode({
32
+ decode: SchemaGetter.transform((s) => s.trim()),
33
+ encode: SchemaGetter.transform((s) => s.trim()),
34
+ }),
35
+ );
36
+
37
+ /**
38
+ * A `Schema` for a URL-safe file path built on top of {@link
39
+ * TrimmedNonEmptyString}.
40
+ *
41
+ * On decode the path is `decodeURIComponent`-ed (percent-escapes are expanded);
42
+ * on encode it is `encodeURIComponent`-ed back into its URL-safe form. The
43
+ * underlying value is also trimmed and required to be non-empty. Use it at the
44
+ * boundary between stored/transmitted encoded paths and the decoded paths your
45
+ * code works with.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { Effect, Schema } from "effect"
50
+ * import { SchemaX } from "@nunofyobiz/effect-extras"
51
+ *
52
+ * // Decoding expands percent-escapes
53
+ * const decoded = Effect.runSync(
54
+ * Schema.decodeEffect(SchemaX.URLSafeFilePath)("a%2Fb.txt"),
55
+ * )
56
+ * assert.deepStrictEqual(decoded, "a/b.txt")
57
+ *
58
+ * // Encoding produces the URL-safe form
59
+ * const encoded = Effect.runSync(
60
+ * Schema.encodeEffect(SchemaX.URLSafeFilePath)("a/b.txt"),
61
+ * )
62
+ * assert.deepStrictEqual(encoded, "a%2Fb.txt")
63
+ * ```
64
+ *
65
+ * @category constructors
66
+ * @since 0.0.0
67
+ */
68
+ export const URLSafeFilePath = TrimmedNonEmptyString.pipe(
69
+ Schema.decode({
70
+ decode: SchemaGetter.transform((path) => decodeURIComponent(path)),
71
+ encode: SchemaGetter.transform((path) => encodeURIComponent(path)),
72
+ }),
73
+ );
74
+
75
+ // Internal — only used to construct nonNegativeBigInt below.
76
+ const clampMinBigInt =
77
+ (min: bigint) =>
78
+ <S extends Schema.Schema<bigint>>(schema: S) =>
79
+ schema.pipe(
80
+ Schema.decode({
81
+ decode: SchemaGetter.transform((value) => BigInt.max(value, min)),
82
+ encode: SchemaGetter.transform((value) => BigInt.max(value, min)),
83
+ }),
84
+ );
85
+
86
+ /**
87
+ * Transforms a `bigint` `Schema` so its value is clamped to be non-negative,
88
+ * mapping any value below `0n` up to `0n` on both decode and encode.
89
+ *
90
+ * Unlike a refinement that *rejects* negative input, this *coerces* it: a
91
+ * negative `bigint` decodes to `0n` rather than failing. Apply it to a `bigint`
92
+ * schema whenever negative magnitudes are meaningless and should be floored at
93
+ * zero rather than treated as errors.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * import { Effect, Schema } from "effect"
98
+ * import { SchemaX } from "@nunofyobiz/effect-extras"
99
+ *
100
+ * const NonNegative = SchemaX.nonNegativeBigInt(Schema.BigInt)
101
+ *
102
+ * // Negative values are clamped up to 0n
103
+ * assert.deepStrictEqual(
104
+ * Effect.runSync(Schema.decodeEffect(NonNegative)(-5n)),
105
+ * 0n,
106
+ * )
107
+ *
108
+ * // Non-negative values pass through unchanged
109
+ * assert.deepStrictEqual(
110
+ * Effect.runSync(Schema.decodeEffect(NonNegative)(7n)),
111
+ * 7n,
112
+ * )
113
+ * ```
114
+ *
115
+ * @category combinators
116
+ * @since 0.0.0
117
+ */
118
+ export const nonNegativeBigInt = clampMinBigInt(0n);
119
+
120
+ /**
121
+ * Extracts the "constructor input" type of a `Schema` — what `.make({...})`
122
+ * accepts.
123
+ *
124
+ * Differs from `S["Type"]` (the decoded type) in that fields carrying
125
+ * constructor defaults (e.g. an `Overrideable` timestamp) are *optional* in
126
+ * `MakeIn` but *required* in `Type`. Use it in signatures that accept a value to
127
+ * construct, so callers can pass a plain object literal without supplying the
128
+ * defaulted, override-branded fields.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * import { Schema } from "effect"
133
+ * import { SchemaX } from "@nunofyobiz/effect-extras"
134
+ *
135
+ * const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
136
+ *
137
+ * const input: SchemaX.MakeIn<typeof Person> = { name: "Ada", age: 36 }
138
+ *
139
+ * assert.deepStrictEqual(input, { name: "Ada", age: 36 })
140
+ * ```
141
+ *
142
+ * @category models
143
+ * @since 0.0.0
144
+ */
145
+ export type MakeIn<S extends { readonly "~type.make.in": unknown }> =
146
+ S["~type.make.in"];
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Schema.Struct utilities — v4 removed the `.pick(...)`, `.omit(...)`, and
150
+ // `Schema.partial(...)` methods that v3 had on `Schema.Struct` instances. These
151
+ // helpers restore the same ergonomics on top of v4's `mapFields` primitive.
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * Returns a new `Schema.Struct` containing only the named `keys` of `schema`.
156
+ *
157
+ * Restores the v3 ergonomics of `mySchema.pick("a", "b")` — v4 removed the
158
+ * `.pick(...)` method from `Schema.Struct` instances, so this rebuilds it on top
159
+ * of v4's `mapFields` primitive. Each picked field keeps its original schema,
160
+ * including any refinements.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { Effect, Schema } from "effect"
165
+ * import { SchemaX } from "@nunofyobiz/effect-extras"
166
+ *
167
+ * const Source = Schema.Struct({
168
+ * a: Schema.Number,
169
+ * b: Schema.String,
170
+ * c: Schema.Boolean,
171
+ * })
172
+ *
173
+ * const Picked = SchemaX.pick(Source, "a", "b")
174
+ *
175
+ * const decoded = Effect.runSync(
176
+ * Schema.decodeEffect(Picked)({ a: 1, b: "hi" }),
177
+ * )
178
+ * assert.deepStrictEqual(decoded, { a: 1, b: "hi" })
179
+ * ```
180
+ *
181
+ * @category combinators
182
+ * @since 0.0.0
183
+ */
184
+ export const pick = <
185
+ const Fields extends Schema.Struct.Fields,
186
+ const Keys extends readonly (keyof Fields & string)[],
187
+ >(
188
+ schema: Schema.Struct<Fields>,
189
+ ...keys: Keys
190
+ ): Schema.Struct<Pick<Fields, Keys[number]>> =>
191
+ schema.mapFields(
192
+ (fields) =>
193
+ Struct.pick(fields, keys as readonly (keyof Fields)[]) as Pick<
194
+ Fields,
195
+ Keys[number]
196
+ >,
197
+ );
198
+
199
+ /**
200
+ * Returns a new `Schema.Struct` with the named `keys` of `schema` removed.
201
+ *
202
+ * The complement of {@link pick}, restoring the v3 ergonomics of
203
+ * `mySchema.omit("a")` that v4 dropped from `Schema.Struct` instances. Every
204
+ * surviving field keeps its original schema, including any refinements.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * import { Effect, Schema } from "effect"
209
+ * import { SchemaX } from "@nunofyobiz/effect-extras"
210
+ *
211
+ * const Source = Schema.Struct({
212
+ * a: Schema.Number,
213
+ * b: Schema.String,
214
+ * c: Schema.Boolean,
215
+ * })
216
+ *
217
+ * const Omitted = SchemaX.omit(Source, "c")
218
+ *
219
+ * const decoded = Effect.runSync(
220
+ * Schema.decodeEffect(Omitted)({ a: 1, b: "hi" }),
221
+ * )
222
+ * assert.deepStrictEqual(decoded, { a: 1, b: "hi" })
223
+ * ```
224
+ *
225
+ * @category combinators
226
+ * @since 0.0.0
227
+ */
228
+ export const omit = <
229
+ const Fields extends Schema.Struct.Fields,
230
+ const Keys extends readonly (keyof Fields & string)[],
231
+ >(
232
+ schema: Schema.Struct<Fields>,
233
+ ...keys: Keys
234
+ ): Schema.Struct<Omit<Fields, Keys[number]>> =>
235
+ schema.mapFields(
236
+ (fields) =>
237
+ Struct.omit(fields, keys as readonly (keyof Fields)[]) as Omit<
238
+ Fields,
239
+ Keys[number]
240
+ >,
241
+ );
242
+
243
+ /**
244
+ * Returns a new `Schema.Struct` in which every field of `schema` is made
245
+ * optional.
246
+ *
247
+ * Restores the v3 `Schema.partial(mySchema)` behaviour that v4 removed, by
248
+ * wrapping each field in `Schema.optional`. A decoded value may therefore omit
249
+ * any field; fields that *are* present still have to satisfy their original
250
+ * schema, refinements included.
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * import { Effect, Schema } from "effect"
255
+ * import { SchemaX } from "@nunofyobiz/effect-extras"
256
+ *
257
+ * const Source = Schema.Struct({ a: Schema.Number, b: Schema.String })
258
+ * const Partial = SchemaX.partial(Source)
259
+ *
260
+ * // All fields may be absent
261
+ * assert.deepStrictEqual(Effect.runSync(Schema.decodeEffect(Partial)({})), {})
262
+ *
263
+ * // A present subset still decodes
264
+ * assert.deepStrictEqual(
265
+ * Effect.runSync(Schema.decodeEffect(Partial)({ a: 1 })),
266
+ * { a: 1 },
267
+ * )
268
+ * ```
269
+ *
270
+ * @category combinators
271
+ * @since 0.0.0
272
+ */
273
+ export const partial = <Fields extends Schema.Struct.Fields>(
274
+ schema: Schema.Struct<Fields>,
275
+ ): Schema.Struct<{ [K in keyof Fields]: Schema.optional<Fields[K]> }> =>
276
+ schema.mapFields((fields) => {
277
+ const result: { [K in keyof Fields]?: Schema.optional<Fields[K]> } = {};
278
+ for (const key of Object.keys(fields) as (keyof Fields)[]) {
279
+ result[key] = Schema.optional(fields[key]);
280
+ }
281
+ return result as { [K in keyof Fields]: Schema.optional<Fields[K]> };
282
+ });
283
+
284
+ /**
285
+ * Returns a new `Schema.Struct` containing only the named `keys` of `schema`,
286
+ * with every picked field made optional.
287
+ *
288
+ * Equivalent to `partial(pick(schema, ...keys))`, but reads more directly for
289
+ * the common "partial update over a subset of fields" pattern: select the
290
+ * mutable fields of an entity, then allow any of them to be omitted in the
291
+ * update payload. Composes {@link pick} and {@link partial} in one call.
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * import { Effect, Schema } from "effect"
296
+ * import { SchemaX } from "@nunofyobiz/effect-extras"
297
+ *
298
+ * const Source = Schema.Struct({
299
+ * a: Schema.Number,
300
+ * b: Schema.String,
301
+ * c: Schema.Boolean,
302
+ * })
303
+ *
304
+ * const Update = SchemaX.pickPartial(Source, "a", "b")
305
+ *
306
+ * // Only picked fields are known, and each may be omitted
307
+ * assert.deepStrictEqual(Effect.runSync(Schema.decodeEffect(Update)({})), {})
308
+ * assert.deepStrictEqual(
309
+ * Effect.runSync(Schema.decodeEffect(Update)({ a: 1 })),
310
+ * { a: 1 },
311
+ * )
312
+ * ```
313
+ *
314
+ * @category combinators
315
+ * @since 0.0.0
316
+ */
317
+ export const pickPartial = <
318
+ const Fields extends Schema.Struct.Fields,
319
+ const Keys extends readonly (keyof Fields & string)[],
320
+ >(
321
+ schema: Schema.Struct<Fields>,
322
+ ...keys: Keys
323
+ ): Schema.Struct<{ [K in Keys[number]]: Schema.optional<Fields[K]> }> =>
324
+ partial(pick(schema, ...keys));
@@ -0,0 +1 @@
1
+ export * as SchemaX from "./SchemaX";
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions for working with `Set`.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { dual } from "effect/Function";
7
+
8
+ /**
9
+ * Runs a mutating function against a copy of `set`, leaving the original
10
+ * untouched, and returns the mutated copy.
11
+ *
12
+ * Use it to reuse imperative `Set` mutation code (`.add`, `.delete`) without
13
+ * sacrificing immutability: the callback may freely mutate the set it receives
14
+ * because it operates on a fresh clone. Supports both data-first and data-last
15
+ * (pipeable) call styles.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { SetX } from "@nunofyobiz/effect-extras"
20
+ * import { pipe } from "effect"
21
+ *
22
+ * const original = new Set(["a", "b"])
23
+ *
24
+ * const result = pipe(
25
+ * original,
26
+ * SetX.safelyMutate((set) => {
27
+ * set.delete("a")
28
+ * return set.add("c")
29
+ * })
30
+ * )
31
+ *
32
+ * assert.deepStrictEqual(result, new Set(["b", "c"]))
33
+ * // The original is left intact
34
+ * assert.deepStrictEqual(original, new Set(["a", "b"]))
35
+ * ```
36
+ *
37
+ * @category combinators
38
+ * @since 0.0.0
39
+ */
40
+ export const safelyMutate = dual<
41
+ <A>(mutate: (set: Set<A>) => Set<A>) => (set: Set<A>) => Set<A>,
42
+ <A>(set: Set<A>, mutate: (set: Set<A>) => Set<A>) => Set<A>
43
+ >(2, <A>(set: Set<A>, mutate: (set: Set<A>) => Set<A>): Set<A> => {
44
+ const copy = new Set(set);
45
+ return mutate(copy);
46
+ });
47
+
48
+ /**
49
+ * Returns a new `Set` with `value` added, leaving the input set unchanged.
50
+ *
51
+ * When `value` is already present the input set is returned as-is (no copy),
52
+ * making repeated adds of existing members allocation-free. Supports both
53
+ * data-first and data-last (pipeable) call styles.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { SetX } from "@nunofyobiz/effect-extras"
58
+ * import { pipe } from "effect"
59
+ *
60
+ * // Data-first — adds a new element into a fresh set
61
+ * assert.deepStrictEqual(
62
+ * SetX.add(new Set(["a", "b"]), "c"),
63
+ * new Set(["a", "b", "c"])
64
+ * )
65
+ *
66
+ * // Data-last — existing element leaves the set unchanged
67
+ * assert.deepStrictEqual(
68
+ * pipe(new Set(["a", "b"]), SetX.add("b")),
69
+ * new Set(["a", "b"])
70
+ * )
71
+ * ```
72
+ *
73
+ * @category combinators
74
+ * @since 0.0.0
75
+ */
76
+ export const add = dual<
77
+ <A>(value: A) => (set: Set<A>) => Set<A>,
78
+ <A>(set: Set<A>, value: A) => Set<A>
79
+ >(
80
+ 2,
81
+ <A>(set: Set<A>, value: A): Set<A> =>
82
+ set.has(value) ? set : new Set(set).add(value),
83
+ );
84
+
85
+ /**
86
+ * Returns a new `Set` with `value` removed, leaving the input set unchanged.
87
+ *
88
+ * When `value` is absent the input set is returned as-is (no copy). Supports both
89
+ * data-first and data-last (pipeable) call styles.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { SetX } from "@nunofyobiz/effect-extras"
94
+ * import { pipe } from "effect"
95
+ *
96
+ * // Data-first — removes an existing element into a fresh set
97
+ * assert.deepStrictEqual(
98
+ * SetX.remove(new Set(["a", "b", "c"]), "c"),
99
+ * new Set(["a", "b"])
100
+ * )
101
+ *
102
+ * // Data-last — absent element leaves the set unchanged
103
+ * assert.deepStrictEqual(
104
+ * pipe(new Set(["a", "b"]), SetX.remove("z")),
105
+ * new Set(["a", "b"])
106
+ * )
107
+ * ```
108
+ *
109
+ * @category combinators
110
+ * @since 0.0.0
111
+ */
112
+ export const remove = dual<
113
+ <A>(value: A) => (set: Set<A>) => Set<A>,
114
+ <A>(set: Set<A>, value: A) => Set<A>
115
+ >(2, <A>(set: Set<A>, value: A): Set<A> => {
116
+ if (set.has(value)) {
117
+ const newSet = new Set(set);
118
+ newSet.delete(value);
119
+ return newSet;
120
+ }
121
+ return set;
122
+ });
123
+
124
+ /**
125
+ * Returns a new `Set` with `value` added if it was absent or removed if it was
126
+ * present, leaving the input set unchanged.
127
+ *
128
+ * Use it for membership toggles (selection state, feature flags by key) where a
129
+ * value's presence should flip on each call. Supports both data-first and
130
+ * data-last (pipeable) call styles.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * import { SetX } from "@nunofyobiz/effect-extras"
135
+ * import { pipe } from "effect"
136
+ *
137
+ * // Data-first — absent value gets added
138
+ * assert.deepStrictEqual(
139
+ * SetX.toggle(new Set(["a", "b"]), "c"),
140
+ * new Set(["a", "b", "c"])
141
+ * )
142
+ *
143
+ * // Data-last — present value gets removed
144
+ * assert.deepStrictEqual(
145
+ * pipe(new Set(["a", "b", "c"]), SetX.toggle("b")),
146
+ * new Set(["a", "c"])
147
+ * )
148
+ * ```
149
+ *
150
+ * @category combinators
151
+ * @since 0.0.0
152
+ */
153
+ export const toggle = dual<
154
+ <A>(value: A) => (set: Set<A>) => Set<A>,
155
+ <A>(set: Set<A>, value: A) => Set<A>
156
+ >(
157
+ 2,
158
+ <A>(set: Set<A>, value: A): Set<A> =>
159
+ set.has(value) ? remove(set, value) : add(set, value),
160
+ );
@@ -0,0 +1 @@
1
+ export * as SetX from "./SetX";
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `String` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { dual } from "effect/Function";
7
+
8
+ /**
9
+ * Prepends `start` to `string_`.
10
+ *
11
+ * v4's `String` module has no native `prepend` (only `concat`), so this fills
12
+ * the gap as a pipeable helper.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { pipe } from "effect"
17
+ * import { StringX } from "@nunofyobiz/effect-extras"
18
+ *
19
+ * // data-first
20
+ * assert.deepStrictEqual(StringX.prepend("world", "hello "), "hello world")
21
+ *
22
+ * // data-last (piped)
23
+ * assert.deepStrictEqual(pipe("world", StringX.prepend("hello ")), "hello world")
24
+ * ```
25
+ *
26
+ * @category combinators
27
+ * @since 0.0.0
28
+ */
29
+ export const prepend = dual<
30
+ (start: string) => (string_: string) => string,
31
+ (string_: string, start: string) => string
32
+ >(2, (string_: string, start: string): string => `${start}${string_}`);
33
+
34
+ /**
35
+ * Wraps `string_` between `start` and `end`.
36
+ *
37
+ * No v4 native equivalent — handy for quoting, bracketing, or fencing a value.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { pipe } from "effect"
42
+ * import { StringX } from "@nunofyobiz/effect-extras"
43
+ *
44
+ * // data-first
45
+ * assert.deepStrictEqual(StringX.surround("value", "[", "]"), "[value]")
46
+ *
47
+ * // data-last (piped)
48
+ * assert.deepStrictEqual(pipe("value", StringX.surround("(", ")")), "(value)")
49
+ * ```
50
+ *
51
+ * @category combinators
52
+ * @since 0.0.0
53
+ */
54
+ export const surround = dual<
55
+ (start: string, end: string) => (string_: string) => string,
56
+ (string_: string, start: string, end: string) => string
57
+ >(
58
+ 3,
59
+ (string_: string, start: string, end: string): string =>
60
+ `${start}${string_}${end}`,
61
+ );
62
+
63
+ /**
64
+ * Prepends `start` to `string_` unless `string_` already starts with it.
65
+ *
66
+ * Idempotent: applying it to an already-prefixed string is a no-op, so
67
+ * `ensurePrepend("foofoo", "foo")` stays `"foofoo"` rather than gaining a
68
+ * second `"foo"`.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import { pipe } from "effect"
73
+ * import { StringX } from "@nunofyobiz/effect-extras"
74
+ *
75
+ * // data-first — adds the prefix when missing
76
+ * assert.deepStrictEqual(StringX.ensurePrepend("bar", "foo"), "foobar")
77
+ *
78
+ * // idempotent — already prefixed, returned unchanged
79
+ * assert.deepStrictEqual(StringX.ensurePrepend("foobar", "foo"), "foobar")
80
+ *
81
+ * // data-last (piped)
82
+ * assert.deepStrictEqual(pipe("bar", StringX.ensurePrepend("foo")), "foobar")
83
+ * ```
84
+ *
85
+ * @category combinators
86
+ * @since 0.0.0
87
+ */
88
+ export const ensurePrepend = dual<
89
+ (start: string) => (string_: string) => string,
90
+ (string_: string, start: string) => string
91
+ >(2, (string_: string, start: string): string => {
92
+ if (string_.startsWith(start)) {
93
+ return string_;
94
+ }
95
+
96
+ return `${start}${string_}`;
97
+ });
@@ -0,0 +1 @@
1
+ export * as StringX from "./StringX";