@player-ui/player 0.3.0-next.2 → 0.3.0-next.4
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 +4128 -891
- package/dist/index.d.ts +1227 -50
- package/dist/index.esm.js +4065 -836
- package/package.json +9 -15
- package/src/binding/binding.ts +108 -0
- package/src/binding/index.ts +188 -0
- package/src/binding/resolver.ts +157 -0
- package/src/binding/utils.ts +51 -0
- package/src/binding-grammar/ast.ts +113 -0
- package/src/binding-grammar/custom/index.ts +304 -0
- package/src/binding-grammar/ebnf/binding.ebnf +22 -0
- package/src/binding-grammar/ebnf/index.ts +186 -0
- package/src/binding-grammar/ebnf/types.ts +104 -0
- package/src/binding-grammar/index.ts +4 -0
- package/src/binding-grammar/parsimmon/index.ts +78 -0
- package/src/controllers/constants/index.ts +85 -0
- package/src/controllers/constants/utils.ts +37 -0
- package/src/{data.ts → controllers/data.ts} +6 -6
- package/src/controllers/flow/controller.ts +95 -0
- package/src/controllers/flow/flow.ts +205 -0
- package/src/controllers/flow/index.ts +2 -0
- package/src/controllers/index.ts +5 -0
- package/src/{validation → controllers/validation}/binding-tracker.ts +5 -5
- package/src/{validation → controllers/validation}/controller.ts +15 -14
- package/src/{validation → controllers/validation}/index.ts +0 -0
- package/src/{view → controllers/view}/asset-transform.ts +2 -3
- package/src/{view → controllers/view}/controller.ts +9 -8
- package/src/controllers/view/index.ts +4 -0
- package/src/{view → controllers/view}/store.ts +0 -0
- package/src/{view → controllers/view}/types.ts +2 -1
- package/src/data/dependency-tracker.ts +187 -0
- package/src/data/index.ts +4 -0
- package/src/data/local-model.ts +41 -0
- package/src/data/model.ts +216 -0
- package/src/data/noop-model.ts +18 -0
- package/src/expressions/evaluator-functions.ts +29 -0
- package/src/expressions/evaluator.ts +405 -0
- package/src/expressions/index.ts +3 -0
- package/src/expressions/parser.ts +889 -0
- package/src/expressions/types.ts +200 -0
- package/src/expressions/utils.ts +8 -0
- package/src/index.ts +9 -12
- package/src/logger/consoleLogger.ts +49 -0
- package/src/logger/index.ts +5 -0
- package/src/logger/noopLogger.ts +13 -0
- package/src/logger/proxyLogger.ts +25 -0
- package/src/logger/tapableLogger.ts +38 -0
- package/src/logger/types.ts +6 -0
- package/src/player.ts +21 -18
- package/src/plugins/flow-exp-plugin.ts +2 -3
- package/src/schema/index.ts +2 -0
- package/src/schema/schema.ts +220 -0
- package/src/schema/types.ts +60 -0
- package/src/string-resolver/index.ts +188 -0
- package/src/types.ts +11 -13
- package/src/utils/index.ts +1 -0
- package/src/utils/replaceParams.ts +17 -0
- package/src/validator/index.ts +3 -0
- package/src/validator/registry.ts +20 -0
- package/src/validator/types.ts +75 -0
- package/src/validator/validation-middleware.ts +114 -0
- package/src/view/builder/index.ts +81 -0
- package/src/view/index.ts +5 -4
- package/src/view/parser/index.ts +318 -0
- package/src/view/parser/types.ts +141 -0
- package/src/view/plugins/applicability.ts +78 -0
- package/src/view/plugins/index.ts +5 -0
- package/src/view/plugins/options.ts +4 -0
- package/src/view/plugins/plugin.ts +21 -0
- package/src/view/plugins/string-resolver.ts +149 -0
- package/src/view/plugins/switch.ts +120 -0
- package/src/view/plugins/template-plugin.ts +172 -0
- package/src/view/resolver/index.ts +397 -0
- package/src/view/resolver/types.ts +161 -0
- package/src/view/resolver/utils.ts +57 -0
- package/src/view/view.ts +149 -0
- package/src/utils/desc.d.ts +0 -2
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { SyncWaterfallHook, SyncHook } from 'tapable-ts';
|
|
2
|
+
import { setIn, addLast } from 'timm';
|
|
3
|
+
import dlv from 'dlv';
|
|
4
|
+
import { dequal } from 'dequal';
|
|
5
|
+
import type { BindingInstance, BindingLike } from '../../binding';
|
|
6
|
+
import type {
|
|
7
|
+
DataModelOptions,
|
|
8
|
+
DataModelWithParser,
|
|
9
|
+
Updates,
|
|
10
|
+
} from '../../data';
|
|
11
|
+
import { DependencyModel, withParser } from '../../data';
|
|
12
|
+
import type { Logger } from '../../logger';
|
|
13
|
+
import type { Node } from '../parser';
|
|
14
|
+
import { NodeType } from '../parser';
|
|
15
|
+
import { caresAboutDataChanges, toNodeResolveOptions } from './utils';
|
|
16
|
+
import type { Resolve } from './types';
|
|
17
|
+
|
|
18
|
+
export * from './types';
|
|
19
|
+
export * from './utils';
|
|
20
|
+
|
|
21
|
+
interface NodeUpdate extends Resolve.ResolvedNode {
|
|
22
|
+
/** A flag to track if a node has changed since the last resolution */
|
|
23
|
+
updated: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Add model context to the data model */
|
|
27
|
+
const withContext = (model: DataModelWithParser): DataModelWithParser => {
|
|
28
|
+
return {
|
|
29
|
+
get: (binding: BindingLike, options?: DataModelOptions): any => {
|
|
30
|
+
return model.get(binding, {
|
|
31
|
+
context: { model },
|
|
32
|
+
...options,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
set: (
|
|
37
|
+
transaction: [BindingLike, any][],
|
|
38
|
+
options?: DataModelOptions
|
|
39
|
+
): Updates => {
|
|
40
|
+
return model.set(transaction, {
|
|
41
|
+
context: { model },
|
|
42
|
+
...options,
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The Resolver is the way to take a parsed AST graph of a view and resolve it to a concrete representation of the current user state
|
|
50
|
+
* It combines the ability to mutate ast nodes before resolving, as well as the mutating the resolved objects while parsing
|
|
51
|
+
*/
|
|
52
|
+
export class Resolver {
|
|
53
|
+
public readonly hooks = {
|
|
54
|
+
/** A hook to allow skipping of the resolution tree for a specific node */
|
|
55
|
+
skipResolve: new SyncWaterfallHook<
|
|
56
|
+
[boolean, Node.Node, Resolve.NodeResolveOptions]
|
|
57
|
+
>(),
|
|
58
|
+
|
|
59
|
+
/** An event emitted before calculating the next update */
|
|
60
|
+
beforeUpdate: new SyncHook<[Set<BindingInstance> | undefined]>(),
|
|
61
|
+
|
|
62
|
+
/** An event emitted after calculating the next update */
|
|
63
|
+
afterUpdate: new SyncHook<[any]>(),
|
|
64
|
+
|
|
65
|
+
/** The options passed to a node to resolve it to an object */
|
|
66
|
+
resolveOptions: new SyncWaterfallHook<
|
|
67
|
+
[Resolve.NodeResolveOptions, Node.Node]
|
|
68
|
+
>(),
|
|
69
|
+
|
|
70
|
+
/** A hook to transform the AST node into a new AST node before resolving it */
|
|
71
|
+
beforeResolve: new SyncWaterfallHook<
|
|
72
|
+
[Node.Node | null, Resolve.NodeResolveOptions]
|
|
73
|
+
>(),
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A hook to transform an AST node into it's resolved value.
|
|
77
|
+
* This runs _before_ any children are resolved
|
|
78
|
+
*/
|
|
79
|
+
resolve: new SyncWaterfallHook<
|
|
80
|
+
[any, Node.Node, Resolve.NodeResolveOptions]
|
|
81
|
+
>(),
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A hook to transform the resolved value of an AST node.
|
|
85
|
+
* This runs _after_ all children nodes are resolved
|
|
86
|
+
*/
|
|
87
|
+
afterResolve: new SyncWaterfallHook<
|
|
88
|
+
[any, Node.Node, Resolve.NodeResolveOptions]
|
|
89
|
+
>(),
|
|
90
|
+
|
|
91
|
+
/** Called at the very end of a node's tree being updated */
|
|
92
|
+
afterNodeUpdate: new SyncHook<
|
|
93
|
+
[Node.Node, Node.Node | undefined, NodeUpdate]
|
|
94
|
+
>(),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The AST tree after beforeResolve is ran mapped to the AST before beforeResolve is ran
|
|
99
|
+
*/
|
|
100
|
+
private readonly ASTMap: Map<Node.Node, Node.Node>;
|
|
101
|
+
/**
|
|
102
|
+
* The root node in the AST tree we want to resolve
|
|
103
|
+
*/
|
|
104
|
+
public readonly root: Node.Node;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The cache of the last resolved values when walking the tree.
|
|
108
|
+
* This gets recycled every update to avoid stale data if a node is unused in an update
|
|
109
|
+
*/
|
|
110
|
+
private resolveCache: Map<Node.Node, Resolve.ResolvedNode>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cache of node IDs that have been processed to track if nodes have duplicate IDs
|
|
114
|
+
*/
|
|
115
|
+
private idCache: Set<string>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The parameters required to resolve AST nodes
|
|
119
|
+
*/
|
|
120
|
+
private readonly options: Resolve.ResolverOptions;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Tapable logger for logging errors encountered during view resolution
|
|
124
|
+
*/
|
|
125
|
+
private logger?: Logger;
|
|
126
|
+
|
|
127
|
+
constructor(root: Node.Node, options: Resolve.ResolverOptions) {
|
|
128
|
+
this.root = root;
|
|
129
|
+
this.options = options;
|
|
130
|
+
this.resolveCache = new Map();
|
|
131
|
+
this.ASTMap = new Map();
|
|
132
|
+
this.logger = options.logger;
|
|
133
|
+
this.idCache = new Set();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public getSourceNode(convertedAST: Node.Node) {
|
|
137
|
+
return this.ASTMap.get(convertedAST);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public update(changes?: Set<BindingInstance>): any {
|
|
141
|
+
this.hooks.beforeUpdate.call(changes);
|
|
142
|
+
const resolveCache = new Map<Node.Node, Resolve.ResolvedNode>();
|
|
143
|
+
this.idCache.clear();
|
|
144
|
+
this.ASTMap.clear();
|
|
145
|
+
|
|
146
|
+
const updated = this.computeTree(
|
|
147
|
+
this.root,
|
|
148
|
+
undefined,
|
|
149
|
+
changes,
|
|
150
|
+
resolveCache,
|
|
151
|
+
toNodeResolveOptions(this.options)
|
|
152
|
+
);
|
|
153
|
+
this.resolveCache = resolveCache;
|
|
154
|
+
this.hooks.afterUpdate.call(updated.value);
|
|
155
|
+
|
|
156
|
+
return updated.value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private getNodeID(node?: Node.Node): string | undefined {
|
|
160
|
+
if (!node) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
(node.type === NodeType.Asset ||
|
|
166
|
+
node.type === NodeType.View ||
|
|
167
|
+
node.type === NodeType.Value) &&
|
|
168
|
+
typeof node.value === 'object' &&
|
|
169
|
+
typeof node.value?.id === 'string'
|
|
170
|
+
) {
|
|
171
|
+
return node.value.id;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private getPreviousResult(node: Node.Node): Resolve.ResolvedNode | undefined {
|
|
176
|
+
if (!node) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const isFirstUpdate = this.resolveCache.size === 0;
|
|
181
|
+
const id = this.getNodeID(node);
|
|
182
|
+
|
|
183
|
+
if (id) {
|
|
184
|
+
if (this.idCache.has(id)) {
|
|
185
|
+
// Only log this conflict once to cut down on noise
|
|
186
|
+
// May want to swap this to logging when we first see the id -- which may not be the first render
|
|
187
|
+
if (isFirstUpdate) {
|
|
188
|
+
if (node.type === NodeType.Asset || node.type === NodeType.View) {
|
|
189
|
+
this.logger?.error(
|
|
190
|
+
`Cache conflict: Found Asset/View nodes that have conflicting ids: ${id}, may cause cache issues.`
|
|
191
|
+
);
|
|
192
|
+
} else if (node.type === NodeType.Value) {
|
|
193
|
+
this.logger?.info(
|
|
194
|
+
`Cache conflict: Found Value nodes that have conflicting ids: ${id}, may cause cache issues. To improve performance make value node IDs globally unique.`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Don't use anything from a prev result if there's a duplicate id detected
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.idCache.add(id);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return this.resolveCache.get(node);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private computeTree(
|
|
210
|
+
node: Node.Node,
|
|
211
|
+
parent: Node.Node | undefined,
|
|
212
|
+
dataChanges: Set<BindingInstance> | undefined,
|
|
213
|
+
cacheUpdate: Map<Node.Node, Resolve.ResolvedNode>,
|
|
214
|
+
options: Resolve.NodeResolveOptions
|
|
215
|
+
): NodeUpdate {
|
|
216
|
+
const dependencyModel = new DependencyModel(options.data.model);
|
|
217
|
+
|
|
218
|
+
dependencyModel.trackSubset('core');
|
|
219
|
+
const depModelWithParser = withContext(
|
|
220
|
+
withParser(dependencyModel, this.options.parseBinding)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const resolveOptions = this.hooks.resolveOptions.call(
|
|
224
|
+
{
|
|
225
|
+
...options,
|
|
226
|
+
data: {
|
|
227
|
+
...options.data,
|
|
228
|
+
model: depModelWithParser,
|
|
229
|
+
},
|
|
230
|
+
evaluate: (exp) =>
|
|
231
|
+
this.options.evaluator.evaluate(exp, { model: depModelWithParser }),
|
|
232
|
+
node,
|
|
233
|
+
},
|
|
234
|
+
node
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const previousResult = this.getPreviousResult(node);
|
|
238
|
+
const previousDeps = previousResult?.dependencies;
|
|
239
|
+
|
|
240
|
+
const dataChanged = caresAboutDataChanges(dataChanges, previousDeps);
|
|
241
|
+
const shouldUseLastValue = this.hooks.skipResolve.call(
|
|
242
|
+
!dataChanged,
|
|
243
|
+
node,
|
|
244
|
+
resolveOptions
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (shouldUseLastValue && previousResult) {
|
|
248
|
+
const update = {
|
|
249
|
+
...previousResult,
|
|
250
|
+
updated: false,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
cacheUpdate.set(node, update);
|
|
254
|
+
|
|
255
|
+
/** Recursively repopulate the AST map given some AST Node and it's resolved AST representation */
|
|
256
|
+
const repopulateASTMapFromCache = (
|
|
257
|
+
resolvedAST: Node.Node,
|
|
258
|
+
AST: Node.Node
|
|
259
|
+
) => {
|
|
260
|
+
this.ASTMap.set(resolvedAST, AST);
|
|
261
|
+
if ('children' in resolvedAST) {
|
|
262
|
+
resolvedAST.children?.forEach(({ value: childAST }) => {
|
|
263
|
+
const { node: childResolvedAST } =
|
|
264
|
+
this.getPreviousResult(childAST) || {};
|
|
265
|
+
if (!childResolvedAST) return;
|
|
266
|
+
|
|
267
|
+
repopulateASTMapFromCache(childResolvedAST, childAST);
|
|
268
|
+
|
|
269
|
+
if (childResolvedAST.type === NodeType.MultiNode) {
|
|
270
|
+
childResolvedAST.values.forEach((mChildAST) => {
|
|
271
|
+
const { node: mChildResolvedAST } =
|
|
272
|
+
this.getPreviousResult(mChildAST) || {};
|
|
273
|
+
if (!mChildResolvedAST) return;
|
|
274
|
+
|
|
275
|
+
repopulateASTMapFromCache(mChildResolvedAST, mChildAST);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const resolvedAST = previousResult.node;
|
|
283
|
+
repopulateASTMapFromCache(resolvedAST, node);
|
|
284
|
+
|
|
285
|
+
this.hooks.afterNodeUpdate.call(node, parent, update);
|
|
286
|
+
|
|
287
|
+
return update;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const resolvedAST = this.hooks.beforeResolve.call(node, resolveOptions) ?? {
|
|
291
|
+
type: NodeType.Empty,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
resolveOptions.node = resolvedAST;
|
|
295
|
+
|
|
296
|
+
this.ASTMap.set(resolvedAST, node);
|
|
297
|
+
|
|
298
|
+
let resolved = this.hooks.resolve.call(
|
|
299
|
+
undefined,
|
|
300
|
+
resolvedAST,
|
|
301
|
+
resolveOptions
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
let updated = !dequal(previousResult?.value, resolved);
|
|
305
|
+
|
|
306
|
+
if (previousResult && !updated) {
|
|
307
|
+
resolved = previousResult?.value;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const childDependencies = new Set<BindingInstance>();
|
|
311
|
+
dependencyModel.trackSubset('children');
|
|
312
|
+
|
|
313
|
+
if ('children' in resolvedAST) {
|
|
314
|
+
resolvedAST.children?.forEach((child) => {
|
|
315
|
+
const computedChildTree = this.computeTree(
|
|
316
|
+
child.value,
|
|
317
|
+
node,
|
|
318
|
+
dataChanges,
|
|
319
|
+
cacheUpdate,
|
|
320
|
+
resolveOptions
|
|
321
|
+
);
|
|
322
|
+
let { updated: childUpdated, value: childValue } = computedChildTree;
|
|
323
|
+
const { node: childNode, dependencies: childTreeDeps } =
|
|
324
|
+
computedChildTree;
|
|
325
|
+
|
|
326
|
+
childTreeDeps.forEach((binding) => childDependencies.add(binding));
|
|
327
|
+
|
|
328
|
+
if (childNode.type === NodeType.MultiNode) {
|
|
329
|
+
childValue = [];
|
|
330
|
+
childNode.values.forEach((mValue) => {
|
|
331
|
+
const mTree = this.computeTree(
|
|
332
|
+
mValue,
|
|
333
|
+
node,
|
|
334
|
+
dataChanges,
|
|
335
|
+
cacheUpdate,
|
|
336
|
+
resolveOptions
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
if (mTree.value !== undefined && mTree.value !== null) {
|
|
340
|
+
childValue.push(mTree.value);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
mTree.dependencies.forEach((bindingDep) =>
|
|
344
|
+
childDependencies.add(bindingDep)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
childUpdated = childUpdated || mTree.updated;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (childValue) {
|
|
352
|
+
if (childNode.type === NodeType.MultiNode && !childNode.override) {
|
|
353
|
+
const arr = addLast(
|
|
354
|
+
dlv(resolved, child.path as any[], []),
|
|
355
|
+
childValue
|
|
356
|
+
);
|
|
357
|
+
resolved = setIn(resolved, child.path, arr);
|
|
358
|
+
} else {
|
|
359
|
+
resolved = setIn(resolved, child.path, childValue);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
updated = updated || childUpdated;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
childDependencies.forEach((bindingDep) =>
|
|
368
|
+
dependencyModel.addChildReadDep(bindingDep)
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
dependencyModel.trackSubset('core');
|
|
372
|
+
if (previousResult && !updated) {
|
|
373
|
+
resolved = previousResult?.value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
resolved = this.hooks.afterResolve.call(resolved, resolvedAST, {
|
|
377
|
+
...resolveOptions,
|
|
378
|
+
getDependencies: (scope?: 'core' | 'children') =>
|
|
379
|
+
dependencyModel.getDependencies(scope),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const update: NodeUpdate = {
|
|
383
|
+
node: resolvedAST,
|
|
384
|
+
updated,
|
|
385
|
+
value: resolved,
|
|
386
|
+
dependencies: new Set([
|
|
387
|
+
...dependencyModel.getDependencies(),
|
|
388
|
+
...childDependencies,
|
|
389
|
+
]),
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
this.hooks.afterNodeUpdate.call(node, parent, update);
|
|
393
|
+
cacheUpdate.set(node, update);
|
|
394
|
+
|
|
395
|
+
return update;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Schema,
|
|
3
|
+
Formatting,
|
|
4
|
+
Validation as ValidationTypes,
|
|
5
|
+
} from '@player-ui/types';
|
|
6
|
+
import type {
|
|
7
|
+
BindingInstance,
|
|
8
|
+
BindingLike,
|
|
9
|
+
BindingFactory,
|
|
10
|
+
} from '../../binding';
|
|
11
|
+
import type {
|
|
12
|
+
DataModelWithParser,
|
|
13
|
+
DataModelImpl,
|
|
14
|
+
DataModelOptions,
|
|
15
|
+
} from '../../data';
|
|
16
|
+
import type { TransitionFunction } from '../../controllers';
|
|
17
|
+
import type { ExpressionEvaluator, ExpressionType } from '../../expressions';
|
|
18
|
+
import type { ValidationResponse } from '../../validator';
|
|
19
|
+
import type { Logger } from '../../logger';
|
|
20
|
+
import type { SchemaController } from '../../schema';
|
|
21
|
+
import type { Node } from '../parser';
|
|
22
|
+
|
|
23
|
+
export declare namespace Resolve {
|
|
24
|
+
export interface Validation {
|
|
25
|
+
/** Fetch the data-type for the given binding */
|
|
26
|
+
type(binding: BindingLike): Schema.DataType | undefined;
|
|
27
|
+
|
|
28
|
+
/** Get all currently applicable validation errors */
|
|
29
|
+
getAll(): Map<BindingInstance, ValidationResponse> | undefined;
|
|
30
|
+
|
|
31
|
+
/** Internal Method to lookup if there is a validation for the given binding */
|
|
32
|
+
_getValidationForBinding(
|
|
33
|
+
binding: BindingLike
|
|
34
|
+
): ValidationResponse | undefined;
|
|
35
|
+
|
|
36
|
+
/** Get field level error for the specific binding */
|
|
37
|
+
get(
|
|
38
|
+
binding: BindingLike,
|
|
39
|
+
options?: {
|
|
40
|
+
/** If this binding should also be tracked for validations */
|
|
41
|
+
track: boolean;
|
|
42
|
+
}
|
|
43
|
+
): ValidationResponse | undefined;
|
|
44
|
+
|
|
45
|
+
/** Get errors for all children regardless of section */
|
|
46
|
+
getChildren(type: ValidationTypes.DisplayTarget): Array<ValidationResponse>;
|
|
47
|
+
|
|
48
|
+
/** Get errors for all children solely in this section */
|
|
49
|
+
getValidationsForSection(): Array<ValidationResponse>;
|
|
50
|
+
|
|
51
|
+
/** Track errors for this binding, and notify the node of changes */
|
|
52
|
+
track: (binding: BindingLike) => void;
|
|
53
|
+
|
|
54
|
+
/** Register node as a section */
|
|
55
|
+
register: (options?: {
|
|
56
|
+
/** While type of Display Target group it should register as */
|
|
57
|
+
type: Exclude<ValidationTypes.DisplayTarget, 'field'>;
|
|
58
|
+
}) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BaseOptions {
|
|
62
|
+
/** A logger to use */
|
|
63
|
+
logger?: Logger;
|
|
64
|
+
|
|
65
|
+
/** An optional set of validation features */
|
|
66
|
+
validation?: Validation;
|
|
67
|
+
|
|
68
|
+
/** Parse a raw valy into an AST node */
|
|
69
|
+
parseNode?: (node: any) => Node.Node | null;
|
|
70
|
+
|
|
71
|
+
/** A function to move the state to a new place */
|
|
72
|
+
transition?: TransitionFunction;
|
|
73
|
+
|
|
74
|
+
/** The hub for data invariants and metaData associated with the data model */
|
|
75
|
+
schema: SchemaController;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface NodeDataOptions {
|
|
79
|
+
/** The data to set or get data from */
|
|
80
|
+
model: DataModelWithParser<DataModelOptions>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A function to format a given a value (given a binding) for display to the user
|
|
84
|
+
* Note: this doesn't persist any changes in the model.
|
|
85
|
+
*/
|
|
86
|
+
format: (binding: BindingLike, value: any) => any;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A function to format a given value using a formatting reference.
|
|
90
|
+
* The default behavior is the identity function.
|
|
91
|
+
*/
|
|
92
|
+
formatValue: (formatReference: Formatting.Reference, value: any) => any;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type NodeResolveOptions = BaseOptions & {
|
|
96
|
+
/** Execute the expression and return it's result */
|
|
97
|
+
evaluate: (exp: ExpressionType) => any;
|
|
98
|
+
|
|
99
|
+
/** All parameters for how to process data */
|
|
100
|
+
data: NodeDataOptions;
|
|
101
|
+
|
|
102
|
+
/** The data dependencies that were requested during the resolution */
|
|
103
|
+
getDependencies?(scope?: 'core' | 'children'): Set<BindingInstance>;
|
|
104
|
+
|
|
105
|
+
/** original node */
|
|
106
|
+
node?: Node.Node;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type ResolverOptions = BaseOptions & {
|
|
110
|
+
/** The data model to set or get data from */
|
|
111
|
+
model: DataModelImpl<DataModelOptions>;
|
|
112
|
+
|
|
113
|
+
/** A formatter function to call */
|
|
114
|
+
format?: (binding: BindingInstance, value: any) => any;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A function to format a given value using a formatting reference.
|
|
118
|
+
* The default behavior is the identity function.
|
|
119
|
+
*/
|
|
120
|
+
formatValue?: (formatReference: Formatting.Reference, value: any) => any;
|
|
121
|
+
|
|
122
|
+
/** An evaluator to execute an expression */
|
|
123
|
+
evaluator: ExpressionEvaluator;
|
|
124
|
+
|
|
125
|
+
/** A fn to parse a raw binding into a binding object */
|
|
126
|
+
parseBinding: BindingFactory;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export interface ResolvedNode {
|
|
130
|
+
/** The original node */
|
|
131
|
+
node: Node.Node;
|
|
132
|
+
|
|
133
|
+
/** The data dependencies that were requested during the resolution */
|
|
134
|
+
dependencies: Set<BindingInstance>;
|
|
135
|
+
|
|
136
|
+
/** The final value */
|
|
137
|
+
value: any;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type NodeTransformFunction = (
|
|
141
|
+
node: Node.Node,
|
|
142
|
+
options: NodeResolveOptions
|
|
143
|
+
) => Node.Node | null;
|
|
144
|
+
|
|
145
|
+
export type NodeResolveFunction = (
|
|
146
|
+
value: any,
|
|
147
|
+
node: Node.Node,
|
|
148
|
+
options: NodeResolveOptions
|
|
149
|
+
) => any;
|
|
150
|
+
|
|
151
|
+
export interface Plugin {
|
|
152
|
+
/** A transform function to migrate an AST to another AST */
|
|
153
|
+
beforeResolve?: NodeTransformFunction;
|
|
154
|
+
|
|
155
|
+
/** A function to transform an AST to a resolved value */
|
|
156
|
+
resolve?: NodeResolveFunction;
|
|
157
|
+
|
|
158
|
+
/** A function to process a resolved value before completing the node */
|
|
159
|
+
afterResolve?: NodeResolveFunction;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BindingInstance, BindingLike } from '../../binding';
|
|
2
|
+
import { isBinding } from '../../binding';
|
|
3
|
+
import type { ExpressionType } from '../../expressions';
|
|
4
|
+
import type { Resolve } from './types';
|
|
5
|
+
|
|
6
|
+
/** Check to see if and of the data-changes affect the given dependencies */
|
|
7
|
+
export function caresAboutDataChanges(
|
|
8
|
+
dataChanges?: Set<BindingInstance>,
|
|
9
|
+
dependencies?: Set<BindingInstance>
|
|
10
|
+
) {
|
|
11
|
+
if (!dataChanges || !dependencies) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const depArray = Array.from(dependencies.values());
|
|
16
|
+
const dataChangeArray = Array.from(dataChanges.values());
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
depArray.find(
|
|
20
|
+
(dep) =>
|
|
21
|
+
!!dataChangeArray.find(
|
|
22
|
+
(change) =>
|
|
23
|
+
change === dep || change.contains(dep) || dep.contains(change)
|
|
24
|
+
)
|
|
25
|
+
) !== undefined
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Convert the options object for a resolver to one for a node */
|
|
30
|
+
export function toNodeResolveOptions(
|
|
31
|
+
resolverOptions: Resolve.ResolverOptions
|
|
32
|
+
): Resolve.NodeResolveOptions {
|
|
33
|
+
return {
|
|
34
|
+
...resolverOptions,
|
|
35
|
+
data: {
|
|
36
|
+
model: resolverOptions.model,
|
|
37
|
+
formatValue: (ref, value) => {
|
|
38
|
+
if (resolverOptions.formatValue) {
|
|
39
|
+
return resolverOptions.formatValue(ref, value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return value;
|
|
43
|
+
},
|
|
44
|
+
format: (bindingLike: BindingLike, value: any) =>
|
|
45
|
+
resolverOptions.format
|
|
46
|
+
? resolverOptions.format(
|
|
47
|
+
isBinding(bindingLike)
|
|
48
|
+
? bindingLike
|
|
49
|
+
: resolverOptions.parseBinding(bindingLike),
|
|
50
|
+
value
|
|
51
|
+
)
|
|
52
|
+
: value,
|
|
53
|
+
},
|
|
54
|
+
evaluate: (exp: ExpressionType) =>
|
|
55
|
+
resolverOptions.evaluator.evaluate(exp, resolverOptions),
|
|
56
|
+
};
|
|
57
|
+
}
|