@player-ui/player 0.4.0 → 0.4.1-next.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 +795 -390
- package/dist/index.d.ts +238 -81
- package/dist/index.esm.js +787 -388
- package/dist/player.dev.js +4768 -5282
- package/dist/player.prod.js +1 -1
- package/package.json +12 -3
- package/src/binding/binding.ts +8 -0
- package/src/binding/index.ts +14 -4
- package/src/binding/resolver.ts +1 -1
- 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} +60 -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 +359 -145
- 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 +55 -8
- package/src/data/noop-model.ts +2 -0
- package/src/expressions/evaluator-functions.ts +24 -2
- package/src/expressions/evaluator.ts +37 -33
- package/src/expressions/index.ts +1 -0
- package/src/expressions/parser.ts +53 -27
- package/src/expressions/types.ts +23 -5
- package/src/expressions/utils.ts +19 -0
- package/src/player.ts +47 -48
- 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 +25 -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 +38 -4
- package/src/view/parser/index.ts +51 -3
- package/src/view/plugins/applicability.ts +1 -1
- package/src/view/plugins/string-resolver.ts +8 -4
- 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/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.4.
|
|
34
|
-
const COMMIT = '
|
|
33
|
+
const PLAYER_VERSION = '0.4.1-next.1';
|
|
34
|
+
const COMMIT = 'fdebd2f39275b88fe5d721a64db743f850cf0910';
|
|
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,10 +288,11 @@ 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
|
|
|
@@ -294,7 +306,7 @@ export class Player {
|
|
|
294
306
|
if (typeof state.onEnd === 'object' && 'exp' in state.onEnd) {
|
|
295
307
|
expressionEvaluator?.evaluate(state.onEnd.exp);
|
|
296
308
|
} else {
|
|
297
|
-
expressionEvaluator?.evaluate(state.onEnd);
|
|
309
|
+
expressionEvaluator?.evaluate(state.onEnd as ExpressionType);
|
|
298
310
|
}
|
|
299
311
|
}
|
|
300
312
|
|
|
@@ -341,7 +353,7 @@ export class Player {
|
|
|
341
353
|
newState = setIn(
|
|
342
354
|
state,
|
|
343
355
|
['param'],
|
|
344
|
-
resolveStrings(state.param)
|
|
356
|
+
resolveStrings(state.param, false)
|
|
345
357
|
) as any;
|
|
346
358
|
}
|
|
347
359
|
|
|
@@ -349,26 +361,18 @@ export class Player {
|
|
|
349
361
|
});
|
|
350
362
|
|
|
351
363
|
flow.hooks.transition.tap('player', (_oldState, newState) => {
|
|
352
|
-
if (newState.value.state_type
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
);
|
|
365
|
-
} catch (error) {
|
|
366
|
-
const state = this.getState();
|
|
367
|
-
if (error instanceof Error && state.status === 'in-progress') {
|
|
368
|
-
state.fail(error);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
});
|
|
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
|
+
);
|
|
372
376
|
}
|
|
373
377
|
|
|
374
378
|
expressionEvaluator.reset();
|
|
@@ -390,6 +394,11 @@ export class Player {
|
|
|
390
394
|
parseBinding,
|
|
391
395
|
transition: flowController.transition,
|
|
392
396
|
model: dataController,
|
|
397
|
+
utils: {
|
|
398
|
+
findPlugin: <Plugin = unknown>(pluginSymbol: symbol) => {
|
|
399
|
+
return this.findPlugin(pluginSymbol) as unknown as Plugin;
|
|
400
|
+
},
|
|
401
|
+
},
|
|
393
402
|
logger: this.logger,
|
|
394
403
|
flowController,
|
|
395
404
|
schema,
|
|
@@ -407,6 +416,7 @@ export class Player {
|
|
|
407
416
|
...validationController.forView(parseBinding),
|
|
408
417
|
type: (b) => schema.getType(parseBinding(b)),
|
|
409
418
|
},
|
|
419
|
+
constants: this.constantsController,
|
|
410
420
|
});
|
|
411
421
|
viewController.hooks.view.tap('player', (view) => {
|
|
412
422
|
validationController.onView(view);
|
|
@@ -414,26 +424,13 @@ export class Player {
|
|
|
414
424
|
});
|
|
415
425
|
this.hooks.viewController.call(viewController);
|
|
416
426
|
|
|
417
|
-
/** Gets formatter for given formatName and formats value if found, returns value otherwise */
|
|
418
|
-
const formatFunction: ExpressionHandler<[unknown, string], any> = (
|
|
419
|
-
ctx,
|
|
420
|
-
value,
|
|
421
|
-
formatName
|
|
422
|
-
) => {
|
|
423
|
-
return (
|
|
424
|
-
schema.getFormatterForType({ type: formatName })?.format(value) ?? value
|
|
425
|
-
);
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
expressionEvaluator.addExpressionFunction('format', formatFunction);
|
|
429
|
-
|
|
430
427
|
return {
|
|
431
428
|
start: () => {
|
|
432
429
|
flowController
|
|
433
430
|
.start()
|
|
434
431
|
.then((endState) => {
|
|
435
432
|
const flowResult: FlowResult = {
|
|
436
|
-
endState: resolveStrings(endState),
|
|
433
|
+
endState: resolveStrings(endState, false),
|
|
437
434
|
data: dataController.serialize(),
|
|
438
435
|
};
|
|
439
436
|
|
|
@@ -504,7 +501,9 @@ export class Player {
|
|
|
504
501
|
ref,
|
|
505
502
|
status: 'completed',
|
|
506
503
|
flow: state.flow,
|
|
507
|
-
|
|
504
|
+
controllers: {
|
|
505
|
+
data: state.controllers.data.makeReadOnly(),
|
|
506
|
+
},
|
|
508
507
|
} as const;
|
|
509
508
|
|
|
510
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
|
|
@@ -166,13 +182,13 @@ function traverseObject<T>(val: T, options: Options): T {
|
|
|
166
182
|
let newVal = val;
|
|
167
183
|
|
|
168
184
|
if (keys.length > 0) {
|
|
169
|
-
|
|
185
|
+
keys.forEach((key) => {
|
|
170
186
|
newVal = setIn(
|
|
171
187
|
newVal as any,
|
|
172
188
|
[key],
|
|
173
189
|
traverseObject((val as any)[key], options)
|
|
174
190
|
) as any;
|
|
175
|
-
}
|
|
191
|
+
});
|
|
176
192
|
}
|
|
177
193
|
|
|
178
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 */
|
|
@@ -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,7 @@ 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';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* A BindingInstance with an indicator of whether or not it's a strong binding
|
|
@@ -37,17 +38,21 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
37
38
|
public validator: MiddlewareChecker;
|
|
38
39
|
public shadowModelPaths: Map<BindingInstance, any>;
|
|
39
40
|
private logger?: Logger;
|
|
41
|
+
private shouldIncludeInvalid?: (options?: DataModelOptions) => boolean;
|
|
40
42
|
|
|
41
43
|
constructor(
|
|
42
44
|
validator: MiddlewareChecker,
|
|
43
45
|
options?: {
|
|
44
46
|
/** A logger instance */
|
|
45
47
|
logger?: Logger;
|
|
48
|
+
/** Optional function to include data staged in shadowModel */
|
|
49
|
+
shouldIncludeInvalid?: (options?: DataModelOptions) => boolean;
|
|
46
50
|
}
|
|
47
51
|
) {
|
|
48
52
|
this.validator = validator;
|
|
49
53
|
this.shadowModelPaths = new Map();
|
|
50
54
|
this.logger = options?.logger;
|
|
55
|
+
this.shouldIncludeInvalid = options?.shouldIncludeInvalid;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
public set(
|
|
@@ -58,8 +63,11 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
58
63
|
const asModel = toModel(this, { ...options, includeInvalid: true }, next);
|
|
59
64
|
const nextTransaction: BatchSetTransaction = [];
|
|
60
65
|
|
|
66
|
+
const includedBindings = new Set<BindingInstance>();
|
|
67
|
+
|
|
61
68
|
transaction.forEach(([binding, value]) => {
|
|
62
69
|
this.shadowModelPaths.set(binding, value);
|
|
70
|
+
includedBindings.add(binding);
|
|
63
71
|
});
|
|
64
72
|
|
|
65
73
|
const invalidBindings: Array<BindingInstance> = [];
|
|
@@ -72,11 +80,15 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
72
80
|
} else if (validations instanceof Set) {
|
|
73
81
|
validations.forEach((validation) => {
|
|
74
82
|
invalidBindings.push(validation.binding);
|
|
75
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
!validation.isStrong &&
|
|
85
|
+
validation.binding.asString() === binding.asString()
|
|
86
|
+
) {
|
|
76
87
|
nextTransaction.push([validation.binding, value]);
|
|
77
88
|
}
|
|
78
89
|
});
|
|
79
|
-
} else {
|
|
90
|
+
} else if (includedBindings.has(binding)) {
|
|
91
|
+
invalidBindings.push(binding);
|
|
80
92
|
this.logger?.debug(
|
|
81
93
|
`Invalid value for path: ${binding.asString()} - ${
|
|
82
94
|
validations.severity
|
|
@@ -85,6 +97,8 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
85
97
|
}
|
|
86
98
|
});
|
|
87
99
|
|
|
100
|
+
let validResults: Updates = [];
|
|
101
|
+
|
|
88
102
|
if (next && nextTransaction.length > 0) {
|
|
89
103
|
// defer clearing the shadow model to prevent validations that are run twice due to weak binding refs still needing the data
|
|
90
104
|
nextTransaction.forEach(([binding]) =>
|
|
@@ -94,9 +108,11 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
94
108
|
if (invalidBindings.length === 0) {
|
|
95
109
|
return result;
|
|
96
110
|
}
|
|
111
|
+
|
|
112
|
+
validResults = result;
|
|
97
113
|
}
|
|
98
114
|
|
|
99
|
-
|
|
115
|
+
const invalidResults = invalidBindings.map((binding) => {
|
|
100
116
|
return {
|
|
101
117
|
binding,
|
|
102
118
|
oldValue: asModel.get(binding),
|
|
@@ -104,6 +120,8 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
104
120
|
force: true,
|
|
105
121
|
};
|
|
106
122
|
});
|
|
123
|
+
|
|
124
|
+
return [...validResults, ...invalidResults];
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
public get(
|
|
@@ -113,7 +131,10 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
113
131
|
) {
|
|
114
132
|
let val = next?.get(binding, options);
|
|
115
133
|
|
|
116
|
-
if (
|
|
134
|
+
if (
|
|
135
|
+
this.shouldIncludeInvalid?.(options) ??
|
|
136
|
+
options?.includeInvalid === true
|
|
137
|
+
) {
|
|
117
138
|
this.shadowModelPaths.forEach((shadowValue, shadowBinding) => {
|
|
118
139
|
if (shadowBinding === binding) {
|
|
119
140
|
val = shadowValue;
|
|
@@ -129,4 +150,17 @@ export class ValidationMiddleware implements DataModelMiddleware {
|
|
|
129
150
|
|
|
130
151
|
return val;
|
|
131
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
|
+
}
|
|
132
166
|
}
|