@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.
Files changed (72) hide show
  1. package/README.md +25 -37
  2. package/jest.config.js +4 -0
  3. package/package.json +1 -1
  4. package/src/Json.ts +4 -0
  5. package/src/expr/README.md +193 -0
  6. package/src/expr/eval.test.ts +202 -0
  7. package/src/expr/eval.ts +248 -0
  8. package/src/expr/expr.ts +91 -0
  9. package/src/expr/index.ts +3 -0
  10. package/src/expr/parser.test.ts +158 -0
  11. package/src/expr/parser.ts +229 -0
  12. package/src/expr/repl.ts +93 -0
  13. package/src/expr/tokenizer.test.ts +539 -0
  14. package/src/expr/tokenizer.ts +117 -0
  15. package/src/fetchVal.test.ts +164 -0
  16. package/src/fetchVal.ts +211 -0
  17. package/src/fp/array.ts +30 -0
  18. package/src/fp/index.ts +3 -0
  19. package/src/fp/result.ts +214 -0
  20. package/src/fp/util.ts +52 -0
  21. package/src/index.ts +55 -0
  22. package/src/initSchema.ts +45 -0
  23. package/src/initVal.ts +96 -0
  24. package/src/module.test.ts +170 -0
  25. package/src/module.ts +333 -0
  26. package/src/patch/deref.test.ts +300 -0
  27. package/src/patch/deref.ts +128 -0
  28. package/src/patch/index.ts +11 -0
  29. package/src/patch/json.test.ts +583 -0
  30. package/src/patch/json.ts +304 -0
  31. package/src/patch/operation.ts +74 -0
  32. package/src/patch/ops.ts +83 -0
  33. package/src/patch/parse.test.ts +202 -0
  34. package/src/patch/parse.ts +187 -0
  35. package/src/patch/patch.ts +46 -0
  36. package/src/patch/util.ts +67 -0
  37. package/src/schema/array.ts +52 -0
  38. package/src/schema/boolean.ts +38 -0
  39. package/src/schema/i18n.ts +65 -0
  40. package/src/schema/image.ts +70 -0
  41. package/src/schema/index.ts +46 -0
  42. package/src/schema/literal.ts +42 -0
  43. package/src/schema/number.ts +45 -0
  44. package/src/schema/object.ts +67 -0
  45. package/src/schema/oneOf.ts +60 -0
  46. package/src/schema/richtext.ts +417 -0
  47. package/src/schema/string.ts +49 -0
  48. package/src/schema/union.ts +62 -0
  49. package/src/selector/ExprProxy.test.ts +203 -0
  50. package/src/selector/ExprProxy.ts +209 -0
  51. package/src/selector/SelectorProxy.test.ts +172 -0
  52. package/src/selector/SelectorProxy.ts +237 -0
  53. package/src/selector/array.ts +37 -0
  54. package/src/selector/boolean.ts +4 -0
  55. package/src/selector/file.ts +14 -0
  56. package/src/selector/i18n.ts +13 -0
  57. package/src/selector/index.ts +159 -0
  58. package/src/selector/number.ts +4 -0
  59. package/src/selector/object.ts +22 -0
  60. package/src/selector/primitive.ts +17 -0
  61. package/src/selector/remote.ts +9 -0
  62. package/src/selector/selector.test.ts +453 -0
  63. package/src/selector/selectorOf.ts +7 -0
  64. package/src/selector/string.ts +4 -0
  65. package/src/source/file.ts +45 -0
  66. package/src/source/i18n.ts +60 -0
  67. package/src/source/index.ts +50 -0
  68. package/src/source/remote.ts +54 -0
  69. package/src/val/array.ts +10 -0
  70. package/src/val/index.ts +90 -0
  71. package/src/val/object.ts +13 -0
  72. package/src/val/primitive.ts +8 -0
