@valbuild/core 0.12.0 → 0.13.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/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,453 @@
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
import { Selector, GenericSelector, SourceOrExpr, Path } from ".";
|
3
|
+
import { string } from "../schema/string";
|
4
|
+
import { array } from "../schema/array";
|
5
|
+
import { SourcePath } from "../val";
|
6
|
+
import { Source } from "../source";
|
7
|
+
import { evaluate } from "../expr/eval";
|
8
|
+
import * as expr from "../expr/expr";
|
9
|
+
import { result } from "../../fp";
|
10
|
+
import { object } from "../schema/object";
|
11
|
+
import { newSelectorProxy, selectorToVal } from "./SelectorProxy";
|
12
|
+
import { newExprSelectorProxy } from "./ExprProxy";
|
13
|
+
import { remote, RemoteSource } from "../source/remote";
|
14
|
+
|
15
|
+
const modules = {
|
16
|
+
"/app/text": "text1",
|
17
|
+
"/app/texts": ["text1", "text2"] as string[],
|
18
|
+
"/app/blog": { title: "blog1", text: "text1" } as {
|
19
|
+
title: string | null;
|
20
|
+
text: string;
|
21
|
+
},
|
22
|
+
"/app/blogs": [
|
23
|
+
{ title: "blog1", text: "text1" },
|
24
|
+
{ title: undefined, text: "text2" },
|
25
|
+
] as { title: string | null; text: string }[],
|
26
|
+
"/app/empty": "",
|
27
|
+
"/app/large/nested": BFV(),
|
28
|
+
};
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
30
|
+
const remoteModules: {
|
31
|
+
[key in keyof TestModules]: RemoteSource<TestModules[key]>;
|
32
|
+
} = {
|
33
|
+
"/app/text": remote("/app/text"),
|
34
|
+
"/app/texts": remote("/app/texts"),
|
35
|
+
"/app/blog": remote("/app/blog"),
|
36
|
+
"/app/blogs": remote("/app/blogs"),
|
37
|
+
"/app/empty": remote("/app/empty"),
|
38
|
+
"/app/large/nested": remote("/app/large/nested"),
|
39
|
+
};
|
40
|
+
|
41
|
+
const SelectorModuleTestCases: {
|
42
|
+
description: string;
|
43
|
+
input: (remote: boolean) => GenericSelector<Source>;
|
44
|
+
expected: Expected;
|
45
|
+
}[] = [
|
46
|
+
{
|
47
|
+
description: "string module lookup",
|
48
|
+
input: (remote) => testModule("/app/text", remote),
|
49
|
+
expected: {
|
50
|
+
val: "text1",
|
51
|
+
[Path]: "/app/text",
|
52
|
+
},
|
53
|
+
},
|
54
|
+
{
|
55
|
+
description: "basic eq",
|
56
|
+
input: (remote) => testModule("/app/text", remote).eq("text1"),
|
57
|
+
expected: {
|
58
|
+
val: true,
|
59
|
+
[Path]: undefined,
|
60
|
+
},
|
61
|
+
},
|
62
|
+
{
|
63
|
+
description: "andThen noop",
|
64
|
+
input: (remote) => testModule("/app/text", remote).andThen((v) => v),
|
65
|
+
expected: {
|
66
|
+
val: "text1",
|
67
|
+
[Path]: "/app/text",
|
68
|
+
},
|
69
|
+
},
|
70
|
+
{
|
71
|
+
description: "array module lookup",
|
72
|
+
input: (remote) => testModule("/app/texts", remote),
|
73
|
+
expected: {
|
74
|
+
val: ["text1", "text2"],
|
75
|
+
[Path]: "/app/texts",
|
76
|
+
},
|
77
|
+
},
|
78
|
+
{
|
79
|
+
description: "string andThen eq",
|
80
|
+
input: (remote) =>
|
81
|
+
testModule("/app/text", remote).andThen((v) => v.eq("text1")),
|
82
|
+
expected: {
|
83
|
+
val: true,
|
84
|
+
[Path]: undefined,
|
85
|
+
},
|
86
|
+
},
|
87
|
+
{
|
88
|
+
description: "empty string andThen eq",
|
89
|
+
input: (remote) =>
|
90
|
+
testModule("/app/empty", remote).andThen((v) => v.eq("text1")),
|
91
|
+
expected: {
|
92
|
+
val: "",
|
93
|
+
[Path]: "/app/empty",
|
94
|
+
},
|
95
|
+
},
|
96
|
+
{
|
97
|
+
description: "andThen literal eq",
|
98
|
+
input: (remote) =>
|
99
|
+
testModule("/app/text", remote)
|
100
|
+
.andThen(() => "foo")
|
101
|
+
.eq("foo"),
|
102
|
+
expected: {
|
103
|
+
val: true,
|
104
|
+
[Path]: undefined,
|
105
|
+
},
|
106
|
+
},
|
107
|
+
{
|
108
|
+
description: "andThen undefined literal eq",
|
109
|
+
input: (remote) =>
|
110
|
+
testModule("/app/text", remote)
|
111
|
+
.andThen(() => undefined)
|
112
|
+
.eq("foo"),
|
113
|
+
expected: {
|
114
|
+
val: false,
|
115
|
+
[Path]: undefined,
|
116
|
+
},
|
117
|
+
},
|
118
|
+
{
|
119
|
+
description: "empty andThen literal eq",
|
120
|
+
input: (remote) =>
|
121
|
+
testModule("/app/empty", remote)
|
122
|
+
.andThen(() => "foo")
|
123
|
+
.eq("foo"),
|
124
|
+
expected: {
|
125
|
+
val: false,
|
126
|
+
[Path]: undefined,
|
127
|
+
},
|
128
|
+
},
|
129
|
+
{
|
130
|
+
description: "string andThen array literal and index",
|
131
|
+
input: (remote) =>
|
132
|
+
testModule("/app/text", remote).andThen((v) => [v, "text2"])[0],
|
133
|
+
expected: { val: "text1", [Path]: "/app/text" },
|
134
|
+
},
|
135
|
+
{
|
136
|
+
description: "string map undefined -> null literal conversion",
|
137
|
+
input: (remote) =>
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
139
|
+
testModule("/app/blogs", remote).map((v) => ({ title: undefined })),
|
140
|
+
expected: {
|
141
|
+
val: [{ title: null }, { title: null }],
|
142
|
+
[Path]: "/app/blogs",
|
143
|
+
},
|
144
|
+
},
|
145
|
+
{
|
146
|
+
description: "array map noop",
|
147
|
+
input: (remote) => testModule("/app/texts", remote).map((v) => v),
|
148
|
+
expected: { val: ["text1", "text2"], [Path]: "/app/texts" },
|
149
|
+
},
|
150
|
+
{
|
151
|
+
description: "array map projection with undefined",
|
152
|
+
input: (remote) =>
|
153
|
+
testModule("/app/blogs", remote).map((v) => ({
|
154
|
+
otherTitle: v.title,
|
155
|
+
other: undefined,
|
156
|
+
})),
|
157
|
+
expected: {
|
158
|
+
val: [
|
159
|
+
{ otherTitle: "blog1", other: null },
|
160
|
+
{ otherTitle: null, other: null },
|
161
|
+
],
|
162
|
+
[Path]: "/app/blogs",
|
163
|
+
},
|
164
|
+
},
|
165
|
+
{
|
166
|
+
description: "array index with eq",
|
167
|
+
input: (remote) => testModule("/app/texts", remote)[0].eq("text1"),
|
168
|
+
expected: { val: true, [Path]: undefined },
|
169
|
+
},
|
170
|
+
{
|
171
|
+
description: "object module lookup",
|
172
|
+
input: (remote) => testModule("/app/blog", remote),
|
173
|
+
expected: { val: { text: "text1", title: "blog1" }, [Path]: "/app/blog" },
|
174
|
+
},
|
175
|
+
{
|
176
|
+
description: "object andThen property lookup",
|
177
|
+
input: (remote) => testModule("/app/blog", remote).andThen((v) => v.title),
|
178
|
+
expected: { val: "blog1", [Path]: '/app/blog."title"' },
|
179
|
+
},
|
180
|
+
{
|
181
|
+
description: "array object manipulation: basic indexed obj",
|
182
|
+
input: (remote) =>
|
183
|
+
testModule("/app/blogs", remote)
|
184
|
+
.map((v) => v)[0]
|
185
|
+
.title.eq("blog1"),
|
186
|
+
expected: {
|
187
|
+
val: true,
|
188
|
+
[Path]: undefined,
|
189
|
+
},
|
190
|
+
},
|
191
|
+
{
|
192
|
+
description: "array object manipulation: filter",
|
193
|
+
input: (remote) =>
|
194
|
+
testModule("/app/blogs", remote).filter((v) => v.title.eq("blog1")),
|
195
|
+
expected: {
|
196
|
+
val: [{ text: "text1", title: "blog1" }],
|
197
|
+
[Path]: "/app/blogs",
|
198
|
+
},
|
199
|
+
},
|
200
|
+
{
|
201
|
+
description: "array object manipulation: map with tuple literal",
|
202
|
+
input: (remote) =>
|
203
|
+
testModule("/app/blogs", remote).map((a) => [1, a.title]),
|
204
|
+
expected: {
|
205
|
+
val: [
|
206
|
+
[1, "blog1"],
|
207
|
+
[1, null],
|
208
|
+
],
|
209
|
+
[Path]: "/app/blogs",
|
210
|
+
},
|
211
|
+
},
|
212
|
+
// TODO: tuple literal was reverted
|
213
|
+
// {
|
214
|
+
// description: "array object manipulation: map with tuple literal",
|
215
|
+
// input: (remote) =>
|
216
|
+
// testModule("/app/blogs", remote).map((a) => [1, a])[0][1].title,
|
217
|
+
// expected: {
|
218
|
+
// val: "blog1",
|
219
|
+
// [Path]: "/app/blogs.0.title",
|
220
|
+
// },
|
221
|
+
// },
|
222
|
+
{
|
223
|
+
description: "array object manipulation: with literals",
|
224
|
+
input: (remote) =>
|
225
|
+
testModule("/app/blogs", remote)
|
226
|
+
.map((v) => ({
|
227
|
+
title: {
|
228
|
+
foo: "string",
|
229
|
+
},
|
230
|
+
subTitle: { bar: v.title },
|
231
|
+
}))[0]
|
232
|
+
.title.foo.eq("string"),
|
233
|
+
expected: { val: true, [Path]: undefined },
|
234
|
+
},
|
235
|
+
{
|
236
|
+
description: "array object manipulation: with literals",
|
237
|
+
input: (remote) =>
|
238
|
+
testModule("/app/blogs", remote)
|
239
|
+
.map((v) => [v.title, v.title])[0][0]
|
240
|
+
.eq("blog1"),
|
241
|
+
expected: { val: true, [Path]: undefined },
|
242
|
+
},
|
243
|
+
{
|
244
|
+
description: "array object manipulation: with large nested objects",
|
245
|
+
input: (remote) =>
|
246
|
+
testModule("/app/large/nested", remote).map((v) => ({
|
247
|
+
title: {
|
248
|
+
foo: "string",
|
249
|
+
},
|
250
|
+
subTitle: { bar: v },
|
251
|
+
}))[0].subTitle.bar.that.even.more.even[0].more.even.more.even.more,
|
252
|
+
expected: {
|
253
|
+
val: "that.even.more.even.more",
|
254
|
+
[Path]:
|
255
|
+
'/app/large/nested.0."that"."even"."more"."even".0."more"."even"."more"."even"."more"',
|
256
|
+
},
|
257
|
+
},
|
258
|
+
];
|
259
|
+
|
260
|
+
const RemoteAndLocaleSelectorModuleTestCases = SelectorModuleTestCases.flatMap(
|
261
|
+
(testCase) => [
|
262
|
+
{
|
263
|
+
input: () => testCase.input(false),
|
264
|
+
description: `local ${testCase.description}`,
|
265
|
+
expected: testCase.expected,
|
266
|
+
remote: false,
|
267
|
+
},
|
268
|
+
{
|
269
|
+
input: () => testCase.input(true),
|
270
|
+
description: `remote ${testCase.description}`,
|
271
|
+
expected: testCase.expected,
|
272
|
+
remote: true,
|
273
|
+
},
|
274
|
+
]
|
275
|
+
);
|
276
|
+
|
277
|
+
describe("selector", () => {
|
278
|
+
test.each(RemoteAndLocaleSelectorModuleTestCases)(
|
279
|
+
"$description",
|
280
|
+
({ input, expected, remote }) => {
|
281
|
+
if (input instanceof Error) {
|
282
|
+
throw input;
|
283
|
+
}
|
284
|
+
// TODO: ideally we should be able to evaluate remote and local
|
285
|
+
if (!remote) {
|
286
|
+
const localeRes = input();
|
287
|
+
expect(selectorToVal(localeRes)).toStrictEqual(expected);
|
288
|
+
} else {
|
289
|
+
const res = evaluate(
|
290
|
+
// @ts-expect-error TODO: fix this
|
291
|
+
input()[SourceOrExpr],
|
292
|
+
(ref) =>
|
293
|
+
newSelectorProxy(
|
294
|
+
modules[ref as keyof typeof modules],
|
295
|
+
ref as SourcePath
|
296
|
+
),
|
297
|
+
[]
|
298
|
+
);
|
299
|
+
if (result.isErr(res)) {
|
300
|
+
throw res.error;
|
301
|
+
}
|
302
|
+
expect(selectorToVal(res.value)).toStrictEqual(
|
303
|
+
// NOTE: all expected values for REMOTE should be changed to return Vals
|
304
|
+
expected
|
305
|
+
);
|
306
|
+
}
|
307
|
+
}
|
308
|
+
);
|
309
|
+
});
|
310
|
+
|
311
|
+
type TestModules = typeof modules;
|
312
|
+
|
313
|
+
type Expected = any; // TODO: should be Val | Expr
|
314
|
+
|
315
|
+
function testModule<P extends keyof TestModules>(
|
316
|
+
sourcePath: P,
|
317
|
+
remote: boolean
|
318
|
+
): Selector<TestModules[P]> {
|
319
|
+
try {
|
320
|
+
if (remote) {
|
321
|
+
return newExprSelectorProxy(
|
322
|
+
root(sourcePath as SourcePath)
|
323
|
+
) as unknown as Selector<TestModules[P]>;
|
324
|
+
}
|
325
|
+
return newSelectorProxy(modules[sourcePath], sourcePath as SourcePath);
|
326
|
+
} catch (e) {
|
327
|
+
// avoid failing all test suite failure on test case creation, instead returns error and throws it inside the test
|
328
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
329
|
+
return e as any;
|
330
|
+
}
|
331
|
+
}
|
332
|
+
|
333
|
+
/** A big schema */
|
334
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
335
|
+
function BFS() {
|
336
|
+
return array(
|
337
|
+
object({
|
338
|
+
title: object({
|
339
|
+
foo: object({
|
340
|
+
inner: array(
|
341
|
+
object({
|
342
|
+
innerInnerTitle: object({
|
343
|
+
even: object({
|
344
|
+
more: string(),
|
345
|
+
}),
|
346
|
+
}),
|
347
|
+
})
|
348
|
+
),
|
349
|
+
}),
|
350
|
+
}),
|
351
|
+
bar: string(),
|
352
|
+
many: array(string()),
|
353
|
+
props: string(),
|
354
|
+
are: string(),
|
355
|
+
here: object({
|
356
|
+
even: object({
|
357
|
+
more: string(),
|
358
|
+
}),
|
359
|
+
}),
|
360
|
+
for: string(),
|
361
|
+
testing: string(),
|
362
|
+
purposes: string(),
|
363
|
+
and: string(),
|
364
|
+
to: string(),
|
365
|
+
make: string(),
|
366
|
+
sure: string(),
|
367
|
+
that: object({
|
368
|
+
even: object({
|
369
|
+
more: object({
|
370
|
+
even: array(
|
371
|
+
object({
|
372
|
+
more: object({
|
373
|
+
even: object({
|
374
|
+
more: object({
|
375
|
+
even: object({
|
376
|
+
more: string(),
|
377
|
+
}),
|
378
|
+
}),
|
379
|
+
}),
|
380
|
+
}),
|
381
|
+
})
|
382
|
+
),
|
383
|
+
}),
|
384
|
+
}),
|
385
|
+
}),
|
386
|
+
the: string(),
|
387
|
+
type: string(),
|
388
|
+
system: string(),
|
389
|
+
works: string(),
|
390
|
+
as: string(),
|
391
|
+
expected: string(),
|
392
|
+
})
|
393
|
+
);
|
394
|
+
}
|
395
|
+
|
396
|
+
/** A big value */
|
397
|
+
function BFV() {
|
398
|
+
return [
|
399
|
+
{
|
400
|
+
title: {
|
401
|
+
foo: {
|
402
|
+
inner: [
|
403
|
+
{
|
404
|
+
innerInnerTitle: {
|
405
|
+
even: { more: "inner.innerInnerTitle.even.more" },
|
406
|
+
},
|
407
|
+
},
|
408
|
+
],
|
409
|
+
},
|
410
|
+
},
|
411
|
+
bar: "bar",
|
412
|
+
many: ["many1", "many2", "many3"],
|
413
|
+
props: "props",
|
414
|
+
are: "are",
|
415
|
+
here: { even: { more: "here.even.more" } },
|
416
|
+
for: "for",
|
417
|
+
testing: "testing",
|
418
|
+
purposes: "purposes",
|
419
|
+
and: "and",
|
420
|
+
to: "to",
|
421
|
+
make: "make",
|
422
|
+
sure: "sure",
|
423
|
+
that: {
|
424
|
+
even: {
|
425
|
+
more: {
|
426
|
+
even: [
|
427
|
+
{
|
428
|
+
more: {
|
429
|
+
even: {
|
430
|
+
more: { even: { more: "that.even.more.even.more" } },
|
431
|
+
},
|
432
|
+
},
|
433
|
+
},
|
434
|
+
],
|
435
|
+
},
|
436
|
+
},
|
437
|
+
},
|
438
|
+
the: "the",
|
439
|
+
type: "type",
|
440
|
+
system: "system",
|
441
|
+
works: "works",
|
442
|
+
as: "as",
|
443
|
+
expected: "expected",
|
444
|
+
},
|
445
|
+
];
|
446
|
+
}
|
447
|
+
|
448
|
+
function root(sourcePath: string) {
|
449
|
+
return new expr.Call(
|
450
|
+
[new expr.Sym("val"), new expr.StringLiteral(sourcePath)],
|
451
|
+
false
|
452
|
+
);
|
453
|
+
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { VAL_EXTENSION } from ".";
|
2
|
+
import { JsonPrimitive } from "../Json";
|
3
|
+
|
4
|
+
export const FILE_REF_PROP = "_ref" as const;
|
5
|
+
|
6
|
+
/**
|
7
|
+
* A file source represents the path to a (local) file.
|
8
|
+
*
|
9
|
+
* It will be resolved into a Asset object.
|
10
|
+
*
|
11
|
+
*/
|
12
|
+
export type FileSource<
|
13
|
+
Metadata extends { readonly [key: string]: JsonPrimitive } | undefined =
|
14
|
+
| { readonly [key: string]: JsonPrimitive }
|
15
|
+
| undefined
|
16
|
+
> = {
|
17
|
+
readonly [FILE_REF_PROP]: string;
|
18
|
+
readonly [VAL_EXTENSION]: "file";
|
19
|
+
readonly metadata?: Metadata;
|
20
|
+
};
|
21
|
+
|
22
|
+
export function file<
|
23
|
+
Metadata extends { readonly [key: string]: JsonPrimitive }
|
24
|
+
>(ref: string, metadata: Metadata): FileSource<Metadata>;
|
25
|
+
export function file(ref: string, metadata?: undefined): FileSource<undefined>;
|
26
|
+
export function file<
|
27
|
+
Metadata extends { readonly [key: string]: JsonPrimitive } | undefined
|
28
|
+
>(ref: string, metadata?: Metadata): FileSource<Metadata> {
|
29
|
+
return {
|
30
|
+
[FILE_REF_PROP]: ref,
|
31
|
+
[VAL_EXTENSION]: "file",
|
32
|
+
metadata,
|
33
|
+
} as FileSource<Metadata>;
|
34
|
+
}
|
35
|
+
|
36
|
+
export function isFile(obj: unknown): obj is FileSource {
|
37
|
+
return (
|
38
|
+
typeof obj === "object" &&
|
39
|
+
obj !== null &&
|
40
|
+
VAL_EXTENSION in obj &&
|
41
|
+
obj[VAL_EXTENSION] === "file" &&
|
42
|
+
FILE_REF_PROP in obj &&
|
43
|
+
typeof obj[FILE_REF_PROP] === "string"
|
44
|
+
);
|
45
|
+
}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { F } from "ts-toolbelt";
|
2
|
+
import { SourcePrimitive, VAL_EXTENSION } from ".";
|
3
|
+
import { FileSource } from "./file";
|
4
|
+
|
5
|
+
/**
|
6
|
+
* I18n sources cannot have nested remote sources.
|
7
|
+
*/
|
8
|
+
export type I18nCompatibleSource =
|
9
|
+
| SourcePrimitive
|
10
|
+
| I18nObject
|
11
|
+
| I18nArray
|
12
|
+
| FileSource;
|
13
|
+
export type I18nObject = { [key in string]: I18nCompatibleSource };
|
14
|
+
export type I18nArray = readonly I18nCompatibleSource[];
|
15
|
+
|
16
|
+
/**
|
17
|
+
* An i18n source is a map of locales to sources.
|
18
|
+
*
|
19
|
+
* Its selector will default to the underlying source. It is possible to call `.all` on i18n sources, which returns an object with all the locales
|
20
|
+
*
|
21
|
+
*/
|
22
|
+
export type I18nSource<
|
23
|
+
Locales extends readonly string[],
|
24
|
+
T extends I18nCompatibleSource
|
25
|
+
> = {
|
26
|
+
readonly [locale in Locales[number]]: T;
|
27
|
+
} & {
|
28
|
+
readonly [VAL_EXTENSION]: "i18n";
|
29
|
+
};
|
30
|
+
|
31
|
+
export type I18n<Locales extends readonly string[]> = <
|
32
|
+
Src extends I18nCompatibleSource
|
33
|
+
>(source: {
|
34
|
+
[locale in Locales[number]]: Src;
|
35
|
+
}) => I18nSource<Locales, Src>;
|
36
|
+
export function i18n<Locales extends readonly string[]>(
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
38
|
+
locales: F.Narrow<Locales>
|
39
|
+
): <Src extends I18nCompatibleSource>(source: {
|
40
|
+
[locale in Locales[number]]: Src;
|
41
|
+
}) => I18nSource<Locales, Src> {
|
42
|
+
return (source) => {
|
43
|
+
return {
|
44
|
+
...source,
|
45
|
+
[VAL_EXTENSION]: "i18n",
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
47
|
+
} as I18nSource<Locales, any>;
|
48
|
+
};
|
49
|
+
}
|
50
|
+
|
51
|
+
export function isI18n(
|
52
|
+
obj: unknown
|
53
|
+
): obj is I18nSource<string[], I18nCompatibleSource> {
|
54
|
+
return (
|
55
|
+
typeof obj === "object" &&
|
56
|
+
obj !== null &&
|
57
|
+
VAL_EXTENSION in obj &&
|
58
|
+
obj[VAL_EXTENSION] === "i18n"
|
59
|
+
);
|
60
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import { FileSource } from "./file";
|
2
|
+
import { I18nSource, I18nCompatibleSource } from "./i18n";
|
3
|
+
import { RemoteSource, RemoteCompatibleSource } from "./remote";
|
4
|
+
|
5
|
+
export type Source =
|
6
|
+
| SourcePrimitive
|
7
|
+
| SourceObject
|
8
|
+
| SourceArray
|
9
|
+
| I18nSource<string[], I18nCompatibleSource>
|
10
|
+
| RemoteSource<RemoteCompatibleSource>
|
11
|
+
| FileSource;
|
12
|
+
|
13
|
+
export type SourceObject = { [key in string]: Source } & {
|
14
|
+
// TODO: update these restricted parameters:
|
15
|
+
fold?: never;
|
16
|
+
andThen?: never;
|
17
|
+
_ref?: never;
|
18
|
+
_type?: never;
|
19
|
+
val?: never;
|
20
|
+
valPath?: never; // used when serializing vals
|
21
|
+
};
|
22
|
+
export type SourceArray = readonly Source[];
|
23
|
+
export type SourcePrimitive = string | number | boolean | null;
|
24
|
+
|
25
|
+
/* Branded extension types: file, remote, i18n */
|
26
|
+
export const VAL_EXTENSION = "_type" as const;
|
27
|
+
|
28
|
+
export function getValExtension(source: Source) {
|
29
|
+
return (
|
30
|
+
source &&
|
31
|
+
typeof source === "object" &&
|
32
|
+
VAL_EXTENSION in source &&
|
33
|
+
source[VAL_EXTENSION]
|
34
|
+
);
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* A phantom type parameter is one that doesn't show up at runtime, but is checked statically (and only) at compile time.
|
39
|
+
*
|
40
|
+
* An example where this is useful is remote types, where the type of the remote source is known at compile time,
|
41
|
+
* but the value is not there before it is fetched.
|
42
|
+
*
|
43
|
+
* @example
|
44
|
+
* type Example<T> = string & PhantomType<T>;
|
45
|
+
*
|
46
|
+
**/
|
47
|
+
declare const PhantomType: unique symbol;
|
48
|
+
export type PhantomType<T> = {
|
49
|
+
[PhantomType]: T;
|
50
|
+
};
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import { SourcePrimitive, VAL_EXTENSION, PhantomType } from ".";
|
2
|
+
import { RichText } from "../schema/richtext";
|
3
|
+
import { FileSource } from "./file";
|
4
|
+
import { I18nCompatibleSource, I18nSource } from "./i18n";
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Remote sources cannot include other remote sources.
|
8
|
+
*/
|
9
|
+
export type RemoteCompatibleSource =
|
10
|
+
| SourcePrimitive
|
11
|
+
| RemoteObject
|
12
|
+
| RemoteArray
|
13
|
+
| RichText
|
14
|
+
| FileSource
|
15
|
+
| I18nSource<string[], I18nCompatibleSource>;
|
16
|
+
export type RemoteObject = { [key in string]: RemoteCompatibleSource };
|
17
|
+
export type RemoteArray = readonly RemoteCompatibleSource[];
|
18
|
+
|
19
|
+
export const REMOTE_REF_PROP = "_ref" as const; // TODO: same as FILE_REF_PROP so use same prop?
|
20
|
+
|
21
|
+
declare const brand: unique symbol;
|
22
|
+
export type RemoteRef = string & { readonly [brand]: "RemoteRef" };
|
23
|
+
|
24
|
+
/**
|
25
|
+
* A remote source is a hash that represents a remote object.
|
26
|
+
*
|
27
|
+
* It will be resolved into a ValRemote object.
|
28
|
+
*/
|
29
|
+
export type RemoteSource<Src extends RemoteCompatibleSource> = {
|
30
|
+
readonly [REMOTE_REF_PROP]: RemoteRef;
|
31
|
+
readonly [VAL_EXTENSION]: "remote";
|
32
|
+
} & PhantomType<Src>;
|
33
|
+
|
34
|
+
export function remote<Src extends RemoteCompatibleSource>(
|
35
|
+
ref: string
|
36
|
+
): RemoteSource<Src> {
|
37
|
+
return {
|
38
|
+
[REMOTE_REF_PROP]: ref as RemoteRef,
|
39
|
+
[VAL_EXTENSION]: "remote",
|
40
|
+
} as RemoteSource<Src>;
|
41
|
+
}
|
42
|
+
|
43
|
+
export function isRemote(
|
44
|
+
obj: unknown
|
45
|
+
): obj is RemoteSource<RemoteCompatibleSource> {
|
46
|
+
return (
|
47
|
+
typeof obj === "object" &&
|
48
|
+
obj !== null &&
|
49
|
+
VAL_EXTENSION in obj &&
|
50
|
+
obj[VAL_EXTENSION] === "remote" &&
|
51
|
+
REMOTE_REF_PROP in obj &&
|
52
|
+
typeof obj[REMOTE_REF_PROP] === "string"
|
53
|
+
);
|
54
|
+
}
|
package/src/val/array.ts
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
import { SourcePath, Val as UnknownVal } from ".";
|
2
|
+
import { JsonArray } from "../Json";
|
3
|
+
import { Path } from "../selector";
|
4
|
+
|
5
|
+
export type Val<T extends JsonArray> = {
|
6
|
+
readonly [key in keyof T]: UnknownVal<T[key]>;
|
7
|
+
} & {
|
8
|
+
readonly [Path]: SourcePath | undefined;
|
9
|
+
readonly val: T;
|
10
|
+
};
|