@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
package/src/expressions/utils.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isExpressionNode } from './types';
|
|
2
|
+
import type {
|
|
3
|
+
ExpressionHandler,
|
|
4
|
+
ExpressionNode,
|
|
5
|
+
ExpressionObjectType,
|
|
6
|
+
ExpressionType,
|
|
7
|
+
NodeLocation,
|
|
8
|
+
NodePosition,
|
|
9
|
+
} from './types';
|
|
2
10
|
|
|
3
11
|
/** Generates a function by removing the first context argument */
|
|
4
12
|
export function withoutContext<T extends unknown[], Return>(
|
|
@@ -6,3 +14,137 @@ export function withoutContext<T extends unknown[], Return>(
|
|
|
6
14
|
): ExpressionHandler<T, Return> {
|
|
7
15
|
return (_context, ...args) => fn(...args);
|
|
8
16
|
}
|
|
17
|
+
|
|
18
|
+
/** Checks if the location includes the target position */
|
|
19
|
+
function isInRange(position: NodePosition, location: NodeLocation) {
|
|
20
|
+
return (
|
|
21
|
+
position.character >= location.start.character &&
|
|
22
|
+
position.character <= location.end.character
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Get the node in the expression that's closest to the desired position */
|
|
27
|
+
export function findClosestNodeAtPosition(
|
|
28
|
+
node: ExpressionNode,
|
|
29
|
+
position: NodePosition
|
|
30
|
+
): ExpressionNode | undefined {
|
|
31
|
+
// This is just mapping recursively over nodes in the tree
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line default-case
|
|
34
|
+
switch (node.type) {
|
|
35
|
+
case 'Modification':
|
|
36
|
+
case 'Assignment':
|
|
37
|
+
case 'LogicalExpression':
|
|
38
|
+
case 'BinaryExpression': {
|
|
39
|
+
const check =
|
|
40
|
+
findClosestNodeAtPosition(node.left, position) ??
|
|
41
|
+
findClosestNodeAtPosition(node.right, position);
|
|
42
|
+
if (check) {
|
|
43
|
+
return check;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'UnaryExpression': {
|
|
50
|
+
const checkArg = findClosestNodeAtPosition(node.argument, position);
|
|
51
|
+
if (checkArg) {
|
|
52
|
+
return checkArg;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case 'MemberExpression': {
|
|
59
|
+
const checkObject =
|
|
60
|
+
findClosestNodeAtPosition(node.object, position) ??
|
|
61
|
+
findClosestNodeAtPosition(node.property, position);
|
|
62
|
+
if (checkObject) {
|
|
63
|
+
return checkObject;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'ConditionalExpression': {
|
|
70
|
+
const checkObject =
|
|
71
|
+
findClosestNodeAtPosition(node.test, position) ??
|
|
72
|
+
findClosestNodeAtPosition(node.consequent, position) ??
|
|
73
|
+
findClosestNodeAtPosition(node.alternate, position);
|
|
74
|
+
if (checkObject) {
|
|
75
|
+
return checkObject;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'ArrayExpression':
|
|
82
|
+
case 'Compound': {
|
|
83
|
+
const elements =
|
|
84
|
+
node.type === 'ArrayExpression' ? node.elements : node.body;
|
|
85
|
+
|
|
86
|
+
const anyElements = elements.find((e) =>
|
|
87
|
+
findClosestNodeAtPosition(e, position)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (anyElements) {
|
|
91
|
+
return anyElements;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'Object': {
|
|
98
|
+
const checkObject = node.attributes.reduce<ExpressionNode | undefined>(
|
|
99
|
+
(found, next) => {
|
|
100
|
+
return (
|
|
101
|
+
found ??
|
|
102
|
+
findClosestNodeAtPosition(next.key, position) ??
|
|
103
|
+
findClosestNodeAtPosition(next.value, position)
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
undefined
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (checkObject) {
|
|
110
|
+
return checkObject;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'CallExpression': {
|
|
117
|
+
const anyArgs =
|
|
118
|
+
node.args.find((arg) => {
|
|
119
|
+
return findClosestNodeAtPosition(arg, position);
|
|
120
|
+
}) ?? findClosestNodeAtPosition(node.callTarget, position);
|
|
121
|
+
|
|
122
|
+
if (anyArgs) {
|
|
123
|
+
return anyArgs;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Lastly check for yourself
|
|
131
|
+
if (node.location && isInRange(position, node.location)) {
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Checks if the expression is a simple type */
|
|
137
|
+
export function isObjectExpression(
|
|
138
|
+
expr: ExpressionType
|
|
139
|
+
): expr is ExpressionObjectType {
|
|
140
|
+
if (isExpressionNode(expr)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
typeof expr === 'object' &&
|
|
146
|
+
expr !== null &&
|
|
147
|
+
!Array.isArray(expr) &&
|
|
148
|
+
'value' in expr
|
|
149
|
+
);
|
|
150
|
+
}
|
package/src/player.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { setIn } from 'timm';
|
|
2
2
|
import deferred from 'p-defer';
|
|
3
|
-
import queueMicrotask from 'queue-microtask';
|
|
4
3
|
import type { Flow as FlowType, FlowResult } from '@player-ui/types';
|
|
5
4
|
|
|
6
5
|
import { SyncHook, SyncWaterfallHook } from 'tapable-ts';
|
|
7
6
|
import type { Logger } from './logger';
|
|
8
7
|
import { TapableLogger } from './logger';
|
|
9
|
-
import type {
|
|
8
|
+
import type { ExpressionType } from './expressions';
|
|
10
9
|
import { ExpressionEvaluator } from './expressions';
|
|
11
10
|
import { SchemaController } from './schema';
|
|
12
11
|
import { BindingParser } from './binding';
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
FlowController,
|
|
22
21
|
} from './controllers';
|
|
23
22
|
import { FlowExpPlugin } from './plugins/flow-exp-plugin';
|
|
23
|
+
import { DefaultExpPlugin } from './plugins/default-exp-plugin';
|
|
24
24
|
import type {
|
|
25
25
|
PlayerFlowState,
|
|
26
26
|
InProgressState,
|
|
@@ -30,8 +30,8 @@ import type {
|
|
|
30
30
|
import { NOT_STARTED_STATE } from './types';
|
|
31
31
|
|
|
32
32
|
// Variables injected at build time
|
|
33
|
-
const PLAYER_VERSION = '0.3.1
|
|
34
|
-
const COMMIT = '
|
|
33
|
+
const PLAYER_VERSION = '0.3.1';
|
|
34
|
+
const COMMIT = 'e8392cd5df3c84fb9c68daf149ea88c593ce0428';
|
|
35
35
|
|
|
36
36
|
export interface PlayerPlugin {
|
|
37
37
|
/**
|
|
@@ -49,6 +49,18 @@ export interface PlayerPlugin {
|
|
|
49
49
|
apply: (player: Player) => void;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
53
|
+
export interface ExtendedPlayerPlugin<
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
55
|
+
Assets = void,
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
57
|
+
Views = void,
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
59
|
+
Expressions = void,
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
61
|
+
DataTypes = void
|
|
62
|
+
> {}
|
|
63
|
+
|
|
52
64
|
export interface PlayerConfigOptions {
|
|
53
65
|
/** A set of plugins to load */
|
|
54
66
|
plugins?: PlayerPlugin[];
|
|
@@ -117,17 +129,16 @@ export class Player {
|
|
|
117
129
|
};
|
|
118
130
|
|
|
119
131
|
constructor(config?: PlayerConfigOptions) {
|
|
120
|
-
const initialPlugins: PlayerPlugin[] = [];
|
|
121
|
-
const flowExpPlugin = new FlowExpPlugin();
|
|
122
|
-
|
|
123
|
-
initialPlugins.push(flowExpPlugin);
|
|
124
|
-
|
|
125
132
|
if (config?.logger) {
|
|
126
133
|
this.logger.addHandler(config.logger);
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
this.config = config || {};
|
|
130
|
-
this.config.plugins = [
|
|
137
|
+
this.config.plugins = [
|
|
138
|
+
new DefaultExpPlugin(),
|
|
139
|
+
...(this.config.plugins || []),
|
|
140
|
+
new FlowExpPlugin(),
|
|
141
|
+
];
|
|
131
142
|
this.config.plugins?.forEach((plugin) => {
|
|
132
143
|
plugin.apply(this);
|
|
133
144
|
});
|
|
@@ -277,34 +288,41 @@ export class Player {
|
|
|
277
288
|
});
|
|
278
289
|
|
|
279
290
|
/** Resolve any data references in a string */
|
|
280
|
-
function resolveStrings<T>(val: T) {
|
|
291
|
+
function resolveStrings<T>(val: T, formatted?: boolean) {
|
|
281
292
|
return resolveDataRefs(val, {
|
|
282
293
|
model: dataController,
|
|
283
294
|
evaluate: expressionEvaluator.evaluate,
|
|
295
|
+
formatted,
|
|
284
296
|
});
|
|
285
297
|
}
|
|
286
298
|
|
|
287
299
|
flowController.hooks.flow.tap('player', (flow: FlowInstance) => {
|
|
288
300
|
flow.hooks.beforeTransition.tap('player', (state, transitionVal) => {
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
301
|
+
/** Checks to see if there are any transitions for a specific transition state (i.e. next, back). If not, it will default to * */
|
|
302
|
+
const computedTransitionVal = state.transitions[transitionVal]
|
|
303
|
+
? transitionVal
|
|
304
|
+
: '*';
|
|
305
|
+
if (state.onEnd && state.transitions[computedTransitionVal]) {
|
|
293
306
|
if (typeof state.onEnd === 'object' && 'exp' in state.onEnd) {
|
|
294
307
|
expressionEvaluator?.evaluate(state.onEnd.exp);
|
|
295
308
|
} else {
|
|
296
|
-
expressionEvaluator?.evaluate(state.onEnd);
|
|
309
|
+
expressionEvaluator?.evaluate(state.onEnd as ExpressionType);
|
|
297
310
|
}
|
|
298
311
|
}
|
|
299
312
|
|
|
300
|
-
|
|
313
|
+
/** If the transition does not exist, then do not resolve any expressions */
|
|
314
|
+
if (
|
|
315
|
+
!('transitions' in state) ||
|
|
316
|
+
!state.transitions[computedTransitionVal]
|
|
317
|
+
) {
|
|
301
318
|
return state;
|
|
302
319
|
}
|
|
303
320
|
|
|
321
|
+
/** resolves and sets the transition to the computed exp */
|
|
304
322
|
return setIn(
|
|
305
323
|
state,
|
|
306
|
-
['transitions',
|
|
307
|
-
resolveStrings(state.transitions[
|
|
324
|
+
['transitions', computedTransitionVal],
|
|
325
|
+
resolveStrings(state.transitions[computedTransitionVal])
|
|
308
326
|
) as any;
|
|
309
327
|
});
|
|
310
328
|
|
|
@@ -335,7 +353,7 @@ export class Player {
|
|
|
335
353
|
newState = setIn(
|
|
336
354
|
state,
|
|
337
355
|
['param'],
|
|
338
|
-
resolveStrings(state.param)
|
|
356
|
+
resolveStrings(state.param, false)
|
|
339
357
|
) as any;
|
|
340
358
|
}
|
|
341
359
|
|
|
@@ -343,17 +361,18 @@ export class Player {
|
|
|
343
361
|
});
|
|
344
362
|
|
|
345
363
|
flow.hooks.transition.tap('player', (_oldState, newState) => {
|
|
346
|
-
if (newState.value.state_type
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
364
|
+
if (newState.value.state_type !== 'VIEW') {
|
|
365
|
+
validationController.reset();
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
flow.hooks.afterTransition.tap('player', (flowInstance) => {
|
|
370
|
+
const value = flowInstance.currentState?.value;
|
|
371
|
+
if (value && value.state_type === 'ACTION') {
|
|
372
|
+
const { exp } = value;
|
|
373
|
+
flowController?.transition(
|
|
374
|
+
String(expressionEvaluator?.evaluate(exp))
|
|
375
|
+
);
|
|
357
376
|
}
|
|
358
377
|
|
|
359
378
|
expressionEvaluator.reset();
|
|
@@ -375,6 +394,11 @@ export class Player {
|
|
|
375
394
|
parseBinding,
|
|
376
395
|
transition: flowController.transition,
|
|
377
396
|
model: dataController,
|
|
397
|
+
utils: {
|
|
398
|
+
findPlugin: <Plugin = unknown>(pluginSymbol: symbol) => {
|
|
399
|
+
return this.findPlugin(pluginSymbol) as unknown as Plugin;
|
|
400
|
+
},
|
|
401
|
+
},
|
|
378
402
|
logger: this.logger,
|
|
379
403
|
flowController,
|
|
380
404
|
schema,
|
|
@@ -392,6 +416,7 @@ export class Player {
|
|
|
392
416
|
...validationController.forView(parseBinding),
|
|
393
417
|
type: (b) => schema.getType(parseBinding(b)),
|
|
394
418
|
},
|
|
419
|
+
constants: this.constantsController,
|
|
395
420
|
});
|
|
396
421
|
viewController.hooks.view.tap('player', (view) => {
|
|
397
422
|
validationController.onView(view);
|
|
@@ -399,26 +424,13 @@ export class Player {
|
|
|
399
424
|
});
|
|
400
425
|
this.hooks.viewController.call(viewController);
|
|
401
426
|
|
|
402
|
-
/** Gets formatter for given formatName and formats value if found, returns value otherwise */
|
|
403
|
-
const formatFunction: ExpressionHandler<[unknown, string], any> = (
|
|
404
|
-
ctx,
|
|
405
|
-
value,
|
|
406
|
-
formatName
|
|
407
|
-
) => {
|
|
408
|
-
return (
|
|
409
|
-
schema.getFormatterForType({ type: formatName })?.format(value) ?? value
|
|
410
|
-
);
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
expressionEvaluator.addExpressionFunction('format', formatFunction);
|
|
414
|
-
|
|
415
427
|
return {
|
|
416
428
|
start: () => {
|
|
417
429
|
flowController
|
|
418
430
|
.start()
|
|
419
431
|
.then((endState) => {
|
|
420
432
|
const flowResult: FlowResult = {
|
|
421
|
-
endState: resolveStrings(endState),
|
|
433
|
+
endState: resolveStrings(endState, false),
|
|
422
434
|
data: dataController.serialize(),
|
|
423
435
|
};
|
|
424
436
|
|
|
@@ -489,7 +501,9 @@ export class Player {
|
|
|
489
501
|
ref,
|
|
490
502
|
status: 'completed',
|
|
491
503
|
flow: state.flow,
|
|
492
|
-
|
|
504
|
+
controllers: {
|
|
505
|
+
data: state.controllers.data.makeReadOnly(),
|
|
506
|
+
},
|
|
493
507
|
} as const;
|
|
494
508
|
|
|
495
509
|
return maybeUpdateState({
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ExpressionHandler, ExpressionType } from '../expressions';
|
|
2
|
+
import type { SchemaController } from '../schema';
|
|
3
|
+
import type { Player, PlayerPlugin } from '../player';
|
|
4
|
+
|
|
5
|
+
/** Gets formatter for given formatName and formats value if found, returns value otherwise */
|
|
6
|
+
const createFormatFunction = (schema: SchemaController) => {
|
|
7
|
+
/**
|
|
8
|
+
* The generated handler for the given schema
|
|
9
|
+
*/
|
|
10
|
+
const handler: ExpressionHandler<[unknown, string], any> = (
|
|
11
|
+
ctx,
|
|
12
|
+
value,
|
|
13
|
+
formatName
|
|
14
|
+
) => {
|
|
15
|
+
return (
|
|
16
|
+
schema.getFormatterForType({ type: formatName })?.format(value) ?? value
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return handler;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A plugin that provides the out-of-the-box expressions for player
|
|
25
|
+
*/
|
|
26
|
+
export class DefaultExpPlugin implements PlayerPlugin {
|
|
27
|
+
name = 'flow-exp-plugin';
|
|
28
|
+
|
|
29
|
+
apply(player: Player) {
|
|
30
|
+
let formatFunction: ExpressionHandler<[unknown, string]> | undefined;
|
|
31
|
+
|
|
32
|
+
player.hooks.schema.tap(this.name, (schemaController) => {
|
|
33
|
+
formatFunction = createFormatFunction(schemaController);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => {
|
|
37
|
+
if (formatFunction) {
|
|
38
|
+
expEvaluator.addExpressionFunction('format', formatFunction);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
expEvaluator.addExpressionFunction('log', (ctx, ...args) => {
|
|
42
|
+
player.logger.info(...args);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expEvaluator.addExpressionFunction('debug', (ctx, ...args) => {
|
|
46
|
+
player.logger.debug(...args);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expEvaluator.addExpressionFunction(
|
|
50
|
+
'eval',
|
|
51
|
+
(ctx, ...args: [ExpressionType]) => {
|
|
52
|
+
return ctx.evaluate(...args);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
ExpressionObject,
|
|
4
4
|
NavigationFlowState,
|
|
5
5
|
} from '@player-ui/types';
|
|
6
|
-
import type { ExpressionEvaluator } from '../expressions';
|
|
6
|
+
import type { ExpressionEvaluator, ExpressionType } from '../expressions';
|
|
7
7
|
import type { FlowInstance } from '../controllers';
|
|
8
8
|
import type { Player, PlayerPlugin } from '../player';
|
|
9
9
|
|
|
@@ -28,7 +28,7 @@ export class FlowExpPlugin implements PlayerPlugin {
|
|
|
28
28
|
if (typeof exp === 'object' && 'exp' in exp) {
|
|
29
29
|
expressionEvaluator?.evaluate(exp.exp);
|
|
30
30
|
} else {
|
|
31
|
-
expressionEvaluator?.evaluate(exp);
|
|
31
|
+
expressionEvaluator?.evaluate(exp as ExpressionType);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
};
|
package/src/schema/schema.ts
CHANGED
|
@@ -11,8 +11,8 @@ const identify = (val: any) => val;
|
|
|
11
11
|
/** Expand the authored schema into a set of paths -> DataTypes */
|
|
12
12
|
export function parse(
|
|
13
13
|
schema: SchemaType.Schema
|
|
14
|
-
): Map<string, SchemaType.
|
|
15
|
-
const expandedPaths = new Map<string, SchemaType.
|
|
14
|
+
): Map<string, SchemaType.DataTypes> {
|
|
15
|
+
const expandedPaths = new Map<string, SchemaType.DataTypes>();
|
|
16
16
|
|
|
17
17
|
if (!schema.ROOT) {
|
|
18
18
|
return expandedPaths;
|
|
@@ -62,6 +62,10 @@ export function parse(
|
|
|
62
62
|
nestedPath.push('[]');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
if (type.isRecord) {
|
|
66
|
+
nestedPath.push('{}');
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
if (type.type && schema[type.type]) {
|
|
66
70
|
parseQueue.push({
|
|
67
71
|
path: nestedPath,
|
|
@@ -85,14 +89,14 @@ export class SchemaController implements ValidationProvider {
|
|
|
85
89
|
new Map();
|
|
86
90
|
|
|
87
91
|
private types: Map<string, SchemaType.DataType<any>> = new Map();
|
|
88
|
-
public readonly schema: Map<string, SchemaType.
|
|
92
|
+
public readonly schema: Map<string, SchemaType.DataTypes> = new Map();
|
|
89
93
|
|
|
90
94
|
private bindingSchemaNormalizedCache: Map<BindingInstance, string> =
|
|
91
95
|
new Map();
|
|
92
96
|
|
|
93
97
|
public readonly hooks = {
|
|
94
98
|
resolveTypeForBinding: new SyncWaterfallHook<
|
|
95
|
-
[SchemaType.
|
|
99
|
+
[SchemaType.DataTypes | undefined, BindingInstance]
|
|
96
100
|
>(),
|
|
97
101
|
};
|
|
98
102
|
|
|
@@ -135,17 +139,32 @@ export class SchemaController implements ValidationProvider {
|
|
|
135
139
|
return cached;
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
let bindingArray = binding.asArray();
|
|
143
|
+
let normalized = bindingArray
|
|
140
144
|
.map((p) => (typeof p === 'number' ? '[]' : p))
|
|
141
145
|
.join('.');
|
|
142
146
|
|
|
143
|
-
|
|
147
|
+
if (normalized) {
|
|
148
|
+
this.bindingSchemaNormalizedCache.set(binding, normalized);
|
|
149
|
+
bindingArray = normalized.split('.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
bindingArray.forEach((item) => {
|
|
153
|
+
const recordBinding = bindingArray
|
|
154
|
+
.map((p) => (p === item ? '{}' : p))
|
|
155
|
+
.join('.');
|
|
156
|
+
|
|
157
|
+
if (this.schema.get(recordBinding)) {
|
|
158
|
+
this.bindingSchemaNormalizedCache.set(binding, recordBinding);
|
|
159
|
+
bindingArray = recordBinding.split('.');
|
|
160
|
+
normalized = recordBinding;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
144
163
|
|
|
145
164
|
return normalized;
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
public getType(binding: BindingInstance): SchemaType.
|
|
167
|
+
public getType(binding: BindingInstance): SchemaType.DataTypes | undefined {
|
|
149
168
|
return this.hooks.resolveTypeForBinding.call(
|
|
150
169
|
this.schema.get(this.normalizeBinding(binding)),
|
|
151
170
|
binding
|
|
@@ -154,7 +173,7 @@ export class SchemaController implements ValidationProvider {
|
|
|
154
173
|
|
|
155
174
|
public getApparentType(
|
|
156
175
|
binding: BindingInstance
|
|
157
|
-
): SchemaType.
|
|
176
|
+
): SchemaType.DataTypes | undefined {
|
|
158
177
|
const schemaType = this.getType(binding);
|
|
159
178
|
|
|
160
179
|
if (schemaType === undefined) {
|
|
@@ -6,11 +6,22 @@ const DOUBLE_OPEN_CURLY = '{{';
|
|
|
6
6
|
const DOUBLE_CLOSE_CURLY = '}}';
|
|
7
7
|
|
|
8
8
|
export interface Options {
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The model to use when resolving refs
|
|
11
|
+
* Passing `false` will skip trying to resolve any direct model refs ({{foo}})
|
|
12
|
+
*/
|
|
13
|
+
model: false | DataModelWithParser;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A function to evaluate an expression
|
|
17
|
+
* Passing `false` will skip trying to evaluate any expressions (@[ foo() ]@)
|
|
18
|
+
*/
|
|
19
|
+
evaluate: false | ((exp: Expression) => any);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Optionaly resolve binding without formatting in case Type format applies
|
|
23
|
+
*/
|
|
24
|
+
formatted?: boolean;
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
/** Search the given string for the coordinates of the next expression to resolve */
|
|
@@ -70,6 +81,10 @@ export function resolveExpressionsInString(
|
|
|
70
81
|
val: string,
|
|
71
82
|
{ evaluate }: Options
|
|
72
83
|
): string {
|
|
84
|
+
if (!evaluate) {
|
|
85
|
+
return val;
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
const expMatch = /@\[.*?\]@/;
|
|
74
89
|
let newVal = val;
|
|
75
90
|
let match = newVal.match(expMatch);
|
|
@@ -106,10 +121,11 @@ export function resolveExpressionsInString(
|
|
|
106
121
|
|
|
107
122
|
/** Return a string with all data model references resolved */
|
|
108
123
|
export function resolveDataRefsInString(val: string, options: Options): string {
|
|
109
|
-
const { model } = options;
|
|
124
|
+
const { model, formatted = true } = options;
|
|
110
125
|
let workingString = resolveExpressionsInString(val, options);
|
|
111
126
|
|
|
112
127
|
if (
|
|
128
|
+
!model ||
|
|
113
129
|
typeof workingString !== 'string' ||
|
|
114
130
|
workingString.indexOf(DOUBLE_OPEN_CURLY) === -1
|
|
115
131
|
) {
|
|
@@ -133,7 +149,7 @@ export function resolveDataRefsInString(val: string, options: Options): string {
|
|
|
133
149
|
)
|
|
134
150
|
.trim();
|
|
135
151
|
|
|
136
|
-
const evaledVal = model.get(binding, { formatted
|
|
152
|
+
const evaledVal = model.get(binding, { formatted });
|
|
137
153
|
|
|
138
154
|
// Exit early if the string is _just_ a model lookup
|
|
139
155
|
// If the result is a string, we may need further processing for nested bindings
|
|
@@ -160,18 +176,19 @@ function traverseObject<T>(val: T, options: Options): T {
|
|
|
160
176
|
}
|
|
161
177
|
|
|
162
178
|
case 'object': {
|
|
179
|
+
if (!val) return val;
|
|
163
180
|
// TODO: Do we care refs in keys?
|
|
164
181
|
const keys = Object.keys(val);
|
|
165
182
|
let newVal = val;
|
|
166
183
|
|
|
167
184
|
if (keys.length > 0) {
|
|
168
|
-
|
|
185
|
+
keys.forEach((key) => {
|
|
169
186
|
newVal = setIn(
|
|
170
187
|
newVal as any,
|
|
171
188
|
[key],
|
|
172
189
|
traverseObject((val as any)[key], options)
|
|
173
190
|
) as any;
|
|
174
|
-
}
|
|
191
|
+
});
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
return newVal;
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Flow, FlowResult } from '@player-ui/types';
|
|
2
|
-
import type { DataModelWithParser } from './data';
|
|
3
2
|
import type { BindingParser, BindingLike } from './binding';
|
|
4
3
|
import type { SchemaController } from './schema';
|
|
5
4
|
import type { ExpressionEvaluator } from './expressions';
|
|
@@ -10,6 +9,7 @@ import type {
|
|
|
10
9
|
ValidationController,
|
|
11
10
|
FlowController,
|
|
12
11
|
} from './controllers';
|
|
12
|
+
import type { ReadOnlyDataController } from './controllers/data/utils';
|
|
13
13
|
|
|
14
14
|
/** The status for a flow's execution state */
|
|
15
15
|
export type PlayerFlowStatus =
|
|
@@ -86,8 +86,11 @@ export type InProgressState = BaseFlowState<'in-progress'> &
|
|
|
86
86
|
export type CompletedState = BaseFlowState<'completed'> &
|
|
87
87
|
PlayerFlowExecutionData &
|
|
88
88
|
FlowResult & {
|
|
89
|
-
/**
|
|
90
|
-
|
|
89
|
+
/** Readonly Player controllers to provide Player functionality after the flow has ended */
|
|
90
|
+
controllers: {
|
|
91
|
+
/** A read only instance of the Data Controller */
|
|
92
|
+
data: ReadOnlyDataController;
|
|
93
|
+
};
|
|
91
94
|
};
|
|
92
95
|
|
|
93
96
|
/** The flow finished but not successfully */
|