@@ -0,0 +1,203 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { convertLiteralProxy, newExprSelectorProxy } from "./ExprProxy";
3
+ import * as expr from "../expr/expr";
4
+ import { GenericSelector, SelectorSource, SourceOrExpr } from ".";
5
+ import { Source } from "../source";
6
+
7
+ const ExprSelectorTestCases: any[] = [
8
+ {
9
+ description: "basic module",
10
+ input: newExprSelectorProxy<string>(root("/app/foo")),
11
+ expected: "(val '/app/foo')",
12
+ },
13
+ {
14
+ description: "basic prop",
15
+ input: newExprSelectorProxy<string[]>(root("/app/foo"))[0],
16
+ expected: "('0' (val '/app/foo'))",
17
+ },
18
+ {
19
+ description: "noop andThen",
20
+ input: newExprSelectorProxy<string>(root("/app/foo")).andThen((v) => v),
21
+ expected: "!(andThen (val '/app/foo') @[0,0])",
22
+ },
23
+ {
24
+ description: "noop map",
25
+ input: newExprSelectorProxy<string[]>(root("/app/foo")).map((v) => v),
26
+ expected: "!(map (val '/app/foo') @[0,0])",
27
+ },
28
+ {
29
+ description: "eq string",
30
+ input: newExprSelectorProxy<string>(root("/app/foo")).eq("hei"),
31
+ expected: "(eq (val '/app/foo') 'hei')",
32
+ },
33
+ {
34
+ description: "eq undefined",
35
+ input: newExprSelectorProxy<null>(root("/app/foo")).eq(null),
36
+ expected: "(eq (val '/app/foo') ())",
37
+ },
38
+ {
39
+ description: "eq number",
40
+ input: newExprSelectorProxy<number>(root("/app/foo")).eq(1),
41
+ expected: "(eq (val '/app/foo') (json '1'))",
42
+ },
43
+ {
44
+ description: "eq boolean",
45
+ input: newExprSelectorProxy<boolean>(root("/app/foo")).eq(true),
46
+ expected: "(eq (val '/app/foo') (json 'true'))",
47
+ },
48
+ {
49
+ description: "filter string",
50
+ input: newExprSelectorProxy<string[]>(root("/app/foo")).filter((v) =>
51
+ v.eq("hei")
52
+ ),
53
+ expected: "!(filter (val '/app/foo') (eq @[0,0] 'hei'))",
54
+ },
55
+ {
56
+ description: "filter number",
57
+ input: newExprSelectorProxy<number[]>(root("/app/foo")).filter((v) =>
58
+ v.eq(1)
59
+ ),
60
+ expected: "!(filter (val '/app/foo') (eq @[0,0] (json '1')))",
61
+ },
62
+ {
63
+ description: "filter optional",
64
+ input: newExprSelectorProxy<number[]>(root("/app/foo")).filter((v) =>
65
+ v.eq(null)
66
+ ),
67
+ expected: "!(filter (val '/app/foo') (eq @[0,0] ()))",
68
+ },
69
+ {
70
+ description: "basic projection",
71
+ input: newExprSelectorProxy<string[]>(root("/app/foo")).map((v) => ({
72
+ foo: v,
73
+ })),
74
+ expected: "!(map (val '/app/foo') (json '{\"foo\": ${@[0,0]}}'))",
75
+ },
76
+ {
77
+ description: "nested projection",
78
+ input: newExprSelectorProxy<string[]>(root("/app/foo")).map((v) => ({
79
+ foo: {
80
+ bar: v,
81
+ },
82
+ })),
83
+ expected:
84
+ // TODO: this could be more readable
85
+ // Example: "!(map (val '/app/foo') (json '{\"foo\": {\"bar\": {${@[0,0]}}'}'))"
86
+ "!(map (val '/app/foo') (json '{\"foo\": ${'{\"bar\": ${@[0,0]}}'}}'))",
87
+ },
88
+ {
89
+ description: "multi module",
90
+ input: newExprSelectorProxy<string>(root("/app/foo")).andThen(() =>
91
+ newExprSelectorProxy<string[]>(root("/app/bar"))
92
+ ),
93
+ expected: "!(andThen (val '/app/foo') (val '/app/bar'))",
94
+ },
95
+ ];
96
+
97
+ /**
98
+ * Useful test cases for literal conversion
99
+ * There somewhat of an overlap between these cases and the selector cases,
100
+ * we could prune some of these away if we want to
101
+ **/
102
+ const LiteralConversionTestCases: {
103
+ input: SelectorSource;
104
+ expected: string;
105
+ description: string;
106
+ }[] = [
107
+ { description: "basic string", input: "foo", expected: "(json '\"foo\"')" },
108
+ { description: "basic number", input: 1, expected: "(json '1')" },
109
+ { description: "basic boolean", input: true, expected: "(json 'true')" },
110
+ { description: "basic array", input: [1], expected: "(json '[1]')" },
111
+ { description: "basic null", input: null, expected: "()" },
112
+ {
113
+ description: "array with 2 different",
114
+ input: [1, "foo"],
115
+ expected: "(json '[1, \"foo\"]')",
116
+ },
117
+ {
118
+ description: "array with undefined",
119
+ input: [1, null],
120
+ expected: "(json '[1, ${()}]')",
121
+ },
122
+ {
123
+ description: "basic object",
124
+ input: { foo: "one", bar: 1 },
125
+ expected: '(json \'{"foo": "one", "bar": 1}\')',
126
+ },
127
+ {
128
+ description: "nested array",
129
+ input: [1, [1]],
130
+ expected: "(json '[1, [1]]')",
131
+ },
132
+ {
133
+ description: "nested object",
134
+ input: { foo: "one", bar: { zoo: 1 } },
135
+ expected: '(json \'{"foo": "one", "bar": {"zoo": 1}}\')',
136
+ },
137
+ {
138
+ description: "nested object with array",
139
+ input: {
140
+ foo: "one",
141
+ bar: { zoo: [1, "inner"] },
142
+ },
143
+ expected: '(json \'{"foo": "one", "bar": {"zoo": [1, "inner"]}}\')',
144
+ },
145
+ {
146
+ description: "basic interpolation",
147
+ input: [1, newExprSelectorProxy<string[]>(root("/app/foo"))],
148
+ expected: "(json '[1, ${(val '/app/foo')}]')",
149
+ },
150
+ {
151
+ description: "advanced interpolation",
152
+ input: [
153
+ 123,
154
+ newExprSelectorProxy<string[]>(root("/app/foo")).map((v) =>
155
+ v.andThen((a) => ({ bar: a.eq("bar") }))
156
+ ),
157
+ ],
158
+ expected:
159
+ "(json '[123, ${!(map (val '/app/foo') !(andThen @[0,0] (json '{\"bar\": ${(eq @[1,0] 'bar')}}')))}]')",
160
+ },
161
+ {
162
+ description: "interpolation with multiple selectors",
163
+ input: [
164
+ 123,
165
+ newExprSelectorProxy<string[]>(root("/app/foo")).map((v) =>
166
+ v.andThen((a) => ({
167
+ bar: a.eq("bar"),
168
+ more: newExprSelectorProxy<string[]>(root("/app/foo")),
169
+ }))
170
+ ),
171
+ ],
172
+ expected:
173
+ "(json '[123, ${!(map (val '/app/foo') !(andThen @[0,0] (json '{\"bar\": ${(eq @[1,0] 'bar')}, \"more\": ${(val '/app/foo')}}')))}]')",
174
+ },
175
+ ];
176
+
177
+ describe("expr", () => {
178
+ test.each(ExprSelectorTestCases)(
179
+ 'expr selector ($description): "$expected"',
180
+ ({ input, expected }) => {
181
+ const valOrExpr = (input as unknown as GenericSelector<Source>)[
182
+ SourceOrExpr
183
+ ];
184
+ if (valOrExpr instanceof expr.Expr) {
185
+ expect(valOrExpr.transpile()).toBe(expected);
186
+ } else {
187
+ expect(valOrExpr).toBe(expect.any(expr.Expr));
188
+ }
189
+ }
190
+ );
191
+ test.each(LiteralConversionTestCases)(
192
+ 'literal conversion ($description): "$expected"',
193
+ ({ input, expected }) =>
194
+ expect(convertLiteralProxy(input).transpile()).toBe(expected)
195
+ );
196
+ });
197
+
198
+ function root(sourcePath: string) {
199
+ return new expr.Call(
200
+ [new expr.Sym("val"), new expr.StringLiteral(sourcePath)],
201
+ false
202
+ );
203
+ }
@@ -0,0 +1,209 @@
1
+ import { Selector, SelectorSource, SourceOrExpr } from ".";
2
+ import * as expr from "../expr/expr";
3
+ import { Source, SourcePrimitive } from "../source";
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ type AnyFun = (...args: any[]) => any;
7
+
8
+ export function newExprSelectorProxy<T extends Source>(
9
+ root: expr.Expr,
10
+ depth = 0
11
+ ): Selector<T> {
12
+ return new Proxy(new GenericExprSelector(root, depth), {
13
+ get: (target, prop) => {
14
+ if (prop === SourceOrExpr) {
15
+ return target[SourceOrExpr];
16
+ }
17
+ if (!hasOwn(target, prop)) {
18
+ return newExprSelectorProxy(
19
+ new expr.Call([new expr.StringLiteral(prop.toString()), root], false),
20
+ depth
21
+ );
22
+ }
23
+ return Reflect.get(target, prop);
24
+ },
25
+ }) as unknown as Selector<T>;
26
+ }
27
+
28
+ class GenericExprSelector {
29
+ [SourceOrExpr]: expr.Expr;
30
+ constructor(private readonly root: expr.Expr, private readonly depth = 0) {
31
+ this[SourceOrExpr] = root;
32
+ }
33
+
34
+ andThen = (f: AnyFun) => {
35
+ return genericHigherOrderFunction(this.root, "andThen", f, 1, this.depth);
36
+ };
37
+
38
+ map = (f: AnyFun) => {
39
+ return genericHigherOrderFunction(this.root, "map", f, 2, this.depth);
40
+ };
41
+
42
+ filter = (f: AnyFun) => {
43
+ return genericHigherOrderFunction(this.root, "filter", f, 1, this.depth);
44
+ };
45
+
46
+ eq = (other: SourcePrimitive) => {
47
+ return newExprSelectorProxy(
48
+ new expr.Call(
49
+ [
50
+ new expr.Sym("eq"),
51
+ this.root,
52
+ typeof other === "string"
53
+ ? new expr.StringLiteral(other)
54
+ : typeof other === "undefined"
55
+ ? expr.NilSym
56
+ : convertLiteralProxy(other),
57
+ ],
58
+ false
59
+ ),
60
+ this.depth
61
+ );
62
+ };
63
+ }
64
+
65
+ function genericHigherOrderFunction(
66
+ root: expr.Expr,
67
+ name: string,
68
+ f: AnyFun,
69
+ args: number,
70
+ depth: number
71
+ ) {
72
+ const argsExprs: Selector<Source>[] = [];
73
+ for (let i = 0; i < args; i++) {
74
+ argsExprs.push(
75
+ newExprSelectorProxy(new expr.Sym(`@[${depth},${i}]`), depth + 1)
76
+ );
77
+ }
78
+ return newExprSelectorProxy(
79
+ new expr.Call(
80
+ [new expr.Sym(name), root, convertLiteralProxy(f(...argsExprs))],
81
+ true
82
+ ),
83
+ depth
84
+ );
85
+ }
86
+
87
+ function hasOwn<T extends PropertyKey>(obj: object, prop: T): boolean {
88
+ return Object.prototype.hasOwnProperty.call(obj, prop);
89
+ }
90
+
91
+ export function convertLiteralProxy(
92
+ source: expr.Expr | SelectorSource
93
+ ): expr.Expr {
94
+ const [convertedLiteral] = convertObjectToStringExpr(source, true);
95
+ return withJsonCall(convertedLiteral, false)[0];
96
+ }
97
+
98
+ /**
99
+ * Add a json call if the literal must be parsed
100
+ * Happens at the very least at the top level, but may also happen inside e.g. a string template
101
+ *
102
+ */
103
+ function withJsonCall(
104
+ e: expr.Expr,
105
+ isLiteralScope: boolean
106
+ ): [e: expr.Expr, isJson: boolean] {
107
+ if (
108
+ !isLiteralScope &&
109
+ (e instanceof expr.StringLiteral || e instanceof expr.StringTemplate)
110
+ ) {
111
+ return [new expr.Call([new expr.Sym("json"), e], false), true];
112
+ }
113
+ return [e, false];
114
+ }
115
+
116
+ function convertObjectToStringExpr(
117
+ source: expr.Expr | SelectorSource,
118
+ isLiteralScope: boolean
119
+ ): [e: expr.Expr, isJson: boolean] {
120
+ if (source === null || source === undefined) {
121
+ return [expr.NilSym, true];
122
+ } else if (typeof source === "string") {
123
+ return [new expr.StringLiteral(JSON.stringify(source)), false];
124
+ } else if (typeof source === "number" || typeof source === "boolean") {
125
+ return withJsonCall(
126
+ new expr.StringLiteral(source.toString()),
127
+ isLiteralScope
128
+ );
129
+ } else if (typeof source === "object" && SourceOrExpr in source) {
130
+ const selector = source;
131
+ const valOrExpr = selector[SourceOrExpr];
132
+ if (valOrExpr instanceof expr.Expr) {
133
+ return [valOrExpr, true];
134
+ } else {
135
+ // use Val literal - UNTESTED - may happen if we are referencing local content
136
+ return convertObjectToStringExpr(valOrExpr, isLiteralScope);
137
+ }
138
+ } else if (source instanceof expr.Expr) {
139
+ return [source, true];
140
+ } else if (typeof source === "object") {
141
+ // source is a literal object or array, might have nested selectors
142
+ const isArray = Array.isArray(source);
143
+ const entries = isArray
144
+ ? Array.from(source.entries())
145
+ : Object.entries(source);
146
+
147
+ let isStringTemplate = false;
148
+ const converted = entries.map(([key, v]) => {
149
+ const [converted, mustInterpolate] =
150
+ typeof v === "string"
151
+ ? withJsonCall(
152
+ new expr.StringLiteral(JSON.stringify(v)),
153
+ isLiteralScope
154
+ )
155
+ : convertObjectToStringExpr(v, isLiteralScope);
156
+
157
+ isStringTemplate = isStringTemplate || mustInterpolate;
158
+ return [key, converted] as const;
159
+ });
160
+
161
+ if (!isStringTemplate) {
162
+ const value = isArray
163
+ ? `[${converted.map(([, v]) => getStringLiteralValue(v)).join(", ")}]`
164
+ : `{${converted
165
+ .map(
166
+ ([key, v]) =>
167
+ `${JSON.stringify(key)}: ${getStringLiteralValue(v)}`
168
+ )
169
+ .join(", ")}}`;
170
+
171
+ return [new expr.StringLiteral(value), false];
172
+ }
173
+
174
+ return [
175
+ new expr.StringTemplate([
176
+ isArray ? new expr.StringLiteral("[") : new expr.StringLiteral("{"),
177
+ ...converted.flatMap(([key, entry], i) => {
178
+ const convertedEntry =
179
+ typeof entry === "string"
180
+ ? new expr.StringLiteral(JSON.stringify(entry))
181
+ : convertObjectToStringExpr(entry, isLiteralScope)[0];
182
+ const maybeComma =
183
+ i < converted.length - 1 ? [new expr.StringLiteral(", ")] : [];
184
+ if (isArray) {
185
+ return [convertedEntry].concat(maybeComma);
186
+ } else {
187
+ return [
188
+ new expr.StringLiteral(JSON.stringify(key) + ": "),
189
+ convertedEntry,
190
+ ].concat(maybeComma);
191
+ }
192
+ }),
193
+ isArray ? new expr.StringLiteral("]") : new expr.StringLiteral("}"),
194
+ ]),
195
+ true,
196
+ ];
197
+ }
198
+ throw new Error(
199
+ `Unsupported type '${typeof source}': ${JSON.stringify(source)}`
200
+ );
201
+ }
202
+
203
+ function getStringLiteralValue(e: expr.Expr): string {
204
+ if (e instanceof expr.StringLiteral) {
205
+ return e.value;
206
+ } else {
207
+ throw new Error(`expected a string literal but found: ${e.transpile()}`);
208
+ }
209
+ }
@@ -0,0 +1,172 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Selector, GenericSelector, Path } from ".";
3
+ import { number } from "../schema/number";
4
+ import { string } from "../schema/string";
5
+ import { Source } from "../source";
6
+ import { SourcePath } from "../val";
7
+ import { newSelectorProxy, selectorToVal } from "./SelectorProxy";
8
+
9
+ describe("SelectorProxy", () => {
10
+ test("string eq andThen", () => {
11
+ const sourcePath = "/app/text" as SourcePath;
12
+ const string1 = newSelectorProxy("foo", sourcePath) as Selector<string>;
13
+ expectValOrExpr(string1.eq("foo")).toStrictEqual({
14
+ val: true,
15
+ [Path]: undefined,
16
+ });
17
+ expectValOrExpr(string1.eq("foo").andThen(() => string1)).toStrictEqual({
18
+ val: "foo",
19
+ [Path]: sourcePath,
20
+ });
21
+ });
22
+
23
+ test("array filter eq", () => {
24
+ const sourcePath = "/app/texts" as SourcePath;
25
+ const string1 = newSelectorProxy(["foo", "bar"], sourcePath) as Selector<
26
+ string[]
27
+ >;
28
+ expectValOrExpr(
29
+ string1.map((a) => a).filter((a) => a.eq("foo"))
30
+ ).toStrictEqual({
31
+ val: ["foo"],
32
+ [Path]: "/app/texts",
33
+ });
34
+ });
35
+
36
+ test("array numbers", () => {
37
+ const sourcePath = "/app/numbers" as SourcePath;
38
+ const numbersVal = newSelectorProxy([1, 2, 3], sourcePath) as Selector<
39
+ string[]
40
+ >;
41
+ expectValOrExpr(numbersVal).toStrictEqual({
42
+ val: [1, 2, 3],
43
+ [Path]: "/app/numbers",
44
+ });
45
+ });
46
+
47
+ test("array length", () => {
48
+ const sourcePath = "/app/numbers" as SourcePath;
49
+ const numbersVal = newSelectorProxy([1, 2, 3], sourcePath) as Selector<
50
+ string[]
51
+ >;
52
+ expectValOrExpr(numbersVal.length).toStrictEqual({
53
+ val: 3,
54
+ [Path]: undefined,
55
+ });
56
+ });
57
+
58
+ test("array filter match number", () => {
59
+ const sourcePath = "/app/numbers" as SourcePath;
60
+ const numbersVal = newSelectorProxy([1, 2, undefined], sourcePath) as
61
+ | Selector<string[]>
62
+ | Selector<null[]>;
63
+ expectValOrExpr(numbersVal.filter(number())).toStrictEqual({
64
+ val: [1, 2],
65
+ [Path]: "/app/numbers",
66
+ });
67
+ });
68
+
69
+ test("array filter match string / undefined / null", () => {
70
+ const sourcePath = "/app/numbers" as SourcePath;
71
+ const numbersVal = newSelectorProxy(
72
+ [1, 2, undefined, null, "test"],
73
+ sourcePath
74
+ ) as Selector<(number | string | null)[]>;
75
+ expectValOrExpr(numbersVal.filter(string().optional())).toStrictEqual({
76
+ val: [null, null, "test"],
77
+ [Path]: "/app/numbers",
78
+ });
79
+ });
80
+
81
+ test("object lookup", () => {
82
+ const sourcePath = "/app/blog" as SourcePath;
83
+ const blogsVal = newSelectorProxy(
84
+ { title: "title1" },
85
+ sourcePath
86
+ ) as Selector<{ title: string }>;
87
+ expectValOrExpr(blogsVal).toStrictEqual({
88
+ val: { title: "title1" },
89
+ [Path]: "/app/blog",
90
+ });
91
+ });
92
+
93
+ test("object prop", () => {
94
+ const sourcePath = "/app/blog" as SourcePath;
95
+ const blogsVal = newSelectorProxy(
96
+ { title: "title1" },
97
+ sourcePath
98
+ ) as Selector<{ title: string }>;
99
+ expectValOrExpr(blogsVal.title).toStrictEqual({
100
+ val: "title1",
101
+ [Path]: '/app/blog."title"',
102
+ });
103
+ });
104
+
105
+ test("array object index", () => {
106
+ const sourcePath = "/app/blogs" as SourcePath;
107
+ const blogsVal = newSelectorProxy(
108
+ [{ title: "title1" }],
109
+ sourcePath
110
+ ) as Selector<{ title: string }[]>;
111
+ expectValOrExpr(blogsVal[0]).toStrictEqual({
112
+ val: {
113
+ title: "title1",
114
+ },
115
+ [Path]: "/app/blogs.0",
116
+ });
117
+ });
118
+
119
+ test("array map object index", () => {
120
+ const sourcePath = "/app/blogs" as SourcePath;
121
+ const blogsVal = newSelectorProxy(
122
+ [{ title: "title1" }],
123
+ sourcePath
124
+ ) as Selector<{ title: string }[]>;
125
+ expectValOrExpr(blogsVal.map((v) => v)[0]).toStrictEqual({
126
+ val: {
127
+ title: "title1",
128
+ },
129
+ [Path]: "/app/blogs.0",
130
+ });
131
+ });
132
+
133
+ test("map object then index", () => {
134
+ const sourcePath = "/app/blogs" as SourcePath;
135
+ const blogsVal = newSelectorProxy(
136
+ [{ title: "title1" }],
137
+ sourcePath
138
+ ) as Selector<{ title: string }[]>;
139
+ expectValOrExpr(blogsVal.map((blog) => blog)).toStrictEqual({
140
+ val: [{ title: "title1" }],
141
+ [Path]: "/app/blogs",
142
+ });
143
+ });
144
+
145
+ test("map object then index then prop", () => {
146
+ const sourcePath = "/app/blogs" as SourcePath;
147
+ const blogsVal = newSelectorProxy(
148
+ [{ title: "title1" }, { title: "title2" }],
149
+ sourcePath
150
+ ) as Selector<{ title: string }[]>;
151
+ expectValOrExpr(blogsVal.map((blog) => blog)[0].title).toStrictEqual({
152
+ val: "title1",
153
+ [Path]: '/app/blogs.0."title"',
154
+ });
155
+ });
156
+
157
+ test("map object then prop then index", () => {
158
+ const sourcePath = "/app/blogs" as SourcePath;
159
+ const blogsVal = newSelectorProxy(
160
+ [{ title: "title1" }, { title: "title2" }],
161
+ sourcePath
162
+ ) as Selector<{ title: string }[]>;
163
+ expectValOrExpr(blogsVal.map((blog) => blog.title)[0]).toStrictEqual({
164
+ val: "title1",
165
+ [Path]: '/app/blogs.0."title"',
166
+ });
167
+ });
168
+ });
169
+
170
+ function expectValOrExpr<T extends Source>(selector: GenericSelector<T>) {
171
+ return expect(selectorToVal(selector));
172
+ }