@valbuild/core 0.12.0 → 0.13.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.
- package/README.md +25 -37
- package/jest.config.js +4 -0
- package/package.json +1 -1
- package/src/Json.ts +4 -0
- package/src/expr/README.md +193 -0
- package/src/expr/eval.test.ts +202 -0
- package/src/expr/eval.ts +248 -0
- package/src/expr/expr.ts +91 -0
- package/src/expr/index.ts +3 -0
- package/src/expr/parser.test.ts +158 -0
- package/src/expr/parser.ts +229 -0
- package/src/expr/repl.ts +93 -0
- package/src/expr/tokenizer.test.ts +539 -0
- package/src/expr/tokenizer.ts +117 -0
- package/src/fetchVal.test.ts +164 -0
- package/src/fetchVal.ts +211 -0
- package/src/fp/array.ts +30 -0
- package/src/fp/index.ts +3 -0
- package/src/fp/result.ts +214 -0
- package/src/fp/util.ts +52 -0
- package/src/index.ts +55 -0
- package/src/initSchema.ts +45 -0
- package/src/initVal.ts +96 -0
- package/src/module.test.ts +170 -0
- package/src/module.ts +333 -0
- package/src/patch/deref.test.ts +300 -0
- package/src/patch/deref.ts +128 -0
- package/src/patch/index.ts +11 -0
- package/src/patch/json.test.ts +583 -0
- package/src/patch/json.ts +304 -0
- package/src/patch/operation.ts +74 -0
- package/src/patch/ops.ts +83 -0
- package/src/patch/parse.test.ts +202 -0
- package/src/patch/parse.ts +187 -0
- package/src/patch/patch.ts +46 -0
- package/src/patch/util.ts +67 -0
- package/src/schema/array.ts +52 -0
- package/src/schema/boolean.ts +38 -0
- package/src/schema/i18n.ts +65 -0
- package/src/schema/image.ts +70 -0
- package/src/schema/index.ts +46 -0
- package/src/schema/literal.ts +42 -0
- package/src/schema/number.ts +45 -0
- package/src/schema/object.ts +67 -0
- package/src/schema/oneOf.ts +60 -0
- package/src/schema/richtext.ts +417 -0
- package/src/schema/string.ts +49 -0
- package/src/schema/union.ts +62 -0
- package/src/selector/ExprProxy.test.ts +203 -0
- package/src/selector/ExprProxy.ts +209 -0
- package/src/selector/SelectorProxy.test.ts +172 -0
- package/src/selector/SelectorProxy.ts +237 -0
- package/src/selector/array.ts +37 -0
- package/src/selector/boolean.ts +4 -0
- package/src/selector/file.ts +14 -0
- package/src/selector/i18n.ts +13 -0
- package/src/selector/index.ts +159 -0
- package/src/selector/number.ts +4 -0
- package/src/selector/object.ts +22 -0
- package/src/selector/primitive.ts +17 -0
- package/src/selector/remote.ts +9 -0
- package/src/selector/selector.test.ts +453 -0
- package/src/selector/selectorOf.ts +7 -0
- package/src/selector/string.ts +4 -0
- package/src/source/file.ts +45 -0
- package/src/source/i18n.ts +60 -0
- package/src/source/index.ts +50 -0
- package/src/source/remote.ts +54 -0
- package/src/val/array.ts +10 -0
- package/src/val/index.ts +90 -0
- package/src/val/object.ts +13 -0
- package/src/val/primitive.ts +8 -0
@@ -0,0 +1,164 @@
|
|
1
|
+
import { initSchema } from "./initSchema";
|
2
|
+
import { content } from "./module";
|
3
|
+
import { getValPath } from "./val";
|
4
|
+
import { serializedValOfSelectorSource, fetchVal } from "./fetchVal";
|
5
|
+
|
6
|
+
const s = initSchema(["en_US", "no_NB"]);
|
7
|
+
// const i18n = initI18n(["en_US", "no_NB"]);
|
8
|
+
|
9
|
+
describe("serialization of val", () => {
|
10
|
+
test("serialized val: string", () => {
|
11
|
+
const schema = s.string();
|
12
|
+
|
13
|
+
const testVal = content("/app", schema, "foo");
|
14
|
+
|
15
|
+
expect(serializedValOfSelectorSource(testVal)).toStrictEqual({
|
16
|
+
val: "foo",
|
17
|
+
valPath: "/app",
|
18
|
+
});
|
19
|
+
});
|
20
|
+
|
21
|
+
test("serialized val: array", () => {
|
22
|
+
const schema = s.array(s.string());
|
23
|
+
|
24
|
+
const testVal = content("/app", schema, ["foo", "bar"]);
|
25
|
+
|
26
|
+
expect(serializedValOfSelectorSource(testVal)).toStrictEqual({
|
27
|
+
val: [
|
28
|
+
{
|
29
|
+
val: "foo",
|
30
|
+
valPath: "/app.0",
|
31
|
+
},
|
32
|
+
{ val: "bar", valPath: "/app.1" },
|
33
|
+
],
|
34
|
+
valPath: "/app",
|
35
|
+
});
|
36
|
+
|
37
|
+
// ^?
|
38
|
+
});
|
39
|
+
|
40
|
+
test("serialized val: object", () => {
|
41
|
+
const schema = s.object({ foo: s.object({ bar: s.array(s.string()) }) });
|
42
|
+
|
43
|
+
const testVal = content("/app", schema, { foo: { bar: ["foo", "bar"] } });
|
44
|
+
|
45
|
+
expect(serializedValOfSelectorSource(testVal)).toStrictEqual({
|
46
|
+
val: {
|
47
|
+
foo: {
|
48
|
+
val: {
|
49
|
+
bar: {
|
50
|
+
val: [
|
51
|
+
{
|
52
|
+
val: "foo",
|
53
|
+
valPath: '/app."foo"."bar".0',
|
54
|
+
},
|
55
|
+
{ val: "bar", valPath: '/app."foo"."bar".1' },
|
56
|
+
],
|
57
|
+
valPath: '/app."foo"."bar"',
|
58
|
+
},
|
59
|
+
},
|
60
|
+
valPath: '/app."foo"',
|
61
|
+
},
|
62
|
+
},
|
63
|
+
valPath: "/app",
|
64
|
+
});
|
65
|
+
});
|
66
|
+
});
|
67
|
+
|
68
|
+
describe("fetchVal", () => {
|
69
|
+
test("valuate: string", async () => {
|
70
|
+
const schema = s.string();
|
71
|
+
|
72
|
+
const testVal = content("/app", schema, "foo");
|
73
|
+
|
74
|
+
const test = await fetchVal(testVal);
|
75
|
+
// ^? should be Val<string>
|
76
|
+
expect(test.val).toBe("foo");
|
77
|
+
expect(getValPath(test)).toBe("/app");
|
78
|
+
});
|
79
|
+
|
80
|
+
test("valuate: array", async () => {
|
81
|
+
const schema = s.array(s.string());
|
82
|
+
|
83
|
+
const testVal = content("/app", schema, ["foo", "bar"]);
|
84
|
+
|
85
|
+
const test = await fetchVal(testVal);
|
86
|
+
// ^? should be Val<string[]>
|
87
|
+
expect(test.val).toStrictEqual(["foo", "bar"]);
|
88
|
+
expect(test[0].val).toStrictEqual("foo");
|
89
|
+
expect(test[1].val).toStrictEqual("bar");
|
90
|
+
expect(getValPath(test[0])).toStrictEqual("/app.0");
|
91
|
+
expect(getValPath(test[1])).toStrictEqual("/app.1");
|
92
|
+
});
|
93
|
+
|
94
|
+
test("valuate: object", async () => {
|
95
|
+
const schema = s.object({ foo: s.object({ bar: s.array(s.string()) }) });
|
96
|
+
|
97
|
+
const testVal = content("/app", schema, { foo: { bar: ["foo", "bar"] } });
|
98
|
+
|
99
|
+
const test = await fetchVal(testVal);
|
100
|
+
// ^? should be Val<{ foo: { bar: string[] } }>
|
101
|
+
|
102
|
+
expect(test.val).toStrictEqual({ foo: { bar: ["foo", "bar"] } });
|
103
|
+
expect(test.foo.val).toStrictEqual({ bar: ["foo", "bar"] });
|
104
|
+
expect(test.foo.bar.val).toStrictEqual(["foo", "bar"]);
|
105
|
+
expect(test.foo.bar[0].val).toStrictEqual("foo");
|
106
|
+
expect(test.foo.bar[1].val).toStrictEqual("bar");
|
107
|
+
expect(getValPath(test.foo.bar[0])).toStrictEqual('/app."foo"."bar".0');
|
108
|
+
expect(getValPath(test.foo.bar[1])).toStrictEqual('/app."foo"."bar".1');
|
109
|
+
});
|
110
|
+
|
111
|
+
test("valuate: array with map", async () => {
|
112
|
+
const schema = s.array(s.string());
|
113
|
+
|
114
|
+
const testVal = content("/app", schema, ["foo", "bar"]);
|
115
|
+
|
116
|
+
const test = await fetchVal({
|
117
|
+
// ^? should be Val<{ title: string }[]>
|
118
|
+
foo: testVal.map((v) => ({ title: v })),
|
119
|
+
test: testVal,
|
120
|
+
});
|
121
|
+
expect(test.val).toStrictEqual({
|
122
|
+
foo: [{ title: "foo" }, { title: "bar" }],
|
123
|
+
test: ["foo", "bar"],
|
124
|
+
});
|
125
|
+
});
|
126
|
+
|
127
|
+
test("valuate: 2 modules with oneOf", async () => {
|
128
|
+
const testVal1 = content("/testVal1", s.array(s.string()), [
|
129
|
+
"test-val-1-0",
|
130
|
+
"test-val-1-1",
|
131
|
+
]);
|
132
|
+
const testVal2 = content(
|
133
|
+
"/testVal2",
|
134
|
+
s.object({ test1: s.oneOf(testVal1), test2: s.string() }),
|
135
|
+
{
|
136
|
+
test2: "test2 value",
|
137
|
+
test1: testVal1[0],
|
138
|
+
}
|
139
|
+
);
|
140
|
+
|
141
|
+
const test = await fetchVal({
|
142
|
+
// ^?
|
143
|
+
testVal1: testVal1.map((v) => ({ title: v, otherModule: testVal2 })),
|
144
|
+
testVal2: testVal2,
|
145
|
+
});
|
146
|
+
expect(test.val).toStrictEqual({
|
147
|
+
testVal1: [
|
148
|
+
{
|
149
|
+
title: "test-val-1-0",
|
150
|
+
otherModule: { test2: "test2 value", test1: "test-val-1-0" },
|
151
|
+
},
|
152
|
+
{
|
153
|
+
title: "test-val-1-1",
|
154
|
+
otherModule: { test2: "test2 value", test1: "test-val-1-0" },
|
155
|
+
},
|
156
|
+
],
|
157
|
+
testVal2: { test2: "test2 value", test1: "test-val-1-0" },
|
158
|
+
});
|
159
|
+
expect(getValPath(test.testVal1[0].otherModule.test1)).toStrictEqual(
|
160
|
+
"/testVal1.0"
|
161
|
+
);
|
162
|
+
expect(getValPath(test.testVal2.test2)).toStrictEqual('/testVal2."test2"');
|
163
|
+
});
|
164
|
+
});
|
package/src/fetchVal.ts
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
import {
|
2
|
+
GenericSelector,
|
3
|
+
Path,
|
4
|
+
SelectorOf,
|
5
|
+
SelectorSource,
|
6
|
+
SourceOrExpr,
|
7
|
+
} from "./selector";
|
8
|
+
import {
|
9
|
+
isSerializedVal,
|
10
|
+
JsonOfSource,
|
11
|
+
SerializedVal,
|
12
|
+
SourcePath,
|
13
|
+
Val,
|
14
|
+
} from "./val";
|
15
|
+
import {
|
16
|
+
createValPathOfItem,
|
17
|
+
isSelector,
|
18
|
+
newSelectorProxy,
|
19
|
+
} from "./selector/SelectorProxy";
|
20
|
+
import { Json } from "./Json";
|
21
|
+
|
22
|
+
export function fetchVal<T extends SelectorSource>(
|
23
|
+
selector: T,
|
24
|
+
locale?: string
|
25
|
+
): SelectorOf<T> extends GenericSelector<infer S>
|
26
|
+
? Promise<Val<JsonOfSource<S>>>
|
27
|
+
: never {
|
28
|
+
return Promise.resolve(
|
29
|
+
getVal(selector, locale) as unknown
|
30
|
+
) as SelectorOf<T> extends GenericSelector<infer S>
|
31
|
+
? Promise<Val<JsonOfSource<S>>>
|
32
|
+
: never;
|
33
|
+
}
|
34
|
+
|
35
|
+
export function getVal<T extends SelectorSource>(
|
36
|
+
selector: T,
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
38
|
+
locale?: string
|
39
|
+
): SelectorOf<T> extends GenericSelector<infer S>
|
40
|
+
? Val<JsonOfSource<S>>
|
41
|
+
: never {
|
42
|
+
return newValProxy(
|
43
|
+
serializedValOfSelectorSource(selector)
|
44
|
+
) as SelectorOf<T> extends GenericSelector<infer S>
|
45
|
+
? Val<JsonOfSource<S>>
|
46
|
+
: never;
|
47
|
+
}
|
48
|
+
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
50
|
+
function isArrayOrArraySelector(child: any) {
|
51
|
+
if (isSelector(child)) {
|
52
|
+
return (
|
53
|
+
typeof child[SourceOrExpr] === "object" &&
|
54
|
+
typeof child[SourceOrExpr] !== null &&
|
55
|
+
Array.isArray(child[SourceOrExpr])
|
56
|
+
);
|
57
|
+
}
|
58
|
+
return Array.isArray(child);
|
59
|
+
}
|
60
|
+
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
62
|
+
function isObjectOrObjectSelector(child: any) {
|
63
|
+
if (isSelector(child)) {
|
64
|
+
return (
|
65
|
+
typeof child[SourceOrExpr] === "object" &&
|
66
|
+
typeof child[SourceOrExpr] !== null &&
|
67
|
+
!Array.isArray(child[SourceOrExpr])
|
68
|
+
);
|
69
|
+
}
|
70
|
+
return typeof child === "object";
|
71
|
+
}
|
72
|
+
|
73
|
+
export function serializedValOfSelectorSource<T extends SelectorSource>(
|
74
|
+
selector: T
|
75
|
+
) {
|
76
|
+
const wrappedSelector = newSelectorProxy(selector); // NOTE: we do this if call-site uses a literal with selectors inside
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
78
|
+
function rec(child: any): any {
|
79
|
+
const isArray = isArrayOrArraySelector(child);
|
80
|
+
const isObject = isObjectOrObjectSelector(child);
|
81
|
+
if (isArray) {
|
82
|
+
const array = (
|
83
|
+
SourceOrExpr in child ? child[SourceOrExpr] : child
|
84
|
+
) as Array<unknown>;
|
85
|
+
const valPath = Path in child ? (child[Path] as SourcePath) : undefined;
|
86
|
+
return {
|
87
|
+
val: array.map((item, i) =>
|
88
|
+
rec(
|
89
|
+
isSelector(item) // NOTE: We do this since selectors currently do not create selectors of items unless specifically required.
|
90
|
+
? item
|
91
|
+
: newSelectorProxy(item, createValPathOfItem(valPath, i))
|
92
|
+
)
|
93
|
+
),
|
94
|
+
valPath,
|
95
|
+
};
|
96
|
+
} else if (isObject) {
|
97
|
+
const obj = (
|
98
|
+
SourceOrExpr in child ? child[SourceOrExpr] : child
|
99
|
+
) as object;
|
100
|
+
const valPath = Path in child ? (child[Path] as SourcePath) : undefined;
|
101
|
+
return {
|
102
|
+
val:
|
103
|
+
obj !== null &&
|
104
|
+
Object.fromEntries(
|
105
|
+
Object.entries(obj).map(([key, value]) => [
|
106
|
+
key,
|
107
|
+
rec(
|
108
|
+
isSelector(value) // NOTE: We do this since selectors currently do not create selectors of items unless specifically required.
|
109
|
+
? value
|
110
|
+
: newSelectorProxy(value, createValPathOfItem(valPath, key))
|
111
|
+
),
|
112
|
+
])
|
113
|
+
),
|
114
|
+
valPath,
|
115
|
+
};
|
116
|
+
} else if (isSelector(child)) {
|
117
|
+
return {
|
118
|
+
val: rec(child[SourceOrExpr]),
|
119
|
+
valPath: child[Path],
|
120
|
+
};
|
121
|
+
} else {
|
122
|
+
return child;
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
return rec(wrappedSelector);
|
127
|
+
}
|
128
|
+
|
129
|
+
function strip(value: SerializedVal | Json): Json {
|
130
|
+
const val = isSerializedVal(value) ? value.val : value;
|
131
|
+
switch (typeof val) {
|
132
|
+
case "function":
|
133
|
+
case "symbol":
|
134
|
+
throw Error(`Invalid val type: ${typeof val}`);
|
135
|
+
case "object":
|
136
|
+
if (val === null) {
|
137
|
+
return null;
|
138
|
+
} else if (Array.isArray(val)) {
|
139
|
+
return val.map(strip);
|
140
|
+
} else {
|
141
|
+
return Object.fromEntries(
|
142
|
+
Object.entries(val).map(([key, value]) => [
|
143
|
+
key,
|
144
|
+
value && strip(value),
|
145
|
+
])
|
146
|
+
) as Json;
|
147
|
+
}
|
148
|
+
// intentional fallthrough
|
149
|
+
// eslint-disable-next-line no-fallthrough
|
150
|
+
default:
|
151
|
+
return val;
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
function newValProxy<T extends Json>(val: SerializedVal): Val<T> {
|
156
|
+
const source = val.val;
|
157
|
+
switch (typeof source) {
|
158
|
+
case "function":
|
159
|
+
case "symbol":
|
160
|
+
throw Error(`Invalid val type: ${typeof source}`);
|
161
|
+
case "object":
|
162
|
+
if (source !== null) {
|
163
|
+
// Handles both objects and arrays!
|
164
|
+
return new Proxy(source, {
|
165
|
+
has(target, prop: string | symbol) {
|
166
|
+
if (prop === "val") {
|
167
|
+
return true;
|
168
|
+
}
|
169
|
+
if (prop === Path) {
|
170
|
+
return true;
|
171
|
+
}
|
172
|
+
return hasOwn(target, prop);
|
173
|
+
},
|
174
|
+
get(target, prop: string | symbol) {
|
175
|
+
if (prop === Path) {
|
176
|
+
return val.valPath;
|
177
|
+
}
|
178
|
+
if (prop === "val") {
|
179
|
+
return strip(val);
|
180
|
+
}
|
181
|
+
if (Array.isArray(target) && prop === "length") {
|
182
|
+
return target.length;
|
183
|
+
}
|
184
|
+
if (hasOwn(source, prop)) {
|
185
|
+
return newValProxy({
|
186
|
+
val: Reflect.get(target, prop).val,
|
187
|
+
valPath:
|
188
|
+
Reflect.get(target, prop)?.valPath ??
|
189
|
+
createValPathOfItem(
|
190
|
+
val.valPath,
|
191
|
+
Array.isArray(target) ? Number(prop) : prop
|
192
|
+
),
|
193
|
+
} as SerializedVal);
|
194
|
+
}
|
195
|
+
return Reflect.get(target, prop);
|
196
|
+
},
|
197
|
+
}) as unknown as Val<T>;
|
198
|
+
}
|
199
|
+
// intentional fallthrough
|
200
|
+
// eslint-disable-next-line no-fallthrough
|
201
|
+
default:
|
202
|
+
return {
|
203
|
+
[Path]: val.valPath,
|
204
|
+
val: val.val,
|
205
|
+
} as Val<T>;
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
function hasOwn<T extends PropertyKey>(obj: object, prop: T): boolean {
|
210
|
+
return Object.prototype.hasOwnProperty.call(obj, prop);
|
211
|
+
}
|
package/src/fp/array.ts
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
export type NonEmptyArray<T> = [T, ...T[]];
|
2
|
+
export type ReadonlyNonEmptyArray<T> = readonly [T, ...T[]];
|
3
|
+
|
4
|
+
export function isNonEmpty<T>(array: Array<T>): array is NonEmptyArray<T>;
|
5
|
+
export function isNonEmpty<T>(
|
6
|
+
array: ReadonlyArray<T>
|
7
|
+
): array is ReadonlyNonEmptyArray<T> {
|
8
|
+
return array.length > 0;
|
9
|
+
}
|
10
|
+
|
11
|
+
export function flatten<T>(
|
12
|
+
array: ReadonlyNonEmptyArray<ReadonlyNonEmptyArray<T>>
|
13
|
+
): NonEmptyArray<T>;
|
14
|
+
export function flatten<T>(array: ReadonlyArray<ReadonlyArray<T>>): Array<T> {
|
15
|
+
return array.flat(1);
|
16
|
+
}
|
17
|
+
|
18
|
+
export function map<T, U>(
|
19
|
+
fn: (value: T, index: number) => U
|
20
|
+
): {
|
21
|
+
(array: ReadonlyArray<T>): Array<U>;
|
22
|
+
(array: ReadonlyNonEmptyArray<T>): NonEmptyArray<U>;
|
23
|
+
} {
|
24
|
+
function mapFn(array: ReadonlyArray<T>): Array<U>;
|
25
|
+
function mapFn(array: ReadonlyNonEmptyArray<T>): NonEmptyArray<U>;
|
26
|
+
function mapFn(array: ReadonlyArray<T>): Array<U> {
|
27
|
+
return array.map(fn);
|
28
|
+
}
|
29
|
+
return mapFn;
|
30
|
+
}
|
package/src/fp/index.ts
ADDED
package/src/fp/result.ts
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
import { isNonEmpty, NonEmptyArray } from "./array";
|
2
|
+
|
3
|
+
export type Ok<T> = {
|
4
|
+
readonly kind: "ok";
|
5
|
+
readonly value: T;
|
6
|
+
};
|
7
|
+
export type Err<E> = {
|
8
|
+
readonly kind: "err";
|
9
|
+
readonly error: E;
|
10
|
+
};
|
11
|
+
|
12
|
+
export type Result<T, E> = Ok<T> | Err<E>;
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Singleton instance of Ok<void>. Used to optimize results whose Ok values are
|
16
|
+
* void.
|
17
|
+
*/
|
18
|
+
export const voidOk: Ok<void> = Object.freeze({
|
19
|
+
kind: "ok",
|
20
|
+
value: undefined,
|
21
|
+
});
|
22
|
+
|
23
|
+
export function ok<T>(value: T): Ok<T> {
|
24
|
+
if (value === undefined) return voidOk as Ok<T>;
|
25
|
+
return {
|
26
|
+
kind: "ok",
|
27
|
+
value,
|
28
|
+
};
|
29
|
+
}
|
30
|
+
|
31
|
+
export function err<E>(error: E): Err<E> {
|
32
|
+
return {
|
33
|
+
kind: "err",
|
34
|
+
error,
|
35
|
+
};
|
36
|
+
}
|
37
|
+
|
38
|
+
export function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
|
39
|
+
return result === voidOk || result.kind === "ok";
|
40
|
+
}
|
41
|
+
|
42
|
+
export function isErr<T, E>(result: Result<T, E>): result is Err<E> {
|
43
|
+
return result !== voidOk && result.kind === "err";
|
44
|
+
}
|
45
|
+
|
46
|
+
export type OkType<R> = R extends Result<infer T, unknown> ? T : never;
|
47
|
+
export type ErrType<R> = R extends Result<unknown, infer E> ? E : never;
|
48
|
+
|
49
|
+
/**
|
50
|
+
* If all results are Ok (or if results is empty), returns Ok with all the Ok
|
51
|
+
* values concatenated into an array. If any result is Err, returns Err with all
|
52
|
+
* Err values concatenated into an array.
|
53
|
+
*
|
54
|
+
* @see {@link all} for use with simple array types.
|
55
|
+
*/
|
56
|
+
export function allT<T extends unknown[], E>(results: {
|
57
|
+
readonly [P in keyof T]: Result<T[P], E>;
|
58
|
+
}): Result<T, NonEmptyArray<E>> {
|
59
|
+
const values: T[number][] = [];
|
60
|
+
const errors: E[] = [];
|
61
|
+
for (const result of results) {
|
62
|
+
if (isOk(result)) {
|
63
|
+
values.push(result.value);
|
64
|
+
} else {
|
65
|
+
errors.push(result.error);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
if (isNonEmpty(errors)) {
|
69
|
+
return err(errors);
|
70
|
+
} else {
|
71
|
+
return ok(values as T);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
/**
|
76
|
+
* If all results are Ok (or if results is empty), returns Ok with all the Ok
|
77
|
+
* values concatenated into an array. If any result is Err, returns Err with all
|
78
|
+
* Err values concatenated into an array.
|
79
|
+
*
|
80
|
+
* @see {@link allT} for use with tuple types.
|
81
|
+
*/
|
82
|
+
export function all<T, E>(
|
83
|
+
results: readonly Result<T, E>[]
|
84
|
+
): Result<T[], NonEmptyArray<E>> {
|
85
|
+
return allT<T[], E>(results);
|
86
|
+
}
|
87
|
+
|
88
|
+
/**
|
89
|
+
* If all results are Ok (or if results is empty), returns Ok. If any result is
|
90
|
+
* Err, returns Err with all Err values concatenated into an array.
|
91
|
+
*/
|
92
|
+
export function allV<E>(
|
93
|
+
results: readonly Result<unknown, E>[]
|
94
|
+
): Result<void, NonEmptyArray<E>> {
|
95
|
+
const errs: E[] = [];
|
96
|
+
for (const result of results) {
|
97
|
+
if (isErr(result)) {
|
98
|
+
errs.push(result.error);
|
99
|
+
}
|
100
|
+
}
|
101
|
+
if (isNonEmpty(errs)) {
|
102
|
+
return err(errs);
|
103
|
+
}
|
104
|
+
return voidOk;
|
105
|
+
}
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Perform a reduction over an array with a Result-returning reducer. If the
|
109
|
+
* reducer returns Ok, its value is used as the next value. If the reducer
|
110
|
+
* returns Err, it is returned immediately.
|
111
|
+
*
|
112
|
+
* flatMapReduce is a short-circuiting equivalent to:
|
113
|
+
* ```
|
114
|
+
* arr.reduce(
|
115
|
+
* (accRes, current, currentIndex) =>
|
116
|
+
* flatMap((acc) => reducer(acc, current, currentIndex))(accRes),
|
117
|
+
* ok(initVal)
|
118
|
+
* )
|
119
|
+
* ```
|
120
|
+
*/
|
121
|
+
export function flatMapReduce<T, E, A>(
|
122
|
+
reducer: (acc: T, current: A, currentIndex: number) => Result<T, E>,
|
123
|
+
initVal: T
|
124
|
+
): (arr: readonly A[]) => Result<T, E> {
|
125
|
+
return (arr) => {
|
126
|
+
let val: Result<T, E> = ok(initVal);
|
127
|
+
for (let i = 0; i < arr.length && isOk(val); ++i) {
|
128
|
+
val = reducer(val.value, arr[i], i);
|
129
|
+
}
|
130
|
+
return val;
|
131
|
+
};
|
132
|
+
}
|
133
|
+
|
134
|
+
export function map<T0, T1>(
|
135
|
+
onOk: (value: T0) => T1
|
136
|
+
): <E>(result: Result<T0, E>) => Result<T1, E> {
|
137
|
+
return (result) => {
|
138
|
+
if (isOk(result)) {
|
139
|
+
return ok(onOk(result.value));
|
140
|
+
} else {
|
141
|
+
return result;
|
142
|
+
}
|
143
|
+
};
|
144
|
+
}
|
145
|
+
|
146
|
+
export function flatMap<T0, T1, E1>(
|
147
|
+
onOk: (value: T0) => Result<T1, E1>
|
148
|
+
): <E>(result: Result<T0, E>) => Result<T1, E | E1> {
|
149
|
+
return (result) => {
|
150
|
+
if (isOk(result)) {
|
151
|
+
return onOk(result.value);
|
152
|
+
} else {
|
153
|
+
return result;
|
154
|
+
}
|
155
|
+
};
|
156
|
+
}
|
157
|
+
|
158
|
+
export function mapErr<E0, E1>(
|
159
|
+
onErr: (error: E0) => E1
|
160
|
+
): <T>(result: Result<T, E0>) => Result<T, E1> {
|
161
|
+
return (result) => {
|
162
|
+
if (isErr(result)) {
|
163
|
+
return err(onErr(result.error));
|
164
|
+
} else {
|
165
|
+
return result;
|
166
|
+
}
|
167
|
+
};
|
168
|
+
}
|
169
|
+
|
170
|
+
export function fromPredicate<T0, T1 extends T0, E>(
|
171
|
+
refinement: (value: T0) => value is T1,
|
172
|
+
onFalse: (value: T0) => E
|
173
|
+
): (value: T0) => Result<T1, E>;
|
174
|
+
export function fromPredicate<T0, E>(
|
175
|
+
refinement: (value: T0) => boolean,
|
176
|
+
onFalse: (value: T0) => E
|
177
|
+
): <T1 extends T0>(value: T1) => Result<T1, E> {
|
178
|
+
return (value) => {
|
179
|
+
if (refinement(value)) {
|
180
|
+
return ok(value);
|
181
|
+
} else {
|
182
|
+
return err(onFalse(value));
|
183
|
+
}
|
184
|
+
};
|
185
|
+
}
|
186
|
+
|
187
|
+
// NOTE: Function overload resolution seems to fail when declared as overloaded
|
188
|
+
// function type, so a value with a callable type is used instead.
|
189
|
+
export const filterOrElse: {
|
190
|
+
<T0, T1 extends T0, E>(
|
191
|
+
refinement: (value: T0) => value is T1,
|
192
|
+
onFalse: (value: T0) => E
|
193
|
+
): (result: Result<T0, E>) => Result<T1, E>;
|
194
|
+
<T0, E>(refinement: (value: T0) => boolean, onFalse: (value: T0) => E): <
|
195
|
+
T1 extends T0
|
196
|
+
>(
|
197
|
+
result: Result<T1, E>
|
198
|
+
) => Result<T1, E>;
|
199
|
+
} = <T0, E>(
|
200
|
+
refinement: (value: T0) => boolean,
|
201
|
+
onFalse: (value: T0) => E
|
202
|
+
): (<T1 extends T0>(result: Result<T1, E>) => Result<T1, E>) => {
|
203
|
+
return (result) => {
|
204
|
+
if (isOk(result)) {
|
205
|
+
if (refinement(result.value)) {
|
206
|
+
return result;
|
207
|
+
} else {
|
208
|
+
return err(onFalse(result.value));
|
209
|
+
}
|
210
|
+
} else {
|
211
|
+
return result;
|
212
|
+
}
|
213
|
+
};
|
214
|
+
};
|
package/src/fp/util.ts
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
export function pipe<A>(a: A): A;
|
2
|
+
export function pipe<A, B>(a: A, ab: (a: A) => B): B;
|
3
|
+
export function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C;
|
4
|
+
export function pipe<A, B, C, D>(
|
5
|
+
a: A,
|
6
|
+
ab: (a: A) => B,
|
7
|
+
bc: (b: B) => C,
|
8
|
+
cd: (c: C) => D
|
9
|
+
): D;
|
10
|
+
export function pipe<A, B, C, D, E>(
|
11
|
+
a: A,
|
12
|
+
ab: (a: A) => B,
|
13
|
+
bc: (b: B) => C,
|
14
|
+
cd: (c: C) => D,
|
15
|
+
de: (d: D) => E
|
16
|
+
): E;
|
17
|
+
export function pipe<A, B, C, D, E, F>(
|
18
|
+
a: A,
|
19
|
+
ab: (a: A) => B,
|
20
|
+
bc: (b: B) => C,
|
21
|
+
cd: (c: C) => D,
|
22
|
+
de: (d: D) => E,
|
23
|
+
ef: (e: E) => F
|
24
|
+
): F;
|
25
|
+
// "Ought to be enough for everybody" -- attributed to E. Åmdal
|
26
|
+
export function pipe<A, B, C, D, E, F, G>(
|
27
|
+
a: A,
|
28
|
+
ab: (a: A) => B,
|
29
|
+
bc: (b: B) => C,
|
30
|
+
cd: (c: C) => D,
|
31
|
+
de: (d: D) => E,
|
32
|
+
ef: (e: E) => F,
|
33
|
+
fg: (f: F) => G
|
34
|
+
): G;
|
35
|
+
export function pipe(a: unknown, ...fns: ((u: unknown) => unknown)[]): unknown {
|
36
|
+
let current = a;
|
37
|
+
for (const fn of fns) {
|
38
|
+
current = fn(current);
|
39
|
+
}
|
40
|
+
return current;
|
41
|
+
}
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Runs the callback with the supplied value, then returns the value. Useful for
|
45
|
+
* debugging pipes.
|
46
|
+
*/
|
47
|
+
export function tap<T>(callback: (value: T) => void): (value: T) => T {
|
48
|
+
return (value) => {
|
49
|
+
callback(value);
|
50
|
+
return value;
|
51
|
+
};
|
52
|
+
}
|