@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.
Files changed (77) hide show
  1. package/dist/index.cjs.js +4128 -891
  2. package/dist/index.d.ts +1227 -50
  3. package/dist/index.esm.js +4065 -836
  4. package/package.json +9 -15
  5. package/src/binding/binding.ts +108 -0
  6. package/src/binding/index.ts +188 -0
  7. package/src/binding/resolver.ts +157 -0
  8. package/src/binding/utils.ts +51 -0
  9. package/src/binding-grammar/ast.ts +113 -0
  10. package/src/binding-grammar/custom/index.ts +304 -0
  11. package/src/binding-grammar/ebnf/binding.ebnf +22 -0
  12. package/src/binding-grammar/ebnf/index.ts +186 -0
  13. package/src/binding-grammar/ebnf/types.ts +104 -0
  14. package/src/binding-grammar/index.ts +4 -0
  15. package/src/binding-grammar/parsimmon/index.ts +78 -0
  16. package/src/controllers/constants/index.ts +85 -0
  17. package/src/controllers/constants/utils.ts +37 -0
  18. package/src/{data.ts → controllers/data.ts} +6 -6
  19. package/src/controllers/flow/controller.ts +95 -0
  20. package/src/controllers/flow/flow.ts +205 -0
  21. package/src/controllers/flow/index.ts +2 -0
  22. package/src/controllers/index.ts +5 -0
  23. package/src/{validation → controllers/validation}/binding-tracker.ts +5 -5
  24. package/src/{validation → controllers/validation}/controller.ts +15 -14
  25. package/src/{validation → controllers/validation}/index.ts +0 -0
  26. package/src/{view → controllers/view}/asset-transform.ts +2 -3
  27. package/src/{view → controllers/view}/controller.ts +9 -8
  28. package/src/controllers/view/index.ts +4 -0
  29. package/src/{view → controllers/view}/store.ts +0 -0
  30. package/src/{view → controllers/view}/types.ts +2 -1
  31. package/src/data/dependency-tracker.ts +187 -0
  32. package/src/data/index.ts +4 -0
  33. package/src/data/local-model.ts +41 -0
  34. package/src/data/model.ts +216 -0
  35. package/src/data/noop-model.ts +18 -0
  36. package/src/expressions/evaluator-functions.ts +29 -0
  37. package/src/expressions/evaluator.ts +405 -0
  38. package/src/expressions/index.ts +3 -0
  39. package/src/expressions/parser.ts +889 -0
  40. package/src/expressions/types.ts +200 -0
  41. package/src/expressions/utils.ts +8 -0
  42. package/src/index.ts +9 -12
  43. package/src/logger/consoleLogger.ts +49 -0
  44. package/src/logger/index.ts +5 -0
  45. package/src/logger/noopLogger.ts +13 -0
  46. package/src/logger/proxyLogger.ts +25 -0
  47. package/src/logger/tapableLogger.ts +38 -0
  48. package/src/logger/types.ts +6 -0
  49. package/src/player.ts +21 -18
  50. package/src/plugins/flow-exp-plugin.ts +2 -3
  51. package/src/schema/index.ts +2 -0
  52. package/src/schema/schema.ts +220 -0
  53. package/src/schema/types.ts +60 -0
  54. package/src/string-resolver/index.ts +188 -0
  55. package/src/types.ts +11 -13
  56. package/src/utils/index.ts +1 -0
  57. package/src/utils/replaceParams.ts +17 -0
  58. package/src/validator/index.ts +3 -0
  59. package/src/validator/registry.ts +20 -0
  60. package/src/validator/types.ts +75 -0
  61. package/src/validator/validation-middleware.ts +114 -0
  62. package/src/view/builder/index.ts +81 -0
  63. package/src/view/index.ts +5 -4
  64. package/src/view/parser/index.ts +318 -0
  65. package/src/view/parser/types.ts +141 -0
  66. package/src/view/plugins/applicability.ts +78 -0
  67. package/src/view/plugins/index.ts +5 -0
  68. package/src/view/plugins/options.ts +4 -0
  69. package/src/view/plugins/plugin.ts +21 -0
  70. package/src/view/plugins/string-resolver.ts +149 -0
  71. package/src/view/plugins/switch.ts +120 -0
  72. package/src/view/plugins/template-plugin.ts +172 -0
  73. package/src/view/resolver/index.ts +397 -0
  74. package/src/view/resolver/types.ts +161 -0
  75. package/src/view/resolver/utils.ts +57 -0
  76. package/src/view/view.ts +149 -0
  77. package/src/utils/desc.d.ts +0 -2
