@matter/model 0.12.0-alpha.0-20241229-9d9c99934 → 0.12.0-alpha.0-20241231-9ac20db97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/aspects/Constraint.d.ts +24 -15
- package/dist/cjs/aspects/Constraint.d.ts.map +1 -1
- package/dist/cjs/aspects/Constraint.js +268 -198
- package/dist/cjs/aspects/Constraint.js.map +2 -2
- package/dist/cjs/common/FieldValue.d.ts +9 -3
- package/dist/cjs/common/FieldValue.d.ts.map +1 -1
- package/dist/cjs/common/FieldValue.js +1 -1
- package/dist/cjs/common/FieldValue.js.map +1 -1
- package/dist/cjs/logic/definition-validation/ValueValidator.js +1 -1
- package/dist/cjs/logic/definition-validation/ValueValidator.js.map +1 -1
- package/dist/cjs/parser/Lexer.d.ts +3 -3
- package/dist/cjs/parser/Lexer.d.ts.map +1 -1
- package/dist/cjs/parser/Lexer.js +35 -31
- package/dist/cjs/parser/Lexer.js.map +1 -1
- package/dist/cjs/parser/Token.d.ts +5 -2
- package/dist/cjs/parser/Token.d.ts.map +1 -1
- package/dist/cjs/parser/TokenStream.js +2 -2
- package/dist/esm/aspects/Constraint.d.ts +24 -15
- package/dist/esm/aspects/Constraint.d.ts.map +1 -1
- package/dist/esm/aspects/Constraint.js +269 -199
- package/dist/esm/aspects/Constraint.js.map +2 -2
- package/dist/esm/common/FieldValue.d.ts +9 -3
- package/dist/esm/common/FieldValue.d.ts.map +1 -1
- package/dist/esm/common/FieldValue.js +1 -1
- package/dist/esm/common/FieldValue.js.map +1 -1
- package/dist/esm/logic/definition-validation/ValueValidator.js +1 -1
- package/dist/esm/logic/definition-validation/ValueValidator.js.map +1 -1
- package/dist/esm/parser/Lexer.d.ts +3 -3
- package/dist/esm/parser/Lexer.d.ts.map +1 -1
- package/dist/esm/parser/Lexer.js +35 -31
- package/dist/esm/parser/Lexer.js.map +1 -1
- package/dist/esm/parser/Token.d.ts +5 -2
- package/dist/esm/parser/Token.d.ts.map +1 -1
- package/dist/esm/parser/TokenStream.js +2 -2
- package/package.json +4 -4
- package/src/aspects/Constraint.ts +340 -215
- package/src/common/FieldValue.ts +9 -4
- package/src/logic/definition-validation/ValueValidator.ts +1 -1
- package/src/parser/Lexer.ts +38 -40
- package/src/parser/Token.ts +11 -1
- package/src/parser/TokenStream.ts +2 -2
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Lexer } from "#parser/Lexer.js";
|
|
8
|
+
import { BasicToken } from "#parser/Token.js";
|
|
9
|
+
import { TokenStream } from "#parser/TokenStream.js";
|
|
10
|
+
import { camelize } from "@matter/general";
|
|
8
11
|
import { FieldValue } from "../common/index.js";
|
|
9
12
|
import { Aspect } from "./Aspect.js";
|
|
10
13
|
|
|
@@ -12,17 +15,15 @@ import { Aspect } from "./Aspect.js";
|
|
|
12
15
|
* An operational view of constraints as defined by the Matter specification.
|
|
13
16
|
*
|
|
14
17
|
* A "constraint" limits possible data values.
|
|
15
|
-
*
|
|
16
|
-
* Formally a constraint is not considered a quality by the specification. It is handled similarly to qualities, though,
|
|
17
|
-
* so we keep it in the same section.
|
|
18
18
|
*/
|
|
19
19
|
export class Constraint extends Aspect<Constraint.Definition> implements Constraint.Ast {
|
|
20
20
|
declare desc?: boolean;
|
|
21
|
-
declare value?:
|
|
22
|
-
declare min?:
|
|
23
|
-
declare max?:
|
|
21
|
+
declare value?: Constraint.Expression;
|
|
22
|
+
declare min?: Constraint.Expression;
|
|
23
|
+
declare max?: Constraint.Expression;
|
|
24
24
|
declare in?: FieldValue;
|
|
25
25
|
declare entry?: Constraint;
|
|
26
|
+
declare cpMax?: number;
|
|
26
27
|
declare parts?: Constraint[];
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -40,7 +41,7 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
|
|
|
40
41
|
if (definition.match(/(?:0b[0x ]*x[0x ]*)|(?:0x[0x_]*x[0x_]*)|(?:00[0x]*x)/i)) {
|
|
41
42
|
break;
|
|
42
43
|
}
|
|
43
|
-
ast =
|
|
44
|
+
ast = Parser.parse(this, definition);
|
|
44
45
|
break;
|
|
45
46
|
|
|
46
47
|
case "number":
|
|
@@ -77,6 +78,9 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
|
|
|
77
78
|
if (ast.entry !== undefined) {
|
|
78
79
|
this.entry = new Constraint(ast.entry);
|
|
79
80
|
}
|
|
81
|
+
if (ast.cpMax !== undefined) {
|
|
82
|
+
this.cpMax = ast.cpMax;
|
|
83
|
+
}
|
|
80
84
|
if (ast.parts !== undefined) {
|
|
81
85
|
this.parts = ast.parts.map(p => new Constraint(p));
|
|
82
86
|
}
|
|
@@ -89,14 +93,40 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
|
|
|
89
93
|
*/
|
|
90
94
|
test(value: FieldValue, properties?: Record<string, any>): boolean {
|
|
91
95
|
// Helper that looks up "reference" field values in properties. This is for constraints such as "min FieldName"
|
|
92
|
-
function valueOf(value:
|
|
96
|
+
function valueOf(value: Constraint.Expression | undefined, raw = false): FieldValue | undefined {
|
|
93
97
|
if (!raw && (typeof value === "string" || Array.isArray(value))) {
|
|
94
98
|
return value.length;
|
|
95
99
|
}
|
|
96
|
-
if (
|
|
97
|
-
const { type
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
101
|
+
const { type } = value;
|
|
102
|
+
switch (type) {
|
|
103
|
+
case FieldValue.reference:
|
|
104
|
+
if (typeof value.name === "string") {
|
|
105
|
+
value = valueOf(properties?.[camelize(value.name)], raw);
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case "+":
|
|
110
|
+
{
|
|
111
|
+
const lhs = valueOf(value.lhs);
|
|
112
|
+
const rhs = valueOf(value.rhs);
|
|
113
|
+
if (typeof lhs === "number" && typeof rhs === "number") {
|
|
114
|
+
return lhs + rhs;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case "-":
|
|
121
|
+
{
|
|
122
|
+
const lhs = valueOf(value.lhs);
|
|
123
|
+
const rhs = valueOf(value.rhs);
|
|
124
|
+
if (typeof lhs === "number" && typeof rhs === "number") {
|
|
125
|
+
return lhs - rhs;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
100
130
|
}
|
|
101
131
|
}
|
|
102
132
|
|
|
@@ -110,7 +140,7 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
|
|
|
110
140
|
if (this.in) {
|
|
111
141
|
let set = valueOf(this.in, true);
|
|
112
142
|
if (!Array.isArray(set)) {
|
|
113
|
-
set = [set];
|
|
143
|
+
set = set === undefined ? [] : [set];
|
|
114
144
|
}
|
|
115
145
|
return (set as unknown[]).indexOf(value) !== -1;
|
|
116
146
|
}
|
|
@@ -126,14 +156,14 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
|
|
|
126
156
|
|
|
127
157
|
if (this.min !== undefined && this.min !== null) {
|
|
128
158
|
const min = valueOf(this.min);
|
|
129
|
-
if (min !== undefined && min !== null &&
|
|
159
|
+
if (min !== undefined && min !== null && min > value) {
|
|
130
160
|
return false;
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
163
|
|
|
134
164
|
if (this.max !== undefined && this.max !== null) {
|
|
135
165
|
const max = valueOf(this.max);
|
|
136
|
-
if (max !== undefined && max !== null &&
|
|
166
|
+
if (max !== undefined && max !== null && max < value) {
|
|
137
167
|
return false;
|
|
138
168
|
}
|
|
139
169
|
}
|
|
@@ -149,7 +179,7 @@ export class Constraint extends Aspect<Constraint.Definition> implements Constra
|
|
|
149
179
|
if (!this.valid && this.definition) {
|
|
150
180
|
return this.definition.toString();
|
|
151
181
|
}
|
|
152
|
-
return
|
|
182
|
+
return Serializer.serialize(this);
|
|
153
183
|
}
|
|
154
184
|
|
|
155
185
|
protected override freeze() {
|
|
@@ -164,7 +194,7 @@ export namespace Constraint {
|
|
|
164
194
|
export type NumberOrIdentifier = number | string;
|
|
165
195
|
|
|
166
196
|
/**
|
|
167
|
-
* Parsed
|
|
197
|
+
* Parsed constraint.
|
|
168
198
|
*/
|
|
169
199
|
export type Ast = {
|
|
170
200
|
/**
|
|
@@ -175,17 +205,17 @@ export namespace Constraint {
|
|
|
175
205
|
/**
|
|
176
206
|
* Constant value.
|
|
177
207
|
*/
|
|
178
|
-
value?:
|
|
208
|
+
value?: Expression;
|
|
179
209
|
|
|
180
210
|
/**
|
|
181
211
|
* Lower bound on value or sequence length.
|
|
182
212
|
*/
|
|
183
|
-
min?:
|
|
213
|
+
min?: Expression;
|
|
184
214
|
|
|
185
215
|
/**
|
|
186
216
|
* Upper bound on value or sequence length.
|
|
187
217
|
*/
|
|
188
|
-
max?:
|
|
218
|
+
max?: Expression;
|
|
189
219
|
|
|
190
220
|
/**
|
|
191
221
|
* Require set membership for the value.
|
|
@@ -197,268 +227,363 @@ export namespace Constraint {
|
|
|
197
227
|
*/
|
|
198
228
|
entry?: Ast;
|
|
199
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Constraint on codepoints in a string.
|
|
232
|
+
*/
|
|
233
|
+
cpMax?: number;
|
|
234
|
+
|
|
200
235
|
/**
|
|
201
236
|
* List of sub-constraints in a sequence.
|
|
202
237
|
*/
|
|
203
238
|
parts?: Ast[];
|
|
204
239
|
};
|
|
205
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Parsed binary operator.
|
|
243
|
+
*/
|
|
244
|
+
export interface BinaryOperator {
|
|
245
|
+
type: "+" | "-";
|
|
246
|
+
|
|
247
|
+
lhs: Expression;
|
|
248
|
+
|
|
249
|
+
rhs: Expression;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Parsed expression.
|
|
254
|
+
*/
|
|
255
|
+
export type Expression = FieldValue | BinaryOperator;
|
|
256
|
+
|
|
206
257
|
/**
|
|
207
258
|
* These are all ways to describe a constraint.
|
|
208
259
|
*/
|
|
209
260
|
export type Definition = (Ast & { definition?: Definition }) | string | number | undefined;
|
|
261
|
+
}
|
|
210
262
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
} else {
|
|
216
|
-
value = Number.parseFloat(numOrName);
|
|
263
|
+
namespace Serializer {
|
|
264
|
+
export function serialize(ast: Constraint.Ast): string {
|
|
265
|
+
if (ast.parts) {
|
|
266
|
+
return ast.parts.map(serialize).join(", ");
|
|
217
267
|
}
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
268
|
+
if (ast.entry) {
|
|
269
|
+
return `${serializeAtom(ast)}[${serialize(ast.entry)}]`;
|
|
270
|
+
}
|
|
271
|
+
if (ast.cpMax) {
|
|
272
|
+
return `${serializeAtom(ast)}{${ast.cpMax}}`;
|
|
273
|
+
}
|
|
274
|
+
return serializeAtom(ast);
|
|
275
|
+
}
|
|
223
276
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
277
|
+
function serializeValue(value: Constraint.Expression, inExpr = false): string {
|
|
278
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date) {
|
|
279
|
+
return FieldValue.serialize(value);
|
|
227
280
|
}
|
|
228
|
-
|
|
229
|
-
|
|
281
|
+
|
|
282
|
+
switch (value.type) {
|
|
283
|
+
case "+":
|
|
284
|
+
case "-":
|
|
285
|
+
const sum = `${serializeValue(value.lhs, true)} ${value.type} ${serializeValue(value.rhs, true)}`;
|
|
286
|
+
if (inExpr) {
|
|
287
|
+
// Ideally only add parenthesis if precedence requires. But nested expressions are not used
|
|
288
|
+
// anywhere as yet (and probably won't be) so don't try to be fancy, just correct
|
|
289
|
+
return `(${sum})`;
|
|
290
|
+
}
|
|
291
|
+
return sum;
|
|
292
|
+
|
|
293
|
+
default:
|
|
294
|
+
return FieldValue.serialize(value);
|
|
230
295
|
}
|
|
231
|
-
|
|
232
|
-
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function serializeAtom(ast: Constraint.Ast) {
|
|
299
|
+
if (ast.desc) {
|
|
300
|
+
return "desc";
|
|
233
301
|
}
|
|
234
|
-
|
|
235
|
-
|
|
302
|
+
|
|
303
|
+
if (ast.value !== undefined && ast.value !== null) {
|
|
304
|
+
return `${serializeValue(ast.value)}`;
|
|
236
305
|
}
|
|
237
|
-
|
|
306
|
+
|
|
307
|
+
if (ast.min !== undefined && ast.min !== null) {
|
|
308
|
+
if (ast.max === undefined || ast.max === null) {
|
|
309
|
+
return `min ${serializeValue(ast.min)}`;
|
|
310
|
+
}
|
|
311
|
+
return `${serializeValue(ast.min)} to ${serializeValue(ast.max)}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (ast.max !== undefined && ast.max !== null) {
|
|
315
|
+
return `max ${serializeValue(ast.max)}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (ast.in !== undefined) {
|
|
319
|
+
return `in ${serializeValue(ast.in)}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return "all";
|
|
238
323
|
}
|
|
324
|
+
}
|
|
239
325
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
case 0:
|
|
243
|
-
return undefined;
|
|
326
|
+
namespace Parser {
|
|
327
|
+
const lexer = new Lexer(["in", "min", "max", "to", "all", "desc", "true", "false"]);
|
|
244
328
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
case "desc":
|
|
248
|
-
return { desc: true };
|
|
329
|
+
export function parse(constraint: Constraint, definition: string): Constraint.Ast {
|
|
330
|
+
const tokens = TokenStream(lexer.lex(definition, (code, message) => constraint.error(code, message)));
|
|
249
331
|
|
|
250
|
-
|
|
251
|
-
case "any":
|
|
252
|
-
return {};
|
|
253
|
-
}
|
|
254
|
-
const value = parseValue(words[0]);
|
|
255
|
-
if (value === undefined || value === null) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
return { value };
|
|
332
|
+
const result = parseParts();
|
|
259
333
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const min = parseValue(words[1]);
|
|
264
|
-
if (min === undefined || min === null) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
return { min: min };
|
|
334
|
+
if (tokens.token && tokens.token?.type !== ",") {
|
|
335
|
+
constraint.error("UNEXPECTED_CONSTRAINT_TOKEN", `Unexpected ${tokens.description}`);
|
|
336
|
+
}
|
|
268
337
|
|
|
269
|
-
|
|
270
|
-
const max = parseValue(words[1]);
|
|
271
|
-
if (max === undefined || max === null) {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
return { max: max };
|
|
338
|
+
return result;
|
|
275
339
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return { in: ref };
|
|
340
|
+
function parseParts(): Constraint.Ast {
|
|
341
|
+
const parts = Array<Constraint.Ast>();
|
|
279
342
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
343
|
+
while (true) {
|
|
344
|
+
const part = parsePart();
|
|
345
|
+
|
|
346
|
+
if (part !== undefined) {
|
|
347
|
+
parts.push(part);
|
|
285
348
|
}
|
|
286
|
-
return;
|
|
287
349
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (words[pos].toLowerCase() === name) {
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
|
-
return parseValue(words[pos]);
|
|
295
|
-
}
|
|
350
|
+
if (tokens.done) {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
296
353
|
|
|
297
|
-
|
|
354
|
+
if (tokens.token?.type !== ",") {
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
298
357
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
ast.min = min;
|
|
302
|
-
}
|
|
358
|
+
tokens.next();
|
|
359
|
+
}
|
|
303
360
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
361
|
+
if (!parts.length) {
|
|
362
|
+
return {};
|
|
363
|
+
}
|
|
308
364
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
365
|
+
if (parts.length === 1) {
|
|
366
|
+
return parts[0];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { parts };
|
|
314
370
|
}
|
|
315
371
|
|
|
316
|
-
|
|
317
|
-
|
|
372
|
+
function parsePart(): Constraint.Ast | undefined {
|
|
373
|
+
const result = parsePartWithoutSubconstraint();
|
|
318
374
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
*/
|
|
322
|
-
export function parse(constraint: Constraint, definition: string): Ast {
|
|
323
|
-
let pos = 2;
|
|
324
|
-
let current: string | undefined = definition[0];
|
|
325
|
-
let peeked: string | undefined = definition[1];
|
|
326
|
-
|
|
327
|
-
function next() {
|
|
328
|
-
current = peeked;
|
|
329
|
-
if (pos === definition.length) {
|
|
330
|
-
peeked = undefined;
|
|
331
|
-
} else {
|
|
332
|
-
peeked = definition[pos];
|
|
333
|
-
pos++;
|
|
375
|
+
if (result === undefined) {
|
|
376
|
+
return result;
|
|
334
377
|
}
|
|
335
|
-
}
|
|
336
378
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
379
|
+
switch (tokens.token?.type) {
|
|
380
|
+
case "[":
|
|
381
|
+
{
|
|
382
|
+
tokens.next();
|
|
341
383
|
|
|
342
|
-
|
|
343
|
-
if (word) {
|
|
344
|
-
words.push(word);
|
|
345
|
-
word = "";
|
|
346
|
-
}
|
|
384
|
+
const entry = parseParts();
|
|
347
385
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
386
|
+
if (tokens.token?.type !== ("]" as any)) {
|
|
387
|
+
constraint.error("MISSING_ENTRY_END", 'Entry constraint does not end with "]"');
|
|
388
|
+
}
|
|
352
389
|
|
|
353
|
-
|
|
354
|
-
const atom = parseWords();
|
|
355
|
-
if (atom !== undefined) {
|
|
356
|
-
parts.push(atom);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
390
|
+
tokens.next();
|
|
359
391
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
case " ":
|
|
363
|
-
case "\t":
|
|
364
|
-
case "\r":
|
|
365
|
-
case "\n":
|
|
366
|
-
case "\v":
|
|
367
|
-
case "\f":
|
|
368
|
-
if (word) {
|
|
369
|
-
words.push(word);
|
|
370
|
-
word = "";
|
|
392
|
+
if (entry !== undefined) {
|
|
393
|
+
result.entry = entry;
|
|
371
394
|
}
|
|
372
|
-
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
373
397
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
398
|
+
case "{":
|
|
399
|
+
{
|
|
400
|
+
tokens.next();
|
|
401
|
+
|
|
402
|
+
if (tokens.token?.type !== ("value" as any)) {
|
|
403
|
+
constraint.error(
|
|
404
|
+
"MISSING_CODEPOINT_MAX",
|
|
405
|
+
"Codepoint constraint does not specify maximum codepoint length",
|
|
406
|
+
);
|
|
407
|
+
if (tokens.peeked?.type === "}") {
|
|
408
|
+
tokens.next();
|
|
381
409
|
}
|
|
382
|
-
|
|
410
|
+
} else {
|
|
411
|
+
result.cpMax = FieldValue.numericValue(
|
|
412
|
+
(tokens.token as unknown as BasicToken.Number).value,
|
|
413
|
+
);
|
|
414
|
+
tokens.next();
|
|
383
415
|
}
|
|
384
|
-
if (ast) {
|
|
385
|
-
parts.push(ast);
|
|
386
|
-
}
|
|
387
|
-
break;
|
|
388
416
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
constraint.error("INVALID_CONSTRAINT", 'Unexpected "]"');
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
emit();
|
|
395
|
-
if (parts.length > 1) {
|
|
396
|
-
return { parts: parts };
|
|
417
|
+
if (tokens.token?.type !== ("}" as any)) {
|
|
418
|
+
constraint.error("MISSING_CODEPOINT_END", 'Codepoint constraint does not end with "}"');
|
|
397
419
|
}
|
|
398
|
-
return parts[0];
|
|
399
420
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
421
|
+
tokens.next();
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
403
425
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parsePartWithoutSubconstraint(): Constraint.Ast | undefined {
|
|
430
|
+
const { token } = tokens;
|
|
408
431
|
|
|
409
|
-
|
|
432
|
+
if (!token) {
|
|
433
|
+
return;
|
|
410
434
|
}
|
|
411
435
|
|
|
412
|
-
|
|
413
|
-
|
|
436
|
+
switch (token.type) {
|
|
437
|
+
case "desc":
|
|
438
|
+
tokens.next();
|
|
439
|
+
return { desc: true };
|
|
440
|
+
|
|
441
|
+
case "all":
|
|
442
|
+
tokens.next();
|
|
443
|
+
return {};
|
|
444
|
+
|
|
445
|
+
case "min":
|
|
446
|
+
case "max":
|
|
447
|
+
tokens.next();
|
|
448
|
+
return parseSingleBound(token.type);
|
|
449
|
+
|
|
450
|
+
case "in":
|
|
451
|
+
tokens.next();
|
|
452
|
+
if (tokens.token?.type === "word") {
|
|
453
|
+
const name = tokens.token.value;
|
|
454
|
+
tokens.next();
|
|
455
|
+
return { in: FieldValue.Reference(name) };
|
|
456
|
+
}
|
|
457
|
+
constraint.error("MISSING_IN_FIELD", 'Expected field name to follow "in"');
|
|
458
|
+
break;
|
|
414
459
|
}
|
|
415
460
|
|
|
416
|
-
|
|
461
|
+
const value = parseExpression();
|
|
417
462
|
|
|
418
|
-
if (
|
|
419
|
-
return
|
|
463
|
+
if (value === undefined || tokens.token?.type !== "to") {
|
|
464
|
+
return { value };
|
|
420
465
|
}
|
|
421
466
|
|
|
422
|
-
|
|
423
|
-
}
|
|
467
|
+
tokens.next();
|
|
424
468
|
|
|
425
|
-
|
|
426
|
-
|
|
469
|
+
const max = parseExpression();
|
|
470
|
+
if (max === undefined) {
|
|
471
|
+
constraint.error("MISSING_UPPER_BOUND", `"to" must be followed by upper boundary value`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
427
474
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
475
|
+
return {
|
|
476
|
+
min: value,
|
|
477
|
+
max,
|
|
478
|
+
};
|
|
431
479
|
}
|
|
432
480
|
|
|
433
|
-
|
|
434
|
-
|
|
481
|
+
function parseSingleBound(kind: "min" | "max"): Constraint.Ast | undefined {
|
|
482
|
+
const bound = parseExpression();
|
|
483
|
+
if (bound === undefined) {
|
|
484
|
+
constraint.error("MISSING_SINGLE_BOUND", `"${kind}" must be followed by boundary value`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
return { [kind]: bound };
|
|
435
488
|
}
|
|
436
489
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
490
|
+
function parseExpression(): Constraint.Expression | undefined {
|
|
491
|
+
const value = parseValueExpression();
|
|
492
|
+
|
|
493
|
+
if (value === undefined) {
|
|
494
|
+
return value;
|
|
440
495
|
}
|
|
441
|
-
return `${FieldValue.serialize(ast.min)} to ${FieldValue.serialize(ast.max)}`;
|
|
442
|
-
}
|
|
443
496
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
497
|
+
switch (tokens.token?.type) {
|
|
498
|
+
case "+":
|
|
499
|
+
case "-":
|
|
500
|
+
const type = tokens.token.type;
|
|
501
|
+
tokens.next();
|
|
502
|
+
const rhs = parseValueExpression();
|
|
503
|
+
if (rhs === undefined) {
|
|
504
|
+
constraint.error("MISSING_RIGHT_OPERAND", `Missing operand after "${type}"`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
447
507
|
|
|
448
|
-
|
|
449
|
-
|
|
508
|
+
return {
|
|
509
|
+
type,
|
|
510
|
+
lhs: value,
|
|
511
|
+
rhs,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return value;
|
|
450
516
|
}
|
|
451
517
|
|
|
452
|
-
|
|
453
|
-
|
|
518
|
+
function parseValueExpression(): Constraint.Expression | undefined {
|
|
519
|
+
const { token } = tokens;
|
|
454
520
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
521
|
+
if (token === undefined) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
switch (token.type) {
|
|
526
|
+
case "value":
|
|
527
|
+
tokens.next();
|
|
528
|
+
return token.value;
|
|
529
|
+
|
|
530
|
+
case "true":
|
|
531
|
+
tokens.next();
|
|
532
|
+
return true;
|
|
533
|
+
|
|
534
|
+
case "false":
|
|
535
|
+
tokens.next();
|
|
536
|
+
return false;
|
|
537
|
+
|
|
538
|
+
case "word":
|
|
539
|
+
const ref = FieldValue.Reference(camelize(token.value));
|
|
540
|
+
tokens.next();
|
|
541
|
+
return ref;
|
|
542
|
+
|
|
543
|
+
case "-":
|
|
544
|
+
case "+": {
|
|
545
|
+
tokens.next();
|
|
546
|
+
|
|
547
|
+
let number = tokens.token?.type === "value" ? tokens.token.value : undefined;
|
|
548
|
+
|
|
549
|
+
if (number !== undefined) {
|
|
550
|
+
tokens.next();
|
|
551
|
+
|
|
552
|
+
if (token.type === "-") {
|
|
553
|
+
if (typeof number === "number") {
|
|
554
|
+
number *= -1;
|
|
555
|
+
} else if (
|
|
556
|
+
FieldValue.is(number, FieldValue.percent) ||
|
|
557
|
+
FieldValue.is(number, FieldValue.celsius)
|
|
558
|
+
) {
|
|
559
|
+
(number as FieldValue.Percent | FieldValue.Celsius).value *= -1;
|
|
560
|
+
} else {
|
|
561
|
+
number = undefined;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (number === undefined) {
|
|
567
|
+
constraint.error("MISSING_NUMBER", `Unary "${token.type}" not followed by numeric value`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return number;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
case "(": {
|
|
575
|
+
tokens.next();
|
|
576
|
+
|
|
577
|
+
const result = parseExpression();
|
|
578
|
+
if (tokens.token?.type !== ")") {
|
|
579
|
+
constraint.error("MISSING_GROUP_END", 'Group does not end with ")"');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
tokens.next();
|
|
583
|
+
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
461
587
|
}
|
|
462
|
-
return serializeAtom(ast);
|
|
463
588
|
}
|
|
464
589
|
}
|