@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,60 @@
|
|
|
1
|
+
import type { Formatting } from '@player-ui/types';
|
|
2
|
+
|
|
3
|
+
export type FormatOptions = Omit<Formatting.Reference, 'type'>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The return types for the schema don't include options.
|
|
7
|
+
* These are already sent to the generic formatter function for that type
|
|
8
|
+
*/
|
|
9
|
+
export type FormatFunction<From, To = From, Options = unknown> = (
|
|
10
|
+
val: From,
|
|
11
|
+
options?: Options
|
|
12
|
+
) => To | undefined;
|
|
13
|
+
|
|
14
|
+
export type FormatHandler<From, To = From> = (val: From) => To;
|
|
15
|
+
|
|
16
|
+
export interface FormatDefinition<DataModelType, UserDisplayType> {
|
|
17
|
+
/**
|
|
18
|
+
* A function to format data (from the data-model to the user).
|
|
19
|
+
* Defaults to the identify function
|
|
20
|
+
*/
|
|
21
|
+
format: FormatHandler<DataModelType, UserDisplayType>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A function to invert the formatting (from the user to the data-model)
|
|
25
|
+
* Defaults to the identify function.
|
|
26
|
+
*/
|
|
27
|
+
deformat: FormatHandler<UserDisplayType, DataModelType>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FormatType<
|
|
31
|
+
DataModelType,
|
|
32
|
+
UserDisplayType = DataModelType,
|
|
33
|
+
Options = undefined
|
|
34
|
+
> {
|
|
35
|
+
/**
|
|
36
|
+
* The name of the formatter.
|
|
37
|
+
* This corresponds to the 'type' format property when creating a DataType
|
|
38
|
+
*/
|
|
39
|
+
name: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* An optional function to format data for display to the user.
|
|
43
|
+
* This goes from dataModel -> UI display
|
|
44
|
+
*/
|
|
45
|
+
format?: FormatFunction<
|
|
46
|
+
DataModelType | UserDisplayType,
|
|
47
|
+
UserDisplayType,
|
|
48
|
+
Options
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* An optional function for undo the action of a format function for storage.
|
|
53
|
+
* This goes from UI -> dataModel
|
|
54
|
+
*/
|
|
55
|
+
deformat?: FormatFunction<
|
|
56
|
+
UserDisplayType | DataModelType,
|
|
57
|
+
DataModelType,
|
|
58
|
+
Options
|
|
59
|
+
>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { setIn } from 'timm';
|
|
2
|
+
import type { Expression } from '@player-ui/types';
|
|
3
|
+
import type { DataModelWithParser } from '../data';
|
|
4
|
+
|
|
5
|
+
const DOUBLE_OPEN_CURLY = '{{';
|
|
6
|
+
const DOUBLE_CLOSE_CURLY = '}}';
|
|
7
|
+
|
|
8
|
+
export interface Options {
|
|
9
|
+
/** The model to use when resolving refs */
|
|
10
|
+
model: DataModelWithParser;
|
|
11
|
+
|
|
12
|
+
/** A function to evaluate an expression */
|
|
13
|
+
evaluate: (exp: Expression) => any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Search the given string for the coordinates of the next expression to resolve */
|
|
17
|
+
export function findNextExp(str: string) {
|
|
18
|
+
const expStart = str.indexOf(DOUBLE_OPEN_CURLY);
|
|
19
|
+
|
|
20
|
+
if (expStart === -1) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let count = 1;
|
|
25
|
+
let offset = expStart + DOUBLE_OPEN_CURLY.length;
|
|
26
|
+
let workingString = str.substring(expStart + DOUBLE_OPEN_CURLY.length);
|
|
27
|
+
|
|
28
|
+
while (count > 0 && workingString.length > 0) {
|
|
29
|
+
// Find the next open or close curly
|
|
30
|
+
const nextCloseCurly = workingString.indexOf(DOUBLE_CLOSE_CURLY);
|
|
31
|
+
|
|
32
|
+
// We can't close anything, so there's no point in going on with life.
|
|
33
|
+
if (nextCloseCurly === -1) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nextOpenCurly = workingString.indexOf(DOUBLE_OPEN_CURLY);
|
|
38
|
+
|
|
39
|
+
if (nextOpenCurly !== -1 && nextOpenCurly < nextCloseCurly) {
|
|
40
|
+
// We've hit another open bracket before closing out the one we want
|
|
41
|
+
// Move everything over and bump our close count by 1
|
|
42
|
+
count++;
|
|
43
|
+
workingString = workingString.substring(
|
|
44
|
+
nextOpenCurly + DOUBLE_OPEN_CURLY.length
|
|
45
|
+
);
|
|
46
|
+
offset += nextOpenCurly + DOUBLE_OPEN_CURLY.length;
|
|
47
|
+
} else {
|
|
48
|
+
// We've hit another closing bracket
|
|
49
|
+
// Decrement our count and updates offsets
|
|
50
|
+
count--;
|
|
51
|
+
workingString = workingString.substring(
|
|
52
|
+
nextCloseCurly + DOUBLE_CLOSE_CURLY.length
|
|
53
|
+
);
|
|
54
|
+
offset += nextCloseCurly + DOUBLE_CLOSE_CURLY.length;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (count !== 0) {
|
|
59
|
+
throw new Error(`Unbalanced {{ and }} in exp: ${str}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
start: expStart,
|
|
64
|
+
end: offset,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Finds any subset of the string wrapped in @[]@ and evaluates it as an expression */
|
|
69
|
+
export function resolveExpressionsInString(
|
|
70
|
+
val: string,
|
|
71
|
+
{ evaluate }: Options
|
|
72
|
+
): string {
|
|
73
|
+
const expMatch = /@\[.*?\]@/;
|
|
74
|
+
let newVal = val;
|
|
75
|
+
let match = newVal.match(expMatch);
|
|
76
|
+
|
|
77
|
+
while (match !== null) {
|
|
78
|
+
const expStrWithBrackets = match[0];
|
|
79
|
+
const matchStart = newVal.indexOf(expStrWithBrackets);
|
|
80
|
+
|
|
81
|
+
const expString = expStrWithBrackets.substr(
|
|
82
|
+
'@['.length,
|
|
83
|
+
expStrWithBrackets.length - '@['.length - ']@'.length
|
|
84
|
+
);
|
|
85
|
+
const expValue = evaluate(expString);
|
|
86
|
+
|
|
87
|
+
// The string is only the expression, return the raw value.
|
|
88
|
+
if (
|
|
89
|
+
matchStart === 0 &&
|
|
90
|
+
expStrWithBrackets === val &&
|
|
91
|
+
typeof expValue !== 'string'
|
|
92
|
+
) {
|
|
93
|
+
return expValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
newVal =
|
|
97
|
+
newVal.substr(0, matchStart) +
|
|
98
|
+
expValue +
|
|
99
|
+
newVal.substr(matchStart + expStrWithBrackets.length);
|
|
100
|
+
// remove the surrounding @[]@ to get the expression
|
|
101
|
+
match = newVal.match(expMatch);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return newVal;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Return a string with all data model references resolved */
|
|
108
|
+
export function resolveDataRefsInString(val: string, options: Options): string {
|
|
109
|
+
const { model } = options;
|
|
110
|
+
let workingString = resolveExpressionsInString(val, options);
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
typeof workingString !== 'string' ||
|
|
114
|
+
workingString.indexOf(DOUBLE_OPEN_CURLY) === -1
|
|
115
|
+
) {
|
|
116
|
+
return workingString;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
while (workingString.indexOf(DOUBLE_OPEN_CURLY) !== -1) {
|
|
120
|
+
const expLocation = findNextExp(workingString);
|
|
121
|
+
|
|
122
|
+
if (!expLocation) {
|
|
123
|
+
return workingString;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { start, end } = expLocation;
|
|
127
|
+
|
|
128
|
+
// Strip out the wrapping curlies from {{binding}} before passing to the model
|
|
129
|
+
const binding = workingString
|
|
130
|
+
.substring(
|
|
131
|
+
start + DOUBLE_OPEN_CURLY.length,
|
|
132
|
+
end - DOUBLE_OPEN_CURLY.length
|
|
133
|
+
)
|
|
134
|
+
.trim();
|
|
135
|
+
|
|
136
|
+
const evaledVal = model.get(binding, { formatted: true });
|
|
137
|
+
|
|
138
|
+
// Exit early if the string is _just_ a model lookup
|
|
139
|
+
// If the result is a string, we may need further processing for nested bindings
|
|
140
|
+
if (
|
|
141
|
+
start === 0 &&
|
|
142
|
+
end === workingString.length &&
|
|
143
|
+
typeof evaledVal !== 'string'
|
|
144
|
+
) {
|
|
145
|
+
return evaledVal;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
workingString =
|
|
149
|
+
workingString.substr(0, start) + evaledVal + workingString.substr(end);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return workingString;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Traverse the thing and replace any model refs */
|
|
156
|
+
function traverseObject<T>(val: T, options: Options): T {
|
|
157
|
+
switch (typeof val) {
|
|
158
|
+
case 'string': {
|
|
159
|
+
return resolveDataRefsInString(val as string, options) as unknown as T;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'object': {
|
|
163
|
+
// TODO: Do we care refs in keys?
|
|
164
|
+
const keys = Object.keys(val);
|
|
165
|
+
let newVal = val;
|
|
166
|
+
|
|
167
|
+
if (keys.length > 0) {
|
|
168
|
+
for (const key of keys) {
|
|
169
|
+
newVal = setIn(
|
|
170
|
+
newVal as any,
|
|
171
|
+
[key],
|
|
172
|
+
traverseObject((val as any)[key], options)
|
|
173
|
+
) as any;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return newVal;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
default:
|
|
181
|
+
return val;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Recursively resolve all model refs in whatever you pass in */
|
|
186
|
+
export function resolveDataRefs<T>(val: T, options: Options): T {
|
|
187
|
+
return traverseObject(val, options);
|
|
188
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import type { Flow, FlowResult } from '@player-ui/types';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import type { DataController } from './data';
|
|
14
|
-
import type { ValidationController } from './validation';
|
|
2
|
+
import type { DataModelWithParser } from './data';
|
|
3
|
+
import type { BindingParser, BindingLike } from './binding';
|
|
4
|
+
import type { SchemaController } from './schema';
|
|
5
|
+
import type { ExpressionEvaluator } from './expressions';
|
|
6
|
+
import type { Logger } from './logger';
|
|
7
|
+
import type {
|
|
8
|
+
ViewController,
|
|
9
|
+
DataController,
|
|
10
|
+
ValidationController,
|
|
11
|
+
FlowController,
|
|
12
|
+
} from './controllers';
|
|
15
13
|
|
|
16
14
|
/** The status for a flow's execution state */
|
|
17
15
|
export type PlayerFlowStatus =
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './replaceParams';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const ANY_CHAR_REGEX = /%([a-zA-Z]+)/g;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Replaces %num in message with the provided parameters in order.
|
|
5
|
+
*
|
|
6
|
+
* @param message - Parameterized string like "This is a %1"
|
|
7
|
+
* @param params - Parameters to replace in message E.g. ['tax2021.amount']
|
|
8
|
+
* @returns A message with the parameters replaced.
|
|
9
|
+
*/
|
|
10
|
+
export function replaceParams(
|
|
11
|
+
message: string,
|
|
12
|
+
params: Record<string, any>
|
|
13
|
+
): string {
|
|
14
|
+
return message
|
|
15
|
+
.slice()
|
|
16
|
+
.replace(ANY_CHAR_REGEX, (keyExpr) => params[keyExpr.slice(1)] || keyExpr);
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ValidatorFunction } from './types';
|
|
2
|
+
|
|
3
|
+
/** A registry that tracks validators */
|
|
4
|
+
export class ValidatorRegistry {
|
|
5
|
+
private registry: Map<string, ValidatorFunction<any>>;
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
this.registry = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Use the given validator name to fetch the handler */
|
|
12
|
+
public get(name: string): ValidatorFunction | undefined {
|
|
13
|
+
return this.registry.get(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Register a new validator */
|
|
17
|
+
public register<T>(name: string, handler: ValidatorFunction<T>) {
|
|
18
|
+
this.registry.set(name, handler);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Validation } from '@player-ui/types';
|
|
2
|
+
|
|
3
|
+
import type { BindingInstance, BindingFactory } from '../binding';
|
|
4
|
+
import type { DataModelWithParser } from '../data';
|
|
5
|
+
import type { ExpressionEvaluatorFunction } from '../expressions';
|
|
6
|
+
import type { Logger } from '../logger';
|
|
7
|
+
import type { ConstantsProvider } from '../controllers';
|
|
8
|
+
|
|
9
|
+
interface BaseValidationResponse<T = Validation.Severity> {
|
|
10
|
+
/** The validation message to show to the user */
|
|
11
|
+
message: string;
|
|
12
|
+
|
|
13
|
+
/** List of parameters associated with a validation. These can be replaced into a templatized message string. */
|
|
14
|
+
parameters?: Record<string, any>;
|
|
15
|
+
|
|
16
|
+
/** How serious is this violation */
|
|
17
|
+
severity: T;
|
|
18
|
+
|
|
19
|
+
/** Where this validation should be displayed */
|
|
20
|
+
displayTarget?: Validation.DisplayTarget;
|
|
21
|
+
|
|
22
|
+
/** The blocking state of this validation */
|
|
23
|
+
blocking?: boolean | 'once';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WarningValidationResponse
|
|
27
|
+
extends BaseValidationResponse<'warning'> {
|
|
28
|
+
/** Warning validations can be dismissed without correcting the error */
|
|
29
|
+
dismiss?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ErrorValidationResponse = BaseValidationResponse<'error'>;
|
|
33
|
+
|
|
34
|
+
export type ValidationResponse =
|
|
35
|
+
| ErrorValidationResponse
|
|
36
|
+
| WarningValidationResponse;
|
|
37
|
+
|
|
38
|
+
type RequiredValidationKeys = 'severity' | 'trigger';
|
|
39
|
+
|
|
40
|
+
export type ValidationObject = Validation.Reference &
|
|
41
|
+
Required<Pick<Validation.Reference, RequiredValidationKeys>>;
|
|
42
|
+
|
|
43
|
+
export interface ValidationProvider {
|
|
44
|
+
getValidationsForBinding?(
|
|
45
|
+
binding: BindingInstance
|
|
46
|
+
): Array<ValidationObject> | undefined;
|
|
47
|
+
|
|
48
|
+
getValidationsForView?(): Array<ValidationObject> | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ValidatorContext {
|
|
52
|
+
/** The data to set or get data from */
|
|
53
|
+
model: DataModelWithParser;
|
|
54
|
+
|
|
55
|
+
/** A means of parsing a binding */
|
|
56
|
+
parseBinding: BindingFactory;
|
|
57
|
+
|
|
58
|
+
/** Execute the expression and return it's result */
|
|
59
|
+
evaluate: ExpressionEvaluatorFunction;
|
|
60
|
+
|
|
61
|
+
/** Logger instance to use */
|
|
62
|
+
logger: Logger;
|
|
63
|
+
|
|
64
|
+
/** The validation object that triggered this function */
|
|
65
|
+
validation: ValidationObject;
|
|
66
|
+
|
|
67
|
+
/** The constants for messages */
|
|
68
|
+
constants: ConstantsProvider;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type ValidatorFunction<Options = unknown> = (
|
|
72
|
+
context: ValidatorContext,
|
|
73
|
+
value: any,
|
|
74
|
+
options?: Options
|
|
75
|
+
) => Omit<BaseValidationResponse, 'severity'> | undefined;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { setIn } from 'timm';
|
|
2
|
+
import type { BindingInstance } from '../binding';
|
|
3
|
+
import type {
|
|
4
|
+
BatchSetTransaction,
|
|
5
|
+
DataModelImpl,
|
|
6
|
+
DataModelOptions,
|
|
7
|
+
DataModelMiddleware,
|
|
8
|
+
Updates,
|
|
9
|
+
} from '../data';
|
|
10
|
+
import { toModel } from '../data';
|
|
11
|
+
import type { Logger } from '../logger';
|
|
12
|
+
|
|
13
|
+
import type { ValidationResponse } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 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
|
|
17
|
+
*/
|
|
18
|
+
export type MiddlewareChecker = (
|
|
19
|
+
binding: BindingInstance,
|
|
20
|
+
model: DataModelImpl
|
|
21
|
+
) => ValidationResponse | Set<BindingInstance> | undefined;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Middleware for the data-model that caches the results of invalid data
|
|
25
|
+
*/
|
|
26
|
+
export class ValidationMiddleware implements DataModelMiddleware {
|
|
27
|
+
public validator: MiddlewareChecker;
|
|
28
|
+
public shadowModelPaths: Map<BindingInstance, any>;
|
|
29
|
+
private logger?: Logger;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
validator: MiddlewareChecker,
|
|
33
|
+
options?: {
|
|
34
|
+
/** A logger instance */
|
|
35
|
+
logger?: Logger;
|
|
36
|
+
}
|
|
37
|
+
) {
|
|
38
|
+
this.validator = validator;
|
|
39
|
+
this.shadowModelPaths = new Map();
|
|
40
|
+
this.logger = options?.logger;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public set(
|
|
44
|
+
transaction: BatchSetTransaction,
|
|
45
|
+
options?: DataModelOptions,
|
|
46
|
+
next?: DataModelImpl
|
|
47
|
+
): Updates {
|
|
48
|
+
const asModel = toModel(this, { ...options, includeInvalid: true }, next);
|
|
49
|
+
const nextTransaction: BatchSetTransaction = [];
|
|
50
|
+
|
|
51
|
+
transaction.forEach(([binding, value]) => {
|
|
52
|
+
this.shadowModelPaths.set(binding, value);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const invalidBindings: Array<BindingInstance> = [];
|
|
56
|
+
|
|
57
|
+
this.shadowModelPaths.forEach((value, binding) => {
|
|
58
|
+
const validations = this.validator(binding, asModel);
|
|
59
|
+
|
|
60
|
+
if (validations === undefined) {
|
|
61
|
+
nextTransaction.push([binding, value]);
|
|
62
|
+
} else if (validations instanceof Set) {
|
|
63
|
+
invalidBindings.push(...validations);
|
|
64
|
+
} else {
|
|
65
|
+
this.logger?.debug(
|
|
66
|
+
`Invalid value for path: ${binding.asString()} - ${
|
|
67
|
+
validations.severity
|
|
68
|
+
} - ${validations.message}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (next && nextTransaction.length > 0) {
|
|
74
|
+
// defer clearing the shadow model to prevent validations that are run twice due to weak binding refs still needing the data
|
|
75
|
+
nextTransaction.forEach(([binding]) =>
|
|
76
|
+
this.shadowModelPaths.delete(binding)
|
|
77
|
+
);
|
|
78
|
+
return next.set(nextTransaction, options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return invalidBindings.map((binding) => {
|
|
82
|
+
return {
|
|
83
|
+
binding,
|
|
84
|
+
oldValue: asModel.get(binding),
|
|
85
|
+
newValue: asModel.get(binding),
|
|
86
|
+
force: true,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public get(
|
|
92
|
+
binding: BindingInstance,
|
|
93
|
+
options?: DataModelOptions,
|
|
94
|
+
next?: DataModelImpl
|
|
95
|
+
) {
|
|
96
|
+
let val = next?.get(binding, options);
|
|
97
|
+
|
|
98
|
+
if (options?.includeInvalid === true) {
|
|
99
|
+
this.shadowModelPaths.forEach((shadowValue, shadowBinding) => {
|
|
100
|
+
if (shadowBinding === binding) {
|
|
101
|
+
val = shadowValue;
|
|
102
|
+
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (binding.contains(shadowBinding)) {
|
|
107
|
+
val = setIn(val, shadowBinding.relative(binding), shadowValue);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return val;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Node, AnyAssetType } from '../parser';
|
|
2
|
+
import { NodeType } from '../parser';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Functions for building AST nodes (relatively) easily
|
|
6
|
+
*/
|
|
7
|
+
export class Builder {
|
|
8
|
+
/**
|
|
9
|
+
* Creates an asset node
|
|
10
|
+
*
|
|
11
|
+
* @param value - the value to put in the asset node
|
|
12
|
+
*/
|
|
13
|
+
static asset<T extends AnyAssetType>(value: T): Node.Asset<T> {
|
|
14
|
+
return {
|
|
15
|
+
type: NodeType.Asset,
|
|
16
|
+
value,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a value node
|
|
22
|
+
*
|
|
23
|
+
* @param v - The object to put in the value node
|
|
24
|
+
*/
|
|
25
|
+
static value(v?: object): Node.Value {
|
|
26
|
+
return {
|
|
27
|
+
type: NodeType.Value,
|
|
28
|
+
value: v,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a multiNode and associates the multiNode as the parent
|
|
34
|
+
* of all the value nodes
|
|
35
|
+
*
|
|
36
|
+
* @param values - the value or applicability nodes to put in the multinode
|
|
37
|
+
*/
|
|
38
|
+
static multiNode(
|
|
39
|
+
...values: (Node.Value | Node.Applicability)[]
|
|
40
|
+
): Node.MultiNode {
|
|
41
|
+
const m: Node.MultiNode = {
|
|
42
|
+
type: NodeType.MultiNode,
|
|
43
|
+
override: true,
|
|
44
|
+
values,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
values.forEach((v) => {
|
|
48
|
+
// eslint-disable-next-line no-param-reassign
|
|
49
|
+
v.parent = m;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return m;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Adds a child node to a node
|
|
57
|
+
*
|
|
58
|
+
* @param node - The node to add a child to
|
|
59
|
+
* @param path - The path at which to add the child
|
|
60
|
+
* @param child - The child node
|
|
61
|
+
*/
|
|
62
|
+
static addChild<N extends Node.BaseWithChildren<NT>, NT extends NodeType>(
|
|
63
|
+
node: N,
|
|
64
|
+
path: Node.PathSegment | Node.PathSegment[],
|
|
65
|
+
child: Node.Node
|
|
66
|
+
) {
|
|
67
|
+
// eslint-disable-next-line no-param-reassign
|
|
68
|
+
child.parent = node as Node.Node;
|
|
69
|
+
|
|
70
|
+
const newChild: Node.Child = {
|
|
71
|
+
path: Array.isArray(path) ? path : [path],
|
|
72
|
+
value: child,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// eslint-disable-next-line no-param-reassign
|
|
76
|
+
node.children = node.children || [];
|
|
77
|
+
node.children.push(newChild);
|
|
78
|
+
|
|
79
|
+
return node;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/view/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
1
|
+
export * from './view';
|
|
2
|
+
export * from './resolver';
|
|
3
|
+
export * from './parser';
|
|
4
|
+
export * from './builder';
|
|
5
|
+
export * from './plugins';
|