package/package.json CHANGED
@@ -1,30 +1,24 @@
1
1
  {
2
2
  "name": "@player-ui/player",
3
- "version": "0.3.0-next.2",
3
+ "version": "0.3.0-next.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org"
7
7
  },
8
8
  "peerDependencies": {},
9
9
  "dependencies": {
10
- "@player-ui/binding": "0.3.0-next.2",
11
- "@player-ui/constants": "0.3.0-next.2",
12
- "@player-ui/data": "0.3.0-next.2",
13
- "@player-ui/expressions": "0.3.0-next.2",
14
- "@player-ui/flow": "0.3.0-next.2",
15
- "@player-ui/logger": "0.3.0-next.2",
16
- "@player-ui/partial-match-registry": "0.3.0-next.2",
17
- "@player-ui/schema": "0.3.0-next.2",
18
- "@player-ui/string-resolver": "0.3.0-next.2",
19
- "@player-ui/types": "0.3.0-next.2",
20
- "@player-ui/utils": "0.3.0-next.2",
21
- "@player-ui/validator": "0.3.0-next.2",
22
- "@player-ui/view": "0.3.0-next.2",
23
- "babel-plugin-preval": "^5.0.0",
10
+ "@player-ui/partial-match-registry": "0.3.0-next.4",
11
+ "@player-ui/types": "0.3.0-next.4",
24
12
  "dequal": "^2.0.2",
25
13
  "p-defer": "^3.0.0",
26
14
  "queue-microtask": "^1.2.3",
27
15
  "tapable-ts": "^0.1.0",
16
+ "nested-error-stacks": "^2.1.1",
17
+ "@types/nested-error-stacks": "^2.1.0",
18
+ "parsimmon": "^1.12.0",
19
+ "@types/parsimmon": "^1.10.0",
20
+ "arr-flatten": "^1.1.0",
21
+ "ebnf": "^1.9.0",
28
22
  "timm": "^1.6.2",
29
23
  "@babel/runtime": "7.15.4"
30
24
  },
