@player-tools/fluent 0.12.1--canary.241.6077
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/dist/cjs/index.cjs +2396 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2276 -0
- package/dist/index.mjs +2276 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
- package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
- package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
- package/src/core/base-builder/__tests__/registry.test.ts +534 -0
- package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
- package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
- package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
- package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
- package/src/core/base-builder/__tests__/switch.test.ts +815 -0
- package/src/core/base-builder/__tests__/template.test.ts +596 -0
- package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
- package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
- package/src/core/base-builder/conditional/index.ts +64 -0
- package/src/core/base-builder/context.ts +152 -0
- package/src/core/base-builder/errors.ts +69 -0
- package/src/core/base-builder/fluent-builder-base.ts +308 -0
- package/src/core/base-builder/guards.ts +137 -0
- package/src/core/base-builder/id/generator.ts +290 -0
- package/src/core/base-builder/id/registry.ts +152 -0
- package/src/core/base-builder/index.ts +72 -0
- package/src/core/base-builder/resolution/path-resolver.ts +116 -0
- package/src/core/base-builder/resolution/pipeline.ts +103 -0
- package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -0
- package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
- package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
- package/src/core/base-builder/resolution/steps/builders.ts +84 -0
- package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
- package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
- package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
- package/src/core/base-builder/resolution/steps/switches.ts +71 -0
- package/src/core/base-builder/resolution/steps/templates.ts +40 -0
- package/src/core/base-builder/resolution/value-resolver.ts +333 -0
- package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
- package/src/core/base-builder/storage/value-storage.ts +282 -0
- package/src/core/base-builder/types.ts +266 -0
- package/src/core/base-builder/utils.ts +10 -0
- package/src/core/flow/__tests__/index.test.ts +292 -0
- package/src/core/flow/index.ts +118 -0
- package/src/core/index.ts +8 -0
- package/src/core/mocks/generated/action.builder.ts +92 -0
- package/src/core/mocks/generated/choice-item.builder.ts +120 -0
- package/src/core/mocks/generated/choice.builder.ts +134 -0
- package/src/core/mocks/generated/collection.builder.ts +93 -0
- package/src/core/mocks/generated/field-collection.builder.ts +86 -0
- package/src/core/mocks/generated/index.ts +10 -0
- package/src/core/mocks/generated/info.builder.ts +64 -0
- package/src/core/mocks/generated/input.builder.ts +63 -0
- package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
- package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
- package/src/core/mocks/generated/text.builder.ts +47 -0
- package/src/core/mocks/index.ts +1 -0
- package/src/core/mocks/types/action.ts +92 -0
- package/src/core/mocks/types/choice.ts +129 -0
- package/src/core/mocks/types/collection.ts +140 -0
- package/src/core/mocks/types/info.ts +7 -0
- package/src/core/mocks/types/input.ts +7 -0
- package/src/core/mocks/types/text.ts +5 -0
- package/src/core/schema/__tests__/index.test.ts +127 -0
- package/src/core/schema/index.ts +195 -0
- package/src/core/schema/types.ts +7 -0
- package/src/core/switch/__tests__/index.test.ts +156 -0
- package/src/core/switch/index.ts +81 -0
- package/src/core/tagged-template/README.md +448 -0
- package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
- package/src/core/tagged-template/__tests__/index.test.ts +190 -0
- package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
- package/src/core/tagged-template/binding.ts +95 -0
- package/src/core/tagged-template/expression.ts +92 -0
- package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
- package/src/core/tagged-template/index.ts +5 -0
- package/src/core/tagged-template/std.ts +472 -0
- package/src/core/tagged-template/types.ts +123 -0
- package/src/core/template/__tests__/index.test.ts +380 -0
- package/src/core/template/index.ts +196 -0
- package/src/core/utils/index.ts +160 -0
- package/src/fp/README.md +411 -0
- package/src/fp/__tests__/index.test.ts +1178 -0
- package/src/fp/index.ts +386 -0
- package/src/gen/common.ts +15 -0
- package/src/index.ts +5 -0
- package/src/types.ts +203 -0
- package/types/core/base-builder/conditional/index.d.ts +21 -0
- package/types/core/base-builder/context.d.ts +39 -0
- package/types/core/base-builder/errors.d.ts +45 -0
- package/types/core/base-builder/fluent-builder-base.d.ts +147 -0
- package/types/core/base-builder/guards.d.ts +58 -0
- package/types/core/base-builder/id/generator.d.ts +69 -0
- package/types/core/base-builder/id/registry.d.ts +93 -0
- package/types/core/base-builder/index.d.ts +9 -0
- package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
- package/types/core/base-builder/resolution/pipeline.d.ts +27 -0
- package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/nested-asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
- package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
- package/types/core/base-builder/resolution/value-resolver.d.ts +62 -0
- package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
- package/types/core/base-builder/storage/value-storage.d.ts +82 -0
- package/types/core/base-builder/types.d.ts +183 -0
- package/types/core/base-builder/utils.d.ts +2 -0
- package/types/core/flow/index.d.ts +23 -0
- package/types/core/index.d.ts +8 -0
- package/types/core/mocks/index.d.ts +2 -0
- package/types/core/mocks/types/action.d.ts +58 -0
- package/types/core/mocks/types/choice.d.ts +95 -0
- package/types/core/mocks/types/collection.d.ts +102 -0
- package/types/core/mocks/types/info.d.ts +7 -0
- package/types/core/mocks/types/input.d.ts +7 -0
- package/types/core/mocks/types/text.d.ts +5 -0
- package/types/core/schema/index.d.ts +34 -0
- package/types/core/schema/types.d.ts +5 -0
- package/types/core/switch/index.d.ts +21 -0
- package/types/core/tagged-template/binding.d.ts +19 -0
- package/types/core/tagged-template/expression.d.ts +11 -0
- package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
- package/types/core/tagged-template/index.d.ts +6 -0
- package/types/core/tagged-template/std.d.ts +174 -0
- package/types/core/tagged-template/types.d.ts +69 -0
- package/types/core/template/index.d.ts +97 -0
- package/types/core/utils/index.d.ts +47 -0
- package/types/fp/index.d.ts +149 -0
- package/types/gen/common.d.ts +6 -0
- package/types/index.d.ts +3 -0
- package/types/types.d.ts +163 -0
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
pipe,
|
|
4
|
+
success,
|
|
5
|
+
failure,
|
|
6
|
+
isSuccess,
|
|
7
|
+
isFailure,
|
|
8
|
+
map,
|
|
9
|
+
flatMap,
|
|
10
|
+
recover,
|
|
11
|
+
getOrThrow,
|
|
12
|
+
getOrElse,
|
|
13
|
+
tryResult,
|
|
14
|
+
match,
|
|
15
|
+
pipeResult,
|
|
16
|
+
filterSuccesses,
|
|
17
|
+
mapToResults,
|
|
18
|
+
validate,
|
|
19
|
+
memoize,
|
|
20
|
+
isNotNullish,
|
|
21
|
+
safeArrayAccess,
|
|
22
|
+
combineResults,
|
|
23
|
+
} from "../index";
|
|
24
|
+
import type { Result } from "../../types";
|
|
25
|
+
|
|
26
|
+
describe("fp module", () => {
|
|
27
|
+
describe("pipe function", () => {
|
|
28
|
+
test("pipes a single function", () => {
|
|
29
|
+
const add1 = (x: number) => x + 1;
|
|
30
|
+
const result = pipe(5, add1);
|
|
31
|
+
expect(result).toBe(6);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("pipes two functions", () => {
|
|
35
|
+
const add1 = (x: number) => x + 1;
|
|
36
|
+
const multiply2 = (x: number) => x * 2;
|
|
37
|
+
const result = pipe(5, add1, multiply2);
|
|
38
|
+
expect(result).toBe(12);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("pipes three functions", () => {
|
|
42
|
+
const add1 = (x: number) => x + 1;
|
|
43
|
+
const multiply2 = (x: number) => x * 2;
|
|
44
|
+
const toString = (x: number) => x.toString();
|
|
45
|
+
const result = pipe(5, add1, multiply2, toString);
|
|
46
|
+
expect(result).toBe("12");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("pipes four functions", () => {
|
|
50
|
+
const add1 = (x: number) => x + 1;
|
|
51
|
+
const multiply2 = (x: number) => x * 2;
|
|
52
|
+
const toString = (x: number) => x.toString();
|
|
53
|
+
const addPrefix = (x: string) => `value: ${x}`;
|
|
54
|
+
const result = pipe(5, add1, multiply2, toString, addPrefix);
|
|
55
|
+
expect(result).toBe("value: 12");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("pipes five functions", () => {
|
|
59
|
+
const add1 = (x: number) => x + 1;
|
|
60
|
+
const multiply2 = (x: number) => x * 2;
|
|
61
|
+
const toString = (x: number) => x.toString();
|
|
62
|
+
const addPrefix = (x: string) => `value: ${x}`;
|
|
63
|
+
const toUpperCase = (x: string) => x.toUpperCase();
|
|
64
|
+
const result = pipe(5, add1, multiply2, toString, addPrefix, toUpperCase);
|
|
65
|
+
expect(result).toBe("VALUE: 12");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("pipes six functions", () => {
|
|
69
|
+
const add1 = (x: number) => x + 1;
|
|
70
|
+
const multiply2 = (x: number) => x * 2;
|
|
71
|
+
const toString = (x: number) => x.toString();
|
|
72
|
+
const addPrefix = (x: string) => `value: ${x}`;
|
|
73
|
+
const toUpperCase = (x: string) => x.toUpperCase();
|
|
74
|
+
const addSuffix = (x: string) => `${x}!`;
|
|
75
|
+
const result = pipe(
|
|
76
|
+
5,
|
|
77
|
+
add1,
|
|
78
|
+
multiply2,
|
|
79
|
+
toString,
|
|
80
|
+
addPrefix,
|
|
81
|
+
toUpperCase,
|
|
82
|
+
addSuffix,
|
|
83
|
+
);
|
|
84
|
+
expect(result).toBe("VALUE: 12!");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("pipes seven functions", () => {
|
|
88
|
+
const add1 = (x: number) => x + 1;
|
|
89
|
+
const multiply2 = (x: number) => x * 2;
|
|
90
|
+
const toString = (x: number) => x.toString();
|
|
91
|
+
const addPrefix = (x: string) => `value: ${x}`;
|
|
92
|
+
const toUpperCase = (x: string) => x.toUpperCase();
|
|
93
|
+
const addSuffix = (x: string) => `${x}!`;
|
|
94
|
+
const trim = (x: string) => x.trim();
|
|
95
|
+
const result = pipe(
|
|
96
|
+
5,
|
|
97
|
+
add1,
|
|
98
|
+
multiply2,
|
|
99
|
+
toString,
|
|
100
|
+
addPrefix,
|
|
101
|
+
toUpperCase,
|
|
102
|
+
addSuffix,
|
|
103
|
+
trim,
|
|
104
|
+
);
|
|
105
|
+
expect(result).toBe("VALUE: 12!");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("pipes with different types", () => {
|
|
109
|
+
const numberToString = (x: number) => x.toString();
|
|
110
|
+
const stringLength = (x: string) => x.length;
|
|
111
|
+
const isEven = (x: number) => x % 2 === 0;
|
|
112
|
+
const result = pipe(123, numberToString, stringLength, isEven);
|
|
113
|
+
expect(result).toBe(false); // length is 3, which is odd
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("pipes with identity function", () => {
|
|
117
|
+
const identity = <T>(x: T) => x;
|
|
118
|
+
const result = pipe("hello", identity);
|
|
119
|
+
expect(result).toBe("hello");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("pipes with complex transformations", () => {
|
|
123
|
+
const parseJson = (x: string) => JSON.parse(x);
|
|
124
|
+
const getProperty = (x: { value: number }) => x.value;
|
|
125
|
+
const square = (x: number) => x * x;
|
|
126
|
+
const result = pipe('{"value": 5}', parseJson, getProperty, square);
|
|
127
|
+
expect(result).toBe(25);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("success function", () => {
|
|
132
|
+
test("creates a success result with a value", () => {
|
|
133
|
+
const result = success(42);
|
|
134
|
+
expect(result).toEqual({ success: true, value: 42 });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("creates a success result with string value", () => {
|
|
138
|
+
const result = success("hello");
|
|
139
|
+
expect(result).toEqual({ success: true, value: "hello" });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("creates a success result with object value", () => {
|
|
143
|
+
const obj = { name: "test", count: 5 };
|
|
144
|
+
const result = success(obj);
|
|
145
|
+
expect(result).toEqual({ success: true, value: obj });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("creates a success result with null value", () => {
|
|
149
|
+
const result = success(null);
|
|
150
|
+
expect(result).toEqual({ success: true, value: null });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("creates a success result with undefined value", () => {
|
|
154
|
+
const result = success(undefined);
|
|
155
|
+
expect(result).toEqual({ success: true, value: undefined });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("failure function", () => {
|
|
160
|
+
test("creates a failure result with an error", () => {
|
|
161
|
+
const error = new Error("Something went wrong");
|
|
162
|
+
const result = failure(error);
|
|
163
|
+
expect(result).toEqual({ success: false, error });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("creates a failure result with string error", () => {
|
|
167
|
+
const result = failure("Error message");
|
|
168
|
+
expect(result).toEqual({ success: false, error: "Error message" });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("creates a failure result with custom error object", () => {
|
|
172
|
+
const customError = { code: 404, message: "Not found" };
|
|
173
|
+
const result = failure(customError);
|
|
174
|
+
expect(result).toEqual({ success: false, error: customError });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("isSuccess function", () => {
|
|
179
|
+
test("returns true for success results", () => {
|
|
180
|
+
const result = success(42);
|
|
181
|
+
expect(isSuccess(result)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("returns false for failure results", () => {
|
|
185
|
+
const result = failure(new Error("test"));
|
|
186
|
+
expect(isSuccess(result)).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("works as type guard", () => {
|
|
190
|
+
const result: Result<number, Error> = success(42);
|
|
191
|
+
if (isSuccess(result)) {
|
|
192
|
+
// TypeScript should know this is a Success<number>
|
|
193
|
+
expect(result.value).toBe(42);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("isFailure function", () => {
|
|
199
|
+
test("returns false for success results", () => {
|
|
200
|
+
const result = success(42);
|
|
201
|
+
expect(isFailure(result)).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("returns true for failure results", () => {
|
|
205
|
+
const result = failure(new Error("test"));
|
|
206
|
+
expect(isFailure(result)).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("works as type guard", () => {
|
|
210
|
+
const result: Result<number, Error> = failure(new Error("test error"));
|
|
211
|
+
if (isFailure(result)) {
|
|
212
|
+
// TypeScript should know this is a Failure<Error>
|
|
213
|
+
expect(result.error.message).toBe("test error");
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("map function", () => {
|
|
219
|
+
test("maps over success values", () => {
|
|
220
|
+
const result = success(5);
|
|
221
|
+
const mapped = map(result, (x) => x * 2);
|
|
222
|
+
expect(mapped).toEqual(success(10));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("does not map over failure values", () => {
|
|
226
|
+
const error = new Error("test");
|
|
227
|
+
const result = failure(error);
|
|
228
|
+
const mapped = map(result, (x: number) => x * 2);
|
|
229
|
+
expect(mapped).toEqual(failure(error));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("maps to different types", () => {
|
|
233
|
+
const result = success(42);
|
|
234
|
+
const mapped = map(result, (x) => x.toString());
|
|
235
|
+
expect(mapped).toEqual(success("42"));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("maps with complex transformations", () => {
|
|
239
|
+
const result = success({ name: "John", age: 30 });
|
|
240
|
+
const mapped = map(
|
|
241
|
+
result,
|
|
242
|
+
(person) => `${person.name} is ${person.age} years old`,
|
|
243
|
+
);
|
|
244
|
+
expect(mapped).toEqual(success("John is 30 years old"));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("preserves failure type", () => {
|
|
248
|
+
const customError = { code: 500, message: "Server error" };
|
|
249
|
+
const result = failure(customError);
|
|
250
|
+
const mapped = map(result, (x: string) => x.toUpperCase());
|
|
251
|
+
expect(mapped).toEqual(failure(customError));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("flatMap function", () => {
|
|
256
|
+
test("chains success results", () => {
|
|
257
|
+
const result = success(5);
|
|
258
|
+
const chained = flatMap(result, (x) => success(x * 2));
|
|
259
|
+
expect(chained).toEqual(success(10));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("chains to failure", () => {
|
|
263
|
+
const result = success(5);
|
|
264
|
+
const error = new Error("chain error");
|
|
265
|
+
const chained = flatMap(result, () => failure(error));
|
|
266
|
+
expect(chained).toEqual(failure(error));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("does not chain failure results", () => {
|
|
270
|
+
const error = new Error("original error");
|
|
271
|
+
const result = failure(error);
|
|
272
|
+
const chained = flatMap(result, (x: number) => success(x * 2));
|
|
273
|
+
expect(chained).toEqual(failure(error));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("chains with type transformation", () => {
|
|
277
|
+
const result = success(42);
|
|
278
|
+
const chained = flatMap(result, (x) => success(x.toString()));
|
|
279
|
+
expect(chained).toEqual(success("42"));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("chains multiple operations", () => {
|
|
283
|
+
const result = success("5");
|
|
284
|
+
const parsed = flatMap(result, (str) => {
|
|
285
|
+
const num = parseInt(str, 10);
|
|
286
|
+
return isNaN(num) ? failure(new Error("Invalid number")) : success(num);
|
|
287
|
+
});
|
|
288
|
+
const doubled = flatMap(parsed, (num) => success(num * 2));
|
|
289
|
+
expect(doubled).toEqual(success(10));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("stops chain on first failure", () => {
|
|
293
|
+
const result = success("not-a-number");
|
|
294
|
+
const parsed = flatMap(result, (str) => {
|
|
295
|
+
const num = parseInt(str, 10);
|
|
296
|
+
return isNaN(num) ? failure(new Error("Invalid number")) : success(num);
|
|
297
|
+
});
|
|
298
|
+
const doubled = flatMap(parsed, (num) => success(num * 2));
|
|
299
|
+
expect(isFailure(doubled)).toBe(true);
|
|
300
|
+
if (isFailure(doubled)) {
|
|
301
|
+
expect(doubled.error.message).toBe("Invalid number");
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("recover function", () => {
|
|
307
|
+
test("returns success unchanged", () => {
|
|
308
|
+
const result = success(42);
|
|
309
|
+
const recovered = recover(result, () => 0);
|
|
310
|
+
expect(recovered).toEqual(success(42));
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("recovers from failure", () => {
|
|
314
|
+
const error = new Error("test error");
|
|
315
|
+
const result = failure(error);
|
|
316
|
+
const recovered = recover(
|
|
317
|
+
result,
|
|
318
|
+
(err) => `Recovered from: ${err.message}`,
|
|
319
|
+
);
|
|
320
|
+
expect(recovered).toEqual(success("Recovered from: test error"));
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("recovers with default value", () => {
|
|
324
|
+
const result = failure("error");
|
|
325
|
+
const recovered = recover(result, () => "default");
|
|
326
|
+
expect(recovered).toEqual(success("default"));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("recovers with error transformation", () => {
|
|
330
|
+
const result = failure({ code: 404, message: "Not found" });
|
|
331
|
+
const recovered = recover(
|
|
332
|
+
result,
|
|
333
|
+
(err) => `Error ${err.code}: ${err.message}`,
|
|
334
|
+
);
|
|
335
|
+
expect(recovered).toEqual(success("Error 404: Not found"));
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("always returns success", () => {
|
|
339
|
+
const result1 = success(1);
|
|
340
|
+
const result2 = failure("error");
|
|
341
|
+
const recovered1 = recover(result1, () => 0);
|
|
342
|
+
const recovered2 = recover(result2, () => 0);
|
|
343
|
+
|
|
344
|
+
expect(isSuccess(recovered1)).toBe(true);
|
|
345
|
+
expect(isSuccess(recovered2)).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("getOrThrow function", () => {
|
|
350
|
+
test("returns value from success", () => {
|
|
351
|
+
const result = success(42);
|
|
352
|
+
expect(getOrThrow(result)).toBe(42);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("throws error from failure", () => {
|
|
356
|
+
const error = new Error("test error");
|
|
357
|
+
const result = failure(error);
|
|
358
|
+
expect(() => getOrThrow(result)).toThrow("test error");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("throws the exact error object", () => {
|
|
362
|
+
const customError = new TypeError("Type error");
|
|
363
|
+
const result = failure(customError);
|
|
364
|
+
expect(() => getOrThrow(result)).toThrow(customError);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("returns complex values", () => {
|
|
368
|
+
const obj = { name: "test", values: [1, 2, 3] };
|
|
369
|
+
const result = success(obj);
|
|
370
|
+
expect(getOrThrow(result)).toEqual(obj);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("getOrElse function", () => {
|
|
375
|
+
test("returns value from success", () => {
|
|
376
|
+
const result = success(42);
|
|
377
|
+
expect(getOrElse(result, 0)).toBe(42);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("returns default from failure", () => {
|
|
381
|
+
const result = failure(new Error("test"));
|
|
382
|
+
expect(getOrElse(result, 0)).toBe(0);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("returns default with same type", () => {
|
|
386
|
+
const result = failure("error");
|
|
387
|
+
expect(getOrElse(result, "default")).toBe("default");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("returns complex default values", () => {
|
|
391
|
+
const defaultObj = { name: "default", count: 0 };
|
|
392
|
+
const result = failure(new Error("test"));
|
|
393
|
+
expect(getOrElse(result, defaultObj)).toEqual(defaultObj);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("preserves original value type", () => {
|
|
397
|
+
const result = success("hello");
|
|
398
|
+
expect(getOrElse(result, "default")).toBe("hello");
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("tryResult function", () => {
|
|
403
|
+
test("wraps successful function execution", () => {
|
|
404
|
+
const fn = () => 42;
|
|
405
|
+
const result = tryResult(fn);
|
|
406
|
+
expect(result).toEqual(success(42));
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("wraps function that throws Error", () => {
|
|
410
|
+
const fn = () => {
|
|
411
|
+
throw new Error("test error");
|
|
412
|
+
};
|
|
413
|
+
const result = tryResult(fn);
|
|
414
|
+
expect(isFailure(result)).toBe(true);
|
|
415
|
+
if (isFailure(result)) {
|
|
416
|
+
expect(result.error.message).toBe("test error");
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("wraps function that throws string", () => {
|
|
421
|
+
const fn = () => {
|
|
422
|
+
throw "string error";
|
|
423
|
+
};
|
|
424
|
+
const result = tryResult(fn);
|
|
425
|
+
expect(isFailure(result)).toBe(true);
|
|
426
|
+
if (isFailure(result)) {
|
|
427
|
+
expect(result.error.message).toBe("string error");
|
|
428
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("wraps function that throws non-string, non-Error", () => {
|
|
433
|
+
const fn = () => {
|
|
434
|
+
throw { code: 500 };
|
|
435
|
+
};
|
|
436
|
+
const result = tryResult(fn);
|
|
437
|
+
expect(isFailure(result)).toBe(true);
|
|
438
|
+
if (isFailure(result)) {
|
|
439
|
+
expect(result.error.message).toBe("[object Object]");
|
|
440
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("wraps function that throws null", () => {
|
|
445
|
+
const fn = () => {
|
|
446
|
+
throw null;
|
|
447
|
+
};
|
|
448
|
+
const result = tryResult(fn);
|
|
449
|
+
expect(isFailure(result)).toBe(true);
|
|
450
|
+
if (isFailure(result)) {
|
|
451
|
+
expect(result.error.message).toBe("null");
|
|
452
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("wraps function that throws undefined", () => {
|
|
457
|
+
const fn = () => {
|
|
458
|
+
throw undefined;
|
|
459
|
+
};
|
|
460
|
+
const result = tryResult(fn);
|
|
461
|
+
expect(isFailure(result)).toBe(true);
|
|
462
|
+
if (isFailure(result)) {
|
|
463
|
+
expect(result.error.message).toBe("undefined");
|
|
464
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("wraps function with complex return value", () => {
|
|
469
|
+
const fn = () => ({ name: "test", values: [1, 2, 3] });
|
|
470
|
+
const result = tryResult(fn);
|
|
471
|
+
expect(result).toEqual(success({ name: "test", values: [1, 2, 3] }));
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("wraps function that returns undefined", () => {
|
|
475
|
+
const fn = () => undefined;
|
|
476
|
+
const result = tryResult(fn);
|
|
477
|
+
expect(result).toEqual(success(undefined));
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe("match function", () => {
|
|
482
|
+
test("calls onSuccess for success results", () => {
|
|
483
|
+
const result = success(42);
|
|
484
|
+
const matched = match(
|
|
485
|
+
result,
|
|
486
|
+
(value) => `Success: ${value}`,
|
|
487
|
+
(error) => `Error: ${error}`,
|
|
488
|
+
);
|
|
489
|
+
expect(matched).toBe("Success: 42");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("calls onFailure for failure results", () => {
|
|
493
|
+
const error = new Error("test error");
|
|
494
|
+
const result = failure(error);
|
|
495
|
+
const matched = match(
|
|
496
|
+
result,
|
|
497
|
+
(value) => `Success: ${value}`,
|
|
498
|
+
(err) => `Error: ${err.message}`,
|
|
499
|
+
);
|
|
500
|
+
expect(matched).toBe("Error: test error");
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("transforms to different types", () => {
|
|
504
|
+
const successResult = success("hello");
|
|
505
|
+
const failureResult = failure("error");
|
|
506
|
+
|
|
507
|
+
const successMatched = match(
|
|
508
|
+
successResult,
|
|
509
|
+
(value) => value.length,
|
|
510
|
+
() => 0,
|
|
511
|
+
);
|
|
512
|
+
const failureMatched = match(
|
|
513
|
+
failureResult,
|
|
514
|
+
(value: string) => value.length,
|
|
515
|
+
() => 0,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
expect(successMatched).toBe(5);
|
|
519
|
+
expect(failureMatched).toBe(0);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("handles complex transformations", () => {
|
|
523
|
+
const result = success({ name: "John", age: 30 });
|
|
524
|
+
const matched = match(
|
|
525
|
+
result,
|
|
526
|
+
(person) => ({ ...person, isAdult: person.age >= 18 }),
|
|
527
|
+
() => ({ name: "Unknown", age: 0, isAdult: false }),
|
|
528
|
+
);
|
|
529
|
+
expect(matched).toEqual({ name: "John", age: 30, isAdult: true });
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("preserves handler return types", () => {
|
|
533
|
+
const result = failure("error");
|
|
534
|
+
const matched = match(
|
|
535
|
+
result,
|
|
536
|
+
() => true,
|
|
537
|
+
() => false,
|
|
538
|
+
);
|
|
539
|
+
expect(matched).toBe(false);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("works with void handlers", () => {
|
|
543
|
+
let sideEffect = "";
|
|
544
|
+
const result = success("test");
|
|
545
|
+
|
|
546
|
+
match(
|
|
547
|
+
result,
|
|
548
|
+
(value) => {
|
|
549
|
+
sideEffect = `success: ${value}`;
|
|
550
|
+
},
|
|
551
|
+
(error) => {
|
|
552
|
+
sideEffect = `error: ${error}`;
|
|
553
|
+
},
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
expect(sideEffect).toBe("success: test");
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe("integration tests", () => {
|
|
561
|
+
test("combines multiple fp operations", () => {
|
|
562
|
+
const parseNumber = (str: string): Result<number, string> => {
|
|
563
|
+
const num = parseInt(str, 10);
|
|
564
|
+
return isNaN(num) ? failure("Invalid number") : success(num);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const result = pipe(
|
|
568
|
+
"42",
|
|
569
|
+
(str) => parseNumber(str),
|
|
570
|
+
(res) => map(res, (num) => num * 2),
|
|
571
|
+
(res) =>
|
|
572
|
+
flatMap(res, (num) =>
|
|
573
|
+
num > 50 ? success(num) : failure("Too small"),
|
|
574
|
+
),
|
|
575
|
+
(res) => recover(res, () => 0),
|
|
576
|
+
(res) => getOrElse(res, -1),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
expect(result).toBe(84);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("handles error propagation through chain", () => {
|
|
583
|
+
const parseNumber = (str: string): Result<number, string> => {
|
|
584
|
+
const num = parseInt(str, 10);
|
|
585
|
+
return isNaN(num) ? failure("Invalid number") : success(num);
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const result = pipe(
|
|
589
|
+
"not-a-number",
|
|
590
|
+
(str) => parseNumber(str),
|
|
591
|
+
(res) => map(res, (num) => num * 2),
|
|
592
|
+
(res) => flatMap(res, (num) => success(num.toString())),
|
|
593
|
+
(res) =>
|
|
594
|
+
match(
|
|
595
|
+
res,
|
|
596
|
+
(value) => `Result: ${value}`,
|
|
597
|
+
(error) => `Error: ${error}`,
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
expect(result).toBe("Error: Invalid number");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("complex data transformation pipeline", () => {
|
|
605
|
+
interface User {
|
|
606
|
+
id: number;
|
|
607
|
+
name: string;
|
|
608
|
+
email: string;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const validateUser = (data: unknown): Result<User, string> => {
|
|
612
|
+
if (typeof data !== "object" || data === null) {
|
|
613
|
+
return failure("Data must be an object");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const obj = data as Record<string, unknown>;
|
|
617
|
+
|
|
618
|
+
if (typeof obj.id !== "number") {
|
|
619
|
+
return failure("ID must be a number");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (typeof obj.name !== "string") {
|
|
623
|
+
return failure("Name must be a string");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (typeof obj.email !== "string") {
|
|
627
|
+
return failure("Email must be a string");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return success({ id: obj.id, name: obj.name, email: obj.email });
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const userData = { id: 1, name: "John Doe", email: "john@example.com" };
|
|
634
|
+
|
|
635
|
+
const result = pipe(
|
|
636
|
+
userData,
|
|
637
|
+
validateUser,
|
|
638
|
+
(res) =>
|
|
639
|
+
map(res, (user) => ({
|
|
640
|
+
...user,
|
|
641
|
+
displayName: `${user.name} <${user.email}>`,
|
|
642
|
+
})),
|
|
643
|
+
(res) =>
|
|
644
|
+
getOrElse(res, {
|
|
645
|
+
id: 0,
|
|
646
|
+
name: "Unknown",
|
|
647
|
+
email: "",
|
|
648
|
+
displayName: "Unknown",
|
|
649
|
+
}),
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
expect(result).toEqual({
|
|
653
|
+
id: 1,
|
|
654
|
+
name: "John Doe",
|
|
655
|
+
email: "john@example.com",
|
|
656
|
+
displayName: "John Doe <john@example.com>",
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
describe("pipeResult", () => {
|
|
662
|
+
test("should chain successful operations", () => {
|
|
663
|
+
const addOne = (x: number) => success(x + 1);
|
|
664
|
+
const multiplyByTwo = (x: number) => success(x * 2);
|
|
665
|
+
const toString = (x: number) => success(x.toString());
|
|
666
|
+
|
|
667
|
+
const result = pipeResult(success(5), addOne, multiplyByTwo, toString);
|
|
668
|
+
|
|
669
|
+
expect(isSuccess(result)).toBe(true);
|
|
670
|
+
if (isSuccess(result)) {
|
|
671
|
+
expect(result.value).toBe("12"); // (5 + 1) * 2 = 12
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("should short-circuit on first failure", () => {
|
|
676
|
+
const addOne = (x: number) => success(x + 1);
|
|
677
|
+
const failOperation = () => failure(new Error("Operation failed"));
|
|
678
|
+
const multiplyByTwo = (x: number) => success(x * 2);
|
|
679
|
+
|
|
680
|
+
const result = pipeResult(
|
|
681
|
+
success(5),
|
|
682
|
+
addOne,
|
|
683
|
+
failOperation,
|
|
684
|
+
multiplyByTwo, // This should not be called
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
expect(isSuccess(result)).toBe(false);
|
|
688
|
+
if (!isSuccess(result)) {
|
|
689
|
+
expect(result.error.message).toBe("Operation failed");
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("should handle initial failure", () => {
|
|
694
|
+
const addOne = (x: number) => success(x + 1);
|
|
695
|
+
|
|
696
|
+
const result = pipeResult(
|
|
697
|
+
failure(new Error("Initial failure")),
|
|
698
|
+
addOne, // This should not be called
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
expect(isSuccess(result)).toBe(false);
|
|
702
|
+
if (!isSuccess(result)) {
|
|
703
|
+
expect(result.error.message).toBe("Initial failure");
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("should work with single operation", () => {
|
|
708
|
+
const addOne = (x: number) => success(x + 1);
|
|
709
|
+
|
|
710
|
+
const result = pipeResult(success(5), addOne);
|
|
711
|
+
|
|
712
|
+
expect(isSuccess(result)).toBe(true);
|
|
713
|
+
if (isSuccess(result)) {
|
|
714
|
+
expect(result.value).toBe(6);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
describe("filterSuccesses", () => {
|
|
720
|
+
test("filters out failures and returns only success values", () => {
|
|
721
|
+
const results = [
|
|
722
|
+
success(1),
|
|
723
|
+
failure("error1"),
|
|
724
|
+
success(2),
|
|
725
|
+
success(3),
|
|
726
|
+
failure("error2"),
|
|
727
|
+
];
|
|
728
|
+
const filtered = filterSuccesses(results);
|
|
729
|
+
expect(filtered).toEqual([1, 2, 3]);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("returns empty array when all results are failures", () => {
|
|
733
|
+
const results = [failure("error1"), failure("error2"), failure("error3")];
|
|
734
|
+
const filtered = filterSuccesses(results);
|
|
735
|
+
expect(filtered).toEqual([]);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("returns all values when all results are successes", () => {
|
|
739
|
+
const results = [success("a"), success("b"), success("c")];
|
|
740
|
+
const filtered = filterSuccesses(results);
|
|
741
|
+
expect(filtered).toEqual(["a", "b", "c"]);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("handles empty array", () => {
|
|
745
|
+
const results: Array<Result<number, string>> = [];
|
|
746
|
+
const filtered = filterSuccesses(results);
|
|
747
|
+
expect(filtered).toEqual([]);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test("preserves order of successful values", () => {
|
|
751
|
+
const results = [
|
|
752
|
+
success(10),
|
|
753
|
+
failure("error"),
|
|
754
|
+
success(20),
|
|
755
|
+
failure("error"),
|
|
756
|
+
success(30),
|
|
757
|
+
];
|
|
758
|
+
const filtered = filterSuccesses(results);
|
|
759
|
+
expect(filtered).toEqual([10, 20, 30]);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("works with different value types", () => {
|
|
763
|
+
const results = [
|
|
764
|
+
success({ id: 1, name: "Alice" }),
|
|
765
|
+
failure("validation error"),
|
|
766
|
+
success({ id: 2, name: "Bob" }),
|
|
767
|
+
];
|
|
768
|
+
const filtered = filterSuccesses(results);
|
|
769
|
+
expect(filtered).toEqual([
|
|
770
|
+
{ id: 1, name: "Alice" },
|
|
771
|
+
{ id: 2, name: "Bob" },
|
|
772
|
+
]);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
describe("mapToResults", () => {
|
|
777
|
+
test("transforms array of values to array of Results", () => {
|
|
778
|
+
const values = [1, 2, 3];
|
|
779
|
+
const fn = (x: number) => success(x * 2);
|
|
780
|
+
const results = mapToResults(values, fn);
|
|
781
|
+
expect(results).toEqual([success(2), success(4), success(6)]);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test("handles function that returns failures", () => {
|
|
785
|
+
const values = ["1", "not-a-number", "3"];
|
|
786
|
+
const parseNumber = (str: string) => {
|
|
787
|
+
const num = parseInt(str, 10);
|
|
788
|
+
return isNaN(num) ? failure("Invalid number") : success(num);
|
|
789
|
+
};
|
|
790
|
+
const results = mapToResults(values, parseNumber);
|
|
791
|
+
expect(results).toEqual([
|
|
792
|
+
success(1),
|
|
793
|
+
failure("Invalid number"),
|
|
794
|
+
success(3),
|
|
795
|
+
]);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("handles empty array", () => {
|
|
799
|
+
const values: number[] = [];
|
|
800
|
+
const fn = (x: number) => success(x * 2);
|
|
801
|
+
const results = mapToResults(values, fn);
|
|
802
|
+
expect(results).toEqual([]);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("preserves order", () => {
|
|
806
|
+
const values = [5, 4, 3, 2, 1];
|
|
807
|
+
const fn = (x: number) => success(x.toString());
|
|
808
|
+
const results = mapToResults(values, fn);
|
|
809
|
+
expect(results).toEqual([
|
|
810
|
+
success("5"),
|
|
811
|
+
success("4"),
|
|
812
|
+
success("3"),
|
|
813
|
+
success("2"),
|
|
814
|
+
success("1"),
|
|
815
|
+
]);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("works with complex transformations", () => {
|
|
819
|
+
const users = [
|
|
820
|
+
{ name: "Alice", age: 25 },
|
|
821
|
+
{ name: "Bob", age: 17 },
|
|
822
|
+
{ name: "Charlie", age: 30 },
|
|
823
|
+
];
|
|
824
|
+
const validateAdult = (user: { name: string; age: number }) =>
|
|
825
|
+
user.age >= 18
|
|
826
|
+
? success({ ...user, isAdult: true })
|
|
827
|
+
: failure(`${user.name} is not an adult`);
|
|
828
|
+
|
|
829
|
+
const results = mapToResults(users, validateAdult);
|
|
830
|
+
expect(results).toEqual([
|
|
831
|
+
success({ name: "Alice", age: 25, isAdult: true }),
|
|
832
|
+
failure("Bob is not an adult"),
|
|
833
|
+
success({ name: "Charlie", age: 30, isAdult: true }),
|
|
834
|
+
]);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
describe("validate", () => {
|
|
839
|
+
test("returns success when predicate is true", () => {
|
|
840
|
+
const result = validate(5, (x) => x > 0, "Must be positive");
|
|
841
|
+
expect(result).toEqual(success(5));
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("returns failure when predicate is false", () => {
|
|
845
|
+
const result = validate(-5, (x) => x > 0, "Must be positive");
|
|
846
|
+
expect(isFailure(result)).toBe(true);
|
|
847
|
+
if (isFailure(result)) {
|
|
848
|
+
expect(result.error.message).toBe("Must be positive");
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("validates string length", () => {
|
|
853
|
+
const validateLength = (str: string) =>
|
|
854
|
+
validate(
|
|
855
|
+
str,
|
|
856
|
+
(s) => s.length >= 3,
|
|
857
|
+
"String must be at least 3 characters",
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
expect(validateLength("hello")).toEqual(success("hello"));
|
|
861
|
+
expect(isFailure(validateLength("hi"))).toBe(true);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test("validates email format", () => {
|
|
865
|
+
const validateEmail = (email: string) =>
|
|
866
|
+
validate(
|
|
867
|
+
email,
|
|
868
|
+
(e) => e.includes("@") && e.includes("."),
|
|
869
|
+
"Invalid email format",
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
expect(validateEmail("user@example.com")).toEqual(
|
|
873
|
+
success("user@example.com"),
|
|
874
|
+
);
|
|
875
|
+
expect(isFailure(validateEmail("invalid-email"))).toBe(true);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("validates object properties", () => {
|
|
879
|
+
const user = { name: "Alice", age: 25 };
|
|
880
|
+
const validateUser = (u: typeof user) =>
|
|
881
|
+
validate(u, (user) => user.age >= 18, "User must be an adult");
|
|
882
|
+
|
|
883
|
+
expect(validateUser(user)).toEqual(success(user));
|
|
884
|
+
expect(isFailure(validateUser({ name: "Bob", age: 16 }))).toBe(true);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("validates array length", () => {
|
|
888
|
+
const validateArray = (arr: number[]) =>
|
|
889
|
+
validate(arr, (a) => a.length > 0, "Array cannot be empty");
|
|
890
|
+
|
|
891
|
+
expect(validateArray([1, 2, 3])).toEqual(success([1, 2, 3]));
|
|
892
|
+
expect(isFailure(validateArray([]))).toBe(true);
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
describe("memoize", () => {
|
|
897
|
+
test("caches function results", () => {
|
|
898
|
+
let callCount = 0;
|
|
899
|
+
const expensiveFunction = (x: number) => {
|
|
900
|
+
callCount++;
|
|
901
|
+
return x * x;
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
const memoized = memoize(expensiveFunction);
|
|
905
|
+
|
|
906
|
+
expect(memoized(5)).toBe(25);
|
|
907
|
+
expect(callCount).toBe(1);
|
|
908
|
+
|
|
909
|
+
expect(memoized(5)).toBe(25);
|
|
910
|
+
expect(callCount).toBe(1); // Should not increment
|
|
911
|
+
|
|
912
|
+
expect(memoized(3)).toBe(9);
|
|
913
|
+
expect(callCount).toBe(2);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test("handles multiple arguments", () => {
|
|
917
|
+
let callCount = 0;
|
|
918
|
+
const add = (a: number, b: number) => {
|
|
919
|
+
callCount++;
|
|
920
|
+
return a + b;
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const memoizedAdd = memoize(add);
|
|
924
|
+
|
|
925
|
+
expect(memoizedAdd(2, 3)).toBe(5);
|
|
926
|
+
expect(callCount).toBe(1);
|
|
927
|
+
|
|
928
|
+
expect(memoizedAdd(2, 3)).toBe(5);
|
|
929
|
+
expect(callCount).toBe(1);
|
|
930
|
+
|
|
931
|
+
expect(memoizedAdd(3, 2)).toBe(5);
|
|
932
|
+
expect(callCount).toBe(2); // Different argument order
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
test("handles string arguments", () => {
|
|
936
|
+
let callCount = 0;
|
|
937
|
+
const concat = (a: string, b: string) => {
|
|
938
|
+
callCount++;
|
|
939
|
+
return a + b;
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const memoizedConcat = memoize(concat);
|
|
943
|
+
|
|
944
|
+
expect(memoizedConcat("hello", "world")).toBe("helloworld");
|
|
945
|
+
expect(callCount).toBe(1);
|
|
946
|
+
|
|
947
|
+
expect(memoizedConcat("hello", "world")).toBe("helloworld");
|
|
948
|
+
expect(callCount).toBe(1);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test("handles complex object arguments", () => {
|
|
952
|
+
let callCount = 0;
|
|
953
|
+
const processUser = (user: { name: string; age: number }) => {
|
|
954
|
+
callCount++;
|
|
955
|
+
return `${user.name} is ${user.age} years old`;
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const memoizedProcess = memoize(processUser);
|
|
959
|
+
const user1 = { name: "Alice", age: 25 };
|
|
960
|
+
const user2 = { name: "Alice", age: 25 };
|
|
961
|
+
|
|
962
|
+
expect(memoizedProcess(user1)).toBe("Alice is 25 years old");
|
|
963
|
+
expect(callCount).toBe(1);
|
|
964
|
+
|
|
965
|
+
expect(memoizedProcess(user2)).toBe("Alice is 25 years old");
|
|
966
|
+
expect(callCount).toBe(1); // Same content, should be cached
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
test("handles no arguments", () => {
|
|
970
|
+
let callCount = 0;
|
|
971
|
+
const getValue = () => {
|
|
972
|
+
callCount++;
|
|
973
|
+
return 42;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const memoizedGetValue = memoize(getValue);
|
|
977
|
+
|
|
978
|
+
expect(memoizedGetValue()).toBe(42);
|
|
979
|
+
expect(callCount).toBe(1);
|
|
980
|
+
|
|
981
|
+
expect(memoizedGetValue()).toBe(42);
|
|
982
|
+
expect(callCount).toBe(1);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test("handles functions that return objects", () => {
|
|
986
|
+
let callCount = 0;
|
|
987
|
+
const createUser = (name: string) => {
|
|
988
|
+
callCount++;
|
|
989
|
+
return { name, id: Math.random() };
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const memoizedCreateUser = memoize(createUser);
|
|
993
|
+
const user1 = memoizedCreateUser("Alice");
|
|
994
|
+
const user2 = memoizedCreateUser("Alice");
|
|
995
|
+
|
|
996
|
+
expect(callCount).toBe(1);
|
|
997
|
+
expect(user1).toBe(user2); // Should be the exact same object
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
describe("isNotNullish", () => {
|
|
1002
|
+
test("returns true for non-null, non-undefined values", () => {
|
|
1003
|
+
expect(isNotNullish(42)).toBe(true);
|
|
1004
|
+
expect(isNotNullish("hello")).toBe(true);
|
|
1005
|
+
expect(isNotNullish(0)).toBe(true);
|
|
1006
|
+
expect(isNotNullish(false)).toBe(true);
|
|
1007
|
+
expect(isNotNullish([])).toBe(true);
|
|
1008
|
+
expect(isNotNullish({})).toBe(true);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test("returns false for null", () => {
|
|
1012
|
+
expect(isNotNullish(null)).toBe(false);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
test("returns false for undefined", () => {
|
|
1016
|
+
expect(isNotNullish(undefined)).toBe(false);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
test("works as type guard", () => {
|
|
1020
|
+
const value: string | null | undefined = "hello";
|
|
1021
|
+
if (isNotNullish(value)) {
|
|
1022
|
+
// TypeScript should know this is a string
|
|
1023
|
+
expect(value.toUpperCase()).toBe("HELLO");
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
test("filters arrays correctly", () => {
|
|
1028
|
+
const values = [1, null, 2, undefined, 3, null];
|
|
1029
|
+
const filtered = values.filter(isNotNullish);
|
|
1030
|
+
expect(filtered).toEqual([1, 2, 3]);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
test("handles empty string", () => {
|
|
1034
|
+
expect(isNotNullish("")).toBe(true);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test("handles NaN", () => {
|
|
1038
|
+
expect(isNotNullish(NaN)).toBe(true);
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
describe("safeArrayAccess", () => {
|
|
1043
|
+
test("returns success for valid indices", () => {
|
|
1044
|
+
const array = [1, 2, 3, 4, 5];
|
|
1045
|
+
expect(safeArrayAccess(array, 0)).toEqual(success(1));
|
|
1046
|
+
expect(safeArrayAccess(array, 2)).toEqual(success(3));
|
|
1047
|
+
expect(safeArrayAccess(array, 4)).toEqual(success(5));
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
test("returns failure for negative indices", () => {
|
|
1051
|
+
const array = [1, 2, 3];
|
|
1052
|
+
const result = safeArrayAccess(array, -1);
|
|
1053
|
+
expect(isFailure(result)).toBe(true);
|
|
1054
|
+
if (isFailure(result)) {
|
|
1055
|
+
expect(result.error.message).toBe(
|
|
1056
|
+
"Index -1 is out of bounds for array of length 3",
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test("returns failure for indices beyond array length", () => {
|
|
1062
|
+
const array = [1, 2, 3];
|
|
1063
|
+
const result = safeArrayAccess(array, 5);
|
|
1064
|
+
expect(isFailure(result)).toBe(true);
|
|
1065
|
+
if (isFailure(result)) {
|
|
1066
|
+
expect(result.error.message).toBe(
|
|
1067
|
+
"Index 5 is out of bounds for array of length 3",
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
test("handles empty arrays", () => {
|
|
1073
|
+
const array: number[] = [];
|
|
1074
|
+
const result = safeArrayAccess(array, 0);
|
|
1075
|
+
expect(isFailure(result)).toBe(true);
|
|
1076
|
+
if (isFailure(result)) {
|
|
1077
|
+
expect(result.error.message).toBe(
|
|
1078
|
+
"Index 0 is out of bounds for array of length 0",
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
test("works with string arrays", () => {
|
|
1084
|
+
const array = ["a", "b", "c"];
|
|
1085
|
+
expect(safeArrayAccess(array, 1)).toEqual(success("b"));
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
test("works with object arrays", () => {
|
|
1089
|
+
const array = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
1090
|
+
expect(safeArrayAccess(array, 0)).toEqual(success({ id: 1 }));
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
test("handles array with undefined values", () => {
|
|
1094
|
+
const array = [1, undefined, 3];
|
|
1095
|
+
expect(safeArrayAccess(array, 1)).toEqual(success(undefined));
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
describe("combineResults", () => {
|
|
1100
|
+
test("combines all successful results", () => {
|
|
1101
|
+
const results = [success(1), success(2), success(3)];
|
|
1102
|
+
const combined = combineResults(results);
|
|
1103
|
+
expect(combined).toEqual(success([1, 2, 3]));
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
test("returns first failure when any result fails", () => {
|
|
1107
|
+
const results = [
|
|
1108
|
+
success(1),
|
|
1109
|
+
failure("error1"),
|
|
1110
|
+
success(3),
|
|
1111
|
+
failure("error2"),
|
|
1112
|
+
];
|
|
1113
|
+
const combined = combineResults(results);
|
|
1114
|
+
expect(combined).toEqual(failure("error1"));
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
test("handles empty array", () => {
|
|
1118
|
+
const results: Array<Result<number, string>> = [];
|
|
1119
|
+
const combined = combineResults(results);
|
|
1120
|
+
expect(combined).toEqual(success([]));
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test("handles single success", () => {
|
|
1124
|
+
const results = [success(42)];
|
|
1125
|
+
const combined = combineResults(results);
|
|
1126
|
+
expect(combined).toEqual(success([42]));
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
test("handles single failure", () => {
|
|
1130
|
+
const results = [failure("error")];
|
|
1131
|
+
const combined = combineResults(results);
|
|
1132
|
+
expect(combined).toEqual(failure("error"));
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
test("preserves order of successful values", () => {
|
|
1136
|
+
const results = [success("a"), success("b"), success("c")];
|
|
1137
|
+
const combined = combineResults(results);
|
|
1138
|
+
expect(combined).toEqual(success(["a", "b", "c"]));
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("works with different value types", () => {
|
|
1142
|
+
const results = [
|
|
1143
|
+
success({ id: 1, name: "Alice" }),
|
|
1144
|
+
success({ id: 2, name: "Bob" }),
|
|
1145
|
+
];
|
|
1146
|
+
const combined = combineResults(results);
|
|
1147
|
+
expect(combined).toEqual(
|
|
1148
|
+
success([
|
|
1149
|
+
{ id: 1, name: "Alice" },
|
|
1150
|
+
{ id: 2, name: "Bob" },
|
|
1151
|
+
]),
|
|
1152
|
+
);
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
test("stops at first failure and doesn't process remaining", () => {
|
|
1156
|
+
const results = [
|
|
1157
|
+
success(1),
|
|
1158
|
+
failure("first error"),
|
|
1159
|
+
failure("second error"),
|
|
1160
|
+
];
|
|
1161
|
+
const combined = combineResults(results);
|
|
1162
|
+
expect(combined).toEqual(failure("first error"));
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
test("works with mixed error types", () => {
|
|
1166
|
+
const results = [
|
|
1167
|
+
success(1),
|
|
1168
|
+
failure(new Error("error message")),
|
|
1169
|
+
success(3),
|
|
1170
|
+
];
|
|
1171
|
+
const combined = combineResults(results);
|
|
1172
|
+
expect(isFailure(combined)).toBe(true);
|
|
1173
|
+
if (isFailure(combined)) {
|
|
1174
|
+
expect(combined.error.message).toBe("error message");
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
});
|