@typed/guard 1.0.0-beta.4 → 1.0.0-beta.5

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.
package/src/index.test.ts DELETED
@@ -1,366 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import * as Option from "effect/Option";
3
- import * as Schema from "effect/Schema";
4
- import * as Context from "effect/Context";
5
- import { describe, expect, it } from "vitest";
6
- import {
7
- addTag,
8
- any,
9
- bind,
10
- bindTo,
11
- catchAll,
12
- catchTag,
13
- decode,
14
- encode,
15
- filter,
16
- filterMap,
17
- fromSchemaDecode,
18
- fromSchemaEncode,
19
- getGuard,
20
- Guard,
21
- liftPredicate,
22
- map,
23
- mapEffect,
24
- pipe,
25
- provideService,
26
- tap,
27
- Guard as GuardNamespace,
28
- } from "./index.js";
29
-
30
- const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromise(effect);
31
-
32
- describe("@typed/guard", () => {
33
- describe("getGuard", () => {
34
- it("returns the guard when given a Guard function", () => {
35
- const g: Guard<number, number> = (n) => Effect.succeed(Option.some(n));
36
- expect(getGuard(g)).toBe(g);
37
- });
38
-
39
- it("returns asGuard() when given an AsGuard", () => {
40
- const inner: Guard<string, number> = (s) => Effect.succeed(Option.some(Number(s)));
41
- const asGuard = { asGuard: () => inner };
42
- expect(getGuard(asGuard)).toBe(inner);
43
- });
44
- });
45
-
46
- describe("liftPredicate", () => {
47
- it("returns Some when predicate holds", async () => {
48
- const g = liftPredicate((n: number) => n > 0);
49
- const result = await run(g(42));
50
- expect(Option.isSome(result)).toBe(true);
51
- if (Option.isSome(result)) expect(result.value).toBe(42);
52
- });
53
-
54
- it("returns None when predicate fails", async () => {
55
- const g = liftPredicate((n: number) => n > 0);
56
- const result = await run(g(-1));
57
- expect(Option.isNone(result)).toBe(true);
58
- });
59
-
60
- it("supports refinements (narrowing type)", async () => {
61
- const g = liftPredicate((s: string): s is `ok_${string}` => s.startsWith("ok_"));
62
- const result = await run(g("ok_foo"));
63
- expect(Option.isSome(result)).toBe(true);
64
- if (Option.isSome(result)) expect(result.value).toBe("ok_foo");
65
- const none = await run(g("nope"));
66
- expect(Option.isNone(none)).toBe(true);
67
- });
68
- });
69
-
70
- describe("pipe", () => {
71
- it("chains two guards: second runs on first output", async () => {
72
- const g1 = liftPredicate((n: number) => n >= 0);
73
- const g2 = liftPredicate((n: number) => n < 100);
74
- const chained = pipe(g1, g2);
75
- const some = await run(chained(50));
76
- expect(Option.isSome(some)).toBe(true);
77
- if (Option.isSome(some)) expect(some.value).toBe(50);
78
- const none = await run(chained(150));
79
- expect(Option.isNone(none)).toBe(true);
80
- });
81
-
82
- it("returns None when first guard returns None", async () => {
83
- const g1 = liftPredicate((n: number) => n > 0);
84
- const g2 = liftPredicate((n: number) => n < 10);
85
- const chained = pipe(g1, g2);
86
- const result = await run(chained(-1));
87
- expect(Option.isNone(result)).toBe(true);
88
- });
89
- });
90
-
91
- describe("map", () => {
92
- it("maps the output of a guard", async () => {
93
- const base = liftPredicate((n: number) => n > 0);
94
- const g = map(base, (n) => n * 2);
95
- const result = await run(g(3));
96
- expect(Option.isSome(result)).toBe(true);
97
- if (Option.isSome(result)) expect(result.value).toBe(6);
98
- });
99
-
100
- it("preserves None", async () => {
101
- const base = liftPredicate((n: number) => n > 0);
102
- const g = map(base, (n) => n * 2);
103
- const result = await run(g(-1));
104
- expect(Option.isNone(result)).toBe(true);
105
- });
106
- });
107
-
108
- describe("mapEffect", () => {
109
- it("maps output with an Effect", async () => {
110
- const base = liftPredicate((s: string) => s.length > 0);
111
- const g = mapEffect(base, (s) => Effect.succeed(s.toUpperCase()));
112
- const result = await run(g("hello"));
113
- expect(Option.isSome(result)).toBe(true);
114
- if (Option.isSome(result)) expect(result.value).toBe("HELLO");
115
- });
116
-
117
- it("fails when the effect fails", async () => {
118
- const base = liftPredicate((n: number) => n > 0);
119
- const g = mapEffect(base, (n) =>
120
- n > 10 ? Effect.fail("too big" as const) : Effect.succeed(n),
121
- );
122
- await expect(run(g(20))).rejects.toBe("too big");
123
- });
124
- });
125
-
126
- describe("tap", () => {
127
- it("runs side effect and returns original value", async () => {
128
- let side = 0;
129
- const base = liftPredicate((n: number) => n > 0);
130
- const g = tap(base, (n) =>
131
- Effect.sync(() => {
132
- side = n;
133
- }),
134
- );
135
- const result = await run(g(7));
136
- expect(side).toBe(7);
137
- expect(Option.isSome(result)).toBe(true);
138
- if (Option.isSome(result)) expect(result.value).toBe(7);
139
- });
140
- });
141
-
142
- describe("filter", () => {
143
- it("keeps output when predicate holds", async () => {
144
- const base = liftPredicate((n: number) => n >= 0);
145
- const g = filter(base, (n) => n < 100);
146
- const result = await run(g(50));
147
- expect(Option.isSome(result)).toBe(true);
148
- if (Option.isSome(result)) expect(result.value).toBe(50);
149
- });
150
-
151
- it("returns None when predicate fails", async () => {
152
- const base = liftPredicate((n: number) => n >= 0);
153
- const g = filter(base, (n) => n < 100);
154
- const result = await run(g(200));
155
- expect(Option.isNone(result)).toBe(true);
156
- });
157
- });
158
-
159
- describe("filterMap", () => {
160
- it("transforms to Some when f returns Some", async () => {
161
- const base = liftPredicate((n: number) => n > 0);
162
- const g = filterMap(base, (n) => (n % 2 === 0 ? Option.some(n / 2) : Option.none()));
163
- const result = await run(g(4));
164
- expect(Option.isSome(result)).toBe(true);
165
- if (Option.isSome(result)) expect(result.value).toBe(2);
166
- });
167
-
168
- it("returns None when f returns None", async () => {
169
- const base = liftPredicate((n: number) => n > 0);
170
- const g = filterMap(base, (n) => (n % 2 === 0 ? Option.some(n) : Option.none()));
171
- const result = await run(g(3));
172
- expect(Option.isNone(result)).toBe(true);
173
- });
174
- });
175
-
176
- describe("any", () => {
177
- it("returns first matching guard result with _tag and value", async () => {
178
- const guards = {
179
- num: liftPredicate((x: number) => typeof x === "number" && x >= 0),
180
- str: liftPredicate((x: string) => typeof x === "string" && x.length > 0),
181
- };
182
- const g = any(guards);
183
- const numResult = await run(g(42 as number | string));
184
- expect(Option.isSome(numResult)).toBe(true);
185
- if (Option.isSome(numResult)) {
186
- expect(numResult.value._tag).toBe("num");
187
- expect(numResult.value.value).toBe(42);
188
- }
189
- const strResult = await run(g("hi" as number | string));
190
- expect(Option.isSome(strResult)).toBe(true);
191
- if (Option.isSome(strResult)) {
192
- expect(strResult.value._tag).toBe("str");
193
- expect(strResult.value.value).toBe("hi");
194
- }
195
- });
196
-
197
- it("returns None when no guard matches", async () => {
198
- const guards = {
199
- pos: liftPredicate((n: number) => n > 0),
200
- neg: liftPredicate((n: number) => n < 0),
201
- };
202
- const g = any(guards);
203
- const result = await run(g(0));
204
- expect(Option.isNone(result)).toBe(true);
205
- });
206
- });
207
-
208
- describe("catchAll", () => {
209
- it("recovers from failure with a value", async () => {
210
- const failing: Guard<number, number, string> = (n) =>
211
- n > 0 ? Effect.succeed(Option.some(n)) : Effect.fail("negative");
212
- const g = catchAll(failing, () => Effect.succeed(0));
213
- const ok = await run(g(5));
214
- expect(Option.isSome(ok)).toBe(true);
215
- if (Option.isSome(ok)) expect(ok.value).toBe(5);
216
- const recovered = await run(g(-1));
217
- expect(Option.isSome(recovered)).toBe(true);
218
- if (Option.isSome(recovered)) expect(recovered.value).toBe(0);
219
- });
220
- });
221
-
222
- describe("catchTag", () => {
223
- it("catches tagged errors", async () => {
224
- type E = { _tag: "Bad"; n: number } | { _tag: "Other" };
225
- const failing: Guard<number, number, E> = (n) =>
226
- n >= 0 ? Effect.succeed(Option.some(n)) : Effect.fail({ _tag: "Bad" as const, n });
227
- const g = catchTag(failing, "Bad", (e) => Effect.succeed(-e.n));
228
- const ok = await run(g(3));
229
- expect(Option.isSome(ok)).toBe(true);
230
- if (Option.isSome(ok)) expect(ok.value).toBe(3);
231
- const recovered = await run(g(-10));
232
- expect(Option.isSome(recovered)).toBe(true);
233
- if (Option.isSome(recovered)) expect(recovered.value).toBe(10);
234
- });
235
- });
236
-
237
- describe("fromSchemaDecode / fromSchemaEncode", () => {
238
- const NumberFromString = Schema.NumberFromString;
239
-
240
- it("fromSchemaDecode passes decoded value as Some", async () => {
241
- const g = fromSchemaDecode(NumberFromString);
242
- const result = await run(g("42"));
243
- expect(Option.isSome(result)).toBe(true);
244
- if (Option.isSome(result)) expect(result.value).toBe(42);
245
- });
246
-
247
- it("fromSchemaDecode returns Some(NaN) for non-numeric string (NumberFromString behavior)", async () => {
248
- const g = fromSchemaDecode(NumberFromString);
249
- const result = await run(g("not a number"));
250
- expect(Option.isSome(result)).toBe(true);
251
- if (Option.isSome(result)) expect(Number.isNaN(result.value)).toBe(true);
252
- });
253
-
254
- it("fromSchemaEncode passes encoded value as Some", async () => {
255
- const g = fromSchemaEncode(NumberFromString);
256
- const result = await run(g(42));
257
- expect(Option.isSome(result)).toBe(true);
258
- if (Option.isSome(result)) expect(result.value).toBe("42");
259
- });
260
- });
261
-
262
- describe("decode / encode", () => {
263
- const NumberFromString = Schema.NumberFromString;
264
-
265
- it("decode pipes guard output through schema decode", async () => {
266
- const base = liftPredicate((s: string) => s.length > 0);
267
- const g = decode(base, NumberFromString);
268
- const result = await run(g("99"));
269
- expect(Option.isSome(result)).toBe(true);
270
- if (Option.isSome(result)) expect(result.value).toBe(99);
271
- });
272
-
273
- it("encode pipes guard output through schema encode", async () => {
274
- const base = liftPredicate((n: number) => n >= 0);
275
- const g = encode(base, NumberFromString);
276
- const result = await run(g(17));
277
- expect(Option.isSome(result)).toBe(true);
278
- if (Option.isSome(result)) expect(result.value).toBe("17");
279
- });
280
- });
281
-
282
- describe("let / addTag", () => {
283
- it("let attaches a property to the output", async () => {
284
- const { let: let_ } = await import("./index.js");
285
- const base = liftPredicate((n: number) => n > 0);
286
- const g = let_(base, "doubled", 0);
287
- const result = await run(g(3));
288
- expect(Option.isSome(result)).toBe(true);
289
- if (Option.isSome(result)) {
290
- expect((result.value as { doubled: number }).doubled).toBe(0);
291
- }
292
- const base2 = map(
293
- liftPredicate((n: number) => n > 0),
294
- (n) => ({ n }),
295
- );
296
- const g2 = let_(base2, "doubled", 2);
297
- const r2 = await run(g2(3));
298
- expect(Option.isSome(r2)).toBe(true);
299
- if (Option.isSome(r2)) {
300
- expect((r2.value as { n: number; doubled: number }).n).toBe(3);
301
- expect((r2.value as { n: number; doubled: number }).doubled).toBe(2);
302
- }
303
- });
304
-
305
- it("addTag attaches _tag to output", async () => {
306
- const base = liftPredicate((n: number) => n > 0);
307
- const g = addTag(base, "Positive");
308
- const result = await run(g(3));
309
- expect(Option.isSome(result)).toBe(true);
310
- if (Option.isSome(result)) {
311
- expect((result.value as { _tag: string })._tag).toBe("Positive");
312
- }
313
- });
314
- });
315
-
316
- describe("bindTo / bind", () => {
317
- it("bindTo names the output under a key", async () => {
318
- const base = liftPredicate((n: number) => n > 0);
319
- const g = bindTo(base, "value");
320
- const result = await run(g(5));
321
- expect(Option.isSome(result)).toBe(true);
322
- if (Option.isSome(result)) {
323
- expect((result.value as { value: number }).value).toBe(5);
324
- }
325
- });
326
-
327
- it("bind chains and merges object", async () => {
328
- const base = liftPredicate((n: number) => n > 0);
329
- const withA = bindTo(base, "a");
330
- const g = bind(withA, "b", (ctx) => liftPredicate((n: number) => n < 10)(ctx.a));
331
- const result = await run(g(5));
332
- expect(Option.isSome(result)).toBe(true);
333
- if (Option.isSome(result)) {
334
- expect((result.value as { a: number; b: number }).a).toBe(5);
335
- expect((result.value as { a: number; b: number }).b).toBe(5);
336
- }
337
- const none = await run(g(20));
338
- expect(Option.isNone(none)).toBe(true);
339
- });
340
- });
341
-
342
- describe("provideService", () => {
343
- it("provides a service so the guard no longer requires it", async () => {
344
- const Foo = Context.Service<{ readonly n: number }>("Test/Foo");
345
- const guardNeedsFoo: Guard<number, number, never, Context.Service.Identifier<typeof Foo>> = (
346
- i,
347
- ) => Effect.flatMap(Effect.service(Foo), (foo) => Effect.succeed(Option.some(i + foo.n)));
348
- const g = provideService(guardNeedsFoo, Foo, { n: 10 });
349
- const result = await run(g(1));
350
- expect(Option.isSome(result)).toBe(true);
351
- if (Option.isSome(result)) expect(result.value).toBe(11);
352
- });
353
- });
354
-
355
- describe("Guard namespace types", () => {
356
- it("Guard.Input/Output infer from guard", () => {
357
- const g: Guard<string, number> = (s) => Effect.succeed(Option.some(Number(s)));
358
- type In = GuardNamespace.Input<typeof g>;
359
- type Out = GuardNamespace.Output<typeof g>;
360
- const _in: In = "1";
361
- const _out: Out = 1;
362
- expect(_in).toBe("1");
363
- expect(_out).toBe(1);
364
- });
365
- });
366
- });