@praxisui/specification 1.0.0-beta.4 → 1.0.0-beta.41
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/README.md +7 -0
- package/fesm2022/praxisui-specification.mjs +482 -34
- package/fesm2022/praxisui-specification.mjs.map +1 -1
- package/index.d.ts +36 -2
- package/package.json +5 -5
|
@@ -13,6 +13,13 @@ var ComparisonOperator;
|
|
|
13
13
|
ComparisonOperator["STARTS_WITH"] = "startsWith";
|
|
14
14
|
ComparisonOperator["ENDS_WITH"] = "endsWith";
|
|
15
15
|
ComparisonOperator["IN"] = "in";
|
|
16
|
+
ComparisonOperator["NOT_IN"] = "notIn";
|
|
17
|
+
ComparisonOperator["MATCHES"] = "matches";
|
|
18
|
+
ComparisonOperator["NOT_MATCHES"] = "notMatches";
|
|
19
|
+
ComparisonOperator["IS_NULL"] = "isNull";
|
|
20
|
+
ComparisonOperator["IS_NOT_NULL"] = "isNotNull";
|
|
21
|
+
ComparisonOperator["IS_EMPTY"] = "isEmpty";
|
|
22
|
+
ComparisonOperator["IS_NOT_EMPTY"] = "isNotEmpty";
|
|
16
23
|
})(ComparisonOperator || (ComparisonOperator = {}));
|
|
17
24
|
const OPERATOR_SYMBOLS = {
|
|
18
25
|
[ComparisonOperator.EQUALS]: '==',
|
|
@@ -24,7 +31,14 @@ const OPERATOR_SYMBOLS = {
|
|
|
24
31
|
[ComparisonOperator.CONTAINS]: 'contains',
|
|
25
32
|
[ComparisonOperator.STARTS_WITH]: 'startsWith',
|
|
26
33
|
[ComparisonOperator.ENDS_WITH]: 'endsWith',
|
|
27
|
-
[ComparisonOperator.IN]: 'in'
|
|
34
|
+
[ComparisonOperator.IN]: 'in',
|
|
35
|
+
[ComparisonOperator.NOT_IN]: 'not in',
|
|
36
|
+
[ComparisonOperator.MATCHES]: 'matches',
|
|
37
|
+
[ComparisonOperator.NOT_MATCHES]: '!~',
|
|
38
|
+
[ComparisonOperator.IS_NULL]: 'is null',
|
|
39
|
+
[ComparisonOperator.IS_NOT_NULL]: 'is not null',
|
|
40
|
+
[ComparisonOperator.IS_EMPTY]: 'is empty',
|
|
41
|
+
[ComparisonOperator.IS_NOT_EMPTY]: 'is not empty'
|
|
28
42
|
};
|
|
29
43
|
|
|
30
44
|
class FieldSpecification extends Specification {
|
|
@@ -60,6 +74,26 @@ class FieldSpecification extends Specification {
|
|
|
60
74
|
return String(fieldValue).endsWith(String(this.value));
|
|
61
75
|
case ComparisonOperator.IN:
|
|
62
76
|
return Array.isArray(this.value) && this.value.includes(fieldValue);
|
|
77
|
+
case ComparisonOperator.NOT_IN:
|
|
78
|
+
return Array.isArray(this.value) && !this.value.includes(fieldValue);
|
|
79
|
+
case ComparisonOperator.MATCHES:
|
|
80
|
+
if (this.value === undefined || this.value === null) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return this.toRegExp(this.value).test(String(fieldValue));
|
|
84
|
+
case ComparisonOperator.NOT_MATCHES:
|
|
85
|
+
if (this.value === undefined || this.value === null) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return !this.toRegExp(this.value).test(String(fieldValue));
|
|
89
|
+
case ComparisonOperator.IS_NULL:
|
|
90
|
+
return fieldValue === null || fieldValue === undefined;
|
|
91
|
+
case ComparisonOperator.IS_NOT_NULL:
|
|
92
|
+
return fieldValue !== null && fieldValue !== undefined;
|
|
93
|
+
case ComparisonOperator.IS_EMPTY:
|
|
94
|
+
return this.isEmpty(fieldValue);
|
|
95
|
+
case ComparisonOperator.IS_NOT_EMPTY:
|
|
96
|
+
return this.isNotEmpty(fieldValue);
|
|
63
97
|
default:
|
|
64
98
|
throw new Error(`Unsupported operator: ${this.operator}`);
|
|
65
99
|
}
|
|
@@ -90,10 +124,25 @@ class FieldSpecification extends Specification {
|
|
|
90
124
|
case ComparisonOperator.ENDS_WITH:
|
|
91
125
|
return `endsWith(${fieldName}, ${JSON.stringify(this.value)})`;
|
|
92
126
|
case ComparisonOperator.IN:
|
|
127
|
+
case ComparisonOperator.NOT_IN: {
|
|
93
128
|
const values = Array.isArray(this.value)
|
|
94
129
|
? this.value.map((v) => JSON.stringify(v)).join(', ')
|
|
95
130
|
: JSON.stringify(this.value);
|
|
96
|
-
|
|
131
|
+
const keyword = this.operator === ComparisonOperator.NOT_IN ? 'not in' : 'in';
|
|
132
|
+
return `${fieldName} ${keyword} [${values}]`;
|
|
133
|
+
}
|
|
134
|
+
case ComparisonOperator.MATCHES:
|
|
135
|
+
return `${fieldName} matches ${this.formatMatchesValue(this.value)}`;
|
|
136
|
+
case ComparisonOperator.NOT_MATCHES:
|
|
137
|
+
return `${fieldName} !~ ${this.formatMatchesValue(this.value)}`;
|
|
138
|
+
case ComparisonOperator.IS_NULL:
|
|
139
|
+
return `${fieldName} is null`;
|
|
140
|
+
case ComparisonOperator.IS_NOT_NULL:
|
|
141
|
+
return `${fieldName} is not null`;
|
|
142
|
+
case ComparisonOperator.IS_EMPTY:
|
|
143
|
+
return `${fieldName} is empty`;
|
|
144
|
+
case ComparisonOperator.IS_NOT_EMPTY:
|
|
145
|
+
return `${fieldName} is not empty`;
|
|
97
146
|
default:
|
|
98
147
|
return `${fieldName} ${symbol} ${JSON.stringify(this.value)}`;
|
|
99
148
|
}
|
|
@@ -107,6 +156,53 @@ class FieldSpecification extends Specification {
|
|
|
107
156
|
getValue() {
|
|
108
157
|
return this.value;
|
|
109
158
|
}
|
|
159
|
+
toRegExp(value) {
|
|
160
|
+
if (value instanceof RegExp) {
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
const regexLiteral = value.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
165
|
+
if (regexLiteral) {
|
|
166
|
+
return new RegExp(regexLiteral[1], regexLiteral[2]);
|
|
167
|
+
}
|
|
168
|
+
return new RegExp(value);
|
|
169
|
+
}
|
|
170
|
+
return new RegExp(String(value));
|
|
171
|
+
}
|
|
172
|
+
isEmpty(value) {
|
|
173
|
+
// Consider null/undefined as empty to align with DSL semantics for "is empty".
|
|
174
|
+
if (value === null || value === undefined) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
|
178
|
+
return value.length === 0;
|
|
179
|
+
}
|
|
180
|
+
if (value instanceof Set || value instanceof Map) {
|
|
181
|
+
return value.size === 0;
|
|
182
|
+
}
|
|
183
|
+
if (typeof value === 'object') {
|
|
184
|
+
return Object.keys(value).length === 0;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
isNotEmpty(value) {
|
|
189
|
+
if (value === null || value === undefined) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return !this.isEmpty(value);
|
|
193
|
+
}
|
|
194
|
+
formatMatchesValue(value) {
|
|
195
|
+
if (value instanceof RegExp) {
|
|
196
|
+
return value.toString();
|
|
197
|
+
}
|
|
198
|
+
if (typeof value === 'string') {
|
|
199
|
+
const regexLiteral = value.match(/^\/.+\/[gimsuy]*$/);
|
|
200
|
+
if (regexLiteral) {
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return JSON.stringify(value);
|
|
205
|
+
}
|
|
110
206
|
}
|
|
111
207
|
|
|
112
208
|
class AndSpecification extends Specification {
|
|
@@ -125,6 +221,7 @@ class AndSpecification extends Specification {
|
|
|
125
221
|
return {
|
|
126
222
|
type: 'and',
|
|
127
223
|
specs: this.specifications.map((spec) => spec.toJSON()),
|
|
224
|
+
metadata: this.metadata,
|
|
128
225
|
};
|
|
129
226
|
}
|
|
130
227
|
static fromJSON(json) {
|
|
@@ -176,6 +273,7 @@ class OrSpecification extends Specification {
|
|
|
176
273
|
return {
|
|
177
274
|
type: 'or',
|
|
178
275
|
specs: this.specifications.map((spec) => spec.toJSON()),
|
|
276
|
+
metadata: this.metadata,
|
|
179
277
|
};
|
|
180
278
|
}
|
|
181
279
|
static fromJSON(json) {
|
|
@@ -401,7 +499,8 @@ class FunctionSpecification extends Specification {
|
|
|
401
499
|
}
|
|
402
500
|
isSatisfiedBy(obj) {
|
|
403
501
|
const registry = this.registry || FunctionRegistry.getInstance();
|
|
404
|
-
|
|
502
|
+
const resolvedArgs = this.args.map((arg) => this.resolveArg(obj, arg));
|
|
503
|
+
return registry.execute(this.functionName, obj, ...resolvedArgs);
|
|
405
504
|
}
|
|
406
505
|
toJSON() {
|
|
407
506
|
return {
|
|
@@ -439,6 +538,59 @@ class FunctionSpecification extends Specification {
|
|
|
439
538
|
clone() {
|
|
440
539
|
return new FunctionSpecification(this.functionName, [...this.args], this.registry, this.metadata);
|
|
441
540
|
}
|
|
541
|
+
resolveArg(obj, arg) {
|
|
542
|
+
if (Array.isArray(arg)) {
|
|
543
|
+
return arg.map((entry) => this.resolveArg(obj, entry));
|
|
544
|
+
}
|
|
545
|
+
if (this.isFieldArg(arg)) {
|
|
546
|
+
const lookup = this.getPathValue(obj, arg.field);
|
|
547
|
+
if (lookup.found)
|
|
548
|
+
return lookup.value;
|
|
549
|
+
// Backward-compatible fallback when field path is not present.
|
|
550
|
+
return arg.field;
|
|
551
|
+
}
|
|
552
|
+
return arg;
|
|
553
|
+
}
|
|
554
|
+
isFieldArg(arg) {
|
|
555
|
+
return !!(arg
|
|
556
|
+
&& typeof arg === 'object'
|
|
557
|
+
&& arg.type === 'field'
|
|
558
|
+
&& typeof arg.field === 'string');
|
|
559
|
+
}
|
|
560
|
+
getPathValue(source, path) {
|
|
561
|
+
if (!source || typeof source !== 'object') {
|
|
562
|
+
return { found: false, value: undefined };
|
|
563
|
+
}
|
|
564
|
+
const raw = String(path || '').trim();
|
|
565
|
+
if (!raw) {
|
|
566
|
+
return { found: false, value: undefined };
|
|
567
|
+
}
|
|
568
|
+
const blocked = new Set(['__proto__', 'prototype', 'constructor']);
|
|
569
|
+
if (blocked.has(raw)) {
|
|
570
|
+
return { found: false, value: undefined };
|
|
571
|
+
}
|
|
572
|
+
if (raw in source) {
|
|
573
|
+
return { found: true, value: source[raw] };
|
|
574
|
+
}
|
|
575
|
+
if (!raw.includes('.')) {
|
|
576
|
+
return { found: false, value: undefined };
|
|
577
|
+
}
|
|
578
|
+
const segments = raw.split('.').filter(Boolean);
|
|
579
|
+
let cursor = source;
|
|
580
|
+
for (const segment of segments) {
|
|
581
|
+
if (blocked.has(segment)) {
|
|
582
|
+
return { found: false, value: undefined };
|
|
583
|
+
}
|
|
584
|
+
if (cursor == null || (typeof cursor !== 'object' && typeof cursor !== 'function')) {
|
|
585
|
+
return { found: false, value: undefined };
|
|
586
|
+
}
|
|
587
|
+
if (!(segment in cursor)) {
|
|
588
|
+
return { found: false, value: undefined };
|
|
589
|
+
}
|
|
590
|
+
cursor = cursor[segment];
|
|
591
|
+
}
|
|
592
|
+
return { found: true, value: cursor };
|
|
593
|
+
}
|
|
442
594
|
}
|
|
443
595
|
|
|
444
596
|
class AtLeastSpecification extends Specification {
|
|
@@ -649,6 +801,26 @@ class FieldToFieldSpecification extends Specification {
|
|
|
649
801
|
return String(valueA).endsWith(String(valueB));
|
|
650
802
|
case ComparisonOperator.IN:
|
|
651
803
|
return Array.isArray(valueB) && valueB.includes(valueA);
|
|
804
|
+
case ComparisonOperator.NOT_IN:
|
|
805
|
+
return Array.isArray(valueB) && !valueB.includes(valueA);
|
|
806
|
+
case ComparisonOperator.MATCHES:
|
|
807
|
+
if (valueB === undefined || valueB === null) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
return this.toRegExp(valueB).test(String(valueA));
|
|
811
|
+
case ComparisonOperator.NOT_MATCHES:
|
|
812
|
+
if (valueB === undefined || valueB === null) {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
return !this.toRegExp(valueB).test(String(valueA));
|
|
816
|
+
case ComparisonOperator.IS_NULL:
|
|
817
|
+
return valueA === null || valueA === undefined;
|
|
818
|
+
case ComparisonOperator.IS_NOT_NULL:
|
|
819
|
+
return valueA !== null && valueA !== undefined;
|
|
820
|
+
case ComparisonOperator.IS_EMPTY:
|
|
821
|
+
return this.isEmpty(valueA);
|
|
822
|
+
case ComparisonOperator.IS_NOT_EMPTY:
|
|
823
|
+
return this.isNotEmpty(valueA);
|
|
652
824
|
default:
|
|
653
825
|
throw new Error(`Unsupported operator: ${this.operator}`);
|
|
654
826
|
}
|
|
@@ -688,7 +860,22 @@ class FieldToFieldSpecification extends Specification {
|
|
|
688
860
|
case ComparisonOperator.ENDS_WITH:
|
|
689
861
|
return `endsWith(${leftField}, ${rightField})`;
|
|
690
862
|
case ComparisonOperator.IN:
|
|
691
|
-
|
|
863
|
+
case ComparisonOperator.NOT_IN: {
|
|
864
|
+
const keyword = this.operator === ComparisonOperator.NOT_IN ? 'not in' : 'in';
|
|
865
|
+
return `${leftField} ${keyword} ${rightField}`;
|
|
866
|
+
}
|
|
867
|
+
case ComparisonOperator.MATCHES:
|
|
868
|
+
return `${leftField} matches ${rightField}`;
|
|
869
|
+
case ComparisonOperator.NOT_MATCHES:
|
|
870
|
+
return `${leftField} !~ ${rightField}`;
|
|
871
|
+
case ComparisonOperator.IS_NULL:
|
|
872
|
+
return `${leftField} is null`;
|
|
873
|
+
case ComparisonOperator.IS_NOT_NULL:
|
|
874
|
+
return `${leftField} is not null`;
|
|
875
|
+
case ComparisonOperator.IS_EMPTY:
|
|
876
|
+
return `${leftField} is empty`;
|
|
877
|
+
case ComparisonOperator.IS_NOT_EMPTY:
|
|
878
|
+
return `${leftField} is not empty`;
|
|
692
879
|
default:
|
|
693
880
|
return `${leftField} ${symbol} ${rightField}`;
|
|
694
881
|
}
|
|
@@ -711,6 +898,40 @@ class FieldToFieldSpecification extends Specification {
|
|
|
711
898
|
clone() {
|
|
712
899
|
return new FieldToFieldSpecification(this.fieldA, this.operator, this.fieldB, this.transformA ? [...this.transformA] : undefined, this.transformB ? [...this.transformB] : undefined, this.transformRegistry, this.metadata);
|
|
713
900
|
}
|
|
901
|
+
toRegExp(value) {
|
|
902
|
+
if (value instanceof RegExp) {
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
if (typeof value === 'string') {
|
|
906
|
+
const regexLiteral = value.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
907
|
+
if (regexLiteral) {
|
|
908
|
+
return new RegExp(regexLiteral[1], regexLiteral[2]);
|
|
909
|
+
}
|
|
910
|
+
return new RegExp(value);
|
|
911
|
+
}
|
|
912
|
+
return new RegExp(String(value));
|
|
913
|
+
}
|
|
914
|
+
isEmpty(value) {
|
|
915
|
+
if (value === null || value === undefined) {
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
|
919
|
+
return value.length === 0;
|
|
920
|
+
}
|
|
921
|
+
if (value instanceof Set || value instanceof Map) {
|
|
922
|
+
return value.size === 0;
|
|
923
|
+
}
|
|
924
|
+
if (typeof value === 'object') {
|
|
925
|
+
return Object.keys(value).length === 0;
|
|
926
|
+
}
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
isNotEmpty(value) {
|
|
930
|
+
if (value === null || value === undefined) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
return !this.isEmpty(value);
|
|
934
|
+
}
|
|
714
935
|
}
|
|
715
936
|
|
|
716
937
|
class DefaultContextProvider {
|
|
@@ -1778,6 +1999,7 @@ var TokenType;
|
|
|
1778
1999
|
TokenType["NUMBER"] = "NUMBER";
|
|
1779
2000
|
TokenType["BOOLEAN"] = "BOOLEAN";
|
|
1780
2001
|
TokenType["NULL"] = "NULL";
|
|
2002
|
+
TokenType["REGEX"] = "REGEX";
|
|
1781
2003
|
// Operators
|
|
1782
2004
|
TokenType["AND"] = "AND";
|
|
1783
2005
|
TokenType["OR"] = "OR";
|
|
@@ -1792,6 +2014,13 @@ var TokenType;
|
|
|
1792
2014
|
TokenType["GREATER_THAN"] = "GREATER_THAN";
|
|
1793
2015
|
TokenType["GREATER_THAN_OR_EQUAL"] = "GREATER_THAN_OR_EQUAL";
|
|
1794
2016
|
TokenType["IN"] = "IN";
|
|
2017
|
+
TokenType["NOT_IN"] = "NOT_IN";
|
|
2018
|
+
TokenType["MATCHES"] = "MATCHES";
|
|
2019
|
+
TokenType["NOT_MATCHES"] = "NOT_MATCHES";
|
|
2020
|
+
TokenType["IS_NULL"] = "IS_NULL";
|
|
2021
|
+
TokenType["IS_NOT_NULL"] = "IS_NOT_NULL";
|
|
2022
|
+
TokenType["IS_EMPTY"] = "IS_EMPTY";
|
|
2023
|
+
TokenType["IS_NOT_EMPTY"] = "IS_NOT_EMPTY";
|
|
1795
2024
|
// Function tokens
|
|
1796
2025
|
TokenType["CONTAINS"] = "CONTAINS";
|
|
1797
2026
|
TokenType["STARTS_WITH"] = "STARTS_WITH";
|
|
@@ -1818,13 +2047,29 @@ const OPERATOR_KEYWORDS = {
|
|
|
1818
2047
|
'not': TokenType.NOT,
|
|
1819
2048
|
'!': TokenType.NOT,
|
|
1820
2049
|
'xor': TokenType.XOR,
|
|
2050
|
+
'^': TokenType.XOR,
|
|
1821
2051
|
'implies': TokenType.IMPLIES,
|
|
2052
|
+
'=>': TokenType.IMPLIES,
|
|
2053
|
+
'eq': TokenType.EQUALS,
|
|
2054
|
+
'equals': TokenType.EQUALS,
|
|
2055
|
+
'neq': TokenType.NOT_EQUALS,
|
|
2056
|
+
'notequals': TokenType.NOT_EQUALS,
|
|
2057
|
+
'lt': TokenType.LESS_THAN,
|
|
2058
|
+
'lte': TokenType.LESS_THAN_OR_EQUAL,
|
|
2059
|
+
'gt': TokenType.GREATER_THAN,
|
|
2060
|
+
'gte': TokenType.GREATER_THAN_OR_EQUAL,
|
|
1822
2061
|
'in': TokenType.IN,
|
|
1823
|
-
'
|
|
1824
|
-
'
|
|
1825
|
-
'
|
|
1826
|
-
'
|
|
1827
|
-
'
|
|
2062
|
+
'matches': TokenType.MATCHES,
|
|
2063
|
+
'!~': TokenType.NOT_MATCHES,
|
|
2064
|
+
'notmatches': TokenType.NOT_MATCHES,
|
|
2065
|
+
'isnull': TokenType.IS_NULL,
|
|
2066
|
+
'isNull': TokenType.IS_NULL,
|
|
2067
|
+
'isnotnull': TokenType.IS_NOT_NULL,
|
|
2068
|
+
'isNotNull': TokenType.IS_NOT_NULL,
|
|
2069
|
+
'isempty': TokenType.IS_EMPTY,
|
|
2070
|
+
'isEmpty': TokenType.IS_EMPTY,
|
|
2071
|
+
'isnotempty': TokenType.IS_NOT_EMPTY,
|
|
2072
|
+
'isNotEmpty': TokenType.IS_NOT_EMPTY,
|
|
1828
2073
|
'true': TokenType.BOOLEAN,
|
|
1829
2074
|
'false': TokenType.BOOLEAN,
|
|
1830
2075
|
'null': TokenType.NULL
|
|
@@ -1832,6 +2077,8 @@ const OPERATOR_KEYWORDS = {
|
|
|
1832
2077
|
const COMPARISON_OPERATORS = {
|
|
1833
2078
|
'==': TokenType.EQUALS,
|
|
1834
2079
|
'!=': TokenType.NOT_EQUALS,
|
|
2080
|
+
'!~': TokenType.NOT_MATCHES,
|
|
2081
|
+
'~': TokenType.MATCHES,
|
|
1835
2082
|
'<': TokenType.LESS_THAN,
|
|
1836
2083
|
'<=': TokenType.LESS_THAN_OR_EQUAL,
|
|
1837
2084
|
'>': TokenType.GREATER_THAN,
|
|
@@ -1868,6 +2115,10 @@ class DslTokenizer {
|
|
|
1868
2115
|
if (this.position >= this.input.length) {
|
|
1869
2116
|
return this.createToken(TokenType.EOF, '');
|
|
1870
2117
|
}
|
|
2118
|
+
const multiWordOperator = this.readMultiWordOperator();
|
|
2119
|
+
if (multiWordOperator) {
|
|
2120
|
+
return multiWordOperator;
|
|
2121
|
+
}
|
|
1871
2122
|
const char = this.input[this.position];
|
|
1872
2123
|
// Field references ${...}
|
|
1873
2124
|
if (char === '$' && this.peek() === '{') {
|
|
@@ -1877,11 +2128,25 @@ class DslTokenizer {
|
|
|
1877
2128
|
if (char === '"' || char === "'") {
|
|
1878
2129
|
return this.readString(char);
|
|
1879
2130
|
}
|
|
2131
|
+
// Regex literals /pattern/flags
|
|
2132
|
+
if (char === '/') {
|
|
2133
|
+
return this.readRegex();
|
|
2134
|
+
}
|
|
1880
2135
|
// Numbers
|
|
1881
2136
|
if (this.isDigit(char) || (char === '-' && this.isDigit(this.peek()))) {
|
|
1882
2137
|
return this.readNumber();
|
|
1883
2138
|
}
|
|
1884
2139
|
// Multi-character operators
|
|
2140
|
+
if (char === '!' && this.peek() === '~') {
|
|
2141
|
+
const token = this.createToken(TokenType.NOT_MATCHES, '!~');
|
|
2142
|
+
this.advance(2);
|
|
2143
|
+
return token;
|
|
2144
|
+
}
|
|
2145
|
+
if (char === '=' && this.peek() === '>') {
|
|
2146
|
+
const token = this.createToken(TokenType.IMPLIES, '=>');
|
|
2147
|
+
this.advance(2);
|
|
2148
|
+
return token;
|
|
2149
|
+
}
|
|
1885
2150
|
const twoChar = this.input.substr(this.position, 2);
|
|
1886
2151
|
if (COMPARISON_OPERATORS[twoChar]) {
|
|
1887
2152
|
const token = this.createToken(COMPARISON_OPERATORS[twoChar], twoChar);
|
|
@@ -1909,6 +2174,8 @@ class DslTokenizer {
|
|
|
1909
2174
|
return token;
|
|
1910
2175
|
}
|
|
1911
2176
|
return this.createTokenAndAdvance(TokenType.NOT, char);
|
|
2177
|
+
case '~':
|
|
2178
|
+
return this.createTokenAndAdvance(TokenType.MATCHES, char);
|
|
1912
2179
|
case '<':
|
|
1913
2180
|
if (this.peek() === '=') {
|
|
1914
2181
|
const token = this.createToken(TokenType.LESS_THAN_OR_EQUAL, '<=');
|
|
@@ -1944,6 +2211,8 @@ class DslTokenizer {
|
|
|
1944
2211
|
return token;
|
|
1945
2212
|
}
|
|
1946
2213
|
break;
|
|
2214
|
+
case '^':
|
|
2215
|
+
return this.createTokenAndAdvance(TokenType.XOR, char);
|
|
1947
2216
|
}
|
|
1948
2217
|
// Identifiers and keywords
|
|
1949
2218
|
if (this.isAlpha(char) || char === '_') {
|
|
@@ -2011,6 +2280,39 @@ class DslTokenizer {
|
|
|
2011
2280
|
this.advance(); // Skip closing quote
|
|
2012
2281
|
return this.createToken(TokenType.STRING, value, start);
|
|
2013
2282
|
}
|
|
2283
|
+
readRegex() {
|
|
2284
|
+
const start = this.position;
|
|
2285
|
+
this.advance(); // Skip opening /
|
|
2286
|
+
let pattern = '';
|
|
2287
|
+
while (this.position < this.input.length) {
|
|
2288
|
+
const current = this.input[this.position];
|
|
2289
|
+
if (current === '\\') {
|
|
2290
|
+
pattern += current;
|
|
2291
|
+
this.advance();
|
|
2292
|
+
if (this.position < this.input.length) {
|
|
2293
|
+
pattern += this.input[this.position];
|
|
2294
|
+
this.advance();
|
|
2295
|
+
}
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
if (current === '/') {
|
|
2299
|
+
break;
|
|
2300
|
+
}
|
|
2301
|
+
pattern += current;
|
|
2302
|
+
this.advance();
|
|
2303
|
+
}
|
|
2304
|
+
if (this.position >= this.input.length) {
|
|
2305
|
+
throw new Error('Unterminated regex literal');
|
|
2306
|
+
}
|
|
2307
|
+
this.advance(); // Skip closing /
|
|
2308
|
+
let flags = '';
|
|
2309
|
+
while (this.position < this.input.length && this.isAlpha(this.input[this.position])) {
|
|
2310
|
+
flags += this.input[this.position];
|
|
2311
|
+
this.advance();
|
|
2312
|
+
}
|
|
2313
|
+
const value = `/${pattern}/${flags}`;
|
|
2314
|
+
return this.createToken(TokenType.REGEX, value, start);
|
|
2315
|
+
}
|
|
2014
2316
|
readNumber() {
|
|
2015
2317
|
const start = this.position;
|
|
2016
2318
|
let value = '';
|
|
@@ -2043,6 +2345,27 @@ class DslTokenizer {
|
|
|
2043
2345
|
const tokenType = OPERATOR_KEYWORDS[value] || TokenType.IDENTIFIER;
|
|
2044
2346
|
return this.createToken(tokenType, value, start);
|
|
2045
2347
|
}
|
|
2348
|
+
readMultiWordOperator() {
|
|
2349
|
+
const remaining = this.input.slice(this.position);
|
|
2350
|
+
const patterns = [
|
|
2351
|
+
{ regex: /^is\s+not\s+empty\b/i, type: TokenType.IS_NOT_EMPTY },
|
|
2352
|
+
{ regex: /^is\s+not\s+null\b/i, type: TokenType.IS_NOT_NULL },
|
|
2353
|
+
{ regex: /^is\s+empty\b/i, type: TokenType.IS_EMPTY },
|
|
2354
|
+
{ regex: /^is\s+null\b/i, type: TokenType.IS_NULL },
|
|
2355
|
+
{ regex: /^not\s+in\b/i, type: TokenType.NOT_IN },
|
|
2356
|
+
{ regex: /^not\s+matches\b/i, type: TokenType.NOT_MATCHES },
|
|
2357
|
+
];
|
|
2358
|
+
for (const pattern of patterns) {
|
|
2359
|
+
const match = remaining.match(pattern.regex);
|
|
2360
|
+
if (match && match[0]) {
|
|
2361
|
+
const tokenValue = remaining.substring(0, match[0].length);
|
|
2362
|
+
const token = this.createToken(pattern.type, tokenValue, this.position);
|
|
2363
|
+
this.advance(match[0].length);
|
|
2364
|
+
return token;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return null;
|
|
2368
|
+
}
|
|
2046
2369
|
skipWhitespace() {
|
|
2047
2370
|
while (this.position < this.input.length && this.isWhitespace(this.input[this.position])) {
|
|
2048
2371
|
if (this.input[this.position] === '\n') {
|
|
@@ -2103,6 +2426,7 @@ class DslParser {
|
|
|
2103
2426
|
functionRegistry;
|
|
2104
2427
|
tokens = [];
|
|
2105
2428
|
current = 0;
|
|
2429
|
+
lastArgumentTokenType = null;
|
|
2106
2430
|
constructor(functionRegistry) {
|
|
2107
2431
|
this.functionRegistry = functionRegistry;
|
|
2108
2432
|
}
|
|
@@ -2176,16 +2500,19 @@ class DslParser {
|
|
|
2176
2500
|
parseComparison() {
|
|
2177
2501
|
let left = this.parsePrimary();
|
|
2178
2502
|
if (this.matchComparison()) {
|
|
2179
|
-
const
|
|
2503
|
+
const operatorToken = this.previous();
|
|
2504
|
+
const compOp = this.tokenTypeToComparisonOperator(operatorToken.type);
|
|
2180
2505
|
// Parse RHS allowing literals, arrays and identifiers (as values)
|
|
2181
|
-
const rightValue = this.
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
throw new Error('Field to field comparison not yet implemented in parser');
|
|
2185
|
-
}
|
|
2506
|
+
const rightValue = this.operatorRequiresValue(compOp)
|
|
2507
|
+
? this.parseArgument()
|
|
2508
|
+
: this.defaultValueForOperator(compOp);
|
|
2186
2509
|
if (left instanceof FieldSpecification) {
|
|
2187
2510
|
const field = left.getField();
|
|
2188
|
-
|
|
2511
|
+
if ((this.lastArgumentTokenType === TokenType.FIELD_REFERENCE ||
|
|
2512
|
+
this.lastArgumentTokenType === TokenType.IDENTIFIER) &&
|
|
2513
|
+
typeof rightValue === 'string') {
|
|
2514
|
+
return new FieldToFieldSpecification(field, compOp, rightValue);
|
|
2515
|
+
}
|
|
2189
2516
|
return new FieldSpecification(field, compOp, rightValue);
|
|
2190
2517
|
}
|
|
2191
2518
|
throw new Error('Invalid comparison expression');
|
|
@@ -2202,7 +2529,7 @@ class DslParser {
|
|
|
2202
2529
|
const fieldName = this.previous().value;
|
|
2203
2530
|
return new FieldSpecification(fieldName, ComparisonOperator.EQUALS, true);
|
|
2204
2531
|
}
|
|
2205
|
-
if (this.match(TokenType.IDENTIFIER)) {
|
|
2532
|
+
if (this.match(TokenType.IDENTIFIER, TokenType.MATCHES, TokenType.NOT_MATCHES)) {
|
|
2206
2533
|
const identifier = this.previous().value;
|
|
2207
2534
|
// Check if this is a function call
|
|
2208
2535
|
if (this.match(TokenType.LEFT_PAREN)) {
|
|
@@ -2215,14 +2542,17 @@ class DslParser {
|
|
|
2215
2542
|
}
|
|
2216
2543
|
parseFunctionCall(functionName) {
|
|
2217
2544
|
const args = [];
|
|
2545
|
+
const argTokenTypes = [];
|
|
2218
2546
|
if (!this.check(TokenType.RIGHT_PAREN)) {
|
|
2219
2547
|
do {
|
|
2220
2548
|
args.push(this.parseArgument());
|
|
2549
|
+
argTokenTypes.push(this.lastArgumentTokenType ?? TokenType.EOF);
|
|
2221
2550
|
} while (this.match(TokenType.COMMA));
|
|
2222
2551
|
}
|
|
2223
2552
|
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after function arguments");
|
|
2224
2553
|
// Handle special built-in functions
|
|
2225
|
-
|
|
2554
|
+
const normalizedFunctionName = this.normalizeFunctionName(functionName);
|
|
2555
|
+
switch (normalizedFunctionName) {
|
|
2226
2556
|
case 'atLeast':
|
|
2227
2557
|
if (args.length !== 2) {
|
|
2228
2558
|
throw new Error('atLeast requires exactly 2 arguments');
|
|
@@ -2246,38 +2576,61 @@ class DslParser {
|
|
|
2246
2576
|
case 'contains':
|
|
2247
2577
|
case 'startsWith':
|
|
2248
2578
|
case 'endsWith':
|
|
2579
|
+
case 'matches':
|
|
2580
|
+
case 'notMatches':
|
|
2249
2581
|
if (args.length !== 2) {
|
|
2250
|
-
throw new Error(`${
|
|
2582
|
+
throw new Error(`${normalizedFunctionName} requires exactly 2 arguments`);
|
|
2251
2583
|
}
|
|
2252
2584
|
const field = args[0];
|
|
2253
2585
|
const value = args[1];
|
|
2254
|
-
const op = this.functionNameToOperator(
|
|
2586
|
+
const op = this.functionNameToOperator(normalizedFunctionName);
|
|
2255
2587
|
return new FieldSpecification(field, op, value);
|
|
2256
2588
|
default:
|
|
2257
|
-
|
|
2589
|
+
// Custom functions keep identifier args as raw strings (legacy),
|
|
2590
|
+
// but preserve explicit field references (${field}) as typed args.
|
|
2591
|
+
const customArgs = args.map((arg, index) => {
|
|
2592
|
+
const tokenType = argTokenTypes[index];
|
|
2593
|
+
if (tokenType === TokenType.FIELD_REFERENCE && typeof arg === 'string') {
|
|
2594
|
+
return { type: 'field', field: arg };
|
|
2595
|
+
}
|
|
2596
|
+
return arg;
|
|
2597
|
+
});
|
|
2598
|
+
return new FunctionSpecification(normalizedFunctionName, customArgs, this.functionRegistry);
|
|
2258
2599
|
}
|
|
2259
2600
|
}
|
|
2260
2601
|
parseArgument() {
|
|
2602
|
+
this.lastArgumentTokenType = null;
|
|
2261
2603
|
if (this.match(TokenType.STRING)) {
|
|
2604
|
+
this.lastArgumentTokenType = TokenType.STRING;
|
|
2262
2605
|
return this.previous().value;
|
|
2263
2606
|
}
|
|
2264
2607
|
if (this.match(TokenType.NUMBER)) {
|
|
2265
2608
|
const value = this.previous().value;
|
|
2609
|
+
this.lastArgumentTokenType = TokenType.NUMBER;
|
|
2266
2610
|
return value.includes('.') ? parseFloat(value) : parseInt(value, 10);
|
|
2267
2611
|
}
|
|
2612
|
+
if (this.match(TokenType.REGEX)) {
|
|
2613
|
+
this.lastArgumentTokenType = TokenType.REGEX;
|
|
2614
|
+
return this.parseRegexLiteral(this.previous().value);
|
|
2615
|
+
}
|
|
2268
2616
|
if (this.match(TokenType.BOOLEAN)) {
|
|
2617
|
+
this.lastArgumentTokenType = TokenType.BOOLEAN;
|
|
2269
2618
|
return this.previous().value === 'true';
|
|
2270
2619
|
}
|
|
2271
2620
|
if (this.match(TokenType.NULL)) {
|
|
2621
|
+
this.lastArgumentTokenType = TokenType.NULL;
|
|
2272
2622
|
return null;
|
|
2273
2623
|
}
|
|
2274
2624
|
if (this.match(TokenType.FIELD_REFERENCE)) {
|
|
2625
|
+
this.lastArgumentTokenType = TokenType.FIELD_REFERENCE;
|
|
2275
2626
|
return this.previous().value;
|
|
2276
2627
|
}
|
|
2277
|
-
if (this.match(TokenType.IDENTIFIER)) {
|
|
2628
|
+
if (this.match(TokenType.IDENTIFIER, TokenType.MATCHES, TokenType.NOT_MATCHES)) {
|
|
2629
|
+
this.lastArgumentTokenType = this.previous().type;
|
|
2278
2630
|
return this.previous().value;
|
|
2279
2631
|
}
|
|
2280
2632
|
if (this.match(TokenType.LEFT_BRACKET)) {
|
|
2633
|
+
this.lastArgumentTokenType = TokenType.LEFT_BRACKET;
|
|
2281
2634
|
const elements = [];
|
|
2282
2635
|
if (!this.check(TokenType.RIGHT_BRACKET)) {
|
|
2283
2636
|
do {
|
|
@@ -2287,10 +2640,11 @@ class DslParser {
|
|
|
2287
2640
|
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after array elements");
|
|
2288
2641
|
return elements;
|
|
2289
2642
|
}
|
|
2643
|
+
this.lastArgumentTokenType = null;
|
|
2290
2644
|
throw new Error(`Unexpected token in argument: ${this.peek().value}`);
|
|
2291
2645
|
}
|
|
2292
2646
|
matchComparison() {
|
|
2293
|
-
return this.match(TokenType.EQUALS, TokenType.NOT_EQUALS, TokenType.LESS_THAN, TokenType.LESS_THAN_OR_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_THAN_OR_EQUAL, TokenType.IN);
|
|
2647
|
+
return this.match(TokenType.EQUALS, TokenType.NOT_EQUALS, TokenType.LESS_THAN, TokenType.LESS_THAN_OR_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_THAN_OR_EQUAL, TokenType.IN, TokenType.NOT_IN, TokenType.MATCHES, TokenType.NOT_MATCHES, TokenType.IS_NULL, TokenType.IS_NOT_NULL, TokenType.IS_EMPTY, TokenType.IS_NOT_EMPTY);
|
|
2294
2648
|
}
|
|
2295
2649
|
tokenTypeToComparisonOperator(tokenType) {
|
|
2296
2650
|
switch (tokenType) {
|
|
@@ -2308,22 +2662,50 @@ class DslParser {
|
|
|
2308
2662
|
return ComparisonOperator.GREATER_THAN_OR_EQUAL;
|
|
2309
2663
|
case TokenType.IN:
|
|
2310
2664
|
return ComparisonOperator.IN;
|
|
2665
|
+
case TokenType.NOT_IN:
|
|
2666
|
+
return ComparisonOperator.NOT_IN;
|
|
2667
|
+
case TokenType.MATCHES:
|
|
2668
|
+
return ComparisonOperator.MATCHES;
|
|
2669
|
+
case TokenType.NOT_MATCHES:
|
|
2670
|
+
return ComparisonOperator.NOT_MATCHES;
|
|
2671
|
+
case TokenType.IS_NULL:
|
|
2672
|
+
return ComparisonOperator.IS_NULL;
|
|
2673
|
+
case TokenType.IS_NOT_NULL:
|
|
2674
|
+
return ComparisonOperator.IS_NOT_NULL;
|
|
2675
|
+
case TokenType.IS_EMPTY:
|
|
2676
|
+
return ComparisonOperator.IS_EMPTY;
|
|
2677
|
+
case TokenType.IS_NOT_EMPTY:
|
|
2678
|
+
return ComparisonOperator.IS_NOT_EMPTY;
|
|
2311
2679
|
default:
|
|
2312
2680
|
throw new Error(`Unknown comparison operator: ${tokenType}`);
|
|
2313
2681
|
}
|
|
2314
2682
|
}
|
|
2315
2683
|
functionNameToOperator(functionName) {
|
|
2316
|
-
|
|
2684
|
+
const normalizedFunctionName = this.normalizeFunctionName(functionName);
|
|
2685
|
+
switch (normalizedFunctionName) {
|
|
2317
2686
|
case 'contains':
|
|
2318
2687
|
return ComparisonOperator.CONTAINS;
|
|
2319
2688
|
case 'startsWith':
|
|
2320
2689
|
return ComparisonOperator.STARTS_WITH;
|
|
2321
2690
|
case 'endsWith':
|
|
2322
2691
|
return ComparisonOperator.ENDS_WITH;
|
|
2692
|
+
case 'matches':
|
|
2693
|
+
return ComparisonOperator.MATCHES;
|
|
2694
|
+
case 'notMatches':
|
|
2695
|
+
return ComparisonOperator.NOT_MATCHES;
|
|
2323
2696
|
default:
|
|
2324
|
-
throw new Error(`Unknown function: ${
|
|
2697
|
+
throw new Error(`Unknown function: ${normalizedFunctionName}`);
|
|
2325
2698
|
}
|
|
2326
2699
|
}
|
|
2700
|
+
normalizeFunctionName(functionName) {
|
|
2701
|
+
const raw = String(functionName || '').trim();
|
|
2702
|
+
const compactLower = raw.replace(/\s+/g, '').toLowerCase();
|
|
2703
|
+
if (compactLower === 'matches')
|
|
2704
|
+
return 'matches';
|
|
2705
|
+
if (compactLower === 'notmatches')
|
|
2706
|
+
return 'notMatches';
|
|
2707
|
+
return raw;
|
|
2708
|
+
}
|
|
2327
2709
|
extractValue(spec) {
|
|
2328
2710
|
// This is a simplified extraction - in a real implementation
|
|
2329
2711
|
// we'd need to handle more complex cases
|
|
@@ -2332,6 +2714,34 @@ class DslParser {
|
|
|
2332
2714
|
}
|
|
2333
2715
|
return spec;
|
|
2334
2716
|
}
|
|
2717
|
+
operatorRequiresValue(operator) {
|
|
2718
|
+
switch (operator) {
|
|
2719
|
+
case ComparisonOperator.IS_NULL:
|
|
2720
|
+
case ComparisonOperator.IS_NOT_NULL:
|
|
2721
|
+
case ComparisonOperator.IS_EMPTY:
|
|
2722
|
+
case ComparisonOperator.IS_NOT_EMPTY:
|
|
2723
|
+
return false;
|
|
2724
|
+
default:
|
|
2725
|
+
return true;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
defaultValueForOperator(operator) {
|
|
2729
|
+
switch (operator) {
|
|
2730
|
+
case ComparisonOperator.IS_NULL:
|
|
2731
|
+
case ComparisonOperator.IS_NOT_NULL:
|
|
2732
|
+
case ComparisonOperator.IS_EMPTY:
|
|
2733
|
+
case ComparisonOperator.IS_NOT_EMPTY:
|
|
2734
|
+
return null;
|
|
2735
|
+
default:
|
|
2736
|
+
return undefined;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
parseRegexLiteral(raw) {
|
|
2740
|
+
const lastSlash = raw.lastIndexOf('/');
|
|
2741
|
+
const pattern = raw.slice(1, lastSlash);
|
|
2742
|
+
const flags = raw.slice(lastSlash + 1);
|
|
2743
|
+
return new RegExp(pattern, flags);
|
|
2744
|
+
}
|
|
2335
2745
|
match(...types) {
|
|
2336
2746
|
for (const type of types) {
|
|
2337
2747
|
if (this.check(type)) {
|
|
@@ -2671,7 +3081,8 @@ class DslValidator {
|
|
|
2671
3081
|
'contains', 'startsWith', 'endsWith', 'atLeast', 'exactly',
|
|
2672
3082
|
'forEach', 'uniqueBy', 'minLength', 'maxLength',
|
|
2673
3083
|
'requiredIf', 'visibleIf', 'disabledIf', 'readonlyIf',
|
|
2674
|
-
'ifDefined', 'ifNotNull', 'ifExists', 'withDefault'
|
|
3084
|
+
'ifDefined', 'ifNotNull', 'ifExists', 'withDefault',
|
|
3085
|
+
'matches', 'notMatches'
|
|
2675
3086
|
];
|
|
2676
3087
|
constructor(config = {}) {
|
|
2677
3088
|
this.config = {
|
|
@@ -2804,7 +3215,9 @@ class DslValidator {
|
|
|
2804
3215
|
}
|
|
2805
3216
|
}
|
|
2806
3217
|
validateTokens(tokens, input, issues) {
|
|
2807
|
-
for (
|
|
3218
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3219
|
+
const token = tokens[i];
|
|
3220
|
+
const nextToken = tokens[i + 1];
|
|
2808
3221
|
if (token.type === TokenType.EOF)
|
|
2809
3222
|
continue;
|
|
2810
3223
|
// Validate numbers
|
|
@@ -2826,6 +3239,11 @@ class DslValidator {
|
|
|
2826
3239
|
}
|
|
2827
3240
|
// Validate field references
|
|
2828
3241
|
if (token.type === TokenType.FIELD_REFERENCE || token.type === TokenType.IDENTIFIER) {
|
|
3242
|
+
// Function names are identifiers too, but should be validated as functions
|
|
3243
|
+
// (validateSemantics), not as field references.
|
|
3244
|
+
const isFunctionIdentifier = this.isFunctionToken(token, nextToken);
|
|
3245
|
+
if (isFunctionIdentifier)
|
|
3246
|
+
continue;
|
|
2829
3247
|
if (this.config.knownFields.length > 0) {
|
|
2830
3248
|
if (!this.config.knownFields.includes(token.value)) {
|
|
2831
3249
|
issues.push({
|
|
@@ -2853,6 +3271,14 @@ class DslValidator {
|
|
|
2853
3271
|
const prevToken = tokens[i - 1];
|
|
2854
3272
|
// Check for consecutive operators
|
|
2855
3273
|
if (this.isOperatorToken(token) && this.isOperatorToken(nextToken)) {
|
|
3274
|
+
// `matches(` and `notMatches(` can be tokenized as operator-like function names.
|
|
3275
|
+
if (this.isFunctionToken(nextToken, tokens[i + 2])) {
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
// Allow NOT after binary operators (e.g. AND NOT, OR NOT)
|
|
3279
|
+
if (this.isBinaryOperatorToken(token) && nextToken.type === TokenType.NOT) {
|
|
3280
|
+
continue;
|
|
3281
|
+
}
|
|
2856
3282
|
issues.push({
|
|
2857
3283
|
type: ValidationIssueType.UNEXPECTED_TOKEN,
|
|
2858
3284
|
severity: ValidationSeverity.ERROR,
|
|
@@ -2868,6 +3294,9 @@ class DslValidator {
|
|
|
2868
3294
|
}
|
|
2869
3295
|
// Check for operators at the beginning/end
|
|
2870
3296
|
if (i === 0 && this.isBinaryOperatorToken(token)) {
|
|
3297
|
+
if (this.isFunctionToken(token, nextToken)) {
|
|
3298
|
+
continue;
|
|
3299
|
+
}
|
|
2871
3300
|
issues.push({
|
|
2872
3301
|
type: ValidationIssueType.UNEXPECTED_TOKEN,
|
|
2873
3302
|
severity: ValidationSeverity.ERROR,
|
|
@@ -2902,8 +3331,8 @@ class DslValidator {
|
|
|
2902
3331
|
for (let i = 0; i < tokens.length; i++) {
|
|
2903
3332
|
const token = tokens[i];
|
|
2904
3333
|
const nextToken = tokens[i + 1];
|
|
2905
|
-
if (token
|
|
2906
|
-
const functionName = token
|
|
3334
|
+
if (this.isFunctionToken(token, nextToken)) {
|
|
3335
|
+
const functionName = this.normalizeFunctionTokenName(token);
|
|
2907
3336
|
// Check if function is known
|
|
2908
3337
|
const allKnownFunctions = [
|
|
2909
3338
|
...this.BUILT_IN_FUNCTIONS,
|
|
@@ -3015,16 +3444,35 @@ class DslValidator {
|
|
|
3015
3444
|
'ifDefined': 2,
|
|
3016
3445
|
'ifNotNull': 2,
|
|
3017
3446
|
'ifExists': 2,
|
|
3018
|
-
'withDefault': 3
|
|
3447
|
+
'withDefault': 3,
|
|
3448
|
+
'matches': 2,
|
|
3449
|
+
'notMatches': 2
|
|
3019
3450
|
};
|
|
3020
3451
|
return argCounts[functionName] ?? null;
|
|
3021
3452
|
}
|
|
3453
|
+
isFunctionToken(token, nextToken) {
|
|
3454
|
+
if (!token || !nextToken)
|
|
3455
|
+
return false;
|
|
3456
|
+
if (nextToken.type !== TokenType.LEFT_PAREN)
|
|
3457
|
+
return false;
|
|
3458
|
+
return token.type === TokenType.IDENTIFIER
|
|
3459
|
+
|| token.type === TokenType.MATCHES
|
|
3460
|
+
|| token.type === TokenType.NOT_MATCHES;
|
|
3461
|
+
}
|
|
3462
|
+
normalizeFunctionTokenName(token) {
|
|
3463
|
+
if (token.type === TokenType.MATCHES)
|
|
3464
|
+
return 'matches';
|
|
3465
|
+
if (token.type === TokenType.NOT_MATCHES)
|
|
3466
|
+
return 'notMatches';
|
|
3467
|
+
return token.value;
|
|
3468
|
+
}
|
|
3022
3469
|
isOperatorToken(token) {
|
|
3023
3470
|
return [
|
|
3024
3471
|
TokenType.AND, TokenType.OR, TokenType.NOT, TokenType.XOR, TokenType.IMPLIES,
|
|
3025
3472
|
TokenType.EQUALS, TokenType.NOT_EQUALS, TokenType.LESS_THAN,
|
|
3026
3473
|
TokenType.LESS_THAN_OR_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_THAN_OR_EQUAL,
|
|
3027
|
-
TokenType.IN
|
|
3474
|
+
TokenType.IN, TokenType.NOT_IN, TokenType.MATCHES, TokenType.NOT_MATCHES,
|
|
3475
|
+
TokenType.IS_NULL, TokenType.IS_NOT_NULL, TokenType.IS_EMPTY, TokenType.IS_NOT_EMPTY
|
|
3028
3476
|
].includes(token.type);
|
|
3029
3477
|
}
|
|
3030
3478
|
isBinaryOperatorToken(token) {
|
|
@@ -3032,7 +3480,7 @@ class DslValidator {
|
|
|
3032
3480
|
TokenType.AND, TokenType.OR, TokenType.XOR, TokenType.IMPLIES,
|
|
3033
3481
|
TokenType.EQUALS, TokenType.NOT_EQUALS, TokenType.LESS_THAN,
|
|
3034
3482
|
TokenType.LESS_THAN_OR_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_THAN_OR_EQUAL,
|
|
3035
|
-
TokenType.IN
|
|
3483
|
+
TokenType.IN, TokenType.NOT_IN, TokenType.MATCHES, TokenType.NOT_MATCHES
|
|
3036
3484
|
].includes(token.type);
|
|
3037
3485
|
}
|
|
3038
3486
|
suggestSimilarField(fieldName) {
|
|
@@ -3227,8 +3675,8 @@ class SpecificationFactory {
|
|
|
3227
3675
|
static exactly(exact, specs) {
|
|
3228
3676
|
return new ExactlySpecification(exact, specs);
|
|
3229
3677
|
}
|
|
3230
|
-
static fieldToField(fieldA, operator, fieldB, transformA, transformB, registry) {
|
|
3231
|
-
return new FieldToFieldSpecification(fieldA, operator, fieldB, transformA, transformB, registry);
|
|
3678
|
+
static fieldToField(fieldA, operator, fieldB, transformA, transformB, registry, metadata) {
|
|
3679
|
+
return new FieldToFieldSpecification(fieldA, operator, fieldB, transformA, transformB, registry, metadata);
|
|
3232
3680
|
}
|
|
3233
3681
|
static contextual(template, provider) {
|
|
3234
3682
|
return new ContextualSpecification(template, provider);
|