@player-ui/player 0.3.1-next.1 → 0.3.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/dist/index.cjs.js +899 -336
- package/dist/index.d.ts +275 -93
- package/dist/index.esm.js +890 -334
- package/dist/player.dev.js +11429 -0
- package/dist/player.prod.js +2 -0
- package/package.json +16 -5
- package/src/binding/binding.ts +8 -0
- package/src/binding/index.ts +14 -4
- package/src/binding/resolver.ts +2 -4
- package/src/binding-grammar/custom/index.ts +17 -9
- package/src/controllers/constants/index.ts +9 -5
- package/src/controllers/{data.ts → data/controller.ts} +62 -61
- package/src/controllers/data/index.ts +1 -0
- package/src/controllers/data/utils.ts +42 -0
- package/src/controllers/flow/controller.ts +16 -12
- package/src/controllers/flow/flow.ts +6 -1
- package/src/controllers/index.ts +1 -1
- package/src/controllers/validation/binding-tracker.ts +42 -19
- package/src/controllers/validation/controller.ts +375 -148
- package/src/controllers/view/asset-transform.ts +4 -1
- package/src/controllers/view/controller.ts +20 -3
- package/src/data/dependency-tracker.ts +14 -0
- package/src/data/local-model.ts +25 -1
- package/src/data/model.ts +60 -8
- package/src/data/noop-model.ts +2 -0
- package/src/expressions/evaluator-functions.ts +24 -2
- package/src/expressions/evaluator.ts +38 -34
- package/src/expressions/index.ts +1 -0
- package/src/expressions/parser.ts +116 -44
- package/src/expressions/types.ts +50 -17
- package/src/expressions/utils.ts +143 -1
- package/src/player.ts +60 -46
- package/src/plugins/default-exp-plugin.ts +57 -0
- package/src/plugins/flow-exp-plugin.ts +2 -2
- package/src/schema/schema.ts +28 -9
- package/src/string-resolver/index.ts +26 -9
- package/src/types.ts +6 -3
- package/src/validator/binding-map-splice.ts +59 -0
- package/src/validator/index.ts +1 -0
- package/src/validator/types.ts +11 -3
- package/src/validator/validation-middleware.ts +58 -6
- package/src/view/parser/index.ts +51 -3
- package/src/view/plugins/applicability.ts +1 -1
- package/src/view/plugins/string-resolver.ts +35 -9
- package/src/view/plugins/template-plugin.ts +1 -6
- package/src/view/resolver/index.ts +119 -54
- package/src/view/resolver/types.ts +48 -7
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { BindingInstance } from '../binding';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Remove a binding, and any children from from the map
|
|
5
|
+
* If the binding is an array-item, then it will be spliced from the array and the others will be shifted down
|
|
6
|
+
*
|
|
7
|
+
* @param sourceMap - A map of bindings to values
|
|
8
|
+
* @param binding - The binding to remove from the map
|
|
9
|
+
*/
|
|
10
|
+
export function removeBindingAndChildrenFromMap<T>(
|
|
11
|
+
sourceMap: Map<BindingInstance, T>,
|
|
12
|
+
binding: BindingInstance
|
|
13
|
+
): Map<BindingInstance, T> {
|
|
14
|
+
const targetMap = new Map(sourceMap);
|
|
15
|
+
|
|
16
|
+
const parentBinding = binding.parent();
|
|
17
|
+
const property = binding.key();
|
|
18
|
+
|
|
19
|
+
// Clear out any that are sub-bindings of this binding
|
|
20
|
+
|
|
21
|
+
targetMap.forEach((_value, trackedBinding) => {
|
|
22
|
+
if (binding === trackedBinding || binding.contains(trackedBinding)) {
|
|
23
|
+
targetMap.delete(trackedBinding);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (typeof property === 'number') {
|
|
28
|
+
// Splice out this index from the rest
|
|
29
|
+
|
|
30
|
+
// Order matters here b/c we are shifting items in the array
|
|
31
|
+
// Start with the smallest index and work our way down
|
|
32
|
+
const bindingsToRewrite = Array.from(sourceMap.keys())
|
|
33
|
+
.filter((b) => {
|
|
34
|
+
if (parentBinding.contains(b)) {
|
|
35
|
+
const [childIndex] = b.relative(parentBinding);
|
|
36
|
+
return typeof childIndex === 'number' && childIndex > property;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
})
|
|
41
|
+
.sort();
|
|
42
|
+
|
|
43
|
+
bindingsToRewrite.forEach((trackedBinding) => {
|
|
44
|
+
// If the tracked binding is a sub-binding of the parent binding, then we need to
|
|
45
|
+
// update the path to reflect the new index
|
|
46
|
+
|
|
47
|
+
const [childIndex, ...childPath] = trackedBinding.relative(parentBinding);
|
|
48
|
+
|
|
49
|
+
if (typeof childIndex === 'number') {
|
|
50
|
+
const newSegments = [childIndex - 1, ...childPath];
|
|
51
|
+
const newChildBinding = parentBinding.descendent(newSegments);
|
|
52
|
+
targetMap.set(newChildBinding, targetMap.get(trackedBinding) as T);
|
|
53
|
+
targetMap.delete(trackedBinding);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return targetMap;
|
|
59
|
+
}
|
package/src/validator/index.ts
CHANGED
package/src/validator/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Validation } from '@player-ui/types';
|
|
1
|
+
import type { Schema, Validation } from '@player-ui/types';
|
|
2
2
|
|
|
3
3
|
import type { BindingInstance, BindingFactory } from '../binding';
|
|
4
4
|
import type { DataModelWithParser } from '../data';
|
|
@@ -40,12 +40,17 @@ type RequiredValidationKeys = 'severity' | 'trigger';
|
|
|
40
40
|
export type ValidationObject = Validation.Reference &
|
|
41
41
|
Required<Pick<Validation.Reference, RequiredValidationKeys>>;
|
|
42
42
|
|
|
43
|
+
export type ValidationObjectWithHandler = ValidationObject & {
|
|
44
|
+
/** A predefined handler for this validation object */
|
|
45
|
+
handler?: ValidatorFunction;
|
|
46
|
+
};
|
|
47
|
+
|
|
43
48
|
export interface ValidationProvider {
|
|
44
49
|
getValidationsForBinding?(
|
|
45
50
|
binding: BindingInstance
|
|
46
|
-
): Array<
|
|
51
|
+
): Array<ValidationObjectWithHandler> | undefined;
|
|
47
52
|
|
|
48
|
-
getValidationsForView?(): Array<
|
|
53
|
+
getValidationsForView?(): Array<ValidationObjectWithHandler> | undefined;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export interface ValidatorContext {
|
|
@@ -66,6 +71,9 @@ export interface ValidatorContext {
|
|
|
66
71
|
|
|
67
72
|
/** The constants for messages */
|
|
68
73
|
constants: ConstantsProvider;
|
|
74
|
+
|
|
75
|
+
/** The type in the schema that triggered the validation if there is one */
|
|
76
|
+
schemaType: Schema.DataType | undefined;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export type ValidatorFunction<Options = unknown> = (
|
|
@@ -11,6 +11,17 @@ import { toModel } from '../data';
|
|
|
11
11
|
import type { Logger } from '../logger';
|
|
12
12
|
|
|
13
13
|
import type { ValidationResponse } from './types';
|
|
14
|
+
import { removeBindingAndChildrenFromMap } from './binding-map-splice';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A BindingInstance with an indicator of whether or not it's a strong binding
|
|
18
|
+
*/
|
|
19
|
+
export type StrongOrWeakBinding = {
|
|
20
|
+
/** BindingInstance in question */
|
|
21
|
+
binding: BindingInstance;
|
|
22
|
+
/** Boolean indicating whether the relevant BindingInstance is a strong binding */
|
|
23
|
+
isStrong: boolean;
|
|
24
|
+
};
|
|
14
25
|
|
|
15
26
|
/**
|
|
16
27
|
* Returns a validation object if the data is invalid or an set of BindingsInstances if the binding itself is a weak ref of another invalid validation
|
|
@@ -18,7 +29,7 @@ import type { ValidationResponse } from './types';
|
|
|
18
29
|
export type MiddlewareChecker = (
|
|
19
30
|
binding: BindingInstance,
|
|
20
31
|
model: DataModelImpl
|
|
21
|
-
) => ValidationResponse | Set<
|
|
32
|
+
) => ValidationResponse | Set<StrongOrWeakBinding> | undefined;
|
|
22
33
|
|
|
23
34
|
/**
|
|
24
35
|
* Middleware for the data-model that caches the results of invalid data
|
|
@@ -27,17 +38,21 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
27
38
|
public validator: MiddlewareChecker;
|
|
28
39
|
public shadowModelPaths: Map<BindingInstance, any>;
|
|
29
40
|
private logger?: Logger;
|
|
41
|
+
private shouldIncludeInvalid?: (options?: DataModelOptions) => boolean;
|
|
30
42
|
|
|
31
43
|
constructor(
|
|
32
44
|
validator: MiddlewareChecker,
|
|
33
45
|
options?: {
|
|
34
46
|
/** A logger instance */
|
|
35
47
|
logger?: Logger;
|
|
48
|
+
/** Optional function to include data staged in shadowModel */
|
|
49
|
+
shouldIncludeInvalid?: (options?: DataModelOptions) => boolean;
|
|
36
50
|
}
|
|
37
51
|
) {
|
|
38
52
|
this.validator = validator;
|
|
39
53
|
this.shadowModelPaths = new Map();
|
|
40
54
|
this.logger = options?.logger;
|
|
55
|
+
this.shouldIncludeInvalid = options?.shouldIncludeInvalid;
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
public set(
|
|
@@ -48,8 +63,11 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
48
63
|
const asModel = toModel(this, { ...options, includeInvalid: true }, next);
|
|
49
64
|
const nextTransaction: BatchSetTransaction = [];
|
|
50
65
|
|
|
66
|
+
const includedBindings = new Set<BindingInstance>();
|
|
67
|
+
|
|
51
68
|
transaction.forEach(([binding, value]) => {
|
|
52
69
|
this.shadowModelPaths.set(binding, value);
|
|
70
|
+
includedBindings.add(binding);
|
|
53
71
|
});
|
|
54
72
|
|
|
55
73
|
const invalidBindings: Array<BindingInstance> = [];
|
|
@@ -60,8 +78,17 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
60
78
|
if (validations === undefined) {
|
|
61
79
|
nextTransaction.push([binding, value]);
|
|
62
80
|
} else if (validations instanceof Set) {
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
validations.forEach((validation) => {
|
|
82
|
+
invalidBindings.push(validation.binding);
|
|
83
|
+
if (
|
|
84
|
+
!validation.isStrong &&
|
|
85
|
+
validation.binding.asString() === binding.asString()
|
|
86
|
+
) {
|
|
87
|
+
nextTransaction.push([validation.binding, value]);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
} else if (includedBindings.has(binding)) {
|
|
91
|
+
invalidBindings.push(binding);
|
|
65
92
|
this.logger?.debug(
|
|
66
93
|
`Invalid value for path: ${binding.asString()} - ${
|
|
67
94
|
validations.severity
|
|
@@ -70,15 +97,22 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
70
97
|
}
|
|
71
98
|
});
|
|
72
99
|
|
|
100
|
+
let validResults: Updates = [];
|
|
101
|
+
|
|
73
102
|
if (next && nextTransaction.length > 0) {
|
|
74
103
|
// defer clearing the shadow model to prevent validations that are run twice due to weak binding refs still needing the data
|
|
75
104
|
nextTransaction.forEach(([binding]) =>
|
|
76
105
|
this.shadowModelPaths.delete(binding)
|
|
77
106
|
);
|
|
78
|
-
|
|
107
|
+
const result = next.set(nextTransaction, options);
|
|
108
|
+
if (invalidBindings.length === 0) {
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
validResults = result;
|
|
79
113
|
}
|
|
80
114
|
|
|
81
|
-
|
|
115
|
+
const invalidResults = invalidBindings.map((binding) => {
|
|
82
116
|
return {
|
|
83
117
|
binding,
|
|
84
118
|
oldValue: asModel.get(binding),
|
|
@@ -86,6 +120,8 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
86
120
|
force: true,
|
|
87
121
|
};
|
|
88
122
|
});
|
|
123
|
+
|
|
124
|
+
return [...validResults, ...invalidResults];
|
|
89
125
|
}
|
|
90
126
|
|
|
91
127
|
public get(
|
|
@@ -95,7 +131,10 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
95
131
|
) {
|
|
96
132
|
let val = next?.get(binding, options);
|
|
97
133
|
|
|
98
|
-
if (
|
|
134
|
+
if (
|
|
135
|
+
this.shouldIncludeInvalid?.(options) ??
|
|
136
|
+
options?.includeInvalid === true
|
|
137
|
+
) {
|
|
99
138
|
this.shadowModelPaths.forEach((shadowValue, shadowBinding) => {
|
|
100
139
|
if (shadowBinding === binding) {
|
|
101
140
|
val = shadowValue;
|
|
@@ -111,4 +150,17 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
111
150
|
|
|
112
151
|
return val;
|
|
113
152
|
}
|
|
153
|
+
|
|
154
|
+
public delete(
|
|
155
|
+
binding: BindingInstance,
|
|
156
|
+
options?: DataModelOptions,
|
|
157
|
+
next?: DataModelImpl
|
|
158
|
+
) {
|
|
159
|
+
this.shadowModelPaths = removeBindingAndChildrenFromMap(
|
|
160
|
+
this.shadowModelPaths,
|
|
161
|
+
binding
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return next?.delete(binding, options);
|
|
165
|
+
}
|
|
114
166
|
}
|
package/src/view/parser/index.ts
CHANGED
|
@@ -83,6 +83,21 @@ export class Parser {
|
|
|
83
83
|
return tapped;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Checks if there are templated values in the object
|
|
88
|
+
*
|
|
89
|
+
* @param obj - The Parsed Object to check to see if we have a template array type for
|
|
90
|
+
* @param localKey - The key being checked
|
|
91
|
+
*/
|
|
92
|
+
private hasTemplateValues(obj: any, localKey: string) {
|
|
93
|
+
return (
|
|
94
|
+
Object.hasOwnProperty.call(obj, 'template') &&
|
|
95
|
+
Array.isArray(obj?.template) &&
|
|
96
|
+
obj.template.length &&
|
|
97
|
+
obj.template.find((tmpl: any) => tmpl.output === localKey)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
public parseObject(
|
|
87
102
|
obj: object,
|
|
88
103
|
type: Node.ChildrenTypes = NodeType.Value,
|
|
@@ -120,7 +135,13 @@ export class Parser {
|
|
|
120
135
|
|
|
121
136
|
const objEntries = Array.isArray(localObj)
|
|
122
137
|
? localObj.map((v, i) => [i, v])
|
|
123
|
-
:
|
|
138
|
+
: [
|
|
139
|
+
...Object.entries(localObj),
|
|
140
|
+
...Object.getOwnPropertySymbols(localObj).map((s) => [
|
|
141
|
+
s,
|
|
142
|
+
(localObj as any)[s],
|
|
143
|
+
]),
|
|
144
|
+
];
|
|
124
145
|
|
|
125
146
|
const defaultValue: NestedObj = {
|
|
126
147
|
children: [],
|
|
@@ -166,6 +187,13 @@ export class Parser {
|
|
|
166
187
|
template
|
|
167
188
|
);
|
|
168
189
|
|
|
190
|
+
if (templateAST?.type === NodeType.MultiNode) {
|
|
191
|
+
templateAST.values.forEach((v) => {
|
|
192
|
+
// eslint-disable-next-line no-param-reassign
|
|
193
|
+
v.parent = templateAST;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
169
197
|
if (templateAST) {
|
|
170
198
|
return {
|
|
171
199
|
path: [...path, template.output],
|
|
@@ -193,6 +221,26 @@ export class Parser {
|
|
|
193
221
|
NodeType.Switch
|
|
194
222
|
);
|
|
195
223
|
|
|
224
|
+
if (
|
|
225
|
+
localSwitch &&
|
|
226
|
+
localSwitch.type === NodeType.Value &&
|
|
227
|
+
localSwitch.children?.length === 1 &&
|
|
228
|
+
localSwitch.value === undefined
|
|
229
|
+
) {
|
|
230
|
+
const firstChild = localSwitch.children[0];
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
...rest,
|
|
234
|
+
children: [
|
|
235
|
+
...children,
|
|
236
|
+
{
|
|
237
|
+
path: [...path, localKey, ...firstChild.path],
|
|
238
|
+
value: firstChild.value,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
196
244
|
if (localSwitch) {
|
|
197
245
|
return {
|
|
198
246
|
...rest,
|
|
@@ -216,7 +264,7 @@ export class Parser {
|
|
|
216
264
|
const multiNode = this.hooks.onCreateASTNode.call(
|
|
217
265
|
{
|
|
218
266
|
type: NodeType.MultiNode,
|
|
219
|
-
override:
|
|
267
|
+
override: !this.hasTemplateValues(localObj, localKey),
|
|
220
268
|
values: childValues,
|
|
221
269
|
},
|
|
222
270
|
localValue
|
|
@@ -249,7 +297,7 @@ export class Parser {
|
|
|
249
297
|
if (determineNodeType === NodeType.Applicability) {
|
|
250
298
|
const parsedNode = this.hooks.parseNode.call(
|
|
251
299
|
localValue,
|
|
252
|
-
|
|
300
|
+
NodeType.Value,
|
|
253
301
|
options,
|
|
254
302
|
determineNodeType
|
|
255
303
|
);
|
|
@@ -87,15 +87,19 @@ export function resolveAllRefs(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** Traverse up the node tree finding the first available 'path' */
|
|
90
|
-
const findBasePath = (
|
|
90
|
+
const findBasePath = (
|
|
91
|
+
node: Node.Node,
|
|
92
|
+
resolver: Resolver
|
|
93
|
+
): Node.PathSegment[] => {
|
|
91
94
|
const parentNode = node.parent;
|
|
92
95
|
if (!parentNode) {
|
|
93
96
|
return [];
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
if ('children' in parentNode) {
|
|
100
|
+
const original = resolver.getSourceNode(node);
|
|
97
101
|
return (
|
|
98
|
-
parentNode.children?.find((child) => child.value ===
|
|
102
|
+
parentNode.children?.find((child) => child.value === original)?.path ?? []
|
|
99
103
|
);
|
|
100
104
|
}
|
|
101
105
|
|
|
@@ -103,11 +107,17 @@ const findBasePath = (node: Node.Node): Node.PathSegment[] => {
|
|
|
103
107
|
return [];
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
return findBasePath(parentNode);
|
|
110
|
+
return findBasePath(parentNode, resolver);
|
|
107
111
|
};
|
|
108
112
|
|
|
109
113
|
/** A plugin that resolves all string references for each node */
|
|
110
114
|
export default class StringResolverPlugin implements ViewPlugin {
|
|
115
|
+
private propertiesToSkipCache: Map<string, Set<string>>;
|
|
116
|
+
|
|
117
|
+
constructor() {
|
|
118
|
+
this.propertiesToSkipCache = new Map();
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
applyResolver(resolver: Resolver) {
|
|
112
122
|
resolver.hooks.resolve.tap('string-resolver', (value, node, options) => {
|
|
113
123
|
if (node.type === NodeType.Empty || node.type === NodeType.Unknown) {
|
|
@@ -120,13 +130,29 @@ export default class StringResolverPlugin implements ViewPlugin {
|
|
|
120
130
|
node.type === NodeType.View
|
|
121
131
|
) {
|
|
122
132
|
/** Use specified properties to skip during string resolution, or default */
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
let propsToSkip: Set<string>;
|
|
134
|
+
if (node.type === NodeType.Asset || node.type === NodeType.View) {
|
|
135
|
+
propsToSkip = new Set(
|
|
136
|
+
node.plugins?.stringResolver?.propertiesToSkip ?? ['exp']
|
|
137
|
+
);
|
|
138
|
+
if (node.value?.id) {
|
|
139
|
+
this.propertiesToSkipCache.set(node.value.id, propsToSkip);
|
|
140
|
+
}
|
|
141
|
+
} else if (
|
|
142
|
+
node.parent?.type === NodeType.MultiNode &&
|
|
143
|
+
(node.parent?.parent?.type === NodeType.Asset ||
|
|
144
|
+
node.parent?.parent?.type === NodeType.View) &&
|
|
145
|
+
node.parent.parent.value?.id &&
|
|
146
|
+
this.propertiesToSkipCache.has(node.parent.parent.value.id)
|
|
147
|
+
) {
|
|
148
|
+
propsToSkip = this.propertiesToSkipCache.get(
|
|
149
|
+
node.parent.parent.value.id
|
|
150
|
+
) as Set<string>;
|
|
151
|
+
} else {
|
|
152
|
+
propsToSkip = new Set(['exp']);
|
|
153
|
+
}
|
|
128
154
|
|
|
129
|
-
const nodePath = findBasePath(node);
|
|
155
|
+
const nodePath = findBasePath(node, resolver);
|
|
130
156
|
|
|
131
157
|
/** If the path includes something that is supposed to be skipped, this node should be skipped too. */
|
|
132
158
|
if (
|
|
@@ -95,16 +95,11 @@ export default class TemplatePlugin implements ViewPlugin {
|
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
const result: Node.MultiNode = {
|
|
98
|
-
parent: node.parent,
|
|
99
98
|
type: NodeType.MultiNode,
|
|
99
|
+
override: false,
|
|
100
100
|
values,
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
-
result.values.forEach((innerNode) => {
|
|
104
|
-
// eslint-disable-next-line no-param-reassign
|
|
105
|
-
innerNode.parent = result;
|
|
106
|
-
});
|
|
107
|
-
|
|
108
103
|
return result;
|
|
109
104
|
}
|
|
110
105
|
|