@praxisui/specification 1.0.0-beta.30 → 1.0.0-beta.40
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.
|
@@ -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,8 +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,
|
|
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,
|
|
1823
2073
|
'true': TokenType.BOOLEAN,
|
|
1824
2074
|
'false': TokenType.BOOLEAN,
|
|
1825
2075
|
'null': TokenType.NULL
|
|
@@ -1827,6 +2077,8 @@ const OPERATOR_KEYWORDS = {
|
|
|
1827
2077
|
const COMPARISON_OPERATORS = {
|
|
1828
2078
|
'==': TokenType.EQUALS,
|
|
1829
2079
|
'!=': TokenType.NOT_EQUALS,
|
|
2080
|
+
'!~': TokenType.NOT_MATCHES,
|
|
2081
|
+
'~': TokenType.MATCHES,
|
|
1830
2082
|
'<': TokenType.LESS_THAN,
|
|
1831
2083
|
'<=': TokenType.LESS_THAN_OR_EQUAL,
|
|
1832
2084
|
'>': TokenType.GREATER_THAN,
|
|
@@ -1863,6 +2115,10 @@ class DslTokenizer {
|
|
|
1863
2115
|
if (this.position >= this.input.length) {
|
|
1864
2116
|
return this.createToken(TokenType.EOF, '');
|
|
1865
2117
|
}
|
|
2118
|
+
const multiWordOperator = this.readMultiWordOperator();
|
|
2119
|
+
if (multiWordOperator) {
|
|
2120
|
+
return multiWordOperator;
|
|
2121
|
+
}
|
|
1866
2122
|
const char = this.input[this.position];
|
|
1867
2123
|
// Field references ${...}
|
|
1868
2124
|
if (char === '$' && this.peek() === '{') {
|
|
@@ -1872,11 +2128,25 @@ class DslTokenizer {
|
|
|
1872
2128
|
if (char === '"' || char === "'") {
|
|
1873
2129
|
return this.readString(char);
|
|
1874
2130
|
}
|
|
2131
|
+
// Regex literals /pattern/flags
|
|
2132
|
+
if (char === '/') {
|
|
2133
|
+
return this.readRegex();
|
|
2134
|
+
}
|
|
1875
2135
|
// Numbers
|
|
1876
2136
|
if (this.isDigit(char) || (char === '-' && this.isDigit(this.peek()))) {
|
|
1877
2137
|
return this.readNumber();
|
|
1878
2138
|
}
|
|
1879
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
|
+
}
|
|
1880
2150
|
const twoChar = this.input.substr(this.position, 2);
|
|
1881
2151
|
if (COMPARISON_OPERATORS[twoChar]) {
|
|
1882
2152
|
const token = this.createToken(COMPARISON_OPERATORS[twoChar], twoChar);
|
|
@@ -1904,6 +2174,8 @@ class DslTokenizer {
|
|
|
1904
2174
|
return token;
|
|
1905
2175
|
}
|
|
1906
2176
|
return this.createTokenAndAdvance(TokenType.NOT, char);
|
|
2177
|
+
case '~':
|
|
2178
|
+
return this.createTokenAndAdvance(TokenType.MATCHES, char);
|
|
1907
2179
|
case '<':
|
|
1908
2180
|
if (this.peek() === '=') {
|
|
1909
2181
|
const token = this.createToken(TokenType.LESS_THAN_OR_EQUAL, '<=');
|
|
@@ -1939,6 +2211,8 @@ class DslTokenizer {
|
|
|
1939
2211
|
return token;
|
|
1940
2212
|
}
|
|
1941
2213
|
break;
|
|
2214
|
+
case '^':
|
|
2215
|
+
return this.createTokenAndAdvance(TokenType.XOR, char);
|
|
1942
2216
|
}
|
|
1943
2217
|
// Identifiers and keywords
|
|
1944
2218
|
if (this.isAlpha(char) || char === '_') {
|
|
@@ -2006,6 +2280,39 @@ class DslTokenizer {
|
|
|
2006
2280
|
this.advance(); // Skip closing quote
|
|
2007
2281
|
return this.createToken(TokenType.STRING, value, start);
|
|
2008
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
|
+
}
|
|
2009
2316
|
readNumber() {
|
|
2010
2317
|
const start = this.position;
|
|
2011
2318
|
let value = '';
|
|
@@ -2038,6 +2345,27 @@ class DslTokenizer {
|
|
|
2038
2345
|
const tokenType = OPERATOR_KEYWORDS[value] || TokenType.IDENTIFIER;
|
|
2039
2346
|
return this.createToken(tokenType, value, start);
|
|
2040
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
|
+
}
|
|
2041
2369
|
skipWhitespace() {
|
|
2042
2370
|
while (this.position < this.input.length && this.isWhitespace(this.input[this.position])) {
|
|
2043
2371
|
if (this.input[this.position] === '\n') {
|
|
@@ -2098,6 +2426,7 @@ class DslParser {
|
|
|
2098
2426
|
functionRegistry;
|
|
2099
2427
|
tokens = [];
|
|
2100
2428
|
current = 0;
|
|
2429
|
+
lastArgumentTokenType = null;
|
|
2101
2430
|
constructor(functionRegistry) {
|
|
2102
2431
|
this.functionRegistry = functionRegistry;
|
|
2103
2432
|
}
|
|
@@ -2171,16 +2500,19 @@ class DslParser {
|
|
|
2171
2500
|
parseComparison() {
|
|
2172
2501
|
let left = this.parsePrimary();
|
|
2173
2502
|
if (this.matchComparison()) {
|
|
2174
|
-
const
|
|
2503
|
+
const operatorToken = this.previous();
|
|
2504
|
+
const compOp = this.tokenTypeToComparisonOperator(operatorToken.type);
|
|
2175
2505
|
// Parse RHS allowing literals, arrays and identifiers (as values)
|
|
2176
|
-
const rightValue = this.
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
throw new Error('Field to field comparison not yet implemented in parser');
|
|
2180
|
-
}
|
|
2506
|
+
const rightValue = this.operatorRequiresValue(compOp)
|
|
2507
|
+
? this.parseArgument()
|
|
2508
|
+
: this.defaultValueForOperator(compOp);
|
|
2181
2509
|
if (left instanceof FieldSpecification) {
|
|
2182
2510
|
const field = left.getField();
|
|
2183
|
-
|
|
2511
|
+
if ((this.lastArgumentTokenType === TokenType.FIELD_REFERENCE ||
|
|
2512
|
+
this.lastArgumentTokenType === TokenType.IDENTIFIER) &&
|
|
2513
|
+
typeof rightValue === 'string') {
|
|
2514
|
+
return new FieldToFieldSpecification(field, compOp, rightValue);
|
|
2515
|
+
}
|
|
2184
2516
|
return new FieldSpecification(field, compOp, rightValue);
|
|
2185
2517
|
}
|
|
2186
2518
|
throw new Error('Invalid comparison expression');
|
|
@@ -2197,7 +2529,7 @@ class DslParser {
|
|
|
2197
2529
|
const fieldName = this.previous().value;
|
|
2198
2530
|
return new FieldSpecification(fieldName, ComparisonOperator.EQUALS, true);
|
|
2199
2531
|
}
|
|
2200
|
-
if (this.match(TokenType.IDENTIFIER)) {
|
|
2532
|
+
if (this.match(TokenType.IDENTIFIER, TokenType.MATCHES, TokenType.NOT_MATCHES)) {
|
|
2201
2533
|
const identifier = this.previous().value;
|
|
2202
2534
|
// Check if this is a function call
|
|
2203
2535
|
if (this.match(TokenType.LEFT_PAREN)) {
|
|
@@ -2210,14 +2542,17 @@ class DslParser {
|
|
|
2210
2542
|
}
|
|
2211
2543
|
parseFunctionCall(functionName) {
|
|
2212
2544
|
const args = [];
|
|
2545
|
+
const argTokenTypes = [];
|
|
2213
2546
|
if (!this.check(TokenType.RIGHT_PAREN)) {
|
|
2214
2547
|
do {
|
|
2215
2548
|
args.push(this.parseArgument());
|
|
2549
|
+
argTokenTypes.push(this.lastArgumentTokenType ?? TokenType.EOF);
|
|
2216
2550
|
} while (this.match(TokenType.COMMA));
|
|
2217
2551
|
}
|
|
2218
2552
|
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after function arguments");
|
|
2219
2553
|
// Handle special built-in functions
|
|
2220
|
-
|
|
2554
|
+
const normalizedFunctionName = this.normalizeFunctionName(functionName);
|
|
2555
|
+
switch (normalizedFunctionName) {
|
|
2221
2556
|
case 'atLeast':
|
|
2222
2557
|
if (args.length !== 2) {
|
|
2223
2558
|
throw new Error('atLeast requires exactly 2 arguments');
|
|
@@ -2241,38 +2576,61 @@ class DslParser {
|
|
|
2241
2576
|
case 'contains':
|
|
2242
2577
|
case 'startsWith':
|
|
2243
2578
|
case 'endsWith':
|
|
2579
|
+
case 'matches':
|
|
2580
|
+
case 'notMatches':
|
|
2244
2581
|
if (args.length !== 2) {
|
|
2245
|
-
throw new Error(`${
|
|
2582
|
+
throw new Error(`${normalizedFunctionName} requires exactly 2 arguments`);
|
|
2246
2583
|
}
|
|
2247
2584
|
const field = args[0];
|
|
2248
2585
|
const value = args[1];
|
|
2249
|
-
const op = this.functionNameToOperator(
|
|
2586
|
+
const op = this.functionNameToOperator(normalizedFunctionName);
|
|
2250
2587
|
return new FieldSpecification(field, op, value);
|
|
2251
2588
|
default:
|
|
2252
|
-
|
|
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);
|
|
2253
2599
|
}
|
|
2254
2600
|
}
|
|
2255
2601
|
parseArgument() {
|
|
2602
|
+
this.lastArgumentTokenType = null;
|
|
2256
2603
|
if (this.match(TokenType.STRING)) {
|
|
2604
|
+
this.lastArgumentTokenType = TokenType.STRING;
|
|
2257
2605
|
return this.previous().value;
|
|
2258
2606
|
}
|
|
2259
2607
|
if (this.match(TokenType.NUMBER)) {
|
|
2260
2608
|
const value = this.previous().value;
|
|
2609
|
+
this.lastArgumentTokenType = TokenType.NUMBER;
|
|
2261
2610
|
return value.includes('.') ? parseFloat(value) : parseInt(value, 10);
|
|
2262
2611
|
}
|
|
2612
|
+
if (this.match(TokenType.REGEX)) {
|
|
2613
|
+
this.lastArgumentTokenType = TokenType.REGEX;
|
|
2614
|
+
return this.parseRegexLiteral(this.previous().value);
|
|
2615
|
+
}
|
|
2263
2616
|
if (this.match(TokenType.BOOLEAN)) {
|
|
2617
|
+
this.lastArgumentTokenType = TokenType.BOOLEAN;
|
|
2264
2618
|
return this.previous().value === 'true';
|
|
2265
2619
|
}
|
|
2266
2620
|
if (this.match(TokenType.NULL)) {
|
|
2621
|
+
this.lastArgumentTokenType = TokenType.NULL;
|
|
2267
2622
|
return null;
|
|
2268
2623
|
}
|
|
2269
2624
|
if (this.match(TokenType.FIELD_REFERENCE)) {
|
|
2625
|
+
this.lastArgumentTokenType = TokenType.FIELD_REFERENCE;
|
|
2270
2626
|
return this.previous().value;
|
|
2271
2627
|
}
|
|
2272
|
-
if (this.match(TokenType.IDENTIFIER)) {
|
|
2628
|
+
if (this.match(TokenType.IDENTIFIER, TokenType.MATCHES, TokenType.NOT_MATCHES)) {
|
|
2629
|
+
this.lastArgumentTokenType = this.previous().type;
|
|
2273
2630
|
return this.previous().value;
|
|
2274
2631
|
}
|
|
2275
2632
|
if (this.match(TokenType.LEFT_BRACKET)) {
|
|
2633
|
+
this.lastArgumentTokenType = TokenType.LEFT_BRACKET;
|
|
2276
2634
|
const elements = [];
|
|
2277
2635
|
if (!this.check(TokenType.RIGHT_BRACKET)) {
|
|
2278
2636
|
do {
|
|
@@ -2282,10 +2640,11 @@ class DslParser {
|
|
|
2282
2640
|
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after array elements");
|
|
2283
2641
|
return elements;
|
|
2284
2642
|
}
|
|
2643
|
+
this.lastArgumentTokenType = null;
|
|
2285
2644
|
throw new Error(`Unexpected token in argument: ${this.peek().value}`);
|
|
2286
2645
|
}
|
|
2287
2646
|
matchComparison() {
|
|
2288
|
-
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);
|
|
2289
2648
|
}
|
|
2290
2649
|
tokenTypeToComparisonOperator(tokenType) {
|
|
2291
2650
|
switch (tokenType) {
|
|
@@ -2303,22 +2662,50 @@ class DslParser {
|
|
|
2303
2662
|
return ComparisonOperator.GREATER_THAN_OR_EQUAL;
|
|
2304
2663
|
case TokenType.IN:
|
|
2305
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;
|
|
2306
2679
|
default:
|
|
2307
2680
|
throw new Error(`Unknown comparison operator: ${tokenType}`);
|
|
2308
2681
|
}
|
|
2309
2682
|
}
|
|
2310
2683
|
functionNameToOperator(functionName) {
|
|
2311
|
-
|
|
2684
|
+
const normalizedFunctionName = this.normalizeFunctionName(functionName);
|
|
2685
|
+
switch (normalizedFunctionName) {
|
|
2312
2686
|
case 'contains':
|
|
2313
2687
|
return ComparisonOperator.CONTAINS;
|
|
2314
2688
|
case 'startsWith':
|
|
2315
2689
|
return ComparisonOperator.STARTS_WITH;
|
|
2316
2690
|
case 'endsWith':
|
|
2317
2691
|
return ComparisonOperator.ENDS_WITH;
|
|
2692
|
+
case 'matches':
|
|
2693
|
+
return ComparisonOperator.MATCHES;
|
|
2694
|
+
case 'notMatches':
|
|
2695
|
+
return ComparisonOperator.NOT_MATCHES;
|
|
2318
2696
|
default:
|
|
2319
|
-
throw new Error(`Unknown function: ${
|
|
2697
|
+
throw new Error(`Unknown function: ${normalizedFunctionName}`);
|
|
2320
2698
|
}
|
|
2321
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
|
+
}
|
|
2322
2709
|
extractValue(spec) {
|
|
2323
2710
|
// This is a simplified extraction - in a real implementation
|
|
2324
2711
|
// we'd need to handle more complex cases
|
|
@@ -2327,6 +2714,34 @@ class DslParser {
|
|
|
2327
2714
|
}
|
|
2328
2715
|
return spec;
|
|
2329
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
|
+
}
|
|
2330
2745
|
match(...types) {
|
|
2331
2746
|
for (const type of types) {
|
|
2332
2747
|
if (this.check(type)) {
|
|
@@ -2666,7 +3081,8 @@ class DslValidator {
|
|
|
2666
3081
|
'contains', 'startsWith', 'endsWith', 'atLeast', 'exactly',
|
|
2667
3082
|
'forEach', 'uniqueBy', 'minLength', 'maxLength',
|
|
2668
3083
|
'requiredIf', 'visibleIf', 'disabledIf', 'readonlyIf',
|
|
2669
|
-
'ifDefined', 'ifNotNull', 'ifExists', 'withDefault'
|
|
3084
|
+
'ifDefined', 'ifNotNull', 'ifExists', 'withDefault',
|
|
3085
|
+
'matches', 'notMatches'
|
|
2670
3086
|
];
|
|
2671
3087
|
constructor(config = {}) {
|
|
2672
3088
|
this.config = {
|
|
@@ -2799,7 +3215,9 @@ class DslValidator {
|
|
|
2799
3215
|
}
|
|
2800
3216
|
}
|
|
2801
3217
|
validateTokens(tokens, input, issues) {
|
|
2802
|
-
for (
|
|
3218
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3219
|
+
const token = tokens[i];
|
|
3220
|
+
const nextToken = tokens[i + 1];
|
|
2803
3221
|
if (token.type === TokenType.EOF)
|
|
2804
3222
|
continue;
|
|
2805
3223
|
// Validate numbers
|
|
@@ -2821,6 +3239,11 @@ class DslValidator {
|
|
|
2821
3239
|
}
|
|
2822
3240
|
// Validate field references
|
|
2823
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;
|
|
2824
3247
|
if (this.config.knownFields.length > 0) {
|
|
2825
3248
|
if (!this.config.knownFields.includes(token.value)) {
|
|
2826
3249
|
issues.push({
|
|
@@ -2848,6 +3271,14 @@ class DslValidator {
|
|
|
2848
3271
|
const prevToken = tokens[i - 1];
|
|
2849
3272
|
// Check for consecutive operators
|
|
2850
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
|
+
}
|
|
2851
3282
|
issues.push({
|
|
2852
3283
|
type: ValidationIssueType.UNEXPECTED_TOKEN,
|
|
2853
3284
|
severity: ValidationSeverity.ERROR,
|
|
@@ -2863,6 +3294,9 @@ class DslValidator {
|
|
|
2863
3294
|
}
|
|
2864
3295
|
// Check for operators at the beginning/end
|
|
2865
3296
|
if (i === 0 && this.isBinaryOperatorToken(token)) {
|
|
3297
|
+
if (this.isFunctionToken(token, nextToken)) {
|
|
3298
|
+
continue;
|
|
3299
|
+
}
|
|
2866
3300
|
issues.push({
|
|
2867
3301
|
type: ValidationIssueType.UNEXPECTED_TOKEN,
|
|
2868
3302
|
severity: ValidationSeverity.ERROR,
|
|
@@ -2897,8 +3331,8 @@ class DslValidator {
|
|
|
2897
3331
|
for (let i = 0; i < tokens.length; i++) {
|
|
2898
3332
|
const token = tokens[i];
|
|
2899
3333
|
const nextToken = tokens[i + 1];
|
|
2900
|
-
if (token
|
|
2901
|
-
const functionName = token
|
|
3334
|
+
if (this.isFunctionToken(token, nextToken)) {
|
|
3335
|
+
const functionName = this.normalizeFunctionTokenName(token);
|
|
2902
3336
|
// Check if function is known
|
|
2903
3337
|
const allKnownFunctions = [
|
|
2904
3338
|
...this.BUILT_IN_FUNCTIONS,
|
|
@@ -3010,16 +3444,35 @@ class DslValidator {
|
|
|
3010
3444
|
'ifDefined': 2,
|
|
3011
3445
|
'ifNotNull': 2,
|
|
3012
3446
|
'ifExists': 2,
|
|
3013
|
-
'withDefault': 3
|
|
3447
|
+
'withDefault': 3,
|
|
3448
|
+
'matches': 2,
|
|
3449
|
+
'notMatches': 2
|
|
3014
3450
|
};
|
|
3015
3451
|
return argCounts[functionName] ?? null;
|
|
3016
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
|
+
}
|
|
3017
3469
|
isOperatorToken(token) {
|
|
3018
3470
|
return [
|
|
3019
3471
|
TokenType.AND, TokenType.OR, TokenType.NOT, TokenType.XOR, TokenType.IMPLIES,
|
|
3020
3472
|
TokenType.EQUALS, TokenType.NOT_EQUALS, TokenType.LESS_THAN,
|
|
3021
3473
|
TokenType.LESS_THAN_OR_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_THAN_OR_EQUAL,
|
|
3022
|
-
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
|
|
3023
3476
|
].includes(token.type);
|
|
3024
3477
|
}
|
|
3025
3478
|
isBinaryOperatorToken(token) {
|
|
@@ -3027,7 +3480,7 @@ class DslValidator {
|
|
|
3027
3480
|
TokenType.AND, TokenType.OR, TokenType.XOR, TokenType.IMPLIES,
|
|
3028
3481
|
TokenType.EQUALS, TokenType.NOT_EQUALS, TokenType.LESS_THAN,
|
|
3029
3482
|
TokenType.LESS_THAN_OR_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_THAN_OR_EQUAL,
|
|
3030
|
-
TokenType.IN
|
|
3483
|
+
TokenType.IN, TokenType.NOT_IN, TokenType.MATCHES, TokenType.NOT_MATCHES
|
|
3031
3484
|
].includes(token.type);
|
|
3032
3485
|
}
|
|
3033
3486
|
suggestSimilarField(fieldName) {
|
|
@@ -3222,8 +3675,8 @@ class SpecificationFactory {
|
|
|
3222
3675
|
static exactly(exact, specs) {
|
|
3223
3676
|
return new ExactlySpecification(exact, specs);
|
|
3224
3677
|
}
|
|
3225
|
-
static fieldToField(fieldA, operator, fieldB, transformA, transformB, registry) {
|
|
3226
|
-
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);
|
|
3227
3680
|
}
|
|
3228
3681
|
static contextual(template, provider) {
|
|
3229
3682
|
return new ContextualSpecification(template, provider);
|