skyr 0.1.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.
- package/README.md +513 -0
- package/package.json +19 -0
- package/src/di.test.ts +299 -0
- package/src/di.ts +624 -0
- package/src/index.ts +11 -0
- package/src/result.test.ts +1052 -0
- package/src/result.ts +663 -0
- package/src/types.test.ts +254 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type AsyncResult,
|
|
4
|
+
type Fail,
|
|
5
|
+
type Failure,
|
|
6
|
+
type Ok,
|
|
7
|
+
type Result,
|
|
8
|
+
fail,
|
|
9
|
+
fn,
|
|
10
|
+
fromThrowable,
|
|
11
|
+
ok,
|
|
12
|
+
wrapThrowable,
|
|
13
|
+
} from "./index.ts";
|
|
14
|
+
|
|
15
|
+
// Type assertion helpers
|
|
16
|
+
type IsExact<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false;
|
|
17
|
+
function assertType<_T extends true>(_value: _T): void {}
|
|
18
|
+
|
|
19
|
+
// ok() returns Result<T, never>
|
|
20
|
+
test("ok() returns Result<T, never>", () => {
|
|
21
|
+
const result = ok(42);
|
|
22
|
+
type Expected = Result<number, never>;
|
|
23
|
+
assertType<IsExact<typeof result, Expected>>(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("Ok.unwrap() returns T after isOk()", () => {
|
|
27
|
+
const result: Result<number, Failure<"ERR">> = ok(42);
|
|
28
|
+
if (result.isOk()) {
|
|
29
|
+
const value = result.unwrap();
|
|
30
|
+
assertType<IsExact<typeof value, number>>(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// fail() returns Result<never, Failure<Code>>
|
|
35
|
+
test("fail() returns Result<never, Failure<Code>>", () => {
|
|
36
|
+
const result = fail("ERR", "error message");
|
|
37
|
+
type Expected = Result<never, Failure<"ERR">>;
|
|
38
|
+
assertType<IsExact<typeof result, Expected>>(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("Fail.unwrap() returns E after hasFailed()", () => {
|
|
42
|
+
const result: Result<number, Failure<"ERR">> = fail("ERR", "error");
|
|
43
|
+
if (result.hasFailed()) {
|
|
44
|
+
const error = result.unwrap();
|
|
45
|
+
assertType<IsExact<typeof error, Failure<"ERR">>>(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Result union type tests
|
|
50
|
+
test("Result<T, E> is union of Ok<T, E> | Fail<T, E>", () => {
|
|
51
|
+
const result: Result<number, Failure<"ERR">> = ok(42);
|
|
52
|
+
assertType<
|
|
53
|
+
IsExact<
|
|
54
|
+
typeof result,
|
|
55
|
+
Ok<number, Failure<"ERR">> | Fail<number, Failure<"ERR">>
|
|
56
|
+
>
|
|
57
|
+
>(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("Result.unwrap() returns T | E without narrowing", () => {
|
|
61
|
+
const result: Result<number, Failure<"ERR">> = ok(42);
|
|
62
|
+
const value = result.unwrap();
|
|
63
|
+
assertType<IsExact<typeof value, number | Failure<"ERR">>>(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// AsyncResult type tests
|
|
67
|
+
test("AsyncResult<T, E> wraps Promise<Result<T, E>>", () => {
|
|
68
|
+
const asyncResult = fromThrowable(() => Promise.resolve(42));
|
|
69
|
+
type Expected = AsyncResult<number, Failure<"UNKNOWN_FAILURE">>;
|
|
70
|
+
assertType<IsExact<typeof asyncResult, Expected>>(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("AsyncResult.unwrap() returns Promise<T | E>", () => {
|
|
74
|
+
const asyncResult = fromThrowable(() => Promise.resolve(42));
|
|
75
|
+
const promise = asyncResult.unwrap();
|
|
76
|
+
assertType<
|
|
77
|
+
IsExact<typeof promise, Promise<number | Failure<"UNKNOWN_FAILURE">>>
|
|
78
|
+
>(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Failure type tests
|
|
82
|
+
test("Failure<Code> has correct structure", () => {
|
|
83
|
+
type TestFailure = Failure<"TEST_ERR">;
|
|
84
|
+
const failure: TestFailure = { code: "TEST_ERR", message: "test" };
|
|
85
|
+
assertType<IsExact<typeof failure.code, "TEST_ERR">>(true);
|
|
86
|
+
assertType<IsExact<typeof failure.message, string>>(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("Failure with cause is optional", () => {
|
|
90
|
+
type TestFailure = Failure<"TEST_ERR">;
|
|
91
|
+
const withCause: TestFailure = {
|
|
92
|
+
code: "TEST_ERR",
|
|
93
|
+
message: "test",
|
|
94
|
+
cause: "reason",
|
|
95
|
+
};
|
|
96
|
+
const withoutCause: TestFailure = { code: "TEST_ERR", message: "test" };
|
|
97
|
+
assertType<IsExact<typeof withCause, TestFailure>>(true);
|
|
98
|
+
assertType<IsExact<typeof withoutCause, TestFailure>>(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ok() function type tests
|
|
102
|
+
test("ok() infers type from argument", () => {
|
|
103
|
+
const numResult = ok(42);
|
|
104
|
+
const strResult = ok("hello");
|
|
105
|
+
const objResult = ok({ foo: "bar" });
|
|
106
|
+
|
|
107
|
+
assertType<IsExact<typeof numResult, Result<number, never>>>(true);
|
|
108
|
+
assertType<IsExact<typeof strResult, Result<string, never>>>(true);
|
|
109
|
+
assertType<IsExact<typeof objResult, Result<{ foo: string }, never>>>(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// fail() function type tests
|
|
113
|
+
test("fail() curried form preserves code type", () => {
|
|
114
|
+
const NotFound = fail("NOT_FOUND");
|
|
115
|
+
const result = NotFound("Resource not found");
|
|
116
|
+
|
|
117
|
+
assertType<IsExact<typeof result, Result<never, Failure<"NOT_FOUND">>>>(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("fail() immediate form creates correct type", () => {
|
|
121
|
+
const result = fail("ERR", "error message");
|
|
122
|
+
assertType<IsExact<typeof result, Result<never, Failure<"ERR">>>>(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// fromThrowable() type tests
|
|
126
|
+
test("fromThrowable() with sync function", () => {
|
|
127
|
+
const result = fromThrowable(() => 42);
|
|
128
|
+
assertType<
|
|
129
|
+
IsExact<typeof result, Result<number, Failure<"UNKNOWN_FAILURE">>>
|
|
130
|
+
>(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("fromThrowable() with async function", () => {
|
|
134
|
+
const result = fromThrowable(() => Promise.resolve(42));
|
|
135
|
+
assertType<
|
|
136
|
+
IsExact<typeof result, AsyncResult<number, Failure<"UNKNOWN_FAILURE">>>
|
|
137
|
+
>(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("fromThrowable() with custom error mapper", () => {
|
|
141
|
+
type CustomError = { code: number; msg: string };
|
|
142
|
+
const result = fromThrowable(
|
|
143
|
+
() => 42,
|
|
144
|
+
(_e): CustomError => ({ code: 500, msg: "error" }),
|
|
145
|
+
);
|
|
146
|
+
assertType<IsExact<typeof result, Result<number, CustomError>>>(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("fromThrowable() async with custom error mapper", () => {
|
|
150
|
+
type CustomError = { code: number; msg: string };
|
|
151
|
+
const result = fromThrowable(
|
|
152
|
+
() => Promise.resolve(42),
|
|
153
|
+
(_e): CustomError => ({ code: 500, msg: "error" }),
|
|
154
|
+
);
|
|
155
|
+
assertType<IsExact<typeof result, AsyncResult<number, CustomError>>>(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// wrapThrowable() type tests
|
|
159
|
+
test("wrapThrowable() preserves function signature", () => {
|
|
160
|
+
const safeFn = wrapThrowable((x: number, y: string) => x + y.length);
|
|
161
|
+
type Expected = (
|
|
162
|
+
x: number,
|
|
163
|
+
y: string,
|
|
164
|
+
) => Result<number, Failure<"UNKNOWN_FAILURE">>;
|
|
165
|
+
assertType<IsExact<typeof safeFn, Expected>>(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("wrapThrowable() with async function", () => {
|
|
169
|
+
const safeFn = wrapThrowable((x: number) => Promise.resolve(x * 2));
|
|
170
|
+
type Expected = (
|
|
171
|
+
x: number,
|
|
172
|
+
) => AsyncResult<number, Failure<"UNKNOWN_FAILURE">>;
|
|
173
|
+
assertType<IsExact<typeof safeFn, Expected>>(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// fn() type tests
|
|
177
|
+
test("fn() with generator function", () => {
|
|
178
|
+
const compute = fn(function* (x: number) {
|
|
179
|
+
const value = yield* ok(x * 2);
|
|
180
|
+
return value + 1;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const result = compute.run(5);
|
|
184
|
+
assertType<IsExact<typeof result, Result<number, never>>>(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("fn() with async generator", () => {
|
|
188
|
+
const compute = fn(function* (x: number) {
|
|
189
|
+
const value = yield* fromThrowable(() => Promise.resolve(x * 2));
|
|
190
|
+
return value + 1;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const result = compute.run(5);
|
|
194
|
+
assertType<
|
|
195
|
+
IsExact<typeof result, AsyncResult<number, Failure<"UNKNOWN_FAILURE">>>
|
|
196
|
+
>(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("fn() with dependencies", () => {
|
|
200
|
+
const Dep = fn.dependency<string>()("dep");
|
|
201
|
+
|
|
202
|
+
const compute = fn(function* (x: number) {
|
|
203
|
+
const dep = yield* fn.require(Dep);
|
|
204
|
+
return ok(dep + x);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Before injection, should not have .run() available directly
|
|
208
|
+
// After injection, should have .run() available
|
|
209
|
+
const withDep = compute.inject(Dep.impl("test"));
|
|
210
|
+
const result = withDep.run(5);
|
|
211
|
+
assertType<IsExact<typeof result, Result<string, never>>>(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Result method type tests
|
|
215
|
+
test("Result.map() transforms value type", () => {
|
|
216
|
+
const result = ok(42);
|
|
217
|
+
if (result.isOk()) {
|
|
218
|
+
const mapped = result.map((x: number) => x.toString());
|
|
219
|
+
assertType<IsExact<typeof mapped, Result<string, never>>>(true);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("Result.mapFailure() returns Result with new error type", () => {
|
|
224
|
+
const result = fail("ERR", "error");
|
|
225
|
+
if (result.hasFailed()) {
|
|
226
|
+
const mapped = result.mapFailure((e: Failure<"ERR">) =>
|
|
227
|
+
fail("NEW_ERR", e.message),
|
|
228
|
+
);
|
|
229
|
+
// Check that mapped is a Result type
|
|
230
|
+
type CheckResult = typeof mapped extends Result<infer _T, infer _E>
|
|
231
|
+
? true
|
|
232
|
+
: false;
|
|
233
|
+
assertType<IsExact<CheckResult, true>>(true);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("AsyncResult.map() returns AsyncResult", () => {
|
|
238
|
+
const result = fromThrowable(() => Promise.resolve(42));
|
|
239
|
+
const mapped = result.map((x) => x * 2);
|
|
240
|
+
// Check that mapped is an AsyncResult (the exact type has unions that may not match exactly)
|
|
241
|
+
type CheckAsync = typeof mapped extends AsyncResult<infer _U, infer _E>
|
|
242
|
+
? true
|
|
243
|
+
: false;
|
|
244
|
+
assertType<IsExact<CheckAsync, true>>(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("Result.match() returns union of handler return types", () => {
|
|
248
|
+
const result: Result<number, Failure<"ERR">> = ok(42);
|
|
249
|
+
const matched = result.match({
|
|
250
|
+
ok: (v) => v * 2,
|
|
251
|
+
failed: (e) => e.code.length,
|
|
252
|
+
});
|
|
253
|
+
assertType<IsExact<typeof matched, number>>(true);
|
|
254
|
+
});
|