@praxisui/specification 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +85 -0
- package/fesm2022/praxisui-specification.mjs +3495 -0
- package/fesm2022/praxisui-specification.mjs.map +1 -0
- package/index.d.ts +894 -0
- package/package.json +36 -0
|
@@ -0,0 +1,3495 @@
|
|
|
1
|
+
import { Specification } from '@praxisui/specification-core';
|
|
2
|
+
export * from '@praxisui/specification-core';
|
|
3
|
+
|
|
4
|
+
var ComparisonOperator;
|
|
5
|
+
(function (ComparisonOperator) {
|
|
6
|
+
ComparisonOperator["EQUALS"] = "eq";
|
|
7
|
+
ComparisonOperator["NOT_EQUALS"] = "neq";
|
|
8
|
+
ComparisonOperator["LESS_THAN"] = "lt";
|
|
9
|
+
ComparisonOperator["LESS_THAN_OR_EQUAL"] = "lte";
|
|
10
|
+
ComparisonOperator["GREATER_THAN"] = "gt";
|
|
11
|
+
ComparisonOperator["GREATER_THAN_OR_EQUAL"] = "gte";
|
|
12
|
+
ComparisonOperator["CONTAINS"] = "contains";
|
|
13
|
+
ComparisonOperator["STARTS_WITH"] = "startsWith";
|
|
14
|
+
ComparisonOperator["ENDS_WITH"] = "endsWith";
|
|
15
|
+
ComparisonOperator["IN"] = "in";
|
|
16
|
+
})(ComparisonOperator || (ComparisonOperator = {}));
|
|
17
|
+
const OPERATOR_SYMBOLS = {
|
|
18
|
+
[ComparisonOperator.EQUALS]: '==',
|
|
19
|
+
[ComparisonOperator.NOT_EQUALS]: '!=',
|
|
20
|
+
[ComparisonOperator.LESS_THAN]: '<',
|
|
21
|
+
[ComparisonOperator.LESS_THAN_OR_EQUAL]: '<=',
|
|
22
|
+
[ComparisonOperator.GREATER_THAN]: '>',
|
|
23
|
+
[ComparisonOperator.GREATER_THAN_OR_EQUAL]: '>=',
|
|
24
|
+
[ComparisonOperator.CONTAINS]: 'contains',
|
|
25
|
+
[ComparisonOperator.STARTS_WITH]: 'startsWith',
|
|
26
|
+
[ComparisonOperator.ENDS_WITH]: 'endsWith',
|
|
27
|
+
[ComparisonOperator.IN]: 'in'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
class FieldSpecification extends Specification {
|
|
31
|
+
field;
|
|
32
|
+
operator;
|
|
33
|
+
value;
|
|
34
|
+
constructor(field, operator, value, metadata) {
|
|
35
|
+
super(metadata);
|
|
36
|
+
this.field = field;
|
|
37
|
+
this.operator = operator;
|
|
38
|
+
this.value = value;
|
|
39
|
+
}
|
|
40
|
+
isSatisfiedBy(obj) {
|
|
41
|
+
const fieldValue = obj[this.field];
|
|
42
|
+
switch (this.operator) {
|
|
43
|
+
case ComparisonOperator.EQUALS:
|
|
44
|
+
return fieldValue === this.value;
|
|
45
|
+
case ComparisonOperator.NOT_EQUALS:
|
|
46
|
+
return fieldValue !== this.value;
|
|
47
|
+
case ComparisonOperator.LESS_THAN:
|
|
48
|
+
return fieldValue < this.value;
|
|
49
|
+
case ComparisonOperator.LESS_THAN_OR_EQUAL:
|
|
50
|
+
return fieldValue <= this.value;
|
|
51
|
+
case ComparisonOperator.GREATER_THAN:
|
|
52
|
+
return fieldValue > this.value;
|
|
53
|
+
case ComparisonOperator.GREATER_THAN_OR_EQUAL:
|
|
54
|
+
return fieldValue >= this.value;
|
|
55
|
+
case ComparisonOperator.CONTAINS:
|
|
56
|
+
return String(fieldValue).includes(String(this.value));
|
|
57
|
+
case ComparisonOperator.STARTS_WITH:
|
|
58
|
+
return String(fieldValue).startsWith(String(this.value));
|
|
59
|
+
case ComparisonOperator.ENDS_WITH:
|
|
60
|
+
return String(fieldValue).endsWith(String(this.value));
|
|
61
|
+
case ComparisonOperator.IN:
|
|
62
|
+
return Array.isArray(this.value) && this.value.includes(fieldValue);
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unsupported operator: ${this.operator}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
toJSON() {
|
|
68
|
+
return {
|
|
69
|
+
type: 'field',
|
|
70
|
+
field: String(this.field),
|
|
71
|
+
operator: this.operator,
|
|
72
|
+
value: this.value,
|
|
73
|
+
metadata: this.metadata,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
static fromJSON(json) {
|
|
77
|
+
return new FieldSpecification(json.field, json.operator, json.value, json.metadata);
|
|
78
|
+
}
|
|
79
|
+
clone() {
|
|
80
|
+
return new FieldSpecification(this.field, this.operator, this.value, this.metadata);
|
|
81
|
+
}
|
|
82
|
+
toDSL() {
|
|
83
|
+
const fieldName = String(this.field);
|
|
84
|
+
const symbol = OPERATOR_SYMBOLS[this.operator];
|
|
85
|
+
switch (this.operator) {
|
|
86
|
+
case ComparisonOperator.CONTAINS:
|
|
87
|
+
return `contains(${fieldName}, ${JSON.stringify(this.value)})`;
|
|
88
|
+
case ComparisonOperator.STARTS_WITH:
|
|
89
|
+
return `startsWith(${fieldName}, ${JSON.stringify(this.value)})`;
|
|
90
|
+
case ComparisonOperator.ENDS_WITH:
|
|
91
|
+
return `endsWith(${fieldName}, ${JSON.stringify(this.value)})`;
|
|
92
|
+
case ComparisonOperator.IN:
|
|
93
|
+
const values = Array.isArray(this.value)
|
|
94
|
+
? this.value.map((v) => JSON.stringify(v)).join(', ')
|
|
95
|
+
: JSON.stringify(this.value);
|
|
96
|
+
return `${fieldName} in (${values})`;
|
|
97
|
+
default:
|
|
98
|
+
return `${fieldName} ${symbol} ${JSON.stringify(this.value)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
getField() {
|
|
102
|
+
return this.field;
|
|
103
|
+
}
|
|
104
|
+
getOperator() {
|
|
105
|
+
return this.operator;
|
|
106
|
+
}
|
|
107
|
+
getValue() {
|
|
108
|
+
return this.value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class AndSpecification extends Specification {
|
|
113
|
+
specifications;
|
|
114
|
+
constructor(specifications, metadata) {
|
|
115
|
+
super(metadata);
|
|
116
|
+
this.specifications = specifications;
|
|
117
|
+
if (specifications.length === 0) {
|
|
118
|
+
throw new Error('AndSpecification requires at least one specification');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
isSatisfiedBy(obj) {
|
|
122
|
+
return this.specifications.every((spec) => spec.isSatisfiedBy(obj));
|
|
123
|
+
}
|
|
124
|
+
toJSON() {
|
|
125
|
+
return {
|
|
126
|
+
type: 'and',
|
|
127
|
+
specs: this.specifications.map((spec) => spec.toJSON()),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
static fromJSON(json) {
|
|
131
|
+
const specs = json.specs.map((specJson) => Specification.fromJSON(specJson));
|
|
132
|
+
return new AndSpecification(specs);
|
|
133
|
+
}
|
|
134
|
+
toDSL() {
|
|
135
|
+
if (this.specifications.length === 1) {
|
|
136
|
+
return this.specifications[0].toDSL();
|
|
137
|
+
}
|
|
138
|
+
const parts = this.specifications.map((spec) => {
|
|
139
|
+
const dsl = spec.toDSL();
|
|
140
|
+
// Add parentheses if needed for precedence
|
|
141
|
+
const kind = spec.getOperatorKind?.();
|
|
142
|
+
if (kind === 'or' || kind === 'xor') {
|
|
143
|
+
return `(${dsl})`;
|
|
144
|
+
}
|
|
145
|
+
return dsl;
|
|
146
|
+
});
|
|
147
|
+
return parts.join(' && ');
|
|
148
|
+
}
|
|
149
|
+
getSpecifications() {
|
|
150
|
+
return [...this.specifications];
|
|
151
|
+
}
|
|
152
|
+
add(specification) {
|
|
153
|
+
return new AndSpecification([...this.specifications, specification], this.metadata);
|
|
154
|
+
}
|
|
155
|
+
clone() {
|
|
156
|
+
return new AndSpecification(this.specifications.map((spec) => spec.clone()), this.metadata);
|
|
157
|
+
}
|
|
158
|
+
getOperatorKind() {
|
|
159
|
+
return 'and';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class OrSpecification extends Specification {
|
|
164
|
+
specifications;
|
|
165
|
+
constructor(specifications, metadata) {
|
|
166
|
+
super(metadata);
|
|
167
|
+
this.specifications = specifications;
|
|
168
|
+
if (specifications.length === 0) {
|
|
169
|
+
throw new Error('OrSpecification requires at least one specification');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
isSatisfiedBy(obj) {
|
|
173
|
+
return this.specifications.some((spec) => spec.isSatisfiedBy(obj));
|
|
174
|
+
}
|
|
175
|
+
toJSON() {
|
|
176
|
+
return {
|
|
177
|
+
type: 'or',
|
|
178
|
+
specs: this.specifications.map((spec) => spec.toJSON()),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
static fromJSON(json) {
|
|
182
|
+
const specs = json.specs.map((specJson) => Specification.fromJSON(specJson));
|
|
183
|
+
return new OrSpecification(specs);
|
|
184
|
+
}
|
|
185
|
+
toDSL() {
|
|
186
|
+
if (this.specifications.length === 1) {
|
|
187
|
+
return this.specifications[0].toDSL();
|
|
188
|
+
}
|
|
189
|
+
const parts = this.specifications.map((spec) => {
|
|
190
|
+
const dsl = spec.toDSL();
|
|
191
|
+
// Add parentheses if needed for precedence
|
|
192
|
+
const kind = spec.getOperatorKind?.();
|
|
193
|
+
if (kind === 'and') {
|
|
194
|
+
return `(${dsl})`;
|
|
195
|
+
}
|
|
196
|
+
return dsl;
|
|
197
|
+
});
|
|
198
|
+
return parts.join(' || ');
|
|
199
|
+
}
|
|
200
|
+
getSpecifications() {
|
|
201
|
+
return [...this.specifications];
|
|
202
|
+
}
|
|
203
|
+
add(specification) {
|
|
204
|
+
return new OrSpecification([...this.specifications, specification], this.metadata);
|
|
205
|
+
}
|
|
206
|
+
clone() {
|
|
207
|
+
return new OrSpecification(this.specifications.map((spec) => spec.clone()), this.metadata);
|
|
208
|
+
}
|
|
209
|
+
getOperatorKind() {
|
|
210
|
+
return 'or';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class XorSpecification extends Specification {
|
|
215
|
+
specifications;
|
|
216
|
+
constructor(specifications, metadata) {
|
|
217
|
+
super(metadata);
|
|
218
|
+
this.specifications = specifications;
|
|
219
|
+
if (specifications.length < 2) {
|
|
220
|
+
throw new Error('XorSpecification requires at least two specifications');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
isSatisfiedBy(obj) {
|
|
224
|
+
const satisfiedCount = this.specifications.filter((spec) => spec.isSatisfiedBy(obj)).length;
|
|
225
|
+
return satisfiedCount === 1;
|
|
226
|
+
}
|
|
227
|
+
toJSON() {
|
|
228
|
+
return {
|
|
229
|
+
type: 'xor',
|
|
230
|
+
specs: this.specifications.map((spec) => spec.toJSON()),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
static fromJSON(json) {
|
|
234
|
+
const specs = json.specs.map((specJson) => Specification.fromJSON(specJson));
|
|
235
|
+
return new XorSpecification(specs);
|
|
236
|
+
}
|
|
237
|
+
toDSL() {
|
|
238
|
+
const parts = this.specifications.map((spec) => {
|
|
239
|
+
const dsl = spec.toDSL();
|
|
240
|
+
// Add parentheses if needed for precedence
|
|
241
|
+
const kind = spec.getOperatorKind?.();
|
|
242
|
+
if (kind === 'and' || kind === 'or') {
|
|
243
|
+
return `(${dsl})`;
|
|
244
|
+
}
|
|
245
|
+
return dsl;
|
|
246
|
+
});
|
|
247
|
+
return parts.join(' xor ');
|
|
248
|
+
}
|
|
249
|
+
getSpecifications() {
|
|
250
|
+
return [...this.specifications];
|
|
251
|
+
}
|
|
252
|
+
clone() {
|
|
253
|
+
return new XorSpecification(this.specifications.map((spec) => spec.clone()), this.metadata);
|
|
254
|
+
}
|
|
255
|
+
getOperatorKind() {
|
|
256
|
+
return 'xor';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
class ImpliesSpecification extends Specification {
|
|
261
|
+
antecedent;
|
|
262
|
+
consequent;
|
|
263
|
+
constructor(antecedent, consequent, metadata) {
|
|
264
|
+
super(metadata);
|
|
265
|
+
this.antecedent = antecedent;
|
|
266
|
+
this.consequent = consequent;
|
|
267
|
+
}
|
|
268
|
+
isSatisfiedBy(obj) {
|
|
269
|
+
// A implies B is equivalent to (!A || B)
|
|
270
|
+
return (!this.antecedent.isSatisfiedBy(obj) || this.consequent.isSatisfiedBy(obj));
|
|
271
|
+
}
|
|
272
|
+
toJSON() {
|
|
273
|
+
return {
|
|
274
|
+
type: 'implies',
|
|
275
|
+
antecedent: this.antecedent.toJSON(),
|
|
276
|
+
consequent: this.consequent.toJSON(),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
static fromJSON(json) {
|
|
280
|
+
const antecedent = Specification.fromJSON(json.antecedent);
|
|
281
|
+
const consequent = Specification.fromJSON(json.consequent);
|
|
282
|
+
return new ImpliesSpecification(antecedent, consequent);
|
|
283
|
+
}
|
|
284
|
+
toDSL() {
|
|
285
|
+
const antecedentDsl = this.antecedent.toDSL();
|
|
286
|
+
const consequentDsl = this.consequent.toDSL();
|
|
287
|
+
// Add parentheses for complex expressions
|
|
288
|
+
const leftPart = this.antecedent instanceof AndSpecification ||
|
|
289
|
+
this.antecedent instanceof OrSpecification ||
|
|
290
|
+
this.antecedent instanceof XorSpecification
|
|
291
|
+
? `(${antecedentDsl})`
|
|
292
|
+
: antecedentDsl;
|
|
293
|
+
const rightPart = this.consequent instanceof AndSpecification ||
|
|
294
|
+
this.consequent instanceof OrSpecification ||
|
|
295
|
+
this.consequent instanceof XorSpecification
|
|
296
|
+
? `(${consequentDsl})`
|
|
297
|
+
: consequentDsl;
|
|
298
|
+
return `${leftPart} implies ${rightPart}`;
|
|
299
|
+
}
|
|
300
|
+
getAntecedent() {
|
|
301
|
+
return this.antecedent;
|
|
302
|
+
}
|
|
303
|
+
getConsequent() {
|
|
304
|
+
return this.consequent;
|
|
305
|
+
}
|
|
306
|
+
clone() {
|
|
307
|
+
return new ImpliesSpecification(this.antecedent.clone(), this.consequent.clone(), this.metadata);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
class NotSpecification extends Specification {
|
|
312
|
+
specification;
|
|
313
|
+
constructor(specification, metadata) {
|
|
314
|
+
super(metadata);
|
|
315
|
+
this.specification = specification;
|
|
316
|
+
}
|
|
317
|
+
isSatisfiedBy(obj) {
|
|
318
|
+
return !this.specification.isSatisfiedBy(obj);
|
|
319
|
+
}
|
|
320
|
+
toJSON() {
|
|
321
|
+
return {
|
|
322
|
+
type: 'not',
|
|
323
|
+
spec: this.specification.toJSON(),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
static fromJSON(json) {
|
|
327
|
+
const spec = Specification.fromJSON(json.spec);
|
|
328
|
+
return new NotSpecification(spec);
|
|
329
|
+
}
|
|
330
|
+
toDSL() {
|
|
331
|
+
const innerDsl = this.specification.toDSL();
|
|
332
|
+
// Add parentheses for complex expressions
|
|
333
|
+
if (this.specification instanceof AndSpecification ||
|
|
334
|
+
this.specification instanceof OrSpecification ||
|
|
335
|
+
this.specification instanceof XorSpecification ||
|
|
336
|
+
this.specification instanceof ImpliesSpecification) {
|
|
337
|
+
return `!(${innerDsl})`;
|
|
338
|
+
}
|
|
339
|
+
return `!${innerDsl}`;
|
|
340
|
+
}
|
|
341
|
+
getSpecification() {
|
|
342
|
+
return this.specification;
|
|
343
|
+
}
|
|
344
|
+
clone() {
|
|
345
|
+
return new NotSpecification(this.specification.clone(), this.metadata);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
class FunctionRegistry {
|
|
350
|
+
contextKey;
|
|
351
|
+
static instances = new Map();
|
|
352
|
+
functions = new Map();
|
|
353
|
+
constructor(contextKey) {
|
|
354
|
+
this.contextKey = contextKey;
|
|
355
|
+
}
|
|
356
|
+
static getInstance(contextKey = 'default') {
|
|
357
|
+
if (!this.instances.has(contextKey)) {
|
|
358
|
+
this.instances.set(contextKey, new FunctionRegistry(contextKey));
|
|
359
|
+
}
|
|
360
|
+
return this.instances.get(contextKey);
|
|
361
|
+
}
|
|
362
|
+
register(name, fn) {
|
|
363
|
+
this.functions.set(name, fn);
|
|
364
|
+
}
|
|
365
|
+
unregister(name) {
|
|
366
|
+
return this.functions.delete(name);
|
|
367
|
+
}
|
|
368
|
+
get(name) {
|
|
369
|
+
return this.functions.get(name);
|
|
370
|
+
}
|
|
371
|
+
has(name) {
|
|
372
|
+
return this.functions.has(name);
|
|
373
|
+
}
|
|
374
|
+
getAll() {
|
|
375
|
+
return new Map(this.functions);
|
|
376
|
+
}
|
|
377
|
+
clear() {
|
|
378
|
+
this.functions.clear();
|
|
379
|
+
}
|
|
380
|
+
execute(name, obj, ...args) {
|
|
381
|
+
const fn = this.functions.get(name);
|
|
382
|
+
if (!fn) {
|
|
383
|
+
throw new Error(`Function '${name}' not found in registry`);
|
|
384
|
+
}
|
|
385
|
+
return fn(obj, ...args);
|
|
386
|
+
}
|
|
387
|
+
getContextKey() {
|
|
388
|
+
return this.contextKey;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
class FunctionSpecification extends Specification {
|
|
393
|
+
functionName;
|
|
394
|
+
args;
|
|
395
|
+
registry;
|
|
396
|
+
constructor(functionName, args, registry, metadata) {
|
|
397
|
+
super(metadata);
|
|
398
|
+
this.functionName = functionName;
|
|
399
|
+
this.args = args;
|
|
400
|
+
this.registry = registry;
|
|
401
|
+
}
|
|
402
|
+
isSatisfiedBy(obj) {
|
|
403
|
+
const registry = this.registry || FunctionRegistry.getInstance();
|
|
404
|
+
return registry.execute(this.functionName, obj, ...this.args);
|
|
405
|
+
}
|
|
406
|
+
toJSON() {
|
|
407
|
+
return {
|
|
408
|
+
type: 'function',
|
|
409
|
+
name: this.functionName,
|
|
410
|
+
args: this.args,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
static fromJSON(json, registry) {
|
|
414
|
+
return new FunctionSpecification(json.name, json.args, registry);
|
|
415
|
+
}
|
|
416
|
+
toDSL() {
|
|
417
|
+
const argsStr = this.args
|
|
418
|
+
.map((arg) => {
|
|
419
|
+
if (typeof arg === 'string') {
|
|
420
|
+
return JSON.stringify(arg);
|
|
421
|
+
}
|
|
422
|
+
if (typeof arg === 'object' && arg !== null && arg.type === 'field') {
|
|
423
|
+
return `${arg.field}`;
|
|
424
|
+
}
|
|
425
|
+
return JSON.stringify(arg);
|
|
426
|
+
})
|
|
427
|
+
.join(', ');
|
|
428
|
+
return `${this.functionName}(${argsStr})`;
|
|
429
|
+
}
|
|
430
|
+
getFunctionName() {
|
|
431
|
+
return this.functionName;
|
|
432
|
+
}
|
|
433
|
+
getArgs() {
|
|
434
|
+
return [...this.args];
|
|
435
|
+
}
|
|
436
|
+
getRegistry() {
|
|
437
|
+
return this.registry;
|
|
438
|
+
}
|
|
439
|
+
clone() {
|
|
440
|
+
return new FunctionSpecification(this.functionName, [...this.args], this.registry, this.metadata);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
class AtLeastSpecification extends Specification {
|
|
445
|
+
minimum;
|
|
446
|
+
specifications;
|
|
447
|
+
constructor(minimum, specifications, metadata) {
|
|
448
|
+
super(metadata);
|
|
449
|
+
this.minimum = minimum;
|
|
450
|
+
this.specifications = specifications;
|
|
451
|
+
if (minimum < 0) {
|
|
452
|
+
throw new Error('Minimum count must be non-negative');
|
|
453
|
+
}
|
|
454
|
+
if (specifications.length === 0) {
|
|
455
|
+
throw new Error('AtLeastSpecification requires at least one specification');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
isSatisfiedBy(obj) {
|
|
459
|
+
const satisfiedCount = this.specifications.filter((spec) => spec.isSatisfiedBy(obj)).length;
|
|
460
|
+
return satisfiedCount >= this.minimum;
|
|
461
|
+
}
|
|
462
|
+
toJSON() {
|
|
463
|
+
return {
|
|
464
|
+
type: 'atLeast',
|
|
465
|
+
minimum: this.minimum,
|
|
466
|
+
specs: this.specifications.map((spec) => spec.toJSON()),
|
|
467
|
+
metadata: this.metadata,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
static fromJSON(json) {
|
|
471
|
+
const specs = json.specs.map((specJson) => Specification.fromJSON(specJson));
|
|
472
|
+
return new AtLeastSpecification(json.minimum, specs, json.metadata);
|
|
473
|
+
}
|
|
474
|
+
toDSL() {
|
|
475
|
+
const specDsls = this.specifications.map((spec) => spec.toDSL());
|
|
476
|
+
const specsStr = specDsls.join(', ');
|
|
477
|
+
return `atLeast(${this.minimum}, [${specsStr}])`;
|
|
478
|
+
}
|
|
479
|
+
getMinimum() {
|
|
480
|
+
return this.minimum;
|
|
481
|
+
}
|
|
482
|
+
getSpecifications() {
|
|
483
|
+
return [...this.specifications];
|
|
484
|
+
}
|
|
485
|
+
getSatisfiedCount(obj) {
|
|
486
|
+
return this.specifications.filter((spec) => spec.isSatisfiedBy(obj)).length;
|
|
487
|
+
}
|
|
488
|
+
clone() {
|
|
489
|
+
const clonedSpecs = this.specifications.map((spec) => spec.clone());
|
|
490
|
+
return new AtLeastSpecification(this.minimum, clonedSpecs, this.metadata);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
class ExactlySpecification extends Specification {
|
|
495
|
+
exact;
|
|
496
|
+
specifications;
|
|
497
|
+
constructor(exact, specifications, metadata) {
|
|
498
|
+
super(metadata);
|
|
499
|
+
this.exact = exact;
|
|
500
|
+
this.specifications = specifications;
|
|
501
|
+
if (exact < 0) {
|
|
502
|
+
throw new Error('Exact count must be non-negative');
|
|
503
|
+
}
|
|
504
|
+
if (specifications.length === 0) {
|
|
505
|
+
throw new Error('ExactlySpecification requires at least one specification');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
isSatisfiedBy(obj) {
|
|
509
|
+
const satisfiedCount = this.specifications.filter((spec) => spec.isSatisfiedBy(obj)).length;
|
|
510
|
+
return satisfiedCount === this.exact;
|
|
511
|
+
}
|
|
512
|
+
toJSON() {
|
|
513
|
+
return {
|
|
514
|
+
type: 'exactly',
|
|
515
|
+
exact: this.exact,
|
|
516
|
+
specs: this.specifications.map((spec) => spec.toJSON()),
|
|
517
|
+
metadata: this.metadata,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
static fromJSON(json) {
|
|
521
|
+
const specs = json.specs.map((specJson) => Specification.fromJSON(specJson));
|
|
522
|
+
return new ExactlySpecification(json.exact, specs, json.metadata);
|
|
523
|
+
}
|
|
524
|
+
toDSL() {
|
|
525
|
+
const specDsls = this.specifications.map((spec) => spec.toDSL());
|
|
526
|
+
const specsStr = specDsls.join(', ');
|
|
527
|
+
return `exactly(${this.exact}, [${specsStr}])`;
|
|
528
|
+
}
|
|
529
|
+
getExact() {
|
|
530
|
+
return this.exact;
|
|
531
|
+
}
|
|
532
|
+
getSpecifications() {
|
|
533
|
+
return [...this.specifications];
|
|
534
|
+
}
|
|
535
|
+
getSatisfiedCount(obj) {
|
|
536
|
+
return this.specifications.filter((spec) => spec.isSatisfiedBy(obj)).length;
|
|
537
|
+
}
|
|
538
|
+
clone() {
|
|
539
|
+
const clonedSpecs = this.specifications.map((spec) => spec.clone());
|
|
540
|
+
return new ExactlySpecification(this.exact, clonedSpecs, this.metadata);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
class TransformRegistry {
|
|
545
|
+
static instance;
|
|
546
|
+
transforms = new Map();
|
|
547
|
+
constructor() {
|
|
548
|
+
// Register default transforms
|
|
549
|
+
this.registerDefaults();
|
|
550
|
+
}
|
|
551
|
+
static getInstance() {
|
|
552
|
+
if (!this.instance) {
|
|
553
|
+
this.instance = new TransformRegistry();
|
|
554
|
+
}
|
|
555
|
+
return this.instance;
|
|
556
|
+
}
|
|
557
|
+
registerDefaults() {
|
|
558
|
+
this.register('toLowerCase', (value) => String(value).toLowerCase());
|
|
559
|
+
this.register('toUpperCase', (value) => String(value).toUpperCase());
|
|
560
|
+
this.register('trim', (value) => String(value).trim());
|
|
561
|
+
this.register('toString', (value) => String(value));
|
|
562
|
+
this.register('toNumber', (value) => Number(value));
|
|
563
|
+
this.register('toBoolean', (value) => Boolean(value));
|
|
564
|
+
this.register('length', (value) => {
|
|
565
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
|
566
|
+
return value.length;
|
|
567
|
+
}
|
|
568
|
+
return 0;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
register(name, fn) {
|
|
572
|
+
this.transforms.set(name, fn);
|
|
573
|
+
}
|
|
574
|
+
unregister(name) {
|
|
575
|
+
return this.transforms.delete(name);
|
|
576
|
+
}
|
|
577
|
+
get(name) {
|
|
578
|
+
return this.transforms.get(name);
|
|
579
|
+
}
|
|
580
|
+
has(name) {
|
|
581
|
+
return this.transforms.has(name);
|
|
582
|
+
}
|
|
583
|
+
getAll() {
|
|
584
|
+
return new Map(this.transforms);
|
|
585
|
+
}
|
|
586
|
+
clear() {
|
|
587
|
+
this.transforms.clear();
|
|
588
|
+
this.registerDefaults();
|
|
589
|
+
}
|
|
590
|
+
apply(name, value) {
|
|
591
|
+
const transform = this.transforms.get(name);
|
|
592
|
+
if (!transform) {
|
|
593
|
+
throw new Error(`Transform '${name}' not found in registry`);
|
|
594
|
+
}
|
|
595
|
+
return transform(value);
|
|
596
|
+
}
|
|
597
|
+
applyChain(transforms, value) {
|
|
598
|
+
return transforms.reduce((acc, transformName) => {
|
|
599
|
+
return this.apply(transformName, acc);
|
|
600
|
+
}, value);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
class FieldToFieldSpecification extends Specification {
|
|
605
|
+
fieldA;
|
|
606
|
+
operator;
|
|
607
|
+
fieldB;
|
|
608
|
+
transformA;
|
|
609
|
+
transformB;
|
|
610
|
+
transformRegistry;
|
|
611
|
+
constructor(fieldA, operator, fieldB, transformA, transformB, transformRegistry, metadata) {
|
|
612
|
+
super(metadata);
|
|
613
|
+
this.fieldA = fieldA;
|
|
614
|
+
this.operator = operator;
|
|
615
|
+
this.fieldB = fieldB;
|
|
616
|
+
this.transformA = transformA;
|
|
617
|
+
this.transformB = transformB;
|
|
618
|
+
this.transformRegistry = transformRegistry;
|
|
619
|
+
}
|
|
620
|
+
isSatisfiedBy(obj) {
|
|
621
|
+
let valueA = obj[this.fieldA];
|
|
622
|
+
let valueB = obj[this.fieldB];
|
|
623
|
+
const registry = this.transformRegistry || TransformRegistry.getInstance();
|
|
624
|
+
// Apply transforms if specified
|
|
625
|
+
if (this.transformA && this.transformA.length > 0) {
|
|
626
|
+
valueA = registry.applyChain(this.transformA, valueA);
|
|
627
|
+
}
|
|
628
|
+
if (this.transformB && this.transformB.length > 0) {
|
|
629
|
+
valueB = registry.applyChain(this.transformB, valueB);
|
|
630
|
+
}
|
|
631
|
+
switch (this.operator) {
|
|
632
|
+
case ComparisonOperator.EQUALS:
|
|
633
|
+
return valueA === valueB;
|
|
634
|
+
case ComparisonOperator.NOT_EQUALS:
|
|
635
|
+
return valueA !== valueB;
|
|
636
|
+
case ComparisonOperator.LESS_THAN:
|
|
637
|
+
return valueA < valueB;
|
|
638
|
+
case ComparisonOperator.LESS_THAN_OR_EQUAL:
|
|
639
|
+
return valueA <= valueB;
|
|
640
|
+
case ComparisonOperator.GREATER_THAN:
|
|
641
|
+
return valueA > valueB;
|
|
642
|
+
case ComparisonOperator.GREATER_THAN_OR_EQUAL:
|
|
643
|
+
return valueA >= valueB;
|
|
644
|
+
case ComparisonOperator.CONTAINS:
|
|
645
|
+
return String(valueA).includes(String(valueB));
|
|
646
|
+
case ComparisonOperator.STARTS_WITH:
|
|
647
|
+
return String(valueA).startsWith(String(valueB));
|
|
648
|
+
case ComparisonOperator.ENDS_WITH:
|
|
649
|
+
return String(valueA).endsWith(String(valueB));
|
|
650
|
+
case ComparisonOperator.IN:
|
|
651
|
+
return Array.isArray(valueB) && valueB.includes(valueA);
|
|
652
|
+
default:
|
|
653
|
+
throw new Error(`Unsupported operator: ${this.operator}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
toJSON() {
|
|
657
|
+
return {
|
|
658
|
+
type: 'fieldToField',
|
|
659
|
+
fieldA: String(this.fieldA),
|
|
660
|
+
operator: this.operator,
|
|
661
|
+
fieldB: String(this.fieldB),
|
|
662
|
+
transformA: this.transformA,
|
|
663
|
+
transformB: this.transformB,
|
|
664
|
+
metadata: this.metadata,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
static fromJSON(json, transformRegistry) {
|
|
668
|
+
return new FieldToFieldSpecification(json.fieldA, json.operator, json.fieldB, json.transformA, json.transformB, transformRegistry, json.metadata);
|
|
669
|
+
}
|
|
670
|
+
toDSL() {
|
|
671
|
+
const fieldAName = String(this.fieldA);
|
|
672
|
+
const fieldBName = String(this.fieldB);
|
|
673
|
+
let leftField = fieldAName;
|
|
674
|
+
let rightField = fieldBName;
|
|
675
|
+
// Apply transform notation
|
|
676
|
+
if (this.transformA && this.transformA.length > 0) {
|
|
677
|
+
leftField = `${fieldAName}.${this.transformA.join('.')}`;
|
|
678
|
+
}
|
|
679
|
+
if (this.transformB && this.transformB.length > 0) {
|
|
680
|
+
rightField = `${fieldBName}.${this.transformB.join('.')}`;
|
|
681
|
+
}
|
|
682
|
+
const symbol = OPERATOR_SYMBOLS[this.operator];
|
|
683
|
+
switch (this.operator) {
|
|
684
|
+
case ComparisonOperator.CONTAINS:
|
|
685
|
+
return `contains(${leftField}, ${rightField})`;
|
|
686
|
+
case ComparisonOperator.STARTS_WITH:
|
|
687
|
+
return `startsWith(${leftField}, ${rightField})`;
|
|
688
|
+
case ComparisonOperator.ENDS_WITH:
|
|
689
|
+
return `endsWith(${leftField}, ${rightField})`;
|
|
690
|
+
case ComparisonOperator.IN:
|
|
691
|
+
return `${leftField} in ${rightField}`;
|
|
692
|
+
default:
|
|
693
|
+
return `${leftField} ${symbol} ${rightField}`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
getFieldA() {
|
|
697
|
+
return this.fieldA;
|
|
698
|
+
}
|
|
699
|
+
getFieldB() {
|
|
700
|
+
return this.fieldB;
|
|
701
|
+
}
|
|
702
|
+
getOperator() {
|
|
703
|
+
return this.operator;
|
|
704
|
+
}
|
|
705
|
+
getTransformA() {
|
|
706
|
+
return this.transformA ? [...this.transformA] : undefined;
|
|
707
|
+
}
|
|
708
|
+
getTransformB() {
|
|
709
|
+
return this.transformB ? [...this.transformB] : undefined;
|
|
710
|
+
}
|
|
711
|
+
clone() {
|
|
712
|
+
return new FieldToFieldSpecification(this.fieldA, this.operator, this.fieldB, this.transformA ? [...this.transformA] : undefined, this.transformB ? [...this.transformB] : undefined, this.transformRegistry, this.metadata);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
class DefaultContextProvider {
|
|
717
|
+
context;
|
|
718
|
+
constructor(context = {}) {
|
|
719
|
+
this.context = context;
|
|
720
|
+
}
|
|
721
|
+
getValue(path) {
|
|
722
|
+
return this.getNestedValue(this.context, path);
|
|
723
|
+
}
|
|
724
|
+
hasValue(path) {
|
|
725
|
+
try {
|
|
726
|
+
const value = this.getNestedValue(this.context, path);
|
|
727
|
+
return value !== undefined;
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
getNestedValue(obj, path) {
|
|
734
|
+
return path.split('.').reduce((current, key) => {
|
|
735
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
736
|
+
}, obj);
|
|
737
|
+
}
|
|
738
|
+
setContext(context) {
|
|
739
|
+
this.context = context;
|
|
740
|
+
}
|
|
741
|
+
updateContext(updates) {
|
|
742
|
+
this.context = { ...this.context, ...updates };
|
|
743
|
+
}
|
|
744
|
+
getContext() {
|
|
745
|
+
return { ...this.context };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
class DateContextProvider {
|
|
749
|
+
getValue(path) {
|
|
750
|
+
switch (path) {
|
|
751
|
+
case 'now':
|
|
752
|
+
return new Date();
|
|
753
|
+
case 'today':
|
|
754
|
+
const today = new Date();
|
|
755
|
+
today.setHours(0, 0, 0, 0);
|
|
756
|
+
return today;
|
|
757
|
+
case 'timestamp':
|
|
758
|
+
return Date.now();
|
|
759
|
+
default:
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
hasValue(path) {
|
|
764
|
+
return ['now', 'today', 'timestamp'].includes(path);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
class CompositeContextProvider {
|
|
768
|
+
providers;
|
|
769
|
+
constructor(providers) {
|
|
770
|
+
this.providers = providers;
|
|
771
|
+
}
|
|
772
|
+
getValue(path) {
|
|
773
|
+
for (const provider of this.providers) {
|
|
774
|
+
if (provider.hasValue(path)) {
|
|
775
|
+
return provider.getValue(path);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
hasValue(path) {
|
|
781
|
+
return this.providers.some(provider => provider.hasValue(path));
|
|
782
|
+
}
|
|
783
|
+
addProvider(provider) {
|
|
784
|
+
this.providers.push(provider);
|
|
785
|
+
}
|
|
786
|
+
removeProvider(provider) {
|
|
787
|
+
const index = this.providers.indexOf(provider);
|
|
788
|
+
if (index > -1) {
|
|
789
|
+
this.providers.splice(index, 1);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
class ContextualSpecification extends Specification {
|
|
795
|
+
template;
|
|
796
|
+
contextProvider;
|
|
797
|
+
static TOKEN_REGEX = /\$\{([^}]+)\}/g;
|
|
798
|
+
constructor(template, contextProvider, metadata) {
|
|
799
|
+
super(metadata);
|
|
800
|
+
this.template = template;
|
|
801
|
+
this.contextProvider = contextProvider;
|
|
802
|
+
}
|
|
803
|
+
isSatisfiedBy(obj) {
|
|
804
|
+
const resolvedExpression = this.resolveTokens(this.template, obj);
|
|
805
|
+
// This would need to be evaluated by the DSL parser
|
|
806
|
+
// For now, we'll throw an error indicating this needs the parser
|
|
807
|
+
throw new Error(`ContextualSpecification requires DSL parser to evaluate: ${resolvedExpression}`);
|
|
808
|
+
}
|
|
809
|
+
toJSON() {
|
|
810
|
+
return {
|
|
811
|
+
type: 'contextual',
|
|
812
|
+
template: this.template,
|
|
813
|
+
metadata: this.metadata,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
static fromJSON(json, contextProvider) {
|
|
817
|
+
return new ContextualSpecification(json.template, contextProvider, json.metadata);
|
|
818
|
+
}
|
|
819
|
+
toDSL() {
|
|
820
|
+
return this.template;
|
|
821
|
+
}
|
|
822
|
+
resolveTokens(template, obj) {
|
|
823
|
+
const provider = this.contextProvider || new DefaultContextProvider();
|
|
824
|
+
return template.replace(ContextualSpecification.TOKEN_REGEX, (match, tokenPath) => {
|
|
825
|
+
// First try to resolve from context
|
|
826
|
+
if (provider.hasValue(tokenPath)) {
|
|
827
|
+
const value = provider.getValue(tokenPath);
|
|
828
|
+
return this.valueToString(value);
|
|
829
|
+
}
|
|
830
|
+
// Then try to resolve from object fields
|
|
831
|
+
if (tokenPath in obj) {
|
|
832
|
+
const value = obj[tokenPath];
|
|
833
|
+
return this.valueToString(value);
|
|
834
|
+
}
|
|
835
|
+
// If not found, return the original token
|
|
836
|
+
return match;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
valueToString(value) {
|
|
840
|
+
if (value === null || value === undefined) {
|
|
841
|
+
return 'null';
|
|
842
|
+
}
|
|
843
|
+
if (typeof value === 'string') {
|
|
844
|
+
return `"${value}"`;
|
|
845
|
+
}
|
|
846
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
847
|
+
return String(value);
|
|
848
|
+
}
|
|
849
|
+
if (value instanceof Date) {
|
|
850
|
+
return `"${value.toISOString()}"`;
|
|
851
|
+
}
|
|
852
|
+
return JSON.stringify(value);
|
|
853
|
+
}
|
|
854
|
+
getTemplate() {
|
|
855
|
+
return this.template;
|
|
856
|
+
}
|
|
857
|
+
getContextProvider() {
|
|
858
|
+
return this.contextProvider;
|
|
859
|
+
}
|
|
860
|
+
setContextProvider(provider) {
|
|
861
|
+
return new ContextualSpecification(this.template, provider, this.metadata);
|
|
862
|
+
}
|
|
863
|
+
getTokens() {
|
|
864
|
+
const tokens = [];
|
|
865
|
+
let match;
|
|
866
|
+
const regex = new RegExp(ContextualSpecification.TOKEN_REGEX);
|
|
867
|
+
while ((match = regex.exec(this.template)) !== null) {
|
|
868
|
+
tokens.push(match[1]);
|
|
869
|
+
}
|
|
870
|
+
return tokens;
|
|
871
|
+
}
|
|
872
|
+
clone() {
|
|
873
|
+
return new ContextualSpecification(this.template, this.contextProvider, this.metadata);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Types of conditional validation
|
|
879
|
+
*/
|
|
880
|
+
var ConditionalType;
|
|
881
|
+
(function (ConditionalType) {
|
|
882
|
+
ConditionalType["REQUIRED_IF"] = "requiredIf";
|
|
883
|
+
ConditionalType["VISIBLE_IF"] = "visibleIf";
|
|
884
|
+
ConditionalType["DISABLED_IF"] = "disabledIf";
|
|
885
|
+
ConditionalType["READONLY_IF"] = "readonlyIf";
|
|
886
|
+
})(ConditionalType || (ConditionalType = {}));
|
|
887
|
+
/**
|
|
888
|
+
* Base class for conditional validators
|
|
889
|
+
*/
|
|
890
|
+
class ConditionalSpecification extends Specification {
|
|
891
|
+
condition;
|
|
892
|
+
conditionalType;
|
|
893
|
+
constructor(condition, conditionalType, metadata) {
|
|
894
|
+
super(metadata);
|
|
895
|
+
this.condition = condition;
|
|
896
|
+
this.conditionalType = conditionalType;
|
|
897
|
+
}
|
|
898
|
+
getCondition() {
|
|
899
|
+
return this.condition;
|
|
900
|
+
}
|
|
901
|
+
getConditionalType() {
|
|
902
|
+
return this.conditionalType;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Validates that a field is required when a condition is met
|
|
907
|
+
*/
|
|
908
|
+
class RequiredIfSpecification extends ConditionalSpecification {
|
|
909
|
+
field;
|
|
910
|
+
constructor(field, condition, metadata) {
|
|
911
|
+
super(condition, ConditionalType.REQUIRED_IF, metadata);
|
|
912
|
+
this.field = field;
|
|
913
|
+
}
|
|
914
|
+
isSatisfiedBy(obj) {
|
|
915
|
+
// If condition is not met, validation passes (field is not required)
|
|
916
|
+
if (!this.condition.isSatisfiedBy(obj)) {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
// If condition is met, check if field has a value
|
|
920
|
+
const fieldValue = obj[this.field];
|
|
921
|
+
return fieldValue !== null && fieldValue !== undefined && fieldValue !== '';
|
|
922
|
+
}
|
|
923
|
+
toJSON() {
|
|
924
|
+
return {
|
|
925
|
+
type: 'requiredIf',
|
|
926
|
+
field: String(this.field),
|
|
927
|
+
condition: this.condition.toJSON(),
|
|
928
|
+
metadata: this.metadata,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
static fromJSON(json) {
|
|
932
|
+
// This would need the SpecificationFactory to reconstruct the condition
|
|
933
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
934
|
+
}
|
|
935
|
+
toDSL() {
|
|
936
|
+
return `requiredIf(${String(this.field)}, ${this.condition.toDSL()})`;
|
|
937
|
+
}
|
|
938
|
+
clone() {
|
|
939
|
+
return new RequiredIfSpecification(this.field, this.condition.clone(), this.metadata);
|
|
940
|
+
}
|
|
941
|
+
getField() {
|
|
942
|
+
return this.field;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Determines if a field should be visible based on a condition
|
|
947
|
+
*/
|
|
948
|
+
class VisibleIfSpecification extends ConditionalSpecification {
|
|
949
|
+
field;
|
|
950
|
+
constructor(field, condition, metadata) {
|
|
951
|
+
super(condition, ConditionalType.VISIBLE_IF, metadata);
|
|
952
|
+
this.field = field;
|
|
953
|
+
}
|
|
954
|
+
isSatisfiedBy(obj) {
|
|
955
|
+
// This returns whether the field should be visible
|
|
956
|
+
return this.condition.isSatisfiedBy(obj);
|
|
957
|
+
}
|
|
958
|
+
toJSON() {
|
|
959
|
+
return {
|
|
960
|
+
type: 'visibleIf',
|
|
961
|
+
field: String(this.field),
|
|
962
|
+
condition: this.condition.toJSON(),
|
|
963
|
+
metadata: this.metadata,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
static fromJSON(json) {
|
|
967
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
968
|
+
}
|
|
969
|
+
toDSL() {
|
|
970
|
+
return `visibleIf(${String(this.field)}, ${this.condition.toDSL()})`;
|
|
971
|
+
}
|
|
972
|
+
clone() {
|
|
973
|
+
return new VisibleIfSpecification(this.field, this.condition.clone(), this.metadata);
|
|
974
|
+
}
|
|
975
|
+
getField() {
|
|
976
|
+
return this.field;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Determines if a field should be disabled based on a condition
|
|
981
|
+
*/
|
|
982
|
+
class DisabledIfSpecification extends ConditionalSpecification {
|
|
983
|
+
field;
|
|
984
|
+
constructor(field, condition, metadata) {
|
|
985
|
+
super(condition, ConditionalType.DISABLED_IF, metadata);
|
|
986
|
+
this.field = field;
|
|
987
|
+
}
|
|
988
|
+
isSatisfiedBy(obj) {
|
|
989
|
+
// This returns whether the field should be disabled
|
|
990
|
+
return this.condition.isSatisfiedBy(obj);
|
|
991
|
+
}
|
|
992
|
+
toJSON() {
|
|
993
|
+
return {
|
|
994
|
+
type: 'disabledIf',
|
|
995
|
+
field: String(this.field),
|
|
996
|
+
condition: this.condition.toJSON(),
|
|
997
|
+
metadata: this.metadata,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
static fromJSON(json) {
|
|
1001
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
1002
|
+
}
|
|
1003
|
+
toDSL() {
|
|
1004
|
+
return `disabledIf(${String(this.field)}, ${this.condition.toDSL()})`;
|
|
1005
|
+
}
|
|
1006
|
+
clone() {
|
|
1007
|
+
return new DisabledIfSpecification(this.field, this.condition.clone(), this.metadata);
|
|
1008
|
+
}
|
|
1009
|
+
getField() {
|
|
1010
|
+
return this.field;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Determines if a field should be readonly based on a condition
|
|
1015
|
+
*/
|
|
1016
|
+
class ReadonlyIfSpecification extends ConditionalSpecification {
|
|
1017
|
+
field;
|
|
1018
|
+
constructor(field, condition, metadata) {
|
|
1019
|
+
super(condition, ConditionalType.READONLY_IF, metadata);
|
|
1020
|
+
this.field = field;
|
|
1021
|
+
}
|
|
1022
|
+
isSatisfiedBy(obj) {
|
|
1023
|
+
// This returns whether the field should be readonly
|
|
1024
|
+
return this.condition.isSatisfiedBy(obj);
|
|
1025
|
+
}
|
|
1026
|
+
toJSON() {
|
|
1027
|
+
return {
|
|
1028
|
+
type: 'readonlyIf',
|
|
1029
|
+
field: String(this.field),
|
|
1030
|
+
condition: this.condition.toJSON(),
|
|
1031
|
+
metadata: this.metadata,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
static fromJSON(json) {
|
|
1035
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
1036
|
+
}
|
|
1037
|
+
toDSL() {
|
|
1038
|
+
return `readonlyIf(${String(this.field)}, ${this.condition.toDSL()})`;
|
|
1039
|
+
}
|
|
1040
|
+
clone() {
|
|
1041
|
+
return new ReadonlyIfSpecification(this.field, this.condition.clone(), this.metadata);
|
|
1042
|
+
}
|
|
1043
|
+
getField() {
|
|
1044
|
+
return this.field;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Applies a specification to all elements in an array field
|
|
1050
|
+
*/
|
|
1051
|
+
class ForEachSpecification extends Specification {
|
|
1052
|
+
arrayField;
|
|
1053
|
+
itemSpecification;
|
|
1054
|
+
constructor(arrayField, itemSpecification, metadata) {
|
|
1055
|
+
super(metadata);
|
|
1056
|
+
this.arrayField = arrayField;
|
|
1057
|
+
this.itemSpecification = itemSpecification;
|
|
1058
|
+
}
|
|
1059
|
+
isSatisfiedBy(obj) {
|
|
1060
|
+
const arrayValue = obj[this.arrayField];
|
|
1061
|
+
// If not an array, fail validation
|
|
1062
|
+
if (!Array.isArray(arrayValue)) {
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
// Check if all items satisfy the specification
|
|
1066
|
+
return arrayValue.every((item) => {
|
|
1067
|
+
try {
|
|
1068
|
+
return this.itemSpecification.isSatisfiedBy(item);
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
// If item doesn't match expected structure, fail validation
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
toJSON() {
|
|
1077
|
+
return {
|
|
1078
|
+
type: 'forEach',
|
|
1079
|
+
arrayField: String(this.arrayField),
|
|
1080
|
+
itemSpecification: this.itemSpecification.toJSON(),
|
|
1081
|
+
metadata: this.metadata,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
static fromJSON(json) {
|
|
1085
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
1086
|
+
}
|
|
1087
|
+
toDSL() {
|
|
1088
|
+
return `forEach(${String(this.arrayField)}, ${this.itemSpecification.toDSL()})`;
|
|
1089
|
+
}
|
|
1090
|
+
clone() {
|
|
1091
|
+
return new ForEachSpecification(this.arrayField, this.itemSpecification.clone(), this.metadata);
|
|
1092
|
+
}
|
|
1093
|
+
getArrayField() {
|
|
1094
|
+
return this.arrayField;
|
|
1095
|
+
}
|
|
1096
|
+
getItemSpecification() {
|
|
1097
|
+
return this.itemSpecification;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Gets items that fail the specification
|
|
1101
|
+
*/
|
|
1102
|
+
getFailingItems(obj) {
|
|
1103
|
+
const arrayValue = obj[this.arrayField];
|
|
1104
|
+
if (!Array.isArray(arrayValue)) {
|
|
1105
|
+
return [];
|
|
1106
|
+
}
|
|
1107
|
+
const failingItems = [];
|
|
1108
|
+
arrayValue.forEach((item, index) => {
|
|
1109
|
+
if (!this.itemSpecification.isSatisfiedBy(item)) {
|
|
1110
|
+
failingItems.push({
|
|
1111
|
+
index,
|
|
1112
|
+
item,
|
|
1113
|
+
errors: [`Item at index ${index} failed validation`],
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
return failingItems;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Ensures all items in an array are unique based on a field or key selector
|
|
1122
|
+
*/
|
|
1123
|
+
class UniqueBySpecification extends Specification {
|
|
1124
|
+
arrayField;
|
|
1125
|
+
keySelector;
|
|
1126
|
+
constructor(arrayField, keySelector, metadata) {
|
|
1127
|
+
super(metadata);
|
|
1128
|
+
this.arrayField = arrayField;
|
|
1129
|
+
this.keySelector = keySelector;
|
|
1130
|
+
}
|
|
1131
|
+
isSatisfiedBy(obj) {
|
|
1132
|
+
const arrayValue = obj[this.arrayField];
|
|
1133
|
+
// If not an array, pass validation (empty case)
|
|
1134
|
+
if (!Array.isArray(arrayValue)) {
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
// If empty array, pass validation
|
|
1138
|
+
if (arrayValue.length === 0) {
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
const seen = new Set();
|
|
1142
|
+
for (const item of arrayValue) {
|
|
1143
|
+
let key;
|
|
1144
|
+
if (typeof this.keySelector === 'string') {
|
|
1145
|
+
// Field name selector
|
|
1146
|
+
key = item?.[this.keySelector];
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
// Function selector
|
|
1150
|
+
key = this.keySelector(item);
|
|
1151
|
+
}
|
|
1152
|
+
// Convert key to string for Set comparison
|
|
1153
|
+
const keyStr = JSON.stringify(key);
|
|
1154
|
+
if (seen.has(keyStr)) {
|
|
1155
|
+
return false; // Duplicate found
|
|
1156
|
+
}
|
|
1157
|
+
seen.add(keyStr);
|
|
1158
|
+
}
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
toJSON() {
|
|
1162
|
+
return {
|
|
1163
|
+
type: 'uniqueBy',
|
|
1164
|
+
arrayField: String(this.arrayField),
|
|
1165
|
+
keySelector: typeof this.keySelector === 'string' ? this.keySelector : '[Function]',
|
|
1166
|
+
metadata: this.metadata,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
static fromJSON(json) {
|
|
1170
|
+
return new UniqueBySpecification(json.arrayField, json.keySelector, json.metadata);
|
|
1171
|
+
}
|
|
1172
|
+
toDSL() {
|
|
1173
|
+
const selector = typeof this.keySelector === 'string'
|
|
1174
|
+
? `"${this.keySelector}"`
|
|
1175
|
+
: '[Function]';
|
|
1176
|
+
return `uniqueBy(${String(this.arrayField)}, ${selector})`;
|
|
1177
|
+
}
|
|
1178
|
+
clone() {
|
|
1179
|
+
return new UniqueBySpecification(this.arrayField, this.keySelector, this.metadata);
|
|
1180
|
+
}
|
|
1181
|
+
getArrayField() {
|
|
1182
|
+
return this.arrayField;
|
|
1183
|
+
}
|
|
1184
|
+
getKeySelector() {
|
|
1185
|
+
return this.keySelector;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Gets duplicate items found in the array
|
|
1189
|
+
*/
|
|
1190
|
+
getDuplicates(obj) {
|
|
1191
|
+
const arrayValue = obj[this.arrayField];
|
|
1192
|
+
if (!Array.isArray(arrayValue)) {
|
|
1193
|
+
return [];
|
|
1194
|
+
}
|
|
1195
|
+
const keyMap = new Map();
|
|
1196
|
+
arrayValue.forEach((item, index) => {
|
|
1197
|
+
let key;
|
|
1198
|
+
if (typeof this.keySelector === 'string') {
|
|
1199
|
+
key = item?.[this.keySelector];
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
key = this.keySelector(item);
|
|
1203
|
+
}
|
|
1204
|
+
const keyStr = JSON.stringify(key);
|
|
1205
|
+
if (!keyMap.has(keyStr)) {
|
|
1206
|
+
keyMap.set(keyStr, []);
|
|
1207
|
+
}
|
|
1208
|
+
keyMap.get(keyStr).push({ index, item });
|
|
1209
|
+
});
|
|
1210
|
+
// Return only keys that have duplicates
|
|
1211
|
+
return Array.from(keyMap.entries())
|
|
1212
|
+
.filter(([_, items]) => items.length > 1)
|
|
1213
|
+
.map(([keyStr, items]) => ({
|
|
1214
|
+
key: JSON.parse(keyStr),
|
|
1215
|
+
items,
|
|
1216
|
+
}));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Validates that an array has a minimum number of elements
|
|
1221
|
+
*/
|
|
1222
|
+
class MinLengthSpecification extends Specification {
|
|
1223
|
+
arrayField;
|
|
1224
|
+
minLength;
|
|
1225
|
+
constructor(arrayField, minLength, metadata) {
|
|
1226
|
+
super(metadata);
|
|
1227
|
+
this.arrayField = arrayField;
|
|
1228
|
+
this.minLength = minLength;
|
|
1229
|
+
}
|
|
1230
|
+
isSatisfiedBy(obj) {
|
|
1231
|
+
const arrayValue = obj[this.arrayField];
|
|
1232
|
+
if (!Array.isArray(arrayValue)) {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
return arrayValue.length >= this.minLength;
|
|
1236
|
+
}
|
|
1237
|
+
toJSON() {
|
|
1238
|
+
return {
|
|
1239
|
+
type: 'minLength',
|
|
1240
|
+
arrayField: String(this.arrayField),
|
|
1241
|
+
minLength: this.minLength,
|
|
1242
|
+
metadata: this.metadata,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
static fromJSON(json) {
|
|
1246
|
+
return new MinLengthSpecification(json.arrayField, json.minLength, json.metadata);
|
|
1247
|
+
}
|
|
1248
|
+
toDSL() {
|
|
1249
|
+
return `minLength(${String(this.arrayField)}, ${this.minLength})`;
|
|
1250
|
+
}
|
|
1251
|
+
clone() {
|
|
1252
|
+
return new MinLengthSpecification(this.arrayField, this.minLength, this.metadata);
|
|
1253
|
+
}
|
|
1254
|
+
getArrayField() {
|
|
1255
|
+
return this.arrayField;
|
|
1256
|
+
}
|
|
1257
|
+
getMinLength() {
|
|
1258
|
+
return this.minLength;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Validates that an array has a maximum number of elements
|
|
1263
|
+
*/
|
|
1264
|
+
class MaxLengthSpecification extends Specification {
|
|
1265
|
+
arrayField;
|
|
1266
|
+
maxLength;
|
|
1267
|
+
constructor(arrayField, maxLength, metadata) {
|
|
1268
|
+
super(metadata);
|
|
1269
|
+
this.arrayField = arrayField;
|
|
1270
|
+
this.maxLength = maxLength;
|
|
1271
|
+
}
|
|
1272
|
+
isSatisfiedBy(obj) {
|
|
1273
|
+
const arrayValue = obj[this.arrayField];
|
|
1274
|
+
if (!Array.isArray(arrayValue)) {
|
|
1275
|
+
return true; // If not array, max length doesn't apply
|
|
1276
|
+
}
|
|
1277
|
+
return arrayValue.length <= this.maxLength;
|
|
1278
|
+
}
|
|
1279
|
+
toJSON() {
|
|
1280
|
+
return {
|
|
1281
|
+
type: 'maxLength',
|
|
1282
|
+
arrayField: String(this.arrayField),
|
|
1283
|
+
maxLength: this.maxLength,
|
|
1284
|
+
metadata: this.metadata,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
static fromJSON(json) {
|
|
1288
|
+
return new MaxLengthSpecification(json.arrayField, json.maxLength, json.metadata);
|
|
1289
|
+
}
|
|
1290
|
+
toDSL() {
|
|
1291
|
+
return `maxLength(${String(this.arrayField)}, ${this.maxLength})`;
|
|
1292
|
+
}
|
|
1293
|
+
clone() {
|
|
1294
|
+
return new MaxLengthSpecification(this.arrayField, this.maxLength, this.metadata);
|
|
1295
|
+
}
|
|
1296
|
+
getArrayField() {
|
|
1297
|
+
return this.arrayField;
|
|
1298
|
+
}
|
|
1299
|
+
getMaxLength() {
|
|
1300
|
+
return this.maxLength;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Only applies the inner specification if the field is defined (not null, undefined, or empty)
|
|
1306
|
+
*/
|
|
1307
|
+
class IfDefinedSpecification extends Specification {
|
|
1308
|
+
field;
|
|
1309
|
+
innerSpecification;
|
|
1310
|
+
constructor(field, innerSpecification, metadata) {
|
|
1311
|
+
super(metadata);
|
|
1312
|
+
this.field = field;
|
|
1313
|
+
this.innerSpecification = innerSpecification;
|
|
1314
|
+
}
|
|
1315
|
+
isSatisfiedBy(obj) {
|
|
1316
|
+
const fieldValue = obj[this.field];
|
|
1317
|
+
// If field is not defined, validation passes
|
|
1318
|
+
if (this.isUndefined(fieldValue)) {
|
|
1319
|
+
return true;
|
|
1320
|
+
}
|
|
1321
|
+
// If field is defined, apply the inner specification
|
|
1322
|
+
return this.innerSpecification.isSatisfiedBy(obj);
|
|
1323
|
+
}
|
|
1324
|
+
isUndefined(value) {
|
|
1325
|
+
return value === null || value === undefined || value === '';
|
|
1326
|
+
}
|
|
1327
|
+
toJSON() {
|
|
1328
|
+
return {
|
|
1329
|
+
type: 'ifDefined',
|
|
1330
|
+
field: String(this.field),
|
|
1331
|
+
specification: this.innerSpecification.toJSON(),
|
|
1332
|
+
metadata: this.metadata,
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
static fromJSON(json) {
|
|
1336
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
1337
|
+
}
|
|
1338
|
+
toDSL() {
|
|
1339
|
+
return `ifDefined(${String(this.field)}, ${this.innerSpecification.toDSL()})`;
|
|
1340
|
+
}
|
|
1341
|
+
clone() {
|
|
1342
|
+
return new IfDefinedSpecification(this.field, this.innerSpecification.clone(), this.metadata);
|
|
1343
|
+
}
|
|
1344
|
+
getField() {
|
|
1345
|
+
return this.field;
|
|
1346
|
+
}
|
|
1347
|
+
getInnerSpecification() {
|
|
1348
|
+
return this.innerSpecification;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Only applies the inner specification if the field value is not null
|
|
1353
|
+
*/
|
|
1354
|
+
class IfNotNullSpecification extends Specification {
|
|
1355
|
+
field;
|
|
1356
|
+
innerSpecification;
|
|
1357
|
+
constructor(field, innerSpecification, metadata) {
|
|
1358
|
+
super(metadata);
|
|
1359
|
+
this.field = field;
|
|
1360
|
+
this.innerSpecification = innerSpecification;
|
|
1361
|
+
}
|
|
1362
|
+
isSatisfiedBy(obj) {
|
|
1363
|
+
const fieldValue = obj[this.field];
|
|
1364
|
+
// If field is null, validation passes
|
|
1365
|
+
if (fieldValue === null) {
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
// If field is not null, apply the inner specification
|
|
1369
|
+
return this.innerSpecification.isSatisfiedBy(obj);
|
|
1370
|
+
}
|
|
1371
|
+
toJSON() {
|
|
1372
|
+
return {
|
|
1373
|
+
type: 'ifNotNull',
|
|
1374
|
+
field: String(this.field),
|
|
1375
|
+
specification: this.innerSpecification.toJSON(),
|
|
1376
|
+
metadata: this.metadata,
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
static fromJSON(json) {
|
|
1380
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
1381
|
+
}
|
|
1382
|
+
toDSL() {
|
|
1383
|
+
return `ifNotNull(${String(this.field)}, ${this.innerSpecification.toDSL()})`;
|
|
1384
|
+
}
|
|
1385
|
+
clone() {
|
|
1386
|
+
return new IfNotNullSpecification(this.field, this.innerSpecification.clone(), this.metadata);
|
|
1387
|
+
}
|
|
1388
|
+
getField() {
|
|
1389
|
+
return this.field;
|
|
1390
|
+
}
|
|
1391
|
+
getInnerSpecification() {
|
|
1392
|
+
return this.innerSpecification;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Only applies the inner specification if the field exists as a property on the object
|
|
1397
|
+
*/
|
|
1398
|
+
class IfExistsSpecification extends Specification {
|
|
1399
|
+
field;
|
|
1400
|
+
innerSpecification;
|
|
1401
|
+
constructor(field, innerSpecification, metadata) {
|
|
1402
|
+
super(metadata);
|
|
1403
|
+
this.field = field;
|
|
1404
|
+
this.innerSpecification = innerSpecification;
|
|
1405
|
+
}
|
|
1406
|
+
isSatisfiedBy(obj) {
|
|
1407
|
+
// If field doesn't exist as a property, validation passes
|
|
1408
|
+
if (!(this.field in obj)) {
|
|
1409
|
+
return true;
|
|
1410
|
+
}
|
|
1411
|
+
// If field exists, apply the inner specification
|
|
1412
|
+
return this.innerSpecification.isSatisfiedBy(obj);
|
|
1413
|
+
}
|
|
1414
|
+
toJSON() {
|
|
1415
|
+
return {
|
|
1416
|
+
type: 'ifExists',
|
|
1417
|
+
field: String(this.field),
|
|
1418
|
+
specification: this.innerSpecification.toJSON(),
|
|
1419
|
+
metadata: this.metadata,
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
static fromJSON(json) {
|
|
1423
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
1424
|
+
}
|
|
1425
|
+
toDSL() {
|
|
1426
|
+
return `ifExists(${String(this.field)}, ${this.innerSpecification.toDSL()})`;
|
|
1427
|
+
}
|
|
1428
|
+
clone() {
|
|
1429
|
+
return new IfExistsSpecification(this.field, this.innerSpecification.clone(), this.metadata);
|
|
1430
|
+
}
|
|
1431
|
+
getField() {
|
|
1432
|
+
return this.field;
|
|
1433
|
+
}
|
|
1434
|
+
getInnerSpecification() {
|
|
1435
|
+
return this.innerSpecification;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Provides a default value for a field if it's undefined, then applies the specification
|
|
1440
|
+
*/
|
|
1441
|
+
class WithDefaultSpecification extends Specification {
|
|
1442
|
+
field;
|
|
1443
|
+
defaultValue;
|
|
1444
|
+
innerSpecification;
|
|
1445
|
+
constructor(field, defaultValue, innerSpecification, metadata) {
|
|
1446
|
+
super(metadata);
|
|
1447
|
+
this.field = field;
|
|
1448
|
+
this.defaultValue = defaultValue;
|
|
1449
|
+
this.innerSpecification = innerSpecification;
|
|
1450
|
+
}
|
|
1451
|
+
isSatisfiedBy(obj) {
|
|
1452
|
+
// Create a copy of the object with default value if field is undefined
|
|
1453
|
+
const objWithDefault = { ...obj };
|
|
1454
|
+
if (this.isUndefined(obj[this.field])) {
|
|
1455
|
+
objWithDefault[this.field] = this.defaultValue;
|
|
1456
|
+
}
|
|
1457
|
+
return this.innerSpecification.isSatisfiedBy(objWithDefault);
|
|
1458
|
+
}
|
|
1459
|
+
isUndefined(value) {
|
|
1460
|
+
return value === null || value === undefined;
|
|
1461
|
+
}
|
|
1462
|
+
toJSON() {
|
|
1463
|
+
return {
|
|
1464
|
+
type: 'withDefault',
|
|
1465
|
+
field: String(this.field),
|
|
1466
|
+
defaultValue: this.defaultValue,
|
|
1467
|
+
specification: this.innerSpecification.toJSON(),
|
|
1468
|
+
metadata: this.metadata,
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
static fromJSON(json) {
|
|
1472
|
+
throw new Error('Use SpecificationFactory.fromJSON() for proper reconstruction');
|
|
1473
|
+
}
|
|
1474
|
+
toDSL() {
|
|
1475
|
+
const defaultStr = JSON.stringify(this.defaultValue);
|
|
1476
|
+
return `withDefault(${String(this.field)}, ${defaultStr}, ${this.innerSpecification.toDSL()})`;
|
|
1477
|
+
}
|
|
1478
|
+
clone() {
|
|
1479
|
+
return new WithDefaultSpecification(this.field, this.defaultValue, this.innerSpecification.clone(), this.metadata);
|
|
1480
|
+
}
|
|
1481
|
+
getField() {
|
|
1482
|
+
return this.field;
|
|
1483
|
+
}
|
|
1484
|
+
getDefaultValue() {
|
|
1485
|
+
return this.defaultValue;
|
|
1486
|
+
}
|
|
1487
|
+
getInnerSpecification() {
|
|
1488
|
+
return this.innerSpecification;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Form specification that manages validation rules for multiple fields
|
|
1494
|
+
*/
|
|
1495
|
+
class FormSpecification extends Specification {
|
|
1496
|
+
fieldRules = new Map();
|
|
1497
|
+
globalValidations = [];
|
|
1498
|
+
constructor(metadata) {
|
|
1499
|
+
super(metadata);
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Adds validation rules for a specific field
|
|
1503
|
+
*/
|
|
1504
|
+
addFieldRules(field, rules) {
|
|
1505
|
+
this.fieldRules.set(field, rules);
|
|
1506
|
+
return this;
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Adds a global validation rule that applies to the entire form
|
|
1510
|
+
*/
|
|
1511
|
+
addGlobalValidation(specification) {
|
|
1512
|
+
this.globalValidations.push(specification);
|
|
1513
|
+
return this;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Sets required condition for a field
|
|
1517
|
+
*/
|
|
1518
|
+
setRequired(field, condition) {
|
|
1519
|
+
const existing = this.fieldRules.get(field) || {};
|
|
1520
|
+
existing.required = condition;
|
|
1521
|
+
this.fieldRules.set(field, existing);
|
|
1522
|
+
return this;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Sets visibility condition for a field
|
|
1526
|
+
*/
|
|
1527
|
+
setVisible(field, condition) {
|
|
1528
|
+
const existing = this.fieldRules.get(field) || {};
|
|
1529
|
+
existing.visible = condition;
|
|
1530
|
+
this.fieldRules.set(field, existing);
|
|
1531
|
+
return this;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Sets disabled condition for a field
|
|
1535
|
+
*/
|
|
1536
|
+
setDisabled(field, condition) {
|
|
1537
|
+
const existing = this.fieldRules.get(field) || {};
|
|
1538
|
+
existing.disabled = condition;
|
|
1539
|
+
this.fieldRules.set(field, existing);
|
|
1540
|
+
return this;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Sets readonly condition for a field
|
|
1544
|
+
*/
|
|
1545
|
+
setReadonly(field, condition) {
|
|
1546
|
+
const existing = this.fieldRules.get(field) || {};
|
|
1547
|
+
existing.readonly = condition;
|
|
1548
|
+
this.fieldRules.set(field, existing);
|
|
1549
|
+
return this;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Basic satisfaction check - validates all rules
|
|
1553
|
+
*/
|
|
1554
|
+
isSatisfiedBy(obj) {
|
|
1555
|
+
const result = this.validateForm(obj);
|
|
1556
|
+
return result.isValid;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Comprehensive form validation with detailed results
|
|
1560
|
+
*/
|
|
1561
|
+
validateForm(obj) {
|
|
1562
|
+
const result = {
|
|
1563
|
+
isValid: true,
|
|
1564
|
+
fields: {},
|
|
1565
|
+
globalErrors: [],
|
|
1566
|
+
warnings: [],
|
|
1567
|
+
};
|
|
1568
|
+
// Validate each field
|
|
1569
|
+
for (const [field, rules] of this.fieldRules.entries()) {
|
|
1570
|
+
const fieldResult = this.validateField(field, rules, obj);
|
|
1571
|
+
result.fields[String(field)] = fieldResult;
|
|
1572
|
+
if (!fieldResult.isValid) {
|
|
1573
|
+
result.isValid = false;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
// Validate global rules
|
|
1577
|
+
for (const globalSpec of this.globalValidations) {
|
|
1578
|
+
try {
|
|
1579
|
+
if (!globalSpec.isSatisfiedBy(obj)) {
|
|
1580
|
+
const metadata = globalSpec.getMetadata();
|
|
1581
|
+
const message = metadata?.message || 'Global validation failed';
|
|
1582
|
+
result.globalErrors.push(message);
|
|
1583
|
+
result.isValid = false;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
result.globalErrors.push(`Global validation error: ${error}`);
|
|
1588
|
+
result.isValid = false;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return result;
|
|
1592
|
+
}
|
|
1593
|
+
validateField(field, rules, obj) {
|
|
1594
|
+
const fieldResult = {
|
|
1595
|
+
field: String(field),
|
|
1596
|
+
isValid: true,
|
|
1597
|
+
errors: [],
|
|
1598
|
+
warnings: [],
|
|
1599
|
+
metadata: rules.metadata,
|
|
1600
|
+
};
|
|
1601
|
+
// Check if field should be visible
|
|
1602
|
+
if (!this.isFieldVisible(field, rules, obj)) {
|
|
1603
|
+
// If field is not visible, skip validation
|
|
1604
|
+
return fieldResult;
|
|
1605
|
+
}
|
|
1606
|
+
// Check required validation
|
|
1607
|
+
if (rules.required) {
|
|
1608
|
+
const isRequired = typeof rules.required === 'boolean'
|
|
1609
|
+
? rules.required
|
|
1610
|
+
: rules.required.isSatisfiedBy(obj);
|
|
1611
|
+
if (isRequired) {
|
|
1612
|
+
const fieldValue = obj[field];
|
|
1613
|
+
if (fieldValue === null ||
|
|
1614
|
+
fieldValue === undefined ||
|
|
1615
|
+
fieldValue === '') {
|
|
1616
|
+
fieldResult.errors.push(rules.metadata?.message || `${String(field)} is required`);
|
|
1617
|
+
fieldResult.isValid = false;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
// Run validation specifications
|
|
1622
|
+
if (rules.validation) {
|
|
1623
|
+
for (const spec of rules.validation) {
|
|
1624
|
+
try {
|
|
1625
|
+
if (!spec.isSatisfiedBy(obj)) {
|
|
1626
|
+
const metadata = spec.getMetadata();
|
|
1627
|
+
const message = metadata?.message || `${String(field)} validation failed`;
|
|
1628
|
+
fieldResult.errors.push(message);
|
|
1629
|
+
fieldResult.isValid = false;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
catch (error) {
|
|
1633
|
+
fieldResult.errors.push(`Validation error for ${String(field)}: ${error}`);
|
|
1634
|
+
fieldResult.isValid = false;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return fieldResult;
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Checks if a field should be visible
|
|
1642
|
+
*/
|
|
1643
|
+
isFieldVisible(field, rules, obj) {
|
|
1644
|
+
if (!rules?.visible) {
|
|
1645
|
+
return true; // Default to visible
|
|
1646
|
+
}
|
|
1647
|
+
return typeof rules.visible === 'boolean'
|
|
1648
|
+
? rules.visible
|
|
1649
|
+
: rules.visible.isSatisfiedBy(obj);
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Checks if a field should be disabled
|
|
1653
|
+
*/
|
|
1654
|
+
isFieldDisabled(field, obj) {
|
|
1655
|
+
const rules = this.fieldRules.get(field);
|
|
1656
|
+
if (!rules?.disabled) {
|
|
1657
|
+
return false; // Default to enabled
|
|
1658
|
+
}
|
|
1659
|
+
return typeof rules.disabled === 'boolean'
|
|
1660
|
+
? rules.disabled
|
|
1661
|
+
: rules.disabled.isSatisfiedBy(obj);
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Checks if a field should be readonly
|
|
1665
|
+
*/
|
|
1666
|
+
isFieldReadonly(field, obj) {
|
|
1667
|
+
const rules = this.fieldRules.get(field);
|
|
1668
|
+
if (!rules?.readonly) {
|
|
1669
|
+
return false; // Default to editable
|
|
1670
|
+
}
|
|
1671
|
+
return typeof rules.readonly === 'boolean'
|
|
1672
|
+
? rules.readonly
|
|
1673
|
+
: rules.readonly.isSatisfiedBy(obj);
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Gets all configured fields
|
|
1677
|
+
*/
|
|
1678
|
+
getFields() {
|
|
1679
|
+
return Array.from(this.fieldRules.keys());
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Gets rules for a specific field
|
|
1683
|
+
*/
|
|
1684
|
+
getFieldRules(field) {
|
|
1685
|
+
return this.fieldRules.get(field);
|
|
1686
|
+
}
|
|
1687
|
+
toJSON() {
|
|
1688
|
+
const fieldsJson = {};
|
|
1689
|
+
for (const [field, rules] of this.fieldRules.entries()) {
|
|
1690
|
+
fieldsJson[String(field)] = {
|
|
1691
|
+
validation: rules.validation?.map((spec) => spec.toJSON()),
|
|
1692
|
+
required: typeof rules.required === 'object'
|
|
1693
|
+
? rules.required.toJSON()
|
|
1694
|
+
: rules.required,
|
|
1695
|
+
visible: typeof rules.visible === 'object'
|
|
1696
|
+
? rules.visible.toJSON()
|
|
1697
|
+
: rules.visible,
|
|
1698
|
+
disabled: typeof rules.disabled === 'object'
|
|
1699
|
+
? rules.disabled.toJSON()
|
|
1700
|
+
: rules.disabled,
|
|
1701
|
+
readonly: typeof rules.readonly === 'object'
|
|
1702
|
+
? rules.readonly.toJSON()
|
|
1703
|
+
: rules.readonly,
|
|
1704
|
+
metadata: rules.metadata,
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
return {
|
|
1708
|
+
type: 'form',
|
|
1709
|
+
fields: fieldsJson,
|
|
1710
|
+
globalValidations: this.globalValidations.map((spec) => spec.toJSON()),
|
|
1711
|
+
metadata: this.metadata,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
static fromJSON(json) {
|
|
1715
|
+
throw new Error('FormSpecification.fromJSON not yet implemented - requires SpecificationFactory');
|
|
1716
|
+
}
|
|
1717
|
+
toDSL() {
|
|
1718
|
+
const parts = [];
|
|
1719
|
+
// Export field rules
|
|
1720
|
+
for (const [field, rules] of this.fieldRules.entries()) {
|
|
1721
|
+
if (rules.required && typeof rules.required === 'object') {
|
|
1722
|
+
parts.push(`requiredIf(${String(field)}, ${rules.required.toDSL()})`);
|
|
1723
|
+
}
|
|
1724
|
+
if (rules.visible && typeof rules.visible === 'object') {
|
|
1725
|
+
parts.push(`visibleIf(${String(field)}, ${rules.visible.toDSL()})`);
|
|
1726
|
+
}
|
|
1727
|
+
if (rules.disabled && typeof rules.disabled === 'object') {
|
|
1728
|
+
parts.push(`disabledIf(${String(field)}, ${rules.disabled.toDSL()})`);
|
|
1729
|
+
}
|
|
1730
|
+
if (rules.readonly && typeof rules.readonly === 'object') {
|
|
1731
|
+
parts.push(`readonlyIf(${String(field)}, ${rules.readonly.toDSL()})`);
|
|
1732
|
+
}
|
|
1733
|
+
if (rules.validation) {
|
|
1734
|
+
for (const spec of rules.validation) {
|
|
1735
|
+
parts.push(spec.toDSL());
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
// Export global validations
|
|
1740
|
+
for (const spec of this.globalValidations) {
|
|
1741
|
+
parts.push(spec.toDSL());
|
|
1742
|
+
}
|
|
1743
|
+
return parts.join(' && ');
|
|
1744
|
+
}
|
|
1745
|
+
clone() {
|
|
1746
|
+
const cloned = new FormSpecification(this.metadata);
|
|
1747
|
+
// Clone field rules
|
|
1748
|
+
for (const [field, rules] of this.fieldRules.entries()) {
|
|
1749
|
+
const clonedRules = {
|
|
1750
|
+
validation: rules.validation?.map((spec) => spec.clone()),
|
|
1751
|
+
required: typeof rules.required === 'object'
|
|
1752
|
+
? rules.required.clone()
|
|
1753
|
+
: rules.required,
|
|
1754
|
+
visible: typeof rules.visible === 'object'
|
|
1755
|
+
? rules.visible.clone()
|
|
1756
|
+
: rules.visible,
|
|
1757
|
+
disabled: typeof rules.disabled === 'object'
|
|
1758
|
+
? rules.disabled.clone()
|
|
1759
|
+
: rules.disabled,
|
|
1760
|
+
readonly: typeof rules.readonly === 'object'
|
|
1761
|
+
? rules.readonly.clone()
|
|
1762
|
+
: rules.readonly,
|
|
1763
|
+
metadata: rules.metadata,
|
|
1764
|
+
};
|
|
1765
|
+
cloned.fieldRules.set(field, clonedRules);
|
|
1766
|
+
}
|
|
1767
|
+
// Clone global validations
|
|
1768
|
+
cloned.globalValidations = this.globalValidations.map((spec) => spec.clone());
|
|
1769
|
+
return cloned;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
var TokenType;
|
|
1774
|
+
(function (TokenType) {
|
|
1775
|
+
// Literals
|
|
1776
|
+
TokenType["IDENTIFIER"] = "IDENTIFIER";
|
|
1777
|
+
TokenType["STRING"] = "STRING";
|
|
1778
|
+
TokenType["NUMBER"] = "NUMBER";
|
|
1779
|
+
TokenType["BOOLEAN"] = "BOOLEAN";
|
|
1780
|
+
TokenType["NULL"] = "NULL";
|
|
1781
|
+
// Operators
|
|
1782
|
+
TokenType["AND"] = "AND";
|
|
1783
|
+
TokenType["OR"] = "OR";
|
|
1784
|
+
TokenType["NOT"] = "NOT";
|
|
1785
|
+
TokenType["XOR"] = "XOR";
|
|
1786
|
+
TokenType["IMPLIES"] = "IMPLIES";
|
|
1787
|
+
// Comparison operators
|
|
1788
|
+
TokenType["EQUALS"] = "EQUALS";
|
|
1789
|
+
TokenType["NOT_EQUALS"] = "NOT_EQUALS";
|
|
1790
|
+
TokenType["LESS_THAN"] = "LESS_THAN";
|
|
1791
|
+
TokenType["LESS_THAN_OR_EQUAL"] = "LESS_THAN_OR_EQUAL";
|
|
1792
|
+
TokenType["GREATER_THAN"] = "GREATER_THAN";
|
|
1793
|
+
TokenType["GREATER_THAN_OR_EQUAL"] = "GREATER_THAN_OR_EQUAL";
|
|
1794
|
+
TokenType["IN"] = "IN";
|
|
1795
|
+
// Function tokens
|
|
1796
|
+
TokenType["CONTAINS"] = "CONTAINS";
|
|
1797
|
+
TokenType["STARTS_WITH"] = "STARTS_WITH";
|
|
1798
|
+
TokenType["ENDS_WITH"] = "ENDS_WITH";
|
|
1799
|
+
TokenType["AT_LEAST"] = "AT_LEAST";
|
|
1800
|
+
TokenType["EXACTLY"] = "EXACTLY";
|
|
1801
|
+
// Punctuation
|
|
1802
|
+
TokenType["LEFT_PAREN"] = "LEFT_PAREN";
|
|
1803
|
+
TokenType["RIGHT_PAREN"] = "RIGHT_PAREN";
|
|
1804
|
+
TokenType["LEFT_BRACKET"] = "LEFT_BRACKET";
|
|
1805
|
+
TokenType["RIGHT_BRACKET"] = "RIGHT_BRACKET";
|
|
1806
|
+
TokenType["COMMA"] = "COMMA";
|
|
1807
|
+
TokenType["DOT"] = "DOT";
|
|
1808
|
+
// Special
|
|
1809
|
+
TokenType["FIELD_REFERENCE"] = "FIELD_REFERENCE";
|
|
1810
|
+
TokenType["EOF"] = "EOF";
|
|
1811
|
+
TokenType["WHITESPACE"] = "WHITESPACE";
|
|
1812
|
+
})(TokenType || (TokenType = {}));
|
|
1813
|
+
const OPERATOR_KEYWORDS = {
|
|
1814
|
+
'and': TokenType.AND,
|
|
1815
|
+
'&&': TokenType.AND,
|
|
1816
|
+
'or': TokenType.OR,
|
|
1817
|
+
'||': TokenType.OR,
|
|
1818
|
+
'not': TokenType.NOT,
|
|
1819
|
+
'!': TokenType.NOT,
|
|
1820
|
+
'xor': TokenType.XOR,
|
|
1821
|
+
'implies': TokenType.IMPLIES,
|
|
1822
|
+
'in': TokenType.IN,
|
|
1823
|
+
'contains': TokenType.CONTAINS,
|
|
1824
|
+
'startsWith': TokenType.STARTS_WITH,
|
|
1825
|
+
'endsWith': TokenType.ENDS_WITH,
|
|
1826
|
+
'atLeast': TokenType.AT_LEAST,
|
|
1827
|
+
'exactly': TokenType.EXACTLY,
|
|
1828
|
+
'true': TokenType.BOOLEAN,
|
|
1829
|
+
'false': TokenType.BOOLEAN,
|
|
1830
|
+
'null': TokenType.NULL
|
|
1831
|
+
};
|
|
1832
|
+
const COMPARISON_OPERATORS = {
|
|
1833
|
+
'==': TokenType.EQUALS,
|
|
1834
|
+
'!=': TokenType.NOT_EQUALS,
|
|
1835
|
+
'<': TokenType.LESS_THAN,
|
|
1836
|
+
'<=': TokenType.LESS_THAN_OR_EQUAL,
|
|
1837
|
+
'>': TokenType.GREATER_THAN,
|
|
1838
|
+
'>=': TokenType.GREATER_THAN_OR_EQUAL
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1841
|
+
class DslTokenizer {
|
|
1842
|
+
input;
|
|
1843
|
+
position = 0;
|
|
1844
|
+
line = 1;
|
|
1845
|
+
column = 1;
|
|
1846
|
+
constructor(input) {
|
|
1847
|
+
this.input = input;
|
|
1848
|
+
}
|
|
1849
|
+
tokenize() {
|
|
1850
|
+
const tokens = [];
|
|
1851
|
+
while (this.position < this.input.length) {
|
|
1852
|
+
const token = this.nextToken();
|
|
1853
|
+
if (token.type !== TokenType.WHITESPACE) {
|
|
1854
|
+
tokens.push(token);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
tokens.push({
|
|
1858
|
+
type: TokenType.EOF,
|
|
1859
|
+
value: '',
|
|
1860
|
+
position: this.position,
|
|
1861
|
+
line: this.line,
|
|
1862
|
+
column: this.column
|
|
1863
|
+
});
|
|
1864
|
+
return tokens;
|
|
1865
|
+
}
|
|
1866
|
+
nextToken() {
|
|
1867
|
+
this.skipWhitespace();
|
|
1868
|
+
if (this.position >= this.input.length) {
|
|
1869
|
+
return this.createToken(TokenType.EOF, '');
|
|
1870
|
+
}
|
|
1871
|
+
const char = this.input[this.position];
|
|
1872
|
+
// Field references ${...}
|
|
1873
|
+
if (char === '$' && this.peek() === '{') {
|
|
1874
|
+
return this.readFieldReference();
|
|
1875
|
+
}
|
|
1876
|
+
// String literals
|
|
1877
|
+
if (char === '"' || char === "'") {
|
|
1878
|
+
return this.readString(char);
|
|
1879
|
+
}
|
|
1880
|
+
// Numbers
|
|
1881
|
+
if (this.isDigit(char) || (char === '-' && this.isDigit(this.peek()))) {
|
|
1882
|
+
return this.readNumber();
|
|
1883
|
+
}
|
|
1884
|
+
// Multi-character operators
|
|
1885
|
+
const twoChar = this.input.substr(this.position, 2);
|
|
1886
|
+
if (COMPARISON_OPERATORS[twoChar]) {
|
|
1887
|
+
const token = this.createToken(COMPARISON_OPERATORS[twoChar], twoChar);
|
|
1888
|
+
this.advance(2);
|
|
1889
|
+
return token;
|
|
1890
|
+
}
|
|
1891
|
+
// Single character operators and punctuation
|
|
1892
|
+
switch (char) {
|
|
1893
|
+
case '(':
|
|
1894
|
+
return this.createTokenAndAdvance(TokenType.LEFT_PAREN, char);
|
|
1895
|
+
case ')':
|
|
1896
|
+
return this.createTokenAndAdvance(TokenType.RIGHT_PAREN, char);
|
|
1897
|
+
case '[':
|
|
1898
|
+
return this.createTokenAndAdvance(TokenType.LEFT_BRACKET, char);
|
|
1899
|
+
case ']':
|
|
1900
|
+
return this.createTokenAndAdvance(TokenType.RIGHT_BRACKET, char);
|
|
1901
|
+
case ',':
|
|
1902
|
+
return this.createTokenAndAdvance(TokenType.COMMA, char);
|
|
1903
|
+
case '.':
|
|
1904
|
+
return this.createTokenAndAdvance(TokenType.DOT, char);
|
|
1905
|
+
case '!':
|
|
1906
|
+
if (this.peek() === '=') {
|
|
1907
|
+
const token = this.createToken(TokenType.NOT_EQUALS, '!=');
|
|
1908
|
+
this.advance(2);
|
|
1909
|
+
return token;
|
|
1910
|
+
}
|
|
1911
|
+
return this.createTokenAndAdvance(TokenType.NOT, char);
|
|
1912
|
+
case '<':
|
|
1913
|
+
if (this.peek() === '=') {
|
|
1914
|
+
const token = this.createToken(TokenType.LESS_THAN_OR_EQUAL, '<=');
|
|
1915
|
+
this.advance(2);
|
|
1916
|
+
return token;
|
|
1917
|
+
}
|
|
1918
|
+
return this.createTokenAndAdvance(TokenType.LESS_THAN, char);
|
|
1919
|
+
case '>':
|
|
1920
|
+
if (this.peek() === '=') {
|
|
1921
|
+
const token = this.createToken(TokenType.GREATER_THAN_OR_EQUAL, '>=');
|
|
1922
|
+
this.advance(2);
|
|
1923
|
+
return token;
|
|
1924
|
+
}
|
|
1925
|
+
return this.createTokenAndAdvance(TokenType.GREATER_THAN, char);
|
|
1926
|
+
case '=':
|
|
1927
|
+
if (this.peek() === '=') {
|
|
1928
|
+
const token = this.createToken(TokenType.EQUALS, '==');
|
|
1929
|
+
this.advance(2);
|
|
1930
|
+
return token;
|
|
1931
|
+
}
|
|
1932
|
+
break;
|
|
1933
|
+
case '&':
|
|
1934
|
+
if (this.peek() === '&') {
|
|
1935
|
+
const token = this.createToken(TokenType.AND, '&&');
|
|
1936
|
+
this.advance(2);
|
|
1937
|
+
return token;
|
|
1938
|
+
}
|
|
1939
|
+
break;
|
|
1940
|
+
case '|':
|
|
1941
|
+
if (this.peek() === '|') {
|
|
1942
|
+
const token = this.createToken(TokenType.OR, '||');
|
|
1943
|
+
this.advance(2);
|
|
1944
|
+
return token;
|
|
1945
|
+
}
|
|
1946
|
+
break;
|
|
1947
|
+
}
|
|
1948
|
+
// Identifiers and keywords
|
|
1949
|
+
if (this.isAlpha(char) || char === '_') {
|
|
1950
|
+
return this.readIdentifier();
|
|
1951
|
+
}
|
|
1952
|
+
throw new Error(`Unexpected character '${char}' at position ${this.position}`);
|
|
1953
|
+
}
|
|
1954
|
+
readFieldReference() {
|
|
1955
|
+
const start = this.position;
|
|
1956
|
+
this.advance(2); // Skip ${
|
|
1957
|
+
let value = '';
|
|
1958
|
+
while (this.position < this.input.length && this.input[this.position] !== '}') {
|
|
1959
|
+
value += this.input[this.position];
|
|
1960
|
+
this.advance();
|
|
1961
|
+
}
|
|
1962
|
+
if (this.position >= this.input.length) {
|
|
1963
|
+
throw new Error('Unterminated field reference');
|
|
1964
|
+
}
|
|
1965
|
+
this.advance(); // Skip }
|
|
1966
|
+
return this.createToken(TokenType.FIELD_REFERENCE, value, start);
|
|
1967
|
+
}
|
|
1968
|
+
readString(quote) {
|
|
1969
|
+
const start = this.position;
|
|
1970
|
+
this.advance(); // Skip opening quote
|
|
1971
|
+
let value = '';
|
|
1972
|
+
while (this.position < this.input.length && this.input[this.position] !== quote) {
|
|
1973
|
+
if (this.input[this.position] === '\\') {
|
|
1974
|
+
this.advance();
|
|
1975
|
+
if (this.position < this.input.length) {
|
|
1976
|
+
const escaped = this.input[this.position];
|
|
1977
|
+
switch (escaped) {
|
|
1978
|
+
case 'n':
|
|
1979
|
+
value += '\n';
|
|
1980
|
+
break;
|
|
1981
|
+
case 't':
|
|
1982
|
+
value += '\t';
|
|
1983
|
+
break;
|
|
1984
|
+
case 'r':
|
|
1985
|
+
value += '\r';
|
|
1986
|
+
break;
|
|
1987
|
+
case '\\':
|
|
1988
|
+
value += '\\';
|
|
1989
|
+
break;
|
|
1990
|
+
case '"':
|
|
1991
|
+
value += '"';
|
|
1992
|
+
break;
|
|
1993
|
+
case "'":
|
|
1994
|
+
value += "'";
|
|
1995
|
+
break;
|
|
1996
|
+
default:
|
|
1997
|
+
value += escaped;
|
|
1998
|
+
break;
|
|
1999
|
+
}
|
|
2000
|
+
this.advance();
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
else {
|
|
2004
|
+
value += this.input[this.position];
|
|
2005
|
+
this.advance();
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
if (this.position >= this.input.length) {
|
|
2009
|
+
throw new Error('Unterminated string literal');
|
|
2010
|
+
}
|
|
2011
|
+
this.advance(); // Skip closing quote
|
|
2012
|
+
return this.createToken(TokenType.STRING, value, start);
|
|
2013
|
+
}
|
|
2014
|
+
readNumber() {
|
|
2015
|
+
const start = this.position;
|
|
2016
|
+
let value = '';
|
|
2017
|
+
if (this.input[this.position] === '-') {
|
|
2018
|
+
value += this.input[this.position];
|
|
2019
|
+
this.advance();
|
|
2020
|
+
}
|
|
2021
|
+
while (this.position < this.input.length && this.isDigit(this.input[this.position])) {
|
|
2022
|
+
value += this.input[this.position];
|
|
2023
|
+
this.advance();
|
|
2024
|
+
}
|
|
2025
|
+
if (this.position < this.input.length && this.input[this.position] === '.') {
|
|
2026
|
+
value += this.input[this.position];
|
|
2027
|
+
this.advance();
|
|
2028
|
+
while (this.position < this.input.length && this.isDigit(this.input[this.position])) {
|
|
2029
|
+
value += this.input[this.position];
|
|
2030
|
+
this.advance();
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return this.createToken(TokenType.NUMBER, value, start);
|
|
2034
|
+
}
|
|
2035
|
+
readIdentifier() {
|
|
2036
|
+
const start = this.position;
|
|
2037
|
+
let value = '';
|
|
2038
|
+
while (this.position < this.input.length &&
|
|
2039
|
+
(this.isAlphaNumeric(this.input[this.position]) || this.input[this.position] === '_')) {
|
|
2040
|
+
value += this.input[this.position];
|
|
2041
|
+
this.advance();
|
|
2042
|
+
}
|
|
2043
|
+
const tokenType = OPERATOR_KEYWORDS[value] || TokenType.IDENTIFIER;
|
|
2044
|
+
return this.createToken(tokenType, value, start);
|
|
2045
|
+
}
|
|
2046
|
+
skipWhitespace() {
|
|
2047
|
+
while (this.position < this.input.length && this.isWhitespace(this.input[this.position])) {
|
|
2048
|
+
if (this.input[this.position] === '\n') {
|
|
2049
|
+
this.line++;
|
|
2050
|
+
this.column = 1;
|
|
2051
|
+
}
|
|
2052
|
+
else {
|
|
2053
|
+
this.column++;
|
|
2054
|
+
}
|
|
2055
|
+
this.position++;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
createToken(type, value, startPos) {
|
|
2059
|
+
return {
|
|
2060
|
+
type,
|
|
2061
|
+
value,
|
|
2062
|
+
position: startPos ?? this.position,
|
|
2063
|
+
line: this.line,
|
|
2064
|
+
column: this.column - value.length
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
createTokenAndAdvance(type, value) {
|
|
2068
|
+
const token = this.createToken(type, value);
|
|
2069
|
+
this.advance();
|
|
2070
|
+
return token;
|
|
2071
|
+
}
|
|
2072
|
+
advance(count = 1) {
|
|
2073
|
+
for (let i = 0; i < count && this.position < this.input.length; i++) {
|
|
2074
|
+
if (this.input[this.position] === '\n') {
|
|
2075
|
+
this.line++;
|
|
2076
|
+
this.column = 1;
|
|
2077
|
+
}
|
|
2078
|
+
else {
|
|
2079
|
+
this.column++;
|
|
2080
|
+
}
|
|
2081
|
+
this.position++;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
peek(offset = 1) {
|
|
2085
|
+
const pos = this.position + offset;
|
|
2086
|
+
return pos < this.input.length ? this.input[pos] : '';
|
|
2087
|
+
}
|
|
2088
|
+
isDigit(char) {
|
|
2089
|
+
return char >= '0' && char <= '9';
|
|
2090
|
+
}
|
|
2091
|
+
isAlpha(char) {
|
|
2092
|
+
return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z');
|
|
2093
|
+
}
|
|
2094
|
+
isAlphaNumeric(char) {
|
|
2095
|
+
return this.isAlpha(char) || this.isDigit(char);
|
|
2096
|
+
}
|
|
2097
|
+
isWhitespace(char) {
|
|
2098
|
+
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
class DslParser {
|
|
2103
|
+
functionRegistry;
|
|
2104
|
+
tokens = [];
|
|
2105
|
+
current = 0;
|
|
2106
|
+
constructor(functionRegistry) {
|
|
2107
|
+
this.functionRegistry = functionRegistry;
|
|
2108
|
+
}
|
|
2109
|
+
parse(input) {
|
|
2110
|
+
const tokenizer = new DslTokenizer(input);
|
|
2111
|
+
this.tokens = tokenizer.tokenize();
|
|
2112
|
+
this.current = 0;
|
|
2113
|
+
const spec = this.parseExpression();
|
|
2114
|
+
if (!this.isAtEnd()) {
|
|
2115
|
+
throw new Error(`Unexpected token: ${this.peek().value}`);
|
|
2116
|
+
}
|
|
2117
|
+
return spec;
|
|
2118
|
+
}
|
|
2119
|
+
parseExpression() {
|
|
2120
|
+
return this.parseImplies();
|
|
2121
|
+
}
|
|
2122
|
+
parseImplies() {
|
|
2123
|
+
let expr = this.parseXor();
|
|
2124
|
+
while (this.match(TokenType.IMPLIES)) {
|
|
2125
|
+
const right = this.parseXor();
|
|
2126
|
+
expr = new ImpliesSpecification(expr, right);
|
|
2127
|
+
}
|
|
2128
|
+
return expr;
|
|
2129
|
+
}
|
|
2130
|
+
parseXor() {
|
|
2131
|
+
let expr = this.parseOr();
|
|
2132
|
+
while (this.match(TokenType.XOR)) {
|
|
2133
|
+
const right = this.parseOr();
|
|
2134
|
+
if (expr instanceof XorSpecification) {
|
|
2135
|
+
expr = new XorSpecification([...expr.getSpecifications(), right]);
|
|
2136
|
+
}
|
|
2137
|
+
else {
|
|
2138
|
+
expr = new XorSpecification([expr, right]);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return expr;
|
|
2142
|
+
}
|
|
2143
|
+
parseOr() {
|
|
2144
|
+
let expr = this.parseAnd();
|
|
2145
|
+
while (this.match(TokenType.OR)) {
|
|
2146
|
+
const right = this.parseAnd();
|
|
2147
|
+
if (expr instanceof OrSpecification) {
|
|
2148
|
+
expr = new OrSpecification([...expr.getSpecifications(), right]);
|
|
2149
|
+
}
|
|
2150
|
+
else {
|
|
2151
|
+
expr = new OrSpecification([expr, right]);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return expr;
|
|
2155
|
+
}
|
|
2156
|
+
parseAnd() {
|
|
2157
|
+
let expr = this.parseUnary();
|
|
2158
|
+
while (this.match(TokenType.AND)) {
|
|
2159
|
+
const right = this.parseUnary();
|
|
2160
|
+
if (expr instanceof AndSpecification) {
|
|
2161
|
+
expr = new AndSpecification([...expr.getSpecifications(), right]);
|
|
2162
|
+
}
|
|
2163
|
+
else {
|
|
2164
|
+
expr = new AndSpecification([expr, right]);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return expr;
|
|
2168
|
+
}
|
|
2169
|
+
parseUnary() {
|
|
2170
|
+
if (this.match(TokenType.NOT)) {
|
|
2171
|
+
const expr = this.parseUnary();
|
|
2172
|
+
return new NotSpecification(expr);
|
|
2173
|
+
}
|
|
2174
|
+
return this.parseComparison();
|
|
2175
|
+
}
|
|
2176
|
+
parseComparison() {
|
|
2177
|
+
let left = this.parsePrimary();
|
|
2178
|
+
if (this.matchComparison()) {
|
|
2179
|
+
const operator = this.previous().type;
|
|
2180
|
+
// Parse RHS allowing literals, arrays and identifiers (as values)
|
|
2181
|
+
const rightValue = this.parseArgument();
|
|
2182
|
+
// Field to field comparison not implemented
|
|
2183
|
+
if (left instanceof FieldSpecification && rightValue instanceof FieldSpecification) {
|
|
2184
|
+
throw new Error('Field to field comparison not yet implemented in parser');
|
|
2185
|
+
}
|
|
2186
|
+
if (left instanceof FieldSpecification) {
|
|
2187
|
+
const field = left.getField();
|
|
2188
|
+
const compOp = this.tokenTypeToComparisonOperator(operator);
|
|
2189
|
+
return new FieldSpecification(field, compOp, rightValue);
|
|
2190
|
+
}
|
|
2191
|
+
throw new Error('Invalid comparison expression');
|
|
2192
|
+
}
|
|
2193
|
+
return left;
|
|
2194
|
+
}
|
|
2195
|
+
parsePrimary() {
|
|
2196
|
+
if (this.match(TokenType.LEFT_PAREN)) {
|
|
2197
|
+
const expr = this.parseExpression();
|
|
2198
|
+
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression");
|
|
2199
|
+
return expr;
|
|
2200
|
+
}
|
|
2201
|
+
if (this.match(TokenType.FIELD_REFERENCE)) {
|
|
2202
|
+
const fieldName = this.previous().value;
|
|
2203
|
+
return new FieldSpecification(fieldName, ComparisonOperator.EQUALS, true);
|
|
2204
|
+
}
|
|
2205
|
+
if (this.match(TokenType.IDENTIFIER)) {
|
|
2206
|
+
const identifier = this.previous().value;
|
|
2207
|
+
// Check if this is a function call
|
|
2208
|
+
if (this.match(TokenType.LEFT_PAREN)) {
|
|
2209
|
+
return this.parseFunctionCall(identifier);
|
|
2210
|
+
}
|
|
2211
|
+
// Otherwise treat as field reference
|
|
2212
|
+
return new FieldSpecification(identifier, ComparisonOperator.EQUALS, true);
|
|
2213
|
+
}
|
|
2214
|
+
throw new Error(`Unexpected token: ${this.peek().value}`);
|
|
2215
|
+
}
|
|
2216
|
+
parseFunctionCall(functionName) {
|
|
2217
|
+
const args = [];
|
|
2218
|
+
if (!this.check(TokenType.RIGHT_PAREN)) {
|
|
2219
|
+
do {
|
|
2220
|
+
args.push(this.parseArgument());
|
|
2221
|
+
} while (this.match(TokenType.COMMA));
|
|
2222
|
+
}
|
|
2223
|
+
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after function arguments");
|
|
2224
|
+
// Handle special built-in functions
|
|
2225
|
+
switch (functionName) {
|
|
2226
|
+
case 'atLeast':
|
|
2227
|
+
if (args.length !== 2) {
|
|
2228
|
+
throw new Error('atLeast requires exactly 2 arguments');
|
|
2229
|
+
}
|
|
2230
|
+
const min = args[0];
|
|
2231
|
+
const specs = args[1];
|
|
2232
|
+
if (!Array.isArray(specs)) {
|
|
2233
|
+
throw new Error('atLeast second argument must be an array of specifications');
|
|
2234
|
+
}
|
|
2235
|
+
return new AtLeastSpecification(min, specs);
|
|
2236
|
+
case 'exactly':
|
|
2237
|
+
if (args.length !== 2) {
|
|
2238
|
+
throw new Error('exactly requires exactly 2 arguments');
|
|
2239
|
+
}
|
|
2240
|
+
const exact = args[0];
|
|
2241
|
+
const exactSpecs = args[1];
|
|
2242
|
+
if (!Array.isArray(exactSpecs)) {
|
|
2243
|
+
throw new Error('exactly second argument must be an array of specifications');
|
|
2244
|
+
}
|
|
2245
|
+
return new ExactlySpecification(exact, exactSpecs);
|
|
2246
|
+
case 'contains':
|
|
2247
|
+
case 'startsWith':
|
|
2248
|
+
case 'endsWith':
|
|
2249
|
+
if (args.length !== 2) {
|
|
2250
|
+
throw new Error(`${functionName} requires exactly 2 arguments`);
|
|
2251
|
+
}
|
|
2252
|
+
const field = args[0];
|
|
2253
|
+
const value = args[1];
|
|
2254
|
+
const op = this.functionNameToOperator(functionName);
|
|
2255
|
+
return new FieldSpecification(field, op, value);
|
|
2256
|
+
default:
|
|
2257
|
+
return new FunctionSpecification(functionName, args, this.functionRegistry);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
parseArgument() {
|
|
2261
|
+
if (this.match(TokenType.STRING)) {
|
|
2262
|
+
return this.previous().value;
|
|
2263
|
+
}
|
|
2264
|
+
if (this.match(TokenType.NUMBER)) {
|
|
2265
|
+
const value = this.previous().value;
|
|
2266
|
+
return value.includes('.') ? parseFloat(value) : parseInt(value, 10);
|
|
2267
|
+
}
|
|
2268
|
+
if (this.match(TokenType.BOOLEAN)) {
|
|
2269
|
+
return this.previous().value === 'true';
|
|
2270
|
+
}
|
|
2271
|
+
if (this.match(TokenType.NULL)) {
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
if (this.match(TokenType.FIELD_REFERENCE)) {
|
|
2275
|
+
return this.previous().value;
|
|
2276
|
+
}
|
|
2277
|
+
if (this.match(TokenType.IDENTIFIER)) {
|
|
2278
|
+
return this.previous().value;
|
|
2279
|
+
}
|
|
2280
|
+
if (this.match(TokenType.LEFT_BRACKET)) {
|
|
2281
|
+
const elements = [];
|
|
2282
|
+
if (!this.check(TokenType.RIGHT_BRACKET)) {
|
|
2283
|
+
do {
|
|
2284
|
+
elements.push(this.parseArgument());
|
|
2285
|
+
} while (this.match(TokenType.COMMA));
|
|
2286
|
+
}
|
|
2287
|
+
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after array elements");
|
|
2288
|
+
return elements;
|
|
2289
|
+
}
|
|
2290
|
+
throw new Error(`Unexpected token in argument: ${this.peek().value}`);
|
|
2291
|
+
}
|
|
2292
|
+
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);
|
|
2294
|
+
}
|
|
2295
|
+
tokenTypeToComparisonOperator(tokenType) {
|
|
2296
|
+
switch (tokenType) {
|
|
2297
|
+
case TokenType.EQUALS:
|
|
2298
|
+
return ComparisonOperator.EQUALS;
|
|
2299
|
+
case TokenType.NOT_EQUALS:
|
|
2300
|
+
return ComparisonOperator.NOT_EQUALS;
|
|
2301
|
+
case TokenType.LESS_THAN:
|
|
2302
|
+
return ComparisonOperator.LESS_THAN;
|
|
2303
|
+
case TokenType.LESS_THAN_OR_EQUAL:
|
|
2304
|
+
return ComparisonOperator.LESS_THAN_OR_EQUAL;
|
|
2305
|
+
case TokenType.GREATER_THAN:
|
|
2306
|
+
return ComparisonOperator.GREATER_THAN;
|
|
2307
|
+
case TokenType.GREATER_THAN_OR_EQUAL:
|
|
2308
|
+
return ComparisonOperator.GREATER_THAN_OR_EQUAL;
|
|
2309
|
+
case TokenType.IN:
|
|
2310
|
+
return ComparisonOperator.IN;
|
|
2311
|
+
default:
|
|
2312
|
+
throw new Error(`Unknown comparison operator: ${tokenType}`);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
functionNameToOperator(functionName) {
|
|
2316
|
+
switch (functionName) {
|
|
2317
|
+
case 'contains':
|
|
2318
|
+
return ComparisonOperator.CONTAINS;
|
|
2319
|
+
case 'startsWith':
|
|
2320
|
+
return ComparisonOperator.STARTS_WITH;
|
|
2321
|
+
case 'endsWith':
|
|
2322
|
+
return ComparisonOperator.ENDS_WITH;
|
|
2323
|
+
default:
|
|
2324
|
+
throw new Error(`Unknown function: ${functionName}`);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
extractValue(spec) {
|
|
2328
|
+
// This is a simplified extraction - in a real implementation
|
|
2329
|
+
// we'd need to handle more complex cases
|
|
2330
|
+
if (spec instanceof FieldSpecification) {
|
|
2331
|
+
return spec.getValue();
|
|
2332
|
+
}
|
|
2333
|
+
return spec;
|
|
2334
|
+
}
|
|
2335
|
+
match(...types) {
|
|
2336
|
+
for (const type of types) {
|
|
2337
|
+
if (this.check(type)) {
|
|
2338
|
+
this.advance();
|
|
2339
|
+
return true;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return false;
|
|
2343
|
+
}
|
|
2344
|
+
check(type) {
|
|
2345
|
+
if (this.isAtEnd())
|
|
2346
|
+
return false;
|
|
2347
|
+
return this.peek().type === type;
|
|
2348
|
+
}
|
|
2349
|
+
advance() {
|
|
2350
|
+
if (!this.isAtEnd())
|
|
2351
|
+
this.current++;
|
|
2352
|
+
return this.previous();
|
|
2353
|
+
}
|
|
2354
|
+
isAtEnd() {
|
|
2355
|
+
return this.peek().type === TokenType.EOF;
|
|
2356
|
+
}
|
|
2357
|
+
peek() {
|
|
2358
|
+
return this.tokens[this.current];
|
|
2359
|
+
}
|
|
2360
|
+
previous() {
|
|
2361
|
+
return this.tokens[this.current - 1];
|
|
2362
|
+
}
|
|
2363
|
+
consume(type, message) {
|
|
2364
|
+
if (this.check(type))
|
|
2365
|
+
return this.advance();
|
|
2366
|
+
throw new Error(`${message}. Got: ${this.peek().value}`);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
class DslExporter {
|
|
2371
|
+
options;
|
|
2372
|
+
constructor(options = {}) {
|
|
2373
|
+
this.options = {
|
|
2374
|
+
prettyPrint: options.prettyPrint ?? false,
|
|
2375
|
+
indentSize: options.indentSize ?? 2,
|
|
2376
|
+
maxLineLength: options.maxLineLength ?? 80,
|
|
2377
|
+
useParentheses: options.useParentheses ?? 'auto',
|
|
2378
|
+
includeMetadata: options.includeMetadata ?? false,
|
|
2379
|
+
metadataPosition: options.metadataPosition ?? 'before',
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
export(specification) {
|
|
2383
|
+
const dsl = this.exportSpecification(specification, 0);
|
|
2384
|
+
if (this.options.prettyPrint) {
|
|
2385
|
+
return this.formatPretty(dsl);
|
|
2386
|
+
}
|
|
2387
|
+
return dsl;
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Exports specification with metadata comments
|
|
2391
|
+
*/
|
|
2392
|
+
exportWithMetadata(specification) {
|
|
2393
|
+
const originalIncludeMetadata = this.options.includeMetadata;
|
|
2394
|
+
this.options.includeMetadata = true;
|
|
2395
|
+
const result = this.export(specification);
|
|
2396
|
+
this.options.includeMetadata = originalIncludeMetadata;
|
|
2397
|
+
return result;
|
|
2398
|
+
}
|
|
2399
|
+
exportSpecification(spec, depth) {
|
|
2400
|
+
const baseDsl = this.getSpecificationDsl(spec, depth);
|
|
2401
|
+
if (!this.options.includeMetadata) {
|
|
2402
|
+
return baseDsl;
|
|
2403
|
+
}
|
|
2404
|
+
return this.addMetadataComments(spec, baseDsl, depth);
|
|
2405
|
+
}
|
|
2406
|
+
getSpecificationDsl(spec, depth) {
|
|
2407
|
+
if (spec instanceof FieldSpecification) {
|
|
2408
|
+
return this.exportFieldSpecification(spec);
|
|
2409
|
+
}
|
|
2410
|
+
if (spec instanceof AndSpecification) {
|
|
2411
|
+
return this.exportAndSpecification(spec, depth);
|
|
2412
|
+
}
|
|
2413
|
+
if (spec instanceof OrSpecification) {
|
|
2414
|
+
return this.exportOrSpecification(spec, depth);
|
|
2415
|
+
}
|
|
2416
|
+
if (spec instanceof NotSpecification) {
|
|
2417
|
+
return this.exportNotSpecification(spec, depth);
|
|
2418
|
+
}
|
|
2419
|
+
if (spec instanceof XorSpecification) {
|
|
2420
|
+
return this.exportXorSpecification(spec, depth);
|
|
2421
|
+
}
|
|
2422
|
+
if (spec instanceof ImpliesSpecification) {
|
|
2423
|
+
return this.exportImpliesSpecification(spec, depth);
|
|
2424
|
+
}
|
|
2425
|
+
if (spec instanceof FunctionSpecification) {
|
|
2426
|
+
return this.exportFunctionSpecification(spec);
|
|
2427
|
+
}
|
|
2428
|
+
if (spec instanceof AtLeastSpecification) {
|
|
2429
|
+
return this.exportAtLeastSpecification(spec, depth);
|
|
2430
|
+
}
|
|
2431
|
+
if (spec instanceof ExactlySpecification) {
|
|
2432
|
+
return this.exportExactlySpecification(spec, depth);
|
|
2433
|
+
}
|
|
2434
|
+
if (spec instanceof FieldToFieldSpecification) {
|
|
2435
|
+
return this.exportFieldToFieldSpecification(spec);
|
|
2436
|
+
}
|
|
2437
|
+
if (spec instanceof ContextualSpecification) {
|
|
2438
|
+
return this.exportContextualSpecification(spec);
|
|
2439
|
+
}
|
|
2440
|
+
// Fallback to toDSL method
|
|
2441
|
+
return spec.toDSL();
|
|
2442
|
+
}
|
|
2443
|
+
addMetadataComments(spec, dsl, depth) {
|
|
2444
|
+
const metadata = spec.getMetadata();
|
|
2445
|
+
if (!metadata) {
|
|
2446
|
+
return dsl;
|
|
2447
|
+
}
|
|
2448
|
+
const indent = ' '.repeat(depth * this.options.indentSize);
|
|
2449
|
+
const comments = [];
|
|
2450
|
+
// Add metadata comments
|
|
2451
|
+
if (metadata.code) {
|
|
2452
|
+
comments.push(`${indent}// code: ${metadata.code}`);
|
|
2453
|
+
}
|
|
2454
|
+
if (metadata.message) {
|
|
2455
|
+
comments.push(`${indent}// message: ${metadata.message}`);
|
|
2456
|
+
}
|
|
2457
|
+
if (metadata.tag) {
|
|
2458
|
+
comments.push(`${indent}// tag: ${metadata.tag}`);
|
|
2459
|
+
}
|
|
2460
|
+
if (metadata.uiConfig) {
|
|
2461
|
+
const uiConfigStr = JSON.stringify(metadata.uiConfig);
|
|
2462
|
+
comments.push(`${indent}// uiConfig: ${uiConfigStr}`);
|
|
2463
|
+
}
|
|
2464
|
+
// Add any other metadata properties
|
|
2465
|
+
const standardKeys = ['code', 'message', 'tag', 'uiConfig'];
|
|
2466
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
2467
|
+
if (!standardKeys.includes(key) && value !== undefined) {
|
|
2468
|
+
comments.push(`${indent}// ${key}: ${JSON.stringify(value)}`);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if (comments.length === 0) {
|
|
2472
|
+
return dsl;
|
|
2473
|
+
}
|
|
2474
|
+
switch (this.options.metadataPosition) {
|
|
2475
|
+
case 'before':
|
|
2476
|
+
return `${comments.join('\n')}\n${indent}${dsl}`;
|
|
2477
|
+
case 'after':
|
|
2478
|
+
return `${indent}${dsl}\n${comments.join('\n')}`;
|
|
2479
|
+
case 'inline':
|
|
2480
|
+
const firstComment = comments[0] ? ` ${comments[0].trim()}` : '';
|
|
2481
|
+
return `${indent}${dsl}${firstComment}`;
|
|
2482
|
+
default:
|
|
2483
|
+
return dsl;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
exportFieldSpecification(spec) {
|
|
2487
|
+
return spec.toDSL();
|
|
2488
|
+
}
|
|
2489
|
+
exportAndSpecification(spec, depth) {
|
|
2490
|
+
const specifications = spec.getSpecifications();
|
|
2491
|
+
const parts = specifications.map((s) => {
|
|
2492
|
+
const dsl = this.exportSpecification(s, depth + 1);
|
|
2493
|
+
return this.needsParentheses(s, 'and') ? `(${dsl})` : dsl;
|
|
2494
|
+
});
|
|
2495
|
+
if (this.options.prettyPrint && this.shouldBreakLine(parts.join(' && '))) {
|
|
2496
|
+
const indent = ' '.repeat(depth * this.options.indentSize);
|
|
2497
|
+
const nextIndent = ' '.repeat((depth + 1) * this.options.indentSize);
|
|
2498
|
+
return parts.join(` &&\n${nextIndent}`);
|
|
2499
|
+
}
|
|
2500
|
+
return parts.join(' && ');
|
|
2501
|
+
}
|
|
2502
|
+
exportOrSpecification(spec, depth) {
|
|
2503
|
+
const specifications = spec.getSpecifications();
|
|
2504
|
+
const parts = specifications.map((s) => {
|
|
2505
|
+
const dsl = this.exportSpecification(s, depth + 1);
|
|
2506
|
+
return this.needsParentheses(s, 'or') ? `(${dsl})` : dsl;
|
|
2507
|
+
});
|
|
2508
|
+
if (this.options.prettyPrint && this.shouldBreakLine(parts.join(' || '))) {
|
|
2509
|
+
const nextIndent = ' '.repeat((depth + 1) * this.options.indentSize);
|
|
2510
|
+
return parts.join(` ||\n${nextIndent}`);
|
|
2511
|
+
}
|
|
2512
|
+
return parts.join(' || ');
|
|
2513
|
+
}
|
|
2514
|
+
exportNotSpecification(spec, depth) {
|
|
2515
|
+
const innerSpec = spec.getSpecification();
|
|
2516
|
+
const innerDsl = this.exportSpecification(innerSpec, depth);
|
|
2517
|
+
if (this.needsParentheses(innerSpec, 'not')) {
|
|
2518
|
+
return `!(${innerDsl})`;
|
|
2519
|
+
}
|
|
2520
|
+
return `!${innerDsl}`;
|
|
2521
|
+
}
|
|
2522
|
+
exportXorSpecification(spec, depth) {
|
|
2523
|
+
const specifications = spec.getSpecifications();
|
|
2524
|
+
const parts = specifications.map((s) => {
|
|
2525
|
+
const dsl = this.exportSpecification(s, depth + 1);
|
|
2526
|
+
return this.needsParentheses(s, 'xor') ? `(${dsl})` : dsl;
|
|
2527
|
+
});
|
|
2528
|
+
if (this.options.prettyPrint && this.shouldBreakLine(parts.join(' xor '))) {
|
|
2529
|
+
const nextIndent = ' '.repeat((depth + 1) * this.options.indentSize);
|
|
2530
|
+
return parts.join(` xor\n${nextIndent}`);
|
|
2531
|
+
}
|
|
2532
|
+
return parts.join(' xor ');
|
|
2533
|
+
}
|
|
2534
|
+
exportImpliesSpecification(spec, depth) {
|
|
2535
|
+
const antecedent = this.exportSpecification(spec.getAntecedent(), depth + 1);
|
|
2536
|
+
const consequent = this.exportSpecification(spec.getConsequent(), depth + 1);
|
|
2537
|
+
const leftPart = this.needsParentheses(spec.getAntecedent(), 'implies')
|
|
2538
|
+
? `(${antecedent})`
|
|
2539
|
+
: antecedent;
|
|
2540
|
+
const rightPart = this.needsParentheses(spec.getConsequent(), 'implies')
|
|
2541
|
+
? `(${consequent})`
|
|
2542
|
+
: consequent;
|
|
2543
|
+
if (this.options.prettyPrint &&
|
|
2544
|
+
this.shouldBreakLine(`${leftPart} implies ${rightPart}`)) {
|
|
2545
|
+
const nextIndent = ' '.repeat((depth + 1) * this.options.indentSize);
|
|
2546
|
+
return `${leftPart} implies\n${nextIndent}${rightPart}`;
|
|
2547
|
+
}
|
|
2548
|
+
return `${leftPart} implies ${rightPart}`;
|
|
2549
|
+
}
|
|
2550
|
+
exportFunctionSpecification(spec) {
|
|
2551
|
+
return spec.toDSL();
|
|
2552
|
+
}
|
|
2553
|
+
exportAtLeastSpecification(spec, depth) {
|
|
2554
|
+
const minimum = spec.getMinimum();
|
|
2555
|
+
const specifications = spec.getSpecifications();
|
|
2556
|
+
const specDsls = specifications.map((s) => this.exportSpecification(s, depth + 1));
|
|
2557
|
+
if (this.options.prettyPrint && specDsls.length > 2) {
|
|
2558
|
+
const nextIndent = ' '.repeat((depth + 1) * this.options.indentSize);
|
|
2559
|
+
const specsStr = specDsls.join(`,\n${nextIndent}`);
|
|
2560
|
+
return `atLeast(${minimum}, [\n${nextIndent}${specsStr}\n${' '.repeat(depth * this.options.indentSize)}])`;
|
|
2561
|
+
}
|
|
2562
|
+
return `atLeast(${minimum}, [${specDsls.join(', ')}])`;
|
|
2563
|
+
}
|
|
2564
|
+
exportExactlySpecification(spec, depth) {
|
|
2565
|
+
const exact = spec.getExact();
|
|
2566
|
+
const specifications = spec.getSpecifications();
|
|
2567
|
+
const specDsls = specifications.map((s) => this.exportSpecification(s, depth + 1));
|
|
2568
|
+
if (this.options.prettyPrint && specDsls.length > 2) {
|
|
2569
|
+
const nextIndent = ' '.repeat((depth + 1) * this.options.indentSize);
|
|
2570
|
+
const specsStr = specDsls.join(`,\n${nextIndent}`);
|
|
2571
|
+
return `exactly(${exact}, [\n${nextIndent}${specsStr}\n${' '.repeat(depth * this.options.indentSize)}])`;
|
|
2572
|
+
}
|
|
2573
|
+
return `exactly(${exact}, [${specDsls.join(', ')}])`;
|
|
2574
|
+
}
|
|
2575
|
+
exportFieldToFieldSpecification(spec) {
|
|
2576
|
+
return spec.toDSL();
|
|
2577
|
+
}
|
|
2578
|
+
exportContextualSpecification(spec) {
|
|
2579
|
+
return spec.toDSL();
|
|
2580
|
+
}
|
|
2581
|
+
needsParentheses(spec, parentContext) {
|
|
2582
|
+
if (this.options.useParentheses === 'explicit') {
|
|
2583
|
+
return true;
|
|
2584
|
+
}
|
|
2585
|
+
if (this.options.useParentheses === 'minimal') {
|
|
2586
|
+
return false;
|
|
2587
|
+
}
|
|
2588
|
+
// Auto mode - determine based on operator precedence
|
|
2589
|
+
const precedence = this.getOperatorPrecedence(spec);
|
|
2590
|
+
const parentPrecedence = this.getContextPrecedence(parentContext);
|
|
2591
|
+
return precedence < parentPrecedence;
|
|
2592
|
+
}
|
|
2593
|
+
getOperatorPrecedence(spec) {
|
|
2594
|
+
if (spec instanceof NotSpecification)
|
|
2595
|
+
return 5;
|
|
2596
|
+
if (spec instanceof AndSpecification)
|
|
2597
|
+
return 4;
|
|
2598
|
+
if (spec instanceof OrSpecification)
|
|
2599
|
+
return 3;
|
|
2600
|
+
if (spec instanceof XorSpecification)
|
|
2601
|
+
return 2;
|
|
2602
|
+
if (spec instanceof ImpliesSpecification)
|
|
2603
|
+
return 1;
|
|
2604
|
+
return 6; // Highest precedence for primitives
|
|
2605
|
+
}
|
|
2606
|
+
getContextPrecedence(context) {
|
|
2607
|
+
switch (context) {
|
|
2608
|
+
case 'not':
|
|
2609
|
+
return 5;
|
|
2610
|
+
case 'and':
|
|
2611
|
+
return 4;
|
|
2612
|
+
case 'or':
|
|
2613
|
+
return 3;
|
|
2614
|
+
case 'xor':
|
|
2615
|
+
return 2;
|
|
2616
|
+
case 'implies':
|
|
2617
|
+
return 1;
|
|
2618
|
+
default:
|
|
2619
|
+
return 0;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
shouldBreakLine(line) {
|
|
2623
|
+
return line.length > this.options.maxLineLength;
|
|
2624
|
+
}
|
|
2625
|
+
formatPretty(dsl) {
|
|
2626
|
+
// Additional pretty-printing logic could go here
|
|
2627
|
+
// For now, just return the DSL as-is since we handle formatting
|
|
2628
|
+
// in the individual export methods
|
|
2629
|
+
return dsl;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
/**
|
|
2634
|
+
* Types of validation issues that can be found in DSL
|
|
2635
|
+
*/
|
|
2636
|
+
var ValidationIssueType;
|
|
2637
|
+
(function (ValidationIssueType) {
|
|
2638
|
+
ValidationIssueType["SYNTAX_ERROR"] = "SyntaxError";
|
|
2639
|
+
ValidationIssueType["UNKNOWN_FUNCTION"] = "UnknownFunction";
|
|
2640
|
+
ValidationIssueType["UNKNOWN_OPERATOR"] = "UnknownOperator";
|
|
2641
|
+
ValidationIssueType["UNBALANCED_PARENTHESES"] = "UnbalancedParentheses";
|
|
2642
|
+
ValidationIssueType["UNBALANCED_BRACKETS"] = "UnbalancedBrackets";
|
|
2643
|
+
ValidationIssueType["INVALID_TOKEN"] = "InvalidToken";
|
|
2644
|
+
ValidationIssueType["UNEXPECTED_TOKEN"] = "UnexpectedToken";
|
|
2645
|
+
ValidationIssueType["MISSING_ARGUMENT"] = "MissingArgument";
|
|
2646
|
+
ValidationIssueType["TOO_MANY_ARGUMENTS"] = "TooManyArguments";
|
|
2647
|
+
ValidationIssueType["INVALID_FIELD_REFERENCE"] = "InvalidFieldReference";
|
|
2648
|
+
ValidationIssueType["EMPTY_EXPRESSION"] = "EmptyExpression";
|
|
2649
|
+
ValidationIssueType["UNTERMINATED_STRING"] = "UnterminatedString";
|
|
2650
|
+
ValidationIssueType["INVALID_NUMBER"] = "InvalidNumber";
|
|
2651
|
+
ValidationIssueType["DEPRECATED_SYNTAX"] = "DeprecatedSyntax";
|
|
2652
|
+
ValidationIssueType["PERFORMANCE_WARNING"] = "PerformanceWarning";
|
|
2653
|
+
})(ValidationIssueType || (ValidationIssueType = {}));
|
|
2654
|
+
/**
|
|
2655
|
+
* Severity levels for validation issues
|
|
2656
|
+
*/
|
|
2657
|
+
var ValidationSeverity;
|
|
2658
|
+
(function (ValidationSeverity) {
|
|
2659
|
+
ValidationSeverity["ERROR"] = "error";
|
|
2660
|
+
ValidationSeverity["WARNING"] = "warning";
|
|
2661
|
+
ValidationSeverity["INFO"] = "info";
|
|
2662
|
+
ValidationSeverity["HINT"] = "hint";
|
|
2663
|
+
})(ValidationSeverity || (ValidationSeverity = {}));
|
|
2664
|
+
|
|
2665
|
+
/**
|
|
2666
|
+
* Validates DSL expressions and provides detailed error reporting
|
|
2667
|
+
*/
|
|
2668
|
+
class DslValidator {
|
|
2669
|
+
config;
|
|
2670
|
+
BUILT_IN_FUNCTIONS = [
|
|
2671
|
+
'contains', 'startsWith', 'endsWith', 'atLeast', 'exactly',
|
|
2672
|
+
'forEach', 'uniqueBy', 'minLength', 'maxLength',
|
|
2673
|
+
'requiredIf', 'visibleIf', 'disabledIf', 'readonlyIf',
|
|
2674
|
+
'ifDefined', 'ifNotNull', 'ifExists', 'withDefault'
|
|
2675
|
+
];
|
|
2676
|
+
constructor(config = {}) {
|
|
2677
|
+
this.config = {
|
|
2678
|
+
knownFunctions: config.knownFunctions || [],
|
|
2679
|
+
functionRegistry: config.functionRegistry,
|
|
2680
|
+
knownFields: config.knownFields || [],
|
|
2681
|
+
maxComplexity: config.maxComplexity || 50,
|
|
2682
|
+
enablePerformanceWarnings: config.enablePerformanceWarnings ?? true,
|
|
2683
|
+
enableDeprecationWarnings: config.enableDeprecationWarnings ?? true
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Validates a DSL expression and returns all issues found
|
|
2688
|
+
*/
|
|
2689
|
+
validate(input) {
|
|
2690
|
+
const issues = [];
|
|
2691
|
+
try {
|
|
2692
|
+
// Basic validation
|
|
2693
|
+
this.validateBasicStructure(input, issues);
|
|
2694
|
+
// Tokenize and validate tokens
|
|
2695
|
+
const tokenizer = new DslTokenizer(input);
|
|
2696
|
+
const tokens = tokenizer.tokenize();
|
|
2697
|
+
this.validateTokens(tokens, input, issues);
|
|
2698
|
+
this.validateSyntax(tokens, input, issues);
|
|
2699
|
+
this.validateSemantics(tokens, input, issues);
|
|
2700
|
+
this.validateComplexity(tokens, issues);
|
|
2701
|
+
}
|
|
2702
|
+
catch (error) {
|
|
2703
|
+
issues.push({
|
|
2704
|
+
type: ValidationIssueType.SYNTAX_ERROR,
|
|
2705
|
+
severity: ValidationSeverity.ERROR,
|
|
2706
|
+
message: `Unexpected error during validation: ${error}`,
|
|
2707
|
+
position: { start: 0, end: input.length, line: 1, column: 1 }
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
return issues;
|
|
2711
|
+
}
|
|
2712
|
+
validateBasicStructure(input, issues) {
|
|
2713
|
+
// Check for empty expression
|
|
2714
|
+
if (input.trim().length === 0) {
|
|
2715
|
+
issues.push({
|
|
2716
|
+
type: ValidationIssueType.EMPTY_EXPRESSION,
|
|
2717
|
+
severity: ValidationSeverity.ERROR,
|
|
2718
|
+
message: 'Expression cannot be empty',
|
|
2719
|
+
position: { start: 0, end: 0, line: 1, column: 1 },
|
|
2720
|
+
suggestion: 'Add a valid expression'
|
|
2721
|
+
});
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
// Check for unbalanced parentheses
|
|
2725
|
+
this.validateBalancedDelimiters(input, '(', ')', ValidationIssueType.UNBALANCED_PARENTHESES, issues);
|
|
2726
|
+
// Check for unbalanced brackets
|
|
2727
|
+
this.validateBalancedDelimiters(input, '[', ']', ValidationIssueType.UNBALANCED_BRACKETS, issues);
|
|
2728
|
+
// Check for unterminated strings
|
|
2729
|
+
this.validateStringLiterals(input, issues);
|
|
2730
|
+
}
|
|
2731
|
+
validateBalancedDelimiters(input, open, close, issueType, issues) {
|
|
2732
|
+
let count = 0;
|
|
2733
|
+
let openPositions = [];
|
|
2734
|
+
for (let i = 0; i < input.length; i++) {
|
|
2735
|
+
if (input[i] === open) {
|
|
2736
|
+
count++;
|
|
2737
|
+
openPositions.push(i);
|
|
2738
|
+
}
|
|
2739
|
+
else if (input[i] === close) {
|
|
2740
|
+
count--;
|
|
2741
|
+
if (count < 0) {
|
|
2742
|
+
const { line, column } = this.getLineColumn(input, i);
|
|
2743
|
+
issues.push({
|
|
2744
|
+
type: issueType,
|
|
2745
|
+
severity: ValidationSeverity.ERROR,
|
|
2746
|
+
message: `Unexpected closing ${close}`,
|
|
2747
|
+
position: { start: i, end: i + 1, line, column },
|
|
2748
|
+
suggestion: `Remove the extra ${close} or add a matching ${open}`
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
else {
|
|
2752
|
+
openPositions.pop();
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
// Report unclosed delimiters
|
|
2757
|
+
for (const pos of openPositions) {
|
|
2758
|
+
const { line, column } = this.getLineColumn(input, pos);
|
|
2759
|
+
issues.push({
|
|
2760
|
+
type: issueType,
|
|
2761
|
+
severity: ValidationSeverity.ERROR,
|
|
2762
|
+
message: `Unclosed ${open}`,
|
|
2763
|
+
position: { start: pos, end: pos + 1, line, column },
|
|
2764
|
+
suggestion: `Add a closing ${close}`
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
validateStringLiterals(input, issues) {
|
|
2769
|
+
const quotes = ['"', "'"];
|
|
2770
|
+
for (const quote of quotes) {
|
|
2771
|
+
let inString = false;
|
|
2772
|
+
let startPos = -1;
|
|
2773
|
+
for (let i = 0; i < input.length; i++) {
|
|
2774
|
+
if (input[i] === quote) {
|
|
2775
|
+
if (!inString) {
|
|
2776
|
+
inString = true;
|
|
2777
|
+
startPos = i;
|
|
2778
|
+
}
|
|
2779
|
+
else {
|
|
2780
|
+
// Check if it's escaped
|
|
2781
|
+
let escaped = false;
|
|
2782
|
+
let backslashCount = 0;
|
|
2783
|
+
for (let j = i - 1; j >= 0 && input[j] === '\\'; j--) {
|
|
2784
|
+
backslashCount++;
|
|
2785
|
+
}
|
|
2786
|
+
escaped = backslashCount % 2 === 1;
|
|
2787
|
+
if (!escaped) {
|
|
2788
|
+
inString = false;
|
|
2789
|
+
startPos = -1;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
if (inString) {
|
|
2795
|
+
const { line, column } = this.getLineColumn(input, startPos);
|
|
2796
|
+
issues.push({
|
|
2797
|
+
type: ValidationIssueType.UNTERMINATED_STRING,
|
|
2798
|
+
severity: ValidationSeverity.ERROR,
|
|
2799
|
+
message: `Unterminated string literal starting with ${quote}`,
|
|
2800
|
+
position: { start: startPos, end: input.length, line, column },
|
|
2801
|
+
suggestion: `Add a closing ${quote}`
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
validateTokens(tokens, input, issues) {
|
|
2807
|
+
for (const token of tokens) {
|
|
2808
|
+
if (token.type === TokenType.EOF)
|
|
2809
|
+
continue;
|
|
2810
|
+
// Validate numbers
|
|
2811
|
+
if (token.type === TokenType.NUMBER) {
|
|
2812
|
+
if (isNaN(Number(token.value))) {
|
|
2813
|
+
issues.push({
|
|
2814
|
+
type: ValidationIssueType.INVALID_NUMBER,
|
|
2815
|
+
severity: ValidationSeverity.ERROR,
|
|
2816
|
+
message: `Invalid number format: ${token.value}`,
|
|
2817
|
+
position: {
|
|
2818
|
+
start: token.position,
|
|
2819
|
+
end: token.position + token.value.length,
|
|
2820
|
+
line: token.line,
|
|
2821
|
+
column: token.column
|
|
2822
|
+
},
|
|
2823
|
+
suggestion: 'Use a valid number format (e.g., 42, 3.14, -5)'
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
// Validate field references
|
|
2828
|
+
if (token.type === TokenType.FIELD_REFERENCE || token.type === TokenType.IDENTIFIER) {
|
|
2829
|
+
if (this.config.knownFields.length > 0) {
|
|
2830
|
+
if (!this.config.knownFields.includes(token.value)) {
|
|
2831
|
+
issues.push({
|
|
2832
|
+
type: ValidationIssueType.INVALID_FIELD_REFERENCE,
|
|
2833
|
+
severity: ValidationSeverity.WARNING,
|
|
2834
|
+
message: `Unknown field: ${token.value}`,
|
|
2835
|
+
position: {
|
|
2836
|
+
start: token.position,
|
|
2837
|
+
end: token.position + token.value.length,
|
|
2838
|
+
line: token.line,
|
|
2839
|
+
column: token.column
|
|
2840
|
+
},
|
|
2841
|
+
suggestion: this.suggestSimilarField(token.value),
|
|
2842
|
+
help: `Available fields: ${this.config.knownFields.join(', ')}`
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
validateSyntax(tokens, input, issues) {
|
|
2850
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2851
|
+
const token = tokens[i];
|
|
2852
|
+
const nextToken = tokens[i + 1];
|
|
2853
|
+
const prevToken = tokens[i - 1];
|
|
2854
|
+
// Check for consecutive operators
|
|
2855
|
+
if (this.isOperatorToken(token) && this.isOperatorToken(nextToken)) {
|
|
2856
|
+
issues.push({
|
|
2857
|
+
type: ValidationIssueType.UNEXPECTED_TOKEN,
|
|
2858
|
+
severity: ValidationSeverity.ERROR,
|
|
2859
|
+
message: `Unexpected ${nextToken.type} after ${token.type}`,
|
|
2860
|
+
position: {
|
|
2861
|
+
start: nextToken.position,
|
|
2862
|
+
end: nextToken.position + nextToken.value.length,
|
|
2863
|
+
line: nextToken.line,
|
|
2864
|
+
column: nextToken.column
|
|
2865
|
+
},
|
|
2866
|
+
suggestion: 'Add an operand between operators'
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
// Check for operators at the beginning/end
|
|
2870
|
+
if (i === 0 && this.isBinaryOperatorToken(token)) {
|
|
2871
|
+
issues.push({
|
|
2872
|
+
type: ValidationIssueType.UNEXPECTED_TOKEN,
|
|
2873
|
+
severity: ValidationSeverity.ERROR,
|
|
2874
|
+
message: `Expression cannot start with ${token.type}`,
|
|
2875
|
+
position: {
|
|
2876
|
+
start: token.position,
|
|
2877
|
+
end: token.position + token.value.length,
|
|
2878
|
+
line: token.line,
|
|
2879
|
+
column: token.column
|
|
2880
|
+
},
|
|
2881
|
+
suggestion: 'Add an operand before the operator'
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
if (i === tokens.length - 2 && this.isBinaryOperatorToken(token) && nextToken.type === TokenType.EOF) {
|
|
2885
|
+
issues.push({
|
|
2886
|
+
type: ValidationIssueType.UNEXPECTED_TOKEN,
|
|
2887
|
+
severity: ValidationSeverity.ERROR,
|
|
2888
|
+
message: `Expression cannot end with ${token.type}`,
|
|
2889
|
+
position: {
|
|
2890
|
+
start: token.position,
|
|
2891
|
+
end: token.position + token.value.length,
|
|
2892
|
+
line: token.line,
|
|
2893
|
+
column: token.column
|
|
2894
|
+
},
|
|
2895
|
+
suggestion: 'Add an operand after the operator'
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
validateSemantics(tokens, input, issues) {
|
|
2901
|
+
// Check function calls
|
|
2902
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2903
|
+
const token = tokens[i];
|
|
2904
|
+
const nextToken = tokens[i + 1];
|
|
2905
|
+
if (token.type === TokenType.IDENTIFIER && nextToken?.type === TokenType.LEFT_PAREN) {
|
|
2906
|
+
const functionName = token.value;
|
|
2907
|
+
// Check if function is known
|
|
2908
|
+
const allKnownFunctions = [
|
|
2909
|
+
...this.BUILT_IN_FUNCTIONS,
|
|
2910
|
+
...this.config.knownFunctions
|
|
2911
|
+
];
|
|
2912
|
+
if (this.config.functionRegistry) {
|
|
2913
|
+
allKnownFunctions.push(...Array.from(this.config.functionRegistry.getAll().keys()));
|
|
2914
|
+
}
|
|
2915
|
+
if (!allKnownFunctions.includes(functionName)) {
|
|
2916
|
+
issues.push({
|
|
2917
|
+
type: ValidationIssueType.UNKNOWN_FUNCTION,
|
|
2918
|
+
severity: ValidationSeverity.ERROR,
|
|
2919
|
+
message: `Unknown function: ${functionName}`,
|
|
2920
|
+
position: {
|
|
2921
|
+
start: token.position,
|
|
2922
|
+
end: token.position + token.value.length,
|
|
2923
|
+
line: token.line,
|
|
2924
|
+
column: token.column
|
|
2925
|
+
},
|
|
2926
|
+
suggestion: this.suggestSimilarFunction(functionName, allKnownFunctions),
|
|
2927
|
+
help: `Available functions: ${allKnownFunctions.join(', ')}`
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
// Validate function arguments (basic check)
|
|
2931
|
+
this.validateFunctionArguments(functionName, i, tokens, issues);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
validateFunctionArguments(functionName, startIndex, tokens, issues) {
|
|
2936
|
+
// Find the argument list
|
|
2937
|
+
let parenCount = 0;
|
|
2938
|
+
let argCount = 0;
|
|
2939
|
+
let inArgs = false;
|
|
2940
|
+
for (let i = startIndex + 1; i < tokens.length; i++) {
|
|
2941
|
+
const token = tokens[i];
|
|
2942
|
+
if (token.type === TokenType.LEFT_PAREN) {
|
|
2943
|
+
parenCount++;
|
|
2944
|
+
if (parenCount === 1) {
|
|
2945
|
+
inArgs = true;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
else if (token.type === TokenType.RIGHT_PAREN) {
|
|
2949
|
+
parenCount--;
|
|
2950
|
+
if (parenCount === 0) {
|
|
2951
|
+
break;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
else if (token.type === TokenType.COMMA && parenCount === 1) {
|
|
2955
|
+
argCount++;
|
|
2956
|
+
}
|
|
2957
|
+
else if (inArgs && parenCount === 1 && token.type !== TokenType.COMMA) {
|
|
2958
|
+
// We have at least one argument
|
|
2959
|
+
if (argCount === 0) {
|
|
2960
|
+
argCount = 1;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
// Check argument count for known functions
|
|
2965
|
+
const expectedArgs = this.getExpectedArgumentCount(functionName);
|
|
2966
|
+
if (expectedArgs !== null && argCount !== expectedArgs) {
|
|
2967
|
+
const functionToken = tokens[startIndex];
|
|
2968
|
+
const severity = argCount < expectedArgs ? ValidationSeverity.ERROR : ValidationSeverity.WARNING;
|
|
2969
|
+
const message = argCount < expectedArgs
|
|
2970
|
+
? `Function ${functionName} expects ${expectedArgs} arguments, got ${argCount}`
|
|
2971
|
+
: `Function ${functionName} expects ${expectedArgs} arguments, got ${argCount}`;
|
|
2972
|
+
issues.push({
|
|
2973
|
+
type: argCount < expectedArgs ? ValidationIssueType.MISSING_ARGUMENT : ValidationIssueType.TOO_MANY_ARGUMENTS,
|
|
2974
|
+
severity,
|
|
2975
|
+
message,
|
|
2976
|
+
position: {
|
|
2977
|
+
start: functionToken.position,
|
|
2978
|
+
end: functionToken.position + functionToken.value.length,
|
|
2979
|
+
line: functionToken.line,
|
|
2980
|
+
column: functionToken.column
|
|
2981
|
+
},
|
|
2982
|
+
suggestion: `Use exactly ${expectedArgs} arguments`
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
validateComplexity(tokens, issues) {
|
|
2987
|
+
if (!this.config.enablePerformanceWarnings)
|
|
2988
|
+
return;
|
|
2989
|
+
const operatorCount = tokens.filter(token => this.isOperatorToken(token)).length;
|
|
2990
|
+
if (operatorCount > this.config.maxComplexity) {
|
|
2991
|
+
issues.push({
|
|
2992
|
+
type: ValidationIssueType.PERFORMANCE_WARNING,
|
|
2993
|
+
severity: ValidationSeverity.WARNING,
|
|
2994
|
+
message: `Expression complexity is high (${operatorCount} operators). Consider breaking into smaller expressions.`,
|
|
2995
|
+
position: { start: 0, end: 0, line: 1, column: 1 },
|
|
2996
|
+
help: 'Complex expressions can impact performance and readability'
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
getExpectedArgumentCount(functionName) {
|
|
3001
|
+
const argCounts = {
|
|
3002
|
+
'contains': 2,
|
|
3003
|
+
'startsWith': 2,
|
|
3004
|
+
'endsWith': 2,
|
|
3005
|
+
'atLeast': 2,
|
|
3006
|
+
'exactly': 2,
|
|
3007
|
+
'forEach': 2,
|
|
3008
|
+
'uniqueBy': 2,
|
|
3009
|
+
'minLength': 2,
|
|
3010
|
+
'maxLength': 2,
|
|
3011
|
+
'requiredIf': 2,
|
|
3012
|
+
'visibleIf': 2,
|
|
3013
|
+
'disabledIf': 2,
|
|
3014
|
+
'readonlyIf': 2,
|
|
3015
|
+
'ifDefined': 2,
|
|
3016
|
+
'ifNotNull': 2,
|
|
3017
|
+
'ifExists': 2,
|
|
3018
|
+
'withDefault': 3
|
|
3019
|
+
};
|
|
3020
|
+
return argCounts[functionName] ?? null;
|
|
3021
|
+
}
|
|
3022
|
+
isOperatorToken(token) {
|
|
3023
|
+
return [
|
|
3024
|
+
TokenType.AND, TokenType.OR, TokenType.NOT, TokenType.XOR, TokenType.IMPLIES,
|
|
3025
|
+
TokenType.EQUALS, TokenType.NOT_EQUALS, TokenType.LESS_THAN,
|
|
3026
|
+
TokenType.LESS_THAN_OR_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_THAN_OR_EQUAL,
|
|
3027
|
+
TokenType.IN
|
|
3028
|
+
].includes(token.type);
|
|
3029
|
+
}
|
|
3030
|
+
isBinaryOperatorToken(token) {
|
|
3031
|
+
return [
|
|
3032
|
+
TokenType.AND, TokenType.OR, TokenType.XOR, TokenType.IMPLIES,
|
|
3033
|
+
TokenType.EQUALS, TokenType.NOT_EQUALS, TokenType.LESS_THAN,
|
|
3034
|
+
TokenType.LESS_THAN_OR_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_THAN_OR_EQUAL,
|
|
3035
|
+
TokenType.IN
|
|
3036
|
+
].includes(token.type);
|
|
3037
|
+
}
|
|
3038
|
+
suggestSimilarField(fieldName) {
|
|
3039
|
+
return this.findSimilar(fieldName, this.config.knownFields);
|
|
3040
|
+
}
|
|
3041
|
+
suggestSimilarFunction(functionName, knownFunctions) {
|
|
3042
|
+
return this.findSimilar(functionName, knownFunctions);
|
|
3043
|
+
}
|
|
3044
|
+
findSimilar(target, candidates) {
|
|
3045
|
+
if (candidates.length === 0)
|
|
3046
|
+
return undefined;
|
|
3047
|
+
const similarities = candidates.map(candidate => ({
|
|
3048
|
+
candidate,
|
|
3049
|
+
distance: this.levenshteinDistance(target.toLowerCase(), candidate.toLowerCase())
|
|
3050
|
+
}));
|
|
3051
|
+
similarities.sort((a, b) => a.distance - b.distance);
|
|
3052
|
+
// Only suggest if the distance is reasonable
|
|
3053
|
+
if (similarities[0].distance <= Math.max(2, target.length * 0.4)) {
|
|
3054
|
+
return `Did you mean "${similarities[0].candidate}"?`;
|
|
3055
|
+
}
|
|
3056
|
+
return undefined;
|
|
3057
|
+
}
|
|
3058
|
+
levenshteinDistance(a, b) {
|
|
3059
|
+
const matrix = [];
|
|
3060
|
+
for (let i = 0; i <= b.length; i++) {
|
|
3061
|
+
matrix[i] = [i];
|
|
3062
|
+
}
|
|
3063
|
+
for (let j = 0; j <= a.length; j++) {
|
|
3064
|
+
matrix[0][j] = j;
|
|
3065
|
+
}
|
|
3066
|
+
for (let i = 1; i <= b.length; i++) {
|
|
3067
|
+
for (let j = 1; j <= a.length; j++) {
|
|
3068
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
3069
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
3070
|
+
}
|
|
3071
|
+
else {
|
|
3072
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
return matrix[b.length][a.length];
|
|
3077
|
+
}
|
|
3078
|
+
getLineColumn(input, position) {
|
|
3079
|
+
let line = 1;
|
|
3080
|
+
let column = 1;
|
|
3081
|
+
for (let i = 0; i < position && i < input.length; i++) {
|
|
3082
|
+
if (input[i] === '\n') {
|
|
3083
|
+
line++;
|
|
3084
|
+
column = 1;
|
|
3085
|
+
}
|
|
3086
|
+
else {
|
|
3087
|
+
column++;
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
return { line, column };
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
class SpecificationFactory {
|
|
3095
|
+
static fromJSON(json) {
|
|
3096
|
+
// Robustez: validar entrada e inferir quando possível
|
|
3097
|
+
if (json == null) {
|
|
3098
|
+
throw new Error('Invalid specification JSON: null/undefined');
|
|
3099
|
+
}
|
|
3100
|
+
if (Array.isArray(json)) {
|
|
3101
|
+
const specs = json.map((specJson) => SpecificationFactory.fromJSON(specJson));
|
|
3102
|
+
return new AndSpecification(specs);
|
|
3103
|
+
}
|
|
3104
|
+
if (typeof json !== 'object') {
|
|
3105
|
+
throw new Error(`Invalid specification JSON: expected object/array, got ${typeof json}`);
|
|
3106
|
+
}
|
|
3107
|
+
// Inferência quando 'type' está ausente
|
|
3108
|
+
if (!json.type) {
|
|
3109
|
+
if (Array.isArray(json.specs)) {
|
|
3110
|
+
const specs = json.specs.map((specJson) => SpecificationFactory.fromJSON(specJson));
|
|
3111
|
+
return new AndSpecification(specs);
|
|
3112
|
+
}
|
|
3113
|
+
if ('field' in json && 'operator' in json) {
|
|
3114
|
+
return FieldSpecification.fromJSON({ type: 'field', ...json });
|
|
3115
|
+
}
|
|
3116
|
+
if ('antecedent' in json && 'consequent' in json) {
|
|
3117
|
+
const antecedent = SpecificationFactory.fromJSON(json.antecedent);
|
|
3118
|
+
const consequent = SpecificationFactory.fromJSON(json.consequent);
|
|
3119
|
+
return new ImpliesSpecification(antecedent, consequent);
|
|
3120
|
+
}
|
|
3121
|
+
if ('spec' in json) {
|
|
3122
|
+
const notSpec = SpecificationFactory.fromJSON(json.spec);
|
|
3123
|
+
return new NotSpecification(notSpec);
|
|
3124
|
+
}
|
|
3125
|
+
if ('itemSpecification' in json && 'arrayField' in json) {
|
|
3126
|
+
const itemSpec = SpecificationFactory.fromJSON(json.itemSpecification);
|
|
3127
|
+
return new ForEachSpecification(json.arrayField, itemSpec, json.metadata);
|
|
3128
|
+
}
|
|
3129
|
+
// Se não for possível inferir, manter erro claro
|
|
3130
|
+
throw new Error('Unknown specification type: undefined');
|
|
3131
|
+
}
|
|
3132
|
+
switch (json.type) {
|
|
3133
|
+
case 'field':
|
|
3134
|
+
return FieldSpecification.fromJSON(json);
|
|
3135
|
+
case 'and':
|
|
3136
|
+
const andSpecs = json.specs.map((specJson) => SpecificationFactory.fromJSON(specJson));
|
|
3137
|
+
return new AndSpecification(andSpecs);
|
|
3138
|
+
case 'or':
|
|
3139
|
+
const orSpecs = json.specs.map((specJson) => SpecificationFactory.fromJSON(specJson));
|
|
3140
|
+
return new OrSpecification(orSpecs);
|
|
3141
|
+
case 'not':
|
|
3142
|
+
const notSpec = SpecificationFactory.fromJSON(json.spec);
|
|
3143
|
+
return new NotSpecification(notSpec);
|
|
3144
|
+
case 'xor':
|
|
3145
|
+
const xorSpecs = json.specs.map((specJson) => SpecificationFactory.fromJSON(specJson));
|
|
3146
|
+
return new XorSpecification(xorSpecs);
|
|
3147
|
+
case 'implies':
|
|
3148
|
+
const antecedent = SpecificationFactory.fromJSON(json.antecedent);
|
|
3149
|
+
const consequent = SpecificationFactory.fromJSON(json.consequent);
|
|
3150
|
+
return new ImpliesSpecification(antecedent, consequent);
|
|
3151
|
+
case 'function':
|
|
3152
|
+
return FunctionSpecification.fromJSON(json);
|
|
3153
|
+
case 'atLeast':
|
|
3154
|
+
const atLeastSpecs = json.specs.map((specJson) => SpecificationFactory.fromJSON(specJson));
|
|
3155
|
+
return new AtLeastSpecification(json.minimum, atLeastSpecs);
|
|
3156
|
+
case 'exactly':
|
|
3157
|
+
const exactlySpecs = json.specs.map((specJson) => SpecificationFactory.fromJSON(specJson));
|
|
3158
|
+
return new ExactlySpecification(json.exact, exactlySpecs);
|
|
3159
|
+
case 'fieldToField':
|
|
3160
|
+
return FieldToFieldSpecification.fromJSON(json);
|
|
3161
|
+
case 'contextual':
|
|
3162
|
+
return ContextualSpecification.fromJSON(json);
|
|
3163
|
+
case 'requiredIf':
|
|
3164
|
+
const requiredCondition = SpecificationFactory.fromJSON(json.condition);
|
|
3165
|
+
return new RequiredIfSpecification(json.field, requiredCondition, json.metadata);
|
|
3166
|
+
case 'visibleIf':
|
|
3167
|
+
const visibleCondition = SpecificationFactory.fromJSON(json.condition);
|
|
3168
|
+
return new VisibleIfSpecification(json.field, visibleCondition, json.metadata);
|
|
3169
|
+
case 'disabledIf':
|
|
3170
|
+
const disabledCondition = SpecificationFactory.fromJSON(json.condition);
|
|
3171
|
+
return new DisabledIfSpecification(json.field, disabledCondition, json.metadata);
|
|
3172
|
+
case 'readonlyIf':
|
|
3173
|
+
const readonlyCondition = SpecificationFactory.fromJSON(json.condition);
|
|
3174
|
+
return new ReadonlyIfSpecification(json.field, readonlyCondition, json.metadata);
|
|
3175
|
+
case 'forEach':
|
|
3176
|
+
const itemSpec = SpecificationFactory.fromJSON(json.itemSpecification);
|
|
3177
|
+
return new ForEachSpecification(json.arrayField, itemSpec, json.metadata);
|
|
3178
|
+
case 'uniqueBy':
|
|
3179
|
+
return UniqueBySpecification.fromJSON(json);
|
|
3180
|
+
case 'minLength':
|
|
3181
|
+
return MinLengthSpecification.fromJSON(json);
|
|
3182
|
+
case 'maxLength':
|
|
3183
|
+
return MaxLengthSpecification.fromJSON(json);
|
|
3184
|
+
case 'ifDefined':
|
|
3185
|
+
const ifDefinedSpec = SpecificationFactory.fromJSON(json.specification);
|
|
3186
|
+
return new IfDefinedSpecification(json.field, ifDefinedSpec, json.metadata);
|
|
3187
|
+
case 'ifNotNull':
|
|
3188
|
+
const ifNotNullSpec = SpecificationFactory.fromJSON(json.specification);
|
|
3189
|
+
return new IfNotNullSpecification(json.field, ifNotNullSpec, json.metadata);
|
|
3190
|
+
case 'ifExists':
|
|
3191
|
+
const ifExistsSpec = SpecificationFactory.fromJSON(json.specification);
|
|
3192
|
+
return new IfExistsSpecification(json.field, ifExistsSpec, json.metadata);
|
|
3193
|
+
case 'withDefault':
|
|
3194
|
+
const withDefaultSpec = SpecificationFactory.fromJSON(json.specification);
|
|
3195
|
+
return new WithDefaultSpecification(json.field, json.defaultValue, withDefaultSpec, json.metadata);
|
|
3196
|
+
case 'form':
|
|
3197
|
+
// Form specifications require special reconstruction
|
|
3198
|
+
throw new Error('FormSpecification.fromJSON not yet implemented');
|
|
3199
|
+
default:
|
|
3200
|
+
throw new Error(`Unknown specification type: ${json.type}`);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
static field(field, operator, value) {
|
|
3204
|
+
return new FieldSpecification(field, operator, value);
|
|
3205
|
+
}
|
|
3206
|
+
static and(...specs) {
|
|
3207
|
+
return new AndSpecification(specs);
|
|
3208
|
+
}
|
|
3209
|
+
static or(...specs) {
|
|
3210
|
+
return new OrSpecification(specs);
|
|
3211
|
+
}
|
|
3212
|
+
static not(spec) {
|
|
3213
|
+
return new NotSpecification(spec);
|
|
3214
|
+
}
|
|
3215
|
+
static xor(...specs) {
|
|
3216
|
+
return new XorSpecification(specs);
|
|
3217
|
+
}
|
|
3218
|
+
static implies(antecedent, consequent) {
|
|
3219
|
+
return new ImpliesSpecification(antecedent, consequent);
|
|
3220
|
+
}
|
|
3221
|
+
static func(name, args, registry) {
|
|
3222
|
+
return new FunctionSpecification(name, args, registry);
|
|
3223
|
+
}
|
|
3224
|
+
static atLeast(minimum, specs) {
|
|
3225
|
+
return new AtLeastSpecification(minimum, specs);
|
|
3226
|
+
}
|
|
3227
|
+
static exactly(exact, specs) {
|
|
3228
|
+
return new ExactlySpecification(exact, specs);
|
|
3229
|
+
}
|
|
3230
|
+
static fieldToField(fieldA, operator, fieldB, transformA, transformB, registry) {
|
|
3231
|
+
return new FieldToFieldSpecification(fieldA, operator, fieldB, transformA, transformB, registry);
|
|
3232
|
+
}
|
|
3233
|
+
static contextual(template, provider) {
|
|
3234
|
+
return new ContextualSpecification(template, provider);
|
|
3235
|
+
}
|
|
3236
|
+
// Convenience methods for common operators
|
|
3237
|
+
static equals(field, value) {
|
|
3238
|
+
return new FieldSpecification(field, ComparisonOperator.EQUALS, value);
|
|
3239
|
+
}
|
|
3240
|
+
static notEquals(field, value) {
|
|
3241
|
+
return new FieldSpecification(field, ComparisonOperator.NOT_EQUALS, value);
|
|
3242
|
+
}
|
|
3243
|
+
static greaterThan(field, value) {
|
|
3244
|
+
return new FieldSpecification(field, ComparisonOperator.GREATER_THAN, value);
|
|
3245
|
+
}
|
|
3246
|
+
static lessThan(field, value) {
|
|
3247
|
+
return new FieldSpecification(field, ComparisonOperator.LESS_THAN, value);
|
|
3248
|
+
}
|
|
3249
|
+
static contains(field, value) {
|
|
3250
|
+
return new FieldSpecification(field, ComparisonOperator.CONTAINS, value);
|
|
3251
|
+
}
|
|
3252
|
+
static startsWith(field, value) {
|
|
3253
|
+
return new FieldSpecification(field, ComparisonOperator.STARTS_WITH, value);
|
|
3254
|
+
}
|
|
3255
|
+
static endsWith(field, value) {
|
|
3256
|
+
return new FieldSpecification(field, ComparisonOperator.ENDS_WITH, value);
|
|
3257
|
+
}
|
|
3258
|
+
static isIn(field, values) {
|
|
3259
|
+
return new FieldSpecification(field, ComparisonOperator.IN, values);
|
|
3260
|
+
}
|
|
3261
|
+
// Phase 2: Conditional validators
|
|
3262
|
+
static requiredIf(field, condition, metadata) {
|
|
3263
|
+
return new RequiredIfSpecification(field, condition, metadata);
|
|
3264
|
+
}
|
|
3265
|
+
static visibleIf(field, condition, metadata) {
|
|
3266
|
+
return new VisibleIfSpecification(field, condition, metadata);
|
|
3267
|
+
}
|
|
3268
|
+
static disabledIf(field, condition, metadata) {
|
|
3269
|
+
return new DisabledIfSpecification(field, condition, metadata);
|
|
3270
|
+
}
|
|
3271
|
+
static readonlyIf(field, condition, metadata) {
|
|
3272
|
+
return new ReadonlyIfSpecification(field, condition, metadata);
|
|
3273
|
+
}
|
|
3274
|
+
// Phase 2: Collection specifications
|
|
3275
|
+
static forEach(arrayField, itemSpecification, metadata) {
|
|
3276
|
+
return new ForEachSpecification(arrayField, itemSpecification, metadata);
|
|
3277
|
+
}
|
|
3278
|
+
static uniqueBy(arrayField, keySelector, metadata) {
|
|
3279
|
+
return new UniqueBySpecification(arrayField, keySelector, metadata);
|
|
3280
|
+
}
|
|
3281
|
+
static minLength(arrayField, minLength, metadata) {
|
|
3282
|
+
return new MinLengthSpecification(arrayField, minLength, metadata);
|
|
3283
|
+
}
|
|
3284
|
+
static maxLength(arrayField, maxLength, metadata) {
|
|
3285
|
+
return new MaxLengthSpecification(arrayField, maxLength, metadata);
|
|
3286
|
+
}
|
|
3287
|
+
// Phase 2: Optional field handling
|
|
3288
|
+
static ifDefined(field, specification, metadata) {
|
|
3289
|
+
return new IfDefinedSpecification(field, specification, metadata);
|
|
3290
|
+
}
|
|
3291
|
+
static ifNotNull(field, specification, metadata) {
|
|
3292
|
+
return new IfNotNullSpecification(field, specification, metadata);
|
|
3293
|
+
}
|
|
3294
|
+
static ifExists(field, specification, metadata) {
|
|
3295
|
+
return new IfExistsSpecification(field, specification, metadata);
|
|
3296
|
+
}
|
|
3297
|
+
static withDefault(field, defaultValue, specification, metadata) {
|
|
3298
|
+
return new WithDefaultSpecification(field, defaultValue, specification, metadata);
|
|
3299
|
+
}
|
|
3300
|
+
// Phase 2: Form specifications
|
|
3301
|
+
static form(metadata) {
|
|
3302
|
+
return new FormSpecification(metadata);
|
|
3303
|
+
}
|
|
3304
|
+
// Phase 2: Enhanced field specifications with metadata
|
|
3305
|
+
static fieldWithMetadata(field, operator, value, metadata) {
|
|
3306
|
+
return new FieldSpecification(field, operator, value, metadata);
|
|
3307
|
+
}
|
|
3308
|
+
// Convenience methods with metadata support
|
|
3309
|
+
static equalsWithMetadata(field, value, metadata) {
|
|
3310
|
+
return new FieldSpecification(field, ComparisonOperator.EQUALS, value, metadata);
|
|
3311
|
+
}
|
|
3312
|
+
static greaterThanWithMetadata(field, value, metadata) {
|
|
3313
|
+
return new FieldSpecification(field, ComparisonOperator.GREATER_THAN, value, metadata);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
class SpecificationUtils {
|
|
3318
|
+
/**
|
|
3319
|
+
* Simplifies a specification by removing redundant nesting and applying logical rules
|
|
3320
|
+
*/
|
|
3321
|
+
static simplify(spec) {
|
|
3322
|
+
if (spec instanceof AndSpecification) {
|
|
3323
|
+
return this.simplifyAnd(spec);
|
|
3324
|
+
}
|
|
3325
|
+
if (spec instanceof OrSpecification) {
|
|
3326
|
+
return this.simplifyOr(spec);
|
|
3327
|
+
}
|
|
3328
|
+
if (spec instanceof NotSpecification) {
|
|
3329
|
+
return this.simplifyNot(spec);
|
|
3330
|
+
}
|
|
3331
|
+
return spec;
|
|
3332
|
+
}
|
|
3333
|
+
static simplifyAnd(spec) {
|
|
3334
|
+
const specs = spec.getSpecifications();
|
|
3335
|
+
const flattened = [];
|
|
3336
|
+
// Flatten nested AND specifications
|
|
3337
|
+
for (const s of specs) {
|
|
3338
|
+
const simplified = this.simplify(s);
|
|
3339
|
+
if (simplified instanceof AndSpecification) {
|
|
3340
|
+
flattened.push(...simplified.getSpecifications());
|
|
3341
|
+
}
|
|
3342
|
+
else {
|
|
3343
|
+
flattened.push(simplified);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
// Remove duplicates and contradictions
|
|
3347
|
+
const unique = this.removeDuplicates(flattened);
|
|
3348
|
+
if (unique.length === 0) {
|
|
3349
|
+
throw new Error('Empty AND specification');
|
|
3350
|
+
}
|
|
3351
|
+
if (unique.length === 1) {
|
|
3352
|
+
return unique[0];
|
|
3353
|
+
}
|
|
3354
|
+
return new AndSpecification(unique);
|
|
3355
|
+
}
|
|
3356
|
+
static simplifyOr(spec) {
|
|
3357
|
+
const specs = spec.getSpecifications();
|
|
3358
|
+
const flattened = [];
|
|
3359
|
+
// Flatten nested OR specifications
|
|
3360
|
+
for (const s of specs) {
|
|
3361
|
+
const simplified = this.simplify(s);
|
|
3362
|
+
if (simplified instanceof OrSpecification) {
|
|
3363
|
+
flattened.push(...simplified.getSpecifications());
|
|
3364
|
+
}
|
|
3365
|
+
else {
|
|
3366
|
+
flattened.push(simplified);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
// Remove duplicates
|
|
3370
|
+
const unique = this.removeDuplicates(flattened);
|
|
3371
|
+
if (unique.length === 0) {
|
|
3372
|
+
throw new Error('Empty OR specification');
|
|
3373
|
+
}
|
|
3374
|
+
if (unique.length === 1) {
|
|
3375
|
+
return unique[0];
|
|
3376
|
+
}
|
|
3377
|
+
return new OrSpecification(unique);
|
|
3378
|
+
}
|
|
3379
|
+
static simplifyNot(spec) {
|
|
3380
|
+
const inner = spec.getSpecification();
|
|
3381
|
+
// Double negation elimination: !!A = A
|
|
3382
|
+
if (inner instanceof NotSpecification) {
|
|
3383
|
+
return this.simplify(inner.getSpecification());
|
|
3384
|
+
}
|
|
3385
|
+
const simplified = this.simplify(inner);
|
|
3386
|
+
if (simplified === inner) {
|
|
3387
|
+
return spec;
|
|
3388
|
+
}
|
|
3389
|
+
return new NotSpecification(simplified);
|
|
3390
|
+
}
|
|
3391
|
+
static removeDuplicates(specs) {
|
|
3392
|
+
const unique = [];
|
|
3393
|
+
const seen = new Set();
|
|
3394
|
+
for (const spec of specs) {
|
|
3395
|
+
const key = JSON.stringify(spec.toJSON());
|
|
3396
|
+
if (!seen.has(key)) {
|
|
3397
|
+
seen.add(key);
|
|
3398
|
+
unique.push(spec);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
return unique;
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Checks if two specifications are logically equivalent
|
|
3405
|
+
*/
|
|
3406
|
+
static areEquivalent(spec1, spec2) {
|
|
3407
|
+
const json1 = JSON.stringify(this.simplify(spec1).toJSON());
|
|
3408
|
+
const json2 = JSON.stringify(this.simplify(spec2).toJSON());
|
|
3409
|
+
return json1 === json2;
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Gets all field references used in a specification
|
|
3413
|
+
*/
|
|
3414
|
+
static getReferencedFields(spec) {
|
|
3415
|
+
const fields = new Set();
|
|
3416
|
+
this.collectFields(spec, fields);
|
|
3417
|
+
return fields;
|
|
3418
|
+
}
|
|
3419
|
+
static collectFields(spec, fields) {
|
|
3420
|
+
if (spec instanceof FieldSpecification) {
|
|
3421
|
+
fields.add(spec.getField());
|
|
3422
|
+
}
|
|
3423
|
+
else if (spec instanceof AndSpecification ||
|
|
3424
|
+
spec instanceof OrSpecification) {
|
|
3425
|
+
spec.getSpecifications().forEach((s) => this.collectFields(s, fields));
|
|
3426
|
+
}
|
|
3427
|
+
else if (spec instanceof NotSpecification) {
|
|
3428
|
+
this.collectFields(spec.getSpecification(), fields);
|
|
3429
|
+
}
|
|
3430
|
+
// Add more cases as needed for other specification types
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Validates a specification for common issues
|
|
3434
|
+
*/
|
|
3435
|
+
static validate(spec) {
|
|
3436
|
+
const errors = [];
|
|
3437
|
+
const warnings = [];
|
|
3438
|
+
try {
|
|
3439
|
+
this.validateSpecification(spec, errors, warnings);
|
|
3440
|
+
}
|
|
3441
|
+
catch (error) {
|
|
3442
|
+
errors.push(`Validation error: ${error}`);
|
|
3443
|
+
}
|
|
3444
|
+
return {
|
|
3445
|
+
isValid: errors.length === 0,
|
|
3446
|
+
errors,
|
|
3447
|
+
warnings,
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
static validateSpecification(spec, errors, warnings) {
|
|
3451
|
+
if (spec instanceof AndSpecification || spec instanceof OrSpecification) {
|
|
3452
|
+
const specs = spec.getSpecifications();
|
|
3453
|
+
if (specs.length === 0) {
|
|
3454
|
+
errors.push(`${spec.constructor.name} must have at least one specification`);
|
|
3455
|
+
}
|
|
3456
|
+
specs.forEach((s) => this.validateSpecification(s, errors, warnings));
|
|
3457
|
+
}
|
|
3458
|
+
else if (spec instanceof NotSpecification) {
|
|
3459
|
+
this.validateSpecification(spec.getSpecification(), errors, warnings);
|
|
3460
|
+
}
|
|
3461
|
+
// Add more validation rules as needed
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Estimates the complexity of a specification (useful for performance analysis)
|
|
3465
|
+
*/
|
|
3466
|
+
static getComplexity(spec) {
|
|
3467
|
+
if (spec instanceof FieldSpecification) {
|
|
3468
|
+
return 1;
|
|
3469
|
+
}
|
|
3470
|
+
if (spec instanceof AndSpecification || spec instanceof OrSpecification) {
|
|
3471
|
+
return spec
|
|
3472
|
+
.getSpecifications()
|
|
3473
|
+
.reduce((total, s) => total + this.getComplexity(s), 1);
|
|
3474
|
+
}
|
|
3475
|
+
if (spec instanceof NotSpecification) {
|
|
3476
|
+
return 1 + this.getComplexity(spec.getSpecification());
|
|
3477
|
+
}
|
|
3478
|
+
return 1; // Base complexity for unknown types
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
// Utility functions for specifications
|
|
3483
|
+
|
|
3484
|
+
// Main entry point for @praxisui/specification library
|
|
3485
|
+
|
|
3486
|
+
/*
|
|
3487
|
+
* Public API Surface of @praxisui/specification
|
|
3488
|
+
*/
|
|
3489
|
+
|
|
3490
|
+
/**
|
|
3491
|
+
* Generated bundle index. Do not edit.
|
|
3492
|
+
*/
|
|
3493
|
+
|
|
3494
|
+
export { AndSpecification, AtLeastSpecification, COMPARISON_OPERATORS, ComparisonOperator, CompositeContextProvider, ConditionalSpecification, ConditionalType, ContextualSpecification, DateContextProvider, DefaultContextProvider, DisabledIfSpecification, DslExporter, DslParser, DslTokenizer, DslValidator, ExactlySpecification, FieldSpecification, FieldToFieldSpecification, ForEachSpecification, FormSpecification, FunctionRegistry, FunctionSpecification, IfDefinedSpecification, IfExistsSpecification, IfNotNullSpecification, ImpliesSpecification, MaxLengthSpecification, MinLengthSpecification, NotSpecification, OPERATOR_KEYWORDS, OPERATOR_SYMBOLS, OrSpecification, ReadonlyIfSpecification, RequiredIfSpecification, SpecificationFactory, SpecificationUtils, TokenType, TransformRegistry, UniqueBySpecification, ValidationIssueType, ValidationSeverity, VisibleIfSpecification, WithDefaultSpecification, XorSpecification };
|
|
3495
|
+
//# sourceMappingURL=praxisui-specification.mjs.map
|