@player-tools/dsl 0.11.0 → 0.12.1--canary.213.4811
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 +102 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +92 -1
- package/dist/index.mjs +92 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/helpers/mock-data-refs.ts +1 -1
- package/src/__tests__/util.test.tsx +196 -8
- package/src/compiler/schema.ts +15 -7
- package/src/expressions/__tests__/native.test.ts +159 -0
- package/src/expressions/__tests__/testing.test.ts +38 -0
- package/src/expressions/native.ts +141 -0
- package/src/expressions/testing.ts +40 -0
- package/src/index.ts +1 -0
- package/src/string-templates/index.ts +20 -10
- package/src/types.ts +27 -1
- package/src/utils.tsx +80 -6
- package/types/compiler/schema.d.ts +5 -2
- package/types/expressions/native.d.ts +50 -0
- package/types/expressions/testing.d.ts +13 -0
- package/types/index.d.ts +1 -0
- package/types/string-templates/index.d.ts +9 -7
- package/types/types.d.ts +11 -1
- package/types/utils.d.ts +22 -3
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { test, expect, describe } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getObjectReferences,
|
|
4
|
+
makeFunctionByName,
|
|
5
|
+
mapExpressionHandlersToFunctions,
|
|
6
|
+
wrapFunctionInType,
|
|
7
|
+
} from "../utils";
|
|
8
|
+
import { Binding, Expression, ExpressionHandler } from "@player-ui/player";
|
|
9
|
+
import { binding, expression } from "../string-templates";
|
|
10
|
+
import { LocalBazType } from "./helpers/mock-data-refs";
|
|
11
|
+
import { DataTypeRefs } from "../types";
|
|
12
|
+
import { makeBindingsForObject } from "../compiler/schema";
|
|
3
13
|
|
|
4
14
|
describe("Testing the 'getObjectReferences' helper that creates same property references into a new object", () => {
|
|
5
15
|
test("should return the object properties in referenced format", () => {
|
|
6
16
|
const dataTypes = {
|
|
7
|
-
|
|
8
|
-
type: "BooleanType",
|
|
9
|
-
},
|
|
17
|
+
LocalBazType,
|
|
10
18
|
};
|
|
11
19
|
|
|
12
20
|
const validators = {
|
|
@@ -15,12 +23,192 @@ describe("Testing the 'getObjectReferences' helper that creates same property re
|
|
|
15
23
|
},
|
|
16
24
|
};
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
type DTREF = DataTypeRefs<typeof dataTypes>;
|
|
27
|
+
|
|
28
|
+
const dataReferences = getObjectReferences<typeof dataTypes, DTREF>(
|
|
29
|
+
dataTypes,
|
|
30
|
+
);
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
type VALREF = DataTypeRefs<typeof validators>;
|
|
33
|
+
const validatorReferences = getObjectReferences<typeof validators, VALREF>(
|
|
34
|
+
validators,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(dataReferences.LocalBazTypeRef).toStrictEqual({
|
|
38
|
+
type: "LocalBazType",
|
|
23
39
|
});
|
|
24
40
|
expect(validatorReferences.requiredRef).toStrictEqual({ type: "required" });
|
|
25
41
|
});
|
|
26
42
|
});
|
|
43
|
+
|
|
44
|
+
describe("DSL Expression Generation Helper", () => {
|
|
45
|
+
test("Returns the correct serialization for bindings", () => {
|
|
46
|
+
const mockFunction: ExpressionHandler<[Binding], boolean> = (ctx, val) => {
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const mockFunctionDSL = wrapFunctionInType(mockFunction);
|
|
51
|
+
|
|
52
|
+
const foo = binding<string>`some.binding`;
|
|
53
|
+
const test = expression`${mockFunctionDSL(foo)} == true`;
|
|
54
|
+
|
|
55
|
+
expect(test.toValue()).toEqual("mockFunction('some.binding') == true");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("Returns the correct serialization for mixed values and bindings", () => {
|
|
59
|
+
const mockFunction: ExpressionHandler<[string, string, number], boolean> = (
|
|
60
|
+
ctx,
|
|
61
|
+
val,
|
|
62
|
+
) => {
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const mockFunctionDSL = wrapFunctionInType(mockFunction);
|
|
67
|
+
|
|
68
|
+
const foo = binding<string>`some.binding`;
|
|
69
|
+
const test = expression`${mockFunctionDSL(foo.toRefString(), "test", 1)} == true`;
|
|
70
|
+
|
|
71
|
+
expect(test.toValue()).toEqual(
|
|
72
|
+
"mockFunction('{{some.binding}}', 'test', 1) == true",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("Returns the correct serialization for array items", () => {
|
|
77
|
+
const mockFunction: ExpressionHandler<[Array<unknown>], boolean> = (
|
|
78
|
+
ctx,
|
|
79
|
+
val,
|
|
80
|
+
) => {
|
|
81
|
+
return false;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const mockFunctionDSL = wrapFunctionInType(mockFunction);
|
|
85
|
+
|
|
86
|
+
const foo = binding`some.binding`;
|
|
87
|
+
const test = expression`${mockFunctionDSL([1, "2", foo])}`;
|
|
88
|
+
|
|
89
|
+
expect(test.toValue()).toEqual(
|
|
90
|
+
"mockFunction([1, '2', '{{some.binding}}'])",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("Can Dereferenced Binding", () => {
|
|
95
|
+
const mockFunction: ExpressionHandler<[boolean], boolean> = (ctx, val) => {
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mockFunctionDSL = wrapFunctionInType(mockFunction);
|
|
100
|
+
|
|
101
|
+
const foo = binding<boolean>`some.binding`;
|
|
102
|
+
const test = expression`${mockFunctionDSL(foo.toRefString())} == true`;
|
|
103
|
+
|
|
104
|
+
expect(test.toValue()).toEqual("mockFunction('{{some.binding}}') == true");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("Can Pass Nested Expressions", () => {
|
|
108
|
+
const mockFunction: ExpressionHandler<[Expression, number], boolean> = (
|
|
109
|
+
ctx,
|
|
110
|
+
val,
|
|
111
|
+
) => {
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const mockFunctionDSL = wrapFunctionInType(mockFunction);
|
|
116
|
+
|
|
117
|
+
const foo = binding`foo.bar`;
|
|
118
|
+
const bar = expression<string>`${foo} != 1`;
|
|
119
|
+
const test = expression`${mockFunctionDSL(bar, 1)}`;
|
|
120
|
+
|
|
121
|
+
expect(test.toValue()).toEqual("mockFunction('{{foo.bar}} != 1', 1)");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("Mock a function by name and args", () => {
|
|
125
|
+
const testFunction = makeFunctionByName<[string, number, boolean], void>(
|
|
126
|
+
"testFunction",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(testFunction("1", 0, false).toValue()).toMatch(
|
|
130
|
+
"testFunction('1', 0, false)",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("Export Generator", () => {
|
|
135
|
+
const mockFunction: ExpressionHandler<[string, number], boolean> = (
|
|
136
|
+
ctx,
|
|
137
|
+
val,
|
|
138
|
+
) => {
|
|
139
|
+
return false;
|
|
140
|
+
};
|
|
141
|
+
const expressionFunctions = { mockFunction };
|
|
142
|
+
|
|
143
|
+
const usableFunctions =
|
|
144
|
+
mapExpressionHandlersToFunctions<typeof expressionFunctions>(
|
|
145
|
+
expressionFunctions,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(usableFunctions.mockFunction("1", 0).toValue()).toMatch(
|
|
149
|
+
"mockFunction('1', 0)",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("Return Type Chaining", () => {
|
|
154
|
+
const mockFunction: ExpressionHandler<[string, number], string> = (
|
|
155
|
+
ctx,
|
|
156
|
+
val,
|
|
157
|
+
) => {
|
|
158
|
+
return "false";
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const mockFunction2: ExpressionHandler<[string], boolean> = (ctx, val) => {
|
|
162
|
+
return false;
|
|
163
|
+
};
|
|
164
|
+
const expressionFunctions = { mockFunction, mockFunction2 };
|
|
165
|
+
|
|
166
|
+
const usableFunctions =
|
|
167
|
+
mapExpressionHandlersToFunctions<typeof expressionFunctions>(
|
|
168
|
+
expressionFunctions,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(
|
|
172
|
+
usableFunctions
|
|
173
|
+
.mockFunction2(usableFunctions.mockFunction("1", 0).toValue())
|
|
174
|
+
.toValue(),
|
|
175
|
+
).toMatch("mockFunction2('mockFunction('1', 0)')");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("Works with DSL Schema", () => {
|
|
179
|
+
const mockFunction: ExpressionHandler<[string, boolean], string> = (
|
|
180
|
+
ctx,
|
|
181
|
+
val,
|
|
182
|
+
) => {
|
|
183
|
+
return "false";
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const dataTypes = { LocalBazType };
|
|
187
|
+
|
|
188
|
+
type DTREF = DataTypeRefs<typeof dataTypes>;
|
|
189
|
+
|
|
190
|
+
const refableTypes = getObjectReferences<typeof dataTypes, DTREF>(
|
|
191
|
+
dataTypes,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const data = {
|
|
195
|
+
some: {
|
|
196
|
+
value: refableTypes.LocalBazTypeRef,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const schema = makeBindingsForObject(data);
|
|
201
|
+
const expressionFunctions = { mockFunction };
|
|
202
|
+
|
|
203
|
+
const usableFunctions =
|
|
204
|
+
mapExpressionHandlersToFunctions<typeof expressionFunctions>(
|
|
205
|
+
expressionFunctions,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(
|
|
209
|
+
usableFunctions
|
|
210
|
+
.mockFunction("1", schema.some.value.toRefString())
|
|
211
|
+
.toValue(),
|
|
212
|
+
).toMatch("mockFunction('1', '{{some.value}}')");
|
|
213
|
+
});
|
|
214
|
+
});
|
package/src/compiler/schema.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { BindingTemplateInstance } from "../string-templates";
|
|
|
8
8
|
const bindingSymbol = Symbol("binding");
|
|
9
9
|
|
|
10
10
|
/** Symbol to indicate that a schema node should be generated with a different name */
|
|
11
|
-
export const SchemaTypeName = Symbol("Schema Rename");
|
|
11
|
+
export const SchemaTypeName: unique symbol = Symbol("Schema Rename");
|
|
12
12
|
|
|
13
13
|
interface SchemaChildren {
|
|
14
14
|
/** Object property that will be used to create the intermediate type */
|
|
@@ -47,7 +47,15 @@ export class SchemaGenerator {
|
|
|
47
47
|
private generatedDataTypes: Map<string, GeneratedDataType>;
|
|
48
48
|
private logger: LoggingInterface;
|
|
49
49
|
|
|
50
|
-
public hooks
|
|
50
|
+
public hooks: {
|
|
51
|
+
createSchemaNode: SyncWaterfallHook<
|
|
52
|
+
[
|
|
53
|
+
node: Schema.DataType<unknown>,
|
|
54
|
+
originalProperty: Record<string | symbol, unknown>,
|
|
55
|
+
],
|
|
56
|
+
Record<string, any>
|
|
57
|
+
>;
|
|
58
|
+
} = {
|
|
51
59
|
createSchemaNode: new SyncWaterfallHook<
|
|
52
60
|
[
|
|
53
61
|
node: Schema.DataType,
|
|
@@ -197,14 +205,14 @@ export type MakeBindingRefable<T> = {
|
|
|
197
205
|
: T[P] extends unknown[]
|
|
198
206
|
? T[P]
|
|
199
207
|
: MakeBindingRefable<T[P]>;
|
|
200
|
-
} & BindingTemplateInstance
|
|
208
|
+
} & BindingTemplateInstance<T>;
|
|
201
209
|
|
|
202
210
|
/**
|
|
203
211
|
* Adds bindings to an object so that the object can be directly used in JSX
|
|
204
212
|
*/
|
|
205
213
|
export function makeBindingsForObject<Type>(
|
|
206
214
|
obj: Type,
|
|
207
|
-
arrayAccessorKeys = ["_index_"],
|
|
215
|
+
arrayAccessorKeys: string[] = ["_index_"],
|
|
208
216
|
): MakeBindingRefable<Type> {
|
|
209
217
|
/** Proxy to track binding callbacks */
|
|
210
218
|
const accessor = (paths: string[]) => {
|
|
@@ -258,7 +266,7 @@ export function makeBindingsForObject<Type>(
|
|
|
258
266
|
/**
|
|
259
267
|
* Generates binding for an object property
|
|
260
268
|
*/
|
|
261
|
-
export const getBindingFromObject = (obj: any) => {
|
|
269
|
+
export const getBindingFromObject = (obj: any): BindingTemplateInstance => {
|
|
262
270
|
const baseBindings = obj[bindingSymbol] as string[];
|
|
263
271
|
if (!Array.isArray(baseBindings) || baseBindings.length === 0) {
|
|
264
272
|
throw new Error(`Unable to get binding for ${obj}`);
|
|
@@ -270,13 +278,13 @@ export const getBindingFromObject = (obj: any) => {
|
|
|
270
278
|
/**
|
|
271
279
|
* Returns the binding string from an object path
|
|
272
280
|
*/
|
|
273
|
-
export const getBindingStringFromObject = (obj: any) => {
|
|
281
|
+
export const getBindingStringFromObject = (obj: any): string => {
|
|
274
282
|
return getBindingFromObject(obj).toString();
|
|
275
283
|
};
|
|
276
284
|
|
|
277
285
|
/**
|
|
278
286
|
* Returns the ref string from an object path
|
|
279
287
|
*/
|
|
280
|
-
export const getRefStringFromObject = (obj: any) => {
|
|
288
|
+
export const getRefStringFromObject = (obj: any): string => {
|
|
281
289
|
return getBindingFromObject(obj).toRefString();
|
|
282
290
|
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
and,
|
|
4
|
+
assign,
|
|
5
|
+
binding as b,
|
|
6
|
+
expression as e,
|
|
7
|
+
equals,
|
|
8
|
+
nand,
|
|
9
|
+
nor,
|
|
10
|
+
not,
|
|
11
|
+
or,
|
|
12
|
+
} from "../..";
|
|
13
|
+
|
|
14
|
+
describe("assign", () => {
|
|
15
|
+
test("string", () => {
|
|
16
|
+
const binding = b`foo.bar`;
|
|
17
|
+
expect(assign(binding, "1").toValue()).toStrictEqual("{{foo.bar}} = '1'");
|
|
18
|
+
});
|
|
19
|
+
test("number", () => {
|
|
20
|
+
const binding = b`foo.bar`;
|
|
21
|
+
expect(assign(binding, 1).toValue()).toStrictEqual("{{foo.bar}} = 1");
|
|
22
|
+
});
|
|
23
|
+
test("boolean", () => {
|
|
24
|
+
const binding = b`foo.bar`;
|
|
25
|
+
expect(assign(binding, true).toValue()).toStrictEqual("{{foo.bar}} = true");
|
|
26
|
+
});
|
|
27
|
+
test("undefined", () => {
|
|
28
|
+
const binding = b`foo.bar`;
|
|
29
|
+
expect(assign(binding, undefined).toValue()).toStrictEqual(
|
|
30
|
+
"{{foo.bar}} = null",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("not", () => {
|
|
36
|
+
test("binding", () => {
|
|
37
|
+
const binding = b<boolean>`foo.bar`;
|
|
38
|
+
expect(not(binding).toValue()).toStrictEqual("!({{foo.bar}})");
|
|
39
|
+
});
|
|
40
|
+
test("expression", () => {
|
|
41
|
+
const expression = e<boolean>`${b`foo.bar`} = true`;
|
|
42
|
+
|
|
43
|
+
expect(not(expression).toValue()).toStrictEqual("!({{foo.bar}} = true)");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("and", () => {
|
|
48
|
+
test("bindings", () => {
|
|
49
|
+
const binding = b<boolean>`foo.bar`;
|
|
50
|
+
const binding2 = b<boolean>`foo.baz`;
|
|
51
|
+
expect(and(binding, binding2).toValue()).toStrictEqual(
|
|
52
|
+
"{{foo.bar}} && {{foo.baz}}",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("expressions", () => {
|
|
57
|
+
const expression = e<boolean>`${b`foo.bar`} == true`;
|
|
58
|
+
const expression2 = e<boolean>`${b`foo.baz`} == '1'`;
|
|
59
|
+
expect(and(expression, expression2).toValue()).toStrictEqual(
|
|
60
|
+
"({{foo.bar}} == true) && ({{foo.baz}} == '1')",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("or", () => {
|
|
66
|
+
test("bindings", () => {
|
|
67
|
+
const binding = b<boolean>`foo.bar`;
|
|
68
|
+
const binding2 = b<boolean>`foo.baz`;
|
|
69
|
+
expect(or(binding, binding2).toValue()).toStrictEqual(
|
|
70
|
+
"{{foo.bar}} || {{foo.baz}}",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("expressions", () => {
|
|
75
|
+
const expression = e<boolean>`${b`foo.bar`} == true`;
|
|
76
|
+
const expression2 = e<boolean>`${b`foo.baz`} == '1'`;
|
|
77
|
+
expect(or(expression, expression2).toValue()).toStrictEqual(
|
|
78
|
+
"({{foo.bar}} == true) || ({{foo.baz}} == '1')",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("nand", () => {
|
|
84
|
+
test("bindings", () => {
|
|
85
|
+
const binding = b<boolean>`foo.bar`;
|
|
86
|
+
const binding2 = b<boolean>`foo.baz`;
|
|
87
|
+
expect(nand(binding, binding2).toValue()).toStrictEqual(
|
|
88
|
+
"!({{foo.bar}} && {{foo.baz}})",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("expressions", () => {
|
|
93
|
+
const expression = e<boolean>`${b`foo.bar`} == true`;
|
|
94
|
+
const expression2 = e<boolean>`${b`foo.baz`} == '1'`;
|
|
95
|
+
expect(nand(expression, expression2).toValue()).toStrictEqual(
|
|
96
|
+
"!(({{foo.bar}} == true) && ({{foo.baz}} == '1'))",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("nor", () => {
|
|
102
|
+
test("bindings", () => {
|
|
103
|
+
const binding = b<boolean>`foo.bar`;
|
|
104
|
+
const binding2 = b<boolean>`foo.baz`;
|
|
105
|
+
expect(nor(binding, binding2).toValue()).toStrictEqual(
|
|
106
|
+
"!({{foo.bar}} || {{foo.baz}})",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("expressions", () => {
|
|
111
|
+
const expression = e<boolean>`${b`foo.bar`} == true`;
|
|
112
|
+
const expression2 = e<boolean>`${b`foo.baz`} == '1'`;
|
|
113
|
+
expect(nor(expression, expression2).toValue()).toStrictEqual(
|
|
114
|
+
"!(({{foo.bar}} == true) || ({{foo.baz}} == '1'))",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("equals", () => {
|
|
120
|
+
test("binding-string", () => {
|
|
121
|
+
const binding = b`foo.bar`;
|
|
122
|
+
expect(equals(binding, "1").toValue()).toStrictEqual("{{foo.bar}} == '1'");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("binding-number", () => {
|
|
126
|
+
const binding = b`foo.bar`;
|
|
127
|
+
expect(equals(binding, 1).toValue()).toStrictEqual("{{foo.bar}} == 1");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("binding-boolean", () => {
|
|
131
|
+
const binding = b`foo.bar`;
|
|
132
|
+
expect(equals(binding, true).toValue()).toStrictEqual(
|
|
133
|
+
"{{foo.bar}} == true",
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("binding-undefined", () => {
|
|
138
|
+
const binding = b`foo.bar`;
|
|
139
|
+
expect(equals(binding, undefined).toValue()).toStrictEqual(
|
|
140
|
+
"{{foo.bar}} == null",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("bindings", () => {
|
|
145
|
+
const binding = b<boolean>`foo.bar`;
|
|
146
|
+
const binding2 = b<boolean>`foo.baz`;
|
|
147
|
+
expect(equals(binding, binding2).toValue()).toStrictEqual(
|
|
148
|
+
"{{foo.bar}} == {{foo.baz}}",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("expressions", () => {
|
|
153
|
+
const expression = e<boolean>`${b`foo.bar`} == true`;
|
|
154
|
+
const expression2 = e<boolean>`${b`foo.baz`} == '1'`;
|
|
155
|
+
expect(equals(expression, expression2).toValue()).toStrictEqual(
|
|
156
|
+
"({{foo.bar}} == true) == ({{foo.baz}} == '1')",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { expression as e } from "../../string-templates";
|
|
3
|
+
import { testExpression } from "../testing";
|
|
4
|
+
import { ExpressionHandler, withoutContext } from "@player-ui/player";
|
|
5
|
+
|
|
6
|
+
describe("expression testing", () => {
|
|
7
|
+
const initialModel = {
|
|
8
|
+
foo: {
|
|
9
|
+
bar: {
|
|
10
|
+
a: "1",
|
|
11
|
+
b: 2,
|
|
12
|
+
c: false,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
test("basic operations", () => {
|
|
18
|
+
const exp = e`{{foo.bar.a}} = "test"`;
|
|
19
|
+
const result = testExpression(exp, initialModel);
|
|
20
|
+
expect(result).toStrictEqual({
|
|
21
|
+
foo: { bar: { a: "test", b: 2, c: false } },
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("custom function", () => {
|
|
26
|
+
const exp = e`{{foo.bar.c}} = not({{foo.bar.c}})`;
|
|
27
|
+
|
|
28
|
+
const mockExpression: ExpressionHandler<[boolean], boolean> =
|
|
29
|
+
withoutContext((arg) => {
|
|
30
|
+
return !arg;
|
|
31
|
+
});
|
|
32
|
+
const expressionMap = new Map([["not", mockExpression]]);
|
|
33
|
+
const result = testExpression(exp, initialModel, expressionMap);
|
|
34
|
+
expect(result).toStrictEqual({
|
|
35
|
+
foo: { bar: { a: "1", b: 2, c: true } },
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expression as e,
|
|
3
|
+
BindingTemplateInstance,
|
|
4
|
+
ExpressionTemplateInstance,
|
|
5
|
+
isTemplateStringInstance,
|
|
6
|
+
isBindingTemplateInstance,
|
|
7
|
+
} from "..";
|
|
8
|
+
|
|
9
|
+
type Argument<T> =
|
|
10
|
+
| string
|
|
11
|
+
| boolean
|
|
12
|
+
| number
|
|
13
|
+
| undefined
|
|
14
|
+
| BindingTemplateInstance<T>
|
|
15
|
+
| ExpressionTemplateInstance<T>;
|
|
16
|
+
|
|
17
|
+
const escapePrimitive = <T>(
|
|
18
|
+
val: Exclude<
|
|
19
|
+
Argument<T>,
|
|
20
|
+
BindingTemplateInstance<T> | ExpressionTemplateInstance<T>
|
|
21
|
+
>,
|
|
22
|
+
): string => {
|
|
23
|
+
switch (typeof val) {
|
|
24
|
+
case "string": {
|
|
25
|
+
return `'${val}'`;
|
|
26
|
+
}
|
|
27
|
+
case "undefined": {
|
|
28
|
+
return "null";
|
|
29
|
+
}
|
|
30
|
+
default: {
|
|
31
|
+
return `${val}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleBindingOrExpression = (
|
|
37
|
+
val: BindingTemplateInstance | ExpressionTemplateInstance,
|
|
38
|
+
) => {
|
|
39
|
+
if (isBindingTemplateInstance(val)) {
|
|
40
|
+
return val.toRefString();
|
|
41
|
+
} else {
|
|
42
|
+
return `(${val.toValue()})`;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleArgument = <T>(arg: Argument<T>): string => {
|
|
47
|
+
return isTemplateStringInstance(arg)
|
|
48
|
+
? handleBindingOrExpression(arg)
|
|
49
|
+
: escapePrimitive(arg);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Performs an assigment of a value to a binding by returning the expression
|
|
54
|
+
* {{<binding>}} = <value>
|
|
55
|
+
* @param binding
|
|
56
|
+
* @param value
|
|
57
|
+
*/
|
|
58
|
+
export const assign = <T>(
|
|
59
|
+
binding: BindingTemplateInstance<any>,
|
|
60
|
+
value: Argument<T>,
|
|
61
|
+
): ExpressionTemplateInstance<void> => {
|
|
62
|
+
return e`${binding} = ${isTemplateStringInstance(value) ? value : escapePrimitive(value)}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns an equality comparison between the two values
|
|
67
|
+
*/
|
|
68
|
+
export const equals = <A, B>(
|
|
69
|
+
a: Argument<A>,
|
|
70
|
+
b: Argument<B>,
|
|
71
|
+
): ExpressionTemplateInstance<boolean> => {
|
|
72
|
+
return e`${handleArgument(a)} == ${handleArgument(b)}`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns the negated version of the binding/expression
|
|
77
|
+
* by returning !(<value>)
|
|
78
|
+
* @param binding Binding/Expression to invert
|
|
79
|
+
* @returns Negated binding/expression
|
|
80
|
+
*/
|
|
81
|
+
export const not = (
|
|
82
|
+
value: BindingTemplateInstance<boolean> | ExpressionTemplateInstance<boolean>,
|
|
83
|
+
): ExpressionTemplateInstance<boolean> => {
|
|
84
|
+
return e<boolean>`!(${value})`;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates an expression for the logical or'ing of the provided values
|
|
89
|
+
* e.g: <exp1> || <exp2> || ...
|
|
90
|
+
* @param values Array of bindings/expressions to logically or
|
|
91
|
+
* @returns boolean
|
|
92
|
+
*/
|
|
93
|
+
export const or = (
|
|
94
|
+
...values: Array<
|
|
95
|
+
BindingTemplateInstance<boolean> | ExpressionTemplateInstance<boolean>
|
|
96
|
+
>
|
|
97
|
+
): ExpressionTemplateInstance<boolean> => {
|
|
98
|
+
return e`${values.map(handleBindingOrExpression).join(" || ")}` as ExpressionTemplateInstance<boolean>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates an expression for the logical nor'ing of the provided values
|
|
103
|
+
* e.g: !(<exp1> || <exp2> || ...)
|
|
104
|
+
* @param values Array of bindings/expressions to logically nor
|
|
105
|
+
* @returns boolean
|
|
106
|
+
*/
|
|
107
|
+
export const nor = (
|
|
108
|
+
...values: Array<
|
|
109
|
+
BindingTemplateInstance<boolean> | ExpressionTemplateInstance<boolean>
|
|
110
|
+
>
|
|
111
|
+
): ExpressionTemplateInstance<boolean> => {
|
|
112
|
+
return not(or(...values));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates an expression for the logical and'ing of the provided values
|
|
117
|
+
* e.g: <exp1> && <exp2> && ...
|
|
118
|
+
* @param values Array of bindings/expressions to logically and
|
|
119
|
+
* @returns boolean
|
|
120
|
+
*/
|
|
121
|
+
export const and = (
|
|
122
|
+
...values: Array<
|
|
123
|
+
BindingTemplateInstance<boolean> | ExpressionTemplateInstance<boolean>
|
|
124
|
+
>
|
|
125
|
+
): ExpressionTemplateInstance<boolean> => {
|
|
126
|
+
return e`${values.map(handleBindingOrExpression).join(" && ")}` as ExpressionTemplateInstance<boolean>;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Creates an expression for the logical nand'ing of the provided values
|
|
131
|
+
* e.g: !(<exp1> && <exp2> && ...)
|
|
132
|
+
* @param values Array of bindings/expressions to logically nand
|
|
133
|
+
* @returns boolean
|
|
134
|
+
*/
|
|
135
|
+
export const nand = (
|
|
136
|
+
...values: Array<
|
|
137
|
+
BindingTemplateInstance<boolean> | ExpressionTemplateInstance<boolean>
|
|
138
|
+
>
|
|
139
|
+
): ExpressionTemplateInstance<boolean> => {
|
|
140
|
+
return not(and(...values));
|
|
141
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BindingParser,
|
|
3
|
+
ExpressionEvaluator,
|
|
4
|
+
ExpressionHandler,
|
|
5
|
+
ExpressionType,
|
|
6
|
+
LocalModel,
|
|
7
|
+
withParser,
|
|
8
|
+
} from "@player-ui/player";
|
|
9
|
+
import { ExpressionTemplateInstance } from "../string-templates";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Test harness to make testing expressions easier.
|
|
13
|
+
* Given an expreesion and an initial data model the harness will execute the expression
|
|
14
|
+
* on and return the new state of the data model.
|
|
15
|
+
* @param exp expression to execute
|
|
16
|
+
* @param initialData data model to operate on
|
|
17
|
+
* @param expressions expression handlers for functions that are called
|
|
18
|
+
* @returns Final data model state
|
|
19
|
+
*/
|
|
20
|
+
export function testExpression(
|
|
21
|
+
exp: ExpressionTemplateInstance,
|
|
22
|
+
initialData: object,
|
|
23
|
+
expressions?: Map<string, ExpressionHandler<any[], any>>,
|
|
24
|
+
): object {
|
|
25
|
+
// Setup Mock Player model and parsers
|
|
26
|
+
const localModel = new LocalModel(initialData);
|
|
27
|
+
const bindingParser = new BindingParser({
|
|
28
|
+
get: localModel.get,
|
|
29
|
+
set: localModel.set,
|
|
30
|
+
evaluate: (exp: ExpressionType) => {
|
|
31
|
+
return evaluator.evaluate(exp);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const model = withParser(localModel, bindingParser.parse);
|
|
36
|
+
const evaluator = new ExpressionEvaluator({ model });
|
|
37
|
+
expressions?.forEach((fn, name) => evaluator.addExpressionFunction(name, fn));
|
|
38
|
+
evaluator.evaluate(exp.toValue());
|
|
39
|
+
return localModel.get();
|
|
40
|
+
}
|
package/src/index.ts
CHANGED