@@ -0,0 +1,108 @@
1
+ import { getBindingSegments } from './utils';
2
+
3
+ export interface BindingParserOptions {
4
+ /** Get the value for a specific binding */
5
+ get: (binding: BindingInstance) => any;
6
+
7
+ /**
8
+ * Set the values for bindings.
9
+ * This is used when the query syntax needs to modify an object
10
+ */
11
+ set: (transaction: Array<[BindingInstance, any]>) => void;
12
+
13
+ /**
14
+ * Get the result of evaluating an expression
15
+ */
16
+ evaluate: (exp: string) => any;
17
+ }
18
+
19
+ export type Getter = (path: BindingInstance) => any;
20
+
21
+ export type RawBindingSegment = number | string;
22
+ export type RawBinding = string | RawBindingSegment[];
23
+ export type BindingLike = RawBinding | BindingInstance;
24
+ export type BindingFactory = (
25
+ raw: RawBinding,
26
+ options?: Partial<BindingParserOptions>
27
+ ) => BindingInstance;
28
+
29
+ /**
30
+ * A path in the data model
31
+ */
32
+ export class BindingInstance {
33
+ private split: RawBindingSegment[];
34
+ private joined: string;
35
+ private factory: BindingFactory;
36
+
37
+ constructor(
38
+ raw: RawBinding,
39
+ factory = (rawBinding: RawBinding) => new BindingInstance(rawBinding)
40
+ ) {
41
+ const split = Array.isArray(raw) ? raw : raw.split('.');
42
+ this.split = split.map((segment) => {
43
+ if (typeof segment === 'number') {
44
+ return segment;
45
+ }
46
+
47
+ const tryNum = Number(segment);
48
+ return isNaN(tryNum) ? segment : tryNum;
49
+ });
50
+ Object.freeze(this.split);
51
+ this.joined = this.split.join('.');
52
+ this.factory = factory;
53
+ }
54
+
55
+ asArray(): RawBindingSegment[] {
56
+ return this.split;
57
+ }
58
+
59
+ asString(): string {
60
+ return this.joined;
61
+ }
62
+
63
+ /**
64
+ * Check to see if the given binding is a sub-path of the current one
65
+ */
66
+ contains(binding: BindingInstance): boolean {
67
+ // need to account for partial key matches
68
+ // [foo, bar] !== [foo, ba]
69
+ const bindingAsArray = binding.asArray();
70
+
71
+ if (bindingAsArray.length < this.split.length) {
72
+ return false;
73
+ }
74
+
75
+ // Check every overlapping index to make sure they're the same
76
+ // Intentionally use a for loop for speeeed
77
+ for (let i = 0; i < this.split.length; i++) {
78
+ if (this.split[i] !== bindingAsArray[i]) {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ return true;
84
+ }
85
+
86
+ relative(binding: BindingInstance): RawBindingSegment[] {
87
+ return this.asArray().slice(binding.asArray().length);
88
+ }
89
+
90
+ parent(): BindingInstance {
91
+ return this.factory(this.split.slice(0, -1));
92
+ }
93
+
94
+ key(): RawBindingSegment {
95
+ return this.split[this.split.length - 1];
96
+ }
97
+
98
+ /**
99
+ * This is a utility method to get a binding that is a descendent of this binding
100
+ *
101
+ * @param relative - The relative path to descend to
102
+ */
103
+ descendent(relative: BindingLike): BindingInstance {
104
+ const descendentSegments = getBindingSegments(relative);
105
+
106
+ return this.factory(this.split.concat(descendentSegments));
107
+ }
108
+ }
@@ -0,0 +1,188 @@
1
+ import { SyncBailHook, SyncWaterfallHook } from 'tapable-ts';
2
+ import NestedError from 'nested-error-stacks';
3
+ import type { ParserResult, AnyNode } from '../binding-grammar';
4
+ import {
5
+ // We can swap this with whichever parser we want to use
6
+ parseCustom as parseBinding,
7
+ } from '../binding-grammar';
8
+ import type { BindingParserOptions, BindingLike } from './binding';
9
+ import { BindingInstance } from './binding';
10
+ import { isBinding } from './utils';
11
+ import type { NormalizedResult, ResolveBindingASTOptions } from './resolver';
12
+ import { resolveBindingAST } from './resolver';
13
+
14
+ export * from './utils';
15
+ export * from './binding';
16
+
17
+ export const SIMPLE_BINDING_REGEX = /^[\w\-@]+(\.[\w\-@]+)*$/;
18
+
19
+ const DEFAULT_OPTIONS: BindingParserOptions = {
20
+ get: () => {
21
+ throw new Error('Not Implemented');
22
+ },
23
+ set: () => {
24
+ throw new Error('Not Implemented');
25
+ },
26
+ evaluate: () => {
27
+ throw new Error('Not Implemented');
28
+ },
29
+ };
30
+
31
+ /** A parser for creating bindings from a string */
32
+ export class BindingParser {
33
+ private cache: Record<string, BindingInstance>;
34
+ private parseCache: Record<string, ParserResult>;
35
+ private parserOptions: BindingParserOptions;
36
+
37
+ public hooks = {
38
+ skipOptimization: new SyncBailHook<[string], boolean>(),
39
+ beforeResolveNode: new SyncWaterfallHook<
40
+ [AnyNode, Required<NormalizedResult> & ResolveBindingASTOptions]
41
+ >(),
42
+ };
43
+
44
+ constructor(options?: Partial<BindingParserOptions>) {
45
+ this.parserOptions = { ...DEFAULT_OPTIONS, ...options };
46
+ this.cache = {};
47
+ this.parseCache = {};
48
+ this.parse = this.parse.bind(this);
49
+ }
50
+
51
+ /**
52
+ * Takes a binding path, parses it, and returns an equivalent, normalized
53
+ * representation of that path.
54
+ */
55
+ private normalizePath(
56
+ path: string,
57
+ resolveOptions: ResolveBindingASTOptions
58
+ ) {
59
+ if (
60
+ path.match(SIMPLE_BINDING_REGEX) &&
61
+ this.hooks.skipOptimization.call(path) !== true
62
+ ) {
63
+ return { path: path.split('.'), updates: undefined } as NormalizedResult;
64
+ }
65
+
66
+ const ast = this.parseCache[path] ?? parseBinding(path);
67
+ this.parseCache[path] = ast;
68
+
69
+ if (typeof ast !== 'object' || !ast?.status) {
70
+ throw new TypeError(
71
+ `Cannot normalize path "${path}": ${ast?.error ?? 'Unknown Error.'}`
72
+ );
73
+ }
74
+
75
+ try {
76
+ return resolveBindingAST(ast.path, resolveOptions, this.hooks);
77
+ } catch (e: any) {
78
+ throw new NestedError(`Cannot resolve binding: ${path}`, e);
79
+ }
80
+ }
81
+
82
+ private getBindingForNormalizedResult(
83
+ normalized: NormalizedResult
84
+ ): BindingInstance {
85
+ const normalizedStr = normalized.path.join('.');
86
+
87
+ if (this.cache[normalizedStr]) {
88
+ return this.cache[normalizedStr];
89
+ }
90
+
91
+ const created = new BindingInstance(
92
+ normalizedStr === '' ? [] : normalized.path,
93
+ this.parse
94
+ );
95
+ this.cache[normalizedStr] = created;
96
+
97
+ return created;
98
+ }
99
+
100
+ public parse(
101
+ rawBinding: BindingLike,
102
+ overrides: Partial<BindingParserOptions> = {}
103
+ ): BindingInstance {
104
+ if (isBinding(rawBinding)) {
105
+ return rawBinding;
106
+ }
107
+
108
+ const options = {
109
+ ...this.parserOptions,
110
+ ...overrides,
111
+ };
112
+
113
+ let updates: Record<string, any> = {};
114
+
115
+ const joined = Array.isArray(rawBinding)
116
+ ? rawBinding.join('.')
117
+ : String(rawBinding);
118
+
119
+ const normalizeConfig: ResolveBindingASTOptions = {
120
+ getValue: (path: Array<string | number>) => {
121
+ const normalized = this.normalizePath(path.join('.'), normalizeConfig);
122
+
123
+ return options.get(this.getBindingForNormalizedResult(normalized));
124
+ },
125
+ evaluate: (exp) => {
126
+ return options.evaluate(exp);
127
+ },
128
+ convertToPath: (path: any) => {
129
+ if (path === undefined) {
130
+ throw new Error(
131
+ 'Attempted to convert undefined value to binding path'
132
+ );
133
+ }
134
+
135
+ if (
136
+ typeof path !== 'string' &&
137
+ typeof path !== 'number' &&
138
+ typeof path !== 'boolean'
139
+ ) {
140
+ throw new Error(
141
+ `Attempting to convert ${typeof path} to a binding path.`
142
+ );
143
+ }
144
+
145
+ const normalized = this.normalizePath(String(path), normalizeConfig);
146
+
147
+ if (normalized.updates) {
148
+ updates = {
149
+ ...updates,
150
+ ...normalized.updates,
151
+ };
152
+ }
153
+
154
+ const joinedNormalizedPath = normalized.path.join('.');
155
+
156
+ if (joinedNormalizedPath === '') {
157
+ throw new Error('Nested path resolved to an empty path');
158
+ }
159
+
160
+ return joinedNormalizedPath;
161
+ },
162
+ };
163
+
164
+ const normalized = this.normalizePath(joined, normalizeConfig);
165
+
166
+ if (normalized.updates) {
167
+ updates = {
168
+ ...updates,
169
+ ...normalized.updates,
170
+ };
171
+ }
172
+
173
+ const updateKeys = Object.keys(updates);
174
+
175
+ if (updateKeys.length > 0) {
176
+ const updateTransaction = updateKeys.map<[BindingInstance, any]>(
177
+ (updatedBinding) => [
178
+ this.parse(updatedBinding),
179
+ updates[updatedBinding],
180
+ ]
181
+ );
182
+
183
+ options.set(updateTransaction);
184
+ }
185
+
186
+ return this.getBindingForNormalizedResult(normalized);
187
+ }
188
+ }
@@ -0,0 +1,157 @@
1
+ import NestedError from 'nested-error-stacks';
2
+ import type { SyncWaterfallHook } from 'tapable-ts';
3
+ import type { PathNode, AnyNode } from '../binding-grammar';
4
+
5
+ import { maybeConvertToNum } from '.';
6
+ import { findInArray } from './utils';
7
+
8
+ export interface NormalizedResult {
9
+ /** The normalized path */
10
+ path: Array<string | number>;
11
+
12
+ /** Any new updates that need to happen for this binding to be resolved */
13
+ updates?: Record<string, any>;
14
+ }
15
+
16
+ export interface ResolveBindingASTOptions {
17
+ /** Get the value of the model at the given path */
18
+ getValue: (path: Array<string | number>) => any;
19
+
20
+ /** Convert the value into valid path segments */
21
+ convertToPath: (value: any) => string;
22
+
23
+ /** Convert the value into valid path segments */
24
+ evaluate: (exp: string) => any;
25
+ }
26
+
27
+ export interface ResolveBindingASTHooks {
28
+ /** A hook for transforming a node before fully resolving it */
29
+ beforeResolveNode: SyncWaterfallHook<
30
+ [AnyNode, Required<NormalizedResult> & ResolveBindingASTOptions]
31
+ >;
32
+ }
33
+
34
+ /** Given a binding AST, resolve it */
35
+ export function resolveBindingAST(
36
+ bindingPathNode: PathNode,
37
+ options: ResolveBindingASTOptions,
38
+ hooks?: ResolveBindingASTHooks
39
+ ): NormalizedResult {
40
+ const context: Required<NormalizedResult> = {
41
+ updates: {},
42
+ path: [],
43
+ };
44
+
45
+ // let updates: Record<string, any> = {};
46
+ // const path: Array<string | number> = [];
47
+
48
+ /** Get the value for any child node */
49
+ function getValueForNode(node: AnyNode): any {
50
+ if (node.name === 'Value') {
51
+ return node.value;
52
+ }
53
+
54
+ if (node.name === 'PathNode') {
55
+ const nestedResolvedValue = resolveBindingAST(node, options);
56
+
57
+ if (nestedResolvedValue.updates) {
58
+ context.updates = {
59
+ ...context.updates,
60
+ ...nestedResolvedValue.updates,
61
+ };
62
+ }
63
+
64
+ try {
65
+ return options.convertToPath(
66
+ options.getValue(nestedResolvedValue.path)
67
+ );
68
+ } catch (e: any) {
69
+ throw new NestedError(
70
+ `Unable to resolve path segment: ${nestedResolvedValue.path}`,
71
+ e
72
+ );
73
+ }
74
+ }
75
+
76
+ if (node.name === 'Expression') {
77
+ try {
78
+ const actualValue = options.evaluate(node.value);
79
+
80
+ return options.convertToPath(actualValue);
81
+ } catch (e: any) {
82
+ throw new NestedError(`Unable to resolve path: ${node.value}`, e);
83
+ }
84
+ }
85
+
86
+ throw new Error(`Unable to resolve value for node: ${node.name}`);
87
+ }
88
+
89
+ /** Handle when path segments are binding paths (foo.bar) or single segments (foo) */
90
+ function appendPathSegments(segment: string | number) {
91
+ if (typeof segment === 'string' && segment.indexOf('.') > -1) {
92
+ segment.split('.').forEach((i) => {
93
+ context.path.push(maybeConvertToNum(i));
94
+ });
95
+ } else {
96
+ context.path.push(segment);
97
+ }
98
+ }
99
+
100
+ /** Compute the _actual_ binding val from the AST */
101
+ function resolveNode(_node: AnyNode) {
102
+ const resolvedNode =
103
+ hooks?.beforeResolveNode.call(_node, { ...context, ...options }) ?? _node;
104
+
105
+ switch (resolvedNode.name) {
106
+ case 'Expression':
107
+ case 'PathNode':
108
+ appendPathSegments(getValueForNode(resolvedNode));
109
+ break;
110
+
111
+ case 'Value':
112
+ appendPathSegments(resolvedNode.value);
113
+ break;
114
+
115
+ case 'Query': {
116
+ // Look for an object at the path with the given key/val criteria
117
+ const objToQuery: Record<string, any>[] =
118
+ options.getValue(context.path) ?? [];
119
+
120
+ const { key, value } = resolvedNode;
121
+
122
+ const resolvedKey = getValueForNode(key);
123
+ const resolvedValue = value && getValueForNode(value);
124
+
125
+ const index = findInArray(objToQuery, resolvedKey, resolvedValue);
126
+
127
+ if (index === undefined || index === -1) {
128
+ context.updates[
129
+ [...context.path, objToQuery.length, resolvedKey].join('.')
130
+ ] = resolvedValue;
131
+ context.path.push(objToQuery.length);
132
+ } else {
133
+ context.path.push(index);
134
+ }
135
+
136
+ break;
137
+ }
138
+
139
+ case 'Concatenated':
140
+ context.path.push(resolvedNode.value.map(getValueForNode).join(''));
141
+ break;
142
+
143
+ default:
144
+ throw new Error(`Unsupported node type: ${(resolvedNode as any).name}`);
145
+ }
146
+ }
147
+
148
+ bindingPathNode.path.forEach(resolveNode);
149
+
150
+ return {
151
+ path: context.path,
152
+ updates:
153
+ Object.keys(context.updates ?? {}).length > 0
154
+ ? context.updates
155
+ : undefined,
156
+ };
157
+ }
@@ -0,0 +1,51 @@
1
+ import type { BindingLike, BindingInstance } from './binding';
2
+
3
+ /** Check if the parameter representing a binding is already of the Binding class */
4
+ export function isBinding(binding: BindingLike): binding is BindingInstance {
5
+ return !(typeof binding === 'string' || Array.isArray(binding));
6
+ }
7
+
8
+ /** Convert the string to an int if you can, otherwise just return the original string */
9
+ export function maybeConvertToNum(i: string): string | number {
10
+ const asInt = parseInt(i, 10);
11
+
12
+ if (isNaN(asInt)) {
13
+ return i;
14
+ }
15
+
16
+ return asInt;
17
+ }
18
+
19
+ /**
20
+ * utility to convert binding into binding segments.
21
+ */
22
+ export function getBindingSegments(
23
+ binding: BindingLike
24
+ ): Array<string | number> {
25
+ if (Array.isArray(binding)) {
26
+ return binding;
27
+ }
28
+
29
+ if (typeof binding === 'string') {
30
+ return binding.split('.');
31
+ }
32
+
33
+ return binding.asArray();
34
+ }
35
+
36
+ /** Like _.findIndex, but ignores types */
37
+ export function findInArray<T extends Record<string | number, object>>(
38
+ array: Array<T>,
39
+ key: string | number,
40
+ value: T
41
+ ): number | undefined {
42
+ return array.findIndex((obj) => {
43
+ if (obj && typeof obj === 'object') {
44
+ // Intentional double-equals because we want '4' to be coerced to 4
45
+ // eslint-disable-next-line eqeqeq
46
+ return obj[key] == value;
47
+ }
48
+
49
+ return false;
50
+ });
51
+ }
@@ -0,0 +1,113 @@
1
+ export interface Node<T extends string> {
2
+ /** The basic node type */
3
+ name: T;
4
+ }
5
+
6
+ /**
7
+ * An AST node that represents a nested path in the model
8
+ * foo.{{bar}}.baz (this is {{bar}})
9
+ */
10
+ export interface PathNode extends Node<'PathNode'> {
11
+ /** The path in the model that this node represents */
12
+ path: Array<AnyNode>;
13
+ }
14
+
15
+ /**
16
+ * A segment representing a query
17
+ * [foo=bar]
18
+ */
19
+ export interface QueryNode extends Node<'Query'> {
20
+ /** The key to query */
21
+ key: AnyNode;
22
+
23
+ /** The target value */
24
+ value?: AnyNode;
25
+ }
26
+
27
+ /** A simple segment */
28
+ export interface ValueNode extends Node<'Value'> {
29
+ /** The segment value */
30
+ value: string | number;
31
+ }
32
+
33
+ /** A nested expression */
34
+ export interface ExpressionNode extends Node<'Expression'> {
35
+ /** The expression */
36
+ value: string;
37
+ }
38
+
39
+ /** Helper to create a value node */
40
+ export const toValue = (value: string | number): ValueNode => ({
41
+ name: 'Value',
42
+ value,
43
+ });
44
+
45
+ /** Helper to create an expression node */
46
+ export const toExpression = (value: string): ExpressionNode => ({
47
+ name: 'Expression',
48
+ value,
49
+ });
50
+
51
+ /** Helper to create a nested path node */
52
+ export const toPath = (path: Array<AnyNode>): PathNode => ({
53
+ name: 'PathNode',
54
+ path,
55
+ });
56
+
57
+ /** Helper to create a query node */
58
+ export const toQuery = (key: AnyNode, value?: AnyNode): QueryNode => ({
59
+ name: 'Query',
60
+ key,
61
+ value,
62
+ });
63
+
64
+ /** Create a concat node */
65
+ export const toConcatenatedNode = (
66
+ values: Array<PathNode | ValueNode | ExpressionNode>
67
+ ): PathNode | ValueNode | ConcatenatedNode | ExpressionNode => {
68
+ if (values.length === 1) {
69
+ return values[0];
70
+ }
71
+
72
+ return {
73
+ name: 'Concatenated',
74
+ value: values,
75
+ };
76
+ };
77
+
78
+ /**
79
+ * A binding segment that's multiple smaller ones
80
+ * {{foo}}_bar_{{baz}}
81
+ */
82
+ export interface ConcatenatedNode extends Node<'Concatenated'> {
83
+ /** A list of nested paths, or value nodes to concat together to form a segment */
84
+ value: Array<PathNode | ValueNode | ExpressionNode>;
85
+ }
86
+
87
+ export type AnyNode =
88
+ | PathNode
89
+ | QueryNode
90
+ | ValueNode
91
+ | ConcatenatedNode
92
+ | ExpressionNode;
93
+ export type Path = Array<AnyNode>;
94
+
95
+ export interface ParserSuccessResult {
96
+ /** A successful parse result */
97
+ status: true;
98
+
99
+ /** The path the binding represents */
100
+ path: PathNode;
101
+ }
102
+
103
+ export interface ParserFailureResult {
104
+ /** A failed parse result */
105
+ status: false;
106
+
107
+ /** The message representing the reason the parse result failed */
108
+ error: string;
109
+ }
110
+
111
+ export type ParserResult = ParserSuccessResult | ParserFailureResult;
112
+
113
+ export type Parser = (raw: string) => ParserResult;