cddl 0.11.0 → 0.12.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/build/parser.d.ts +1 -0
- package/build/parser.d.ts.map +1 -1
- package/build/parser.js +62 -12
- package/docs/cddl-cheatsheet.md +196 -0
- package/package.json +4 -3
package/build/parser.d.ts
CHANGED
package/build/parser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,YAAY,CAAA;AAE9B,OAAO,EAAE,KAAK,EAAU,MAAM,aAAa,CAAC;AAG5C,OAAO,EAE2C,UAAU,EAE3D,MAAM,UAAU,CAAA;AAoBjB,MAAM,CAAC,OAAO,OAAO,MAAM;;IAEvB,CAAC,EAAE,KAAK,CAAC;IAET,QAAQ,EAAE,KAAK,CAAa;IAC5B,SAAS,EAAE,KAAK,CAAa;
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,YAAY,CAAA;AAE9B,OAAO,EAAE,KAAK,EAAU,MAAM,aAAa,CAAC;AAG5C,OAAO,EAE2C,UAAU,EAE3D,MAAM,UAAU,CAAA;AAoBjB,MAAM,CAAC,OAAO,OAAO,MAAM;;IAEvB,CAAC,EAAE,KAAK,CAAC;IAET,QAAQ,EAAE,KAAK,CAAa;IAC5B,SAAS,EAAE,KAAK,CAAa;IAC7B,cAAc,EAAE,KAAK,CAAa;gBAErB,QAAQ,EAAE,MAAM;IAS7B,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,gBAAgB;IA2CxB,OAAO,CAAC,oBAAoB;IA8d5B,OAAO,CAAC,wBAAwB;IAahC;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,iBAAiB;IAqKzB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,kBAAkB;IAuD1B,OAAO,CAAC,gBAAgB;IA4DxB;;OAEG;IACH,OAAO,CAAC,YAAY;IAcpB,KAAK;IAaL,OAAO,CAAC,WAAW;CAKtB"}
|
package/build/parser.js
CHANGED
|
@@ -26,15 +26,18 @@ export default class Parser {
|
|
|
26
26
|
l;
|
|
27
27
|
curToken = NIL_TOKEN;
|
|
28
28
|
peekToken = NIL_TOKEN;
|
|
29
|
+
peekBelowToken = NIL_TOKEN;
|
|
29
30
|
constructor(filePath) {
|
|
30
31
|
this.#filePath = filePath;
|
|
31
32
|
this.l = new Lexer(fs.readFileSync(filePath, 'utf-8'));
|
|
32
33
|
this.nextToken();
|
|
33
34
|
this.nextToken();
|
|
35
|
+
this.nextToken();
|
|
34
36
|
}
|
|
35
37
|
nextToken() {
|
|
36
38
|
this.curToken = this.peekToken;
|
|
37
|
-
this.peekToken = this.
|
|
39
|
+
this.peekToken = this.peekBelowToken;
|
|
40
|
+
this.peekBelowToken = this.l.nextToken();
|
|
38
41
|
return true;
|
|
39
42
|
}
|
|
40
43
|
parseAssignments() {
|
|
@@ -110,7 +113,10 @@ export default class Parser {
|
|
|
110
113
|
* attireBlock
|
|
111
114
|
* )
|
|
112
115
|
*/
|
|
113
|
-
if (closingTokens.
|
|
116
|
+
if (closingTokens.includes(Tokens.RPAREN) &&
|
|
117
|
+
this.peekToken.Type === Tokens.SLASH &&
|
|
118
|
+
this.peekBelowToken.Type !== Tokens.SLASH &&
|
|
119
|
+
!(this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH)) {
|
|
114
120
|
const propertyType = [];
|
|
115
121
|
while (!closingTokens.includes(this.curToken.Type)) {
|
|
116
122
|
propertyType.push(...this.parsePropertyTypes());
|
|
@@ -123,6 +129,9 @@ export default class Parser {
|
|
|
123
129
|
this.nextToken();
|
|
124
130
|
}
|
|
125
131
|
}
|
|
132
|
+
if (this.curToken.Type === Tokens.RPAREN) {
|
|
133
|
+
this.nextToken();
|
|
134
|
+
}
|
|
126
135
|
if (groupName) {
|
|
127
136
|
const variable = {
|
|
128
137
|
Type: 'variable',
|
|
@@ -163,6 +172,22 @@ export default class Parser {
|
|
|
163
172
|
return [prop];
|
|
164
173
|
}
|
|
165
174
|
while (!closingTokens.includes(this.curToken.Type)) {
|
|
175
|
+
/**
|
|
176
|
+
* check if we have a group choice instead of an assignment
|
|
177
|
+
*/
|
|
178
|
+
if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) {
|
|
179
|
+
if (valuesOrProperties.length === 0) {
|
|
180
|
+
throw this.parserError('Unexpected group choice operator "//" at start of group');
|
|
181
|
+
}
|
|
182
|
+
if (!isChoice) {
|
|
183
|
+
const last = valuesOrProperties.pop();
|
|
184
|
+
valuesOrProperties.push([last]);
|
|
185
|
+
isChoice = true;
|
|
186
|
+
}
|
|
187
|
+
this.nextToken();
|
|
188
|
+
this.nextToken();
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
166
191
|
const propertyType = [];
|
|
167
192
|
const comments = [];
|
|
168
193
|
let isUnwrapped = false;
|
|
@@ -197,13 +222,23 @@ export default class Parser {
|
|
|
197
222
|
this.curToken.Literal === Tokens.LBRACK ||
|
|
198
223
|
this.curToken.Literal === Tokens.LPAREN) {
|
|
199
224
|
const innerGroup = this.parseAssignmentValue();
|
|
200
|
-
|
|
225
|
+
const prop = {
|
|
201
226
|
HasCut: false,
|
|
202
227
|
Occurrence: occurrence,
|
|
203
228
|
Name: '',
|
|
204
229
|
Type: innerGroup,
|
|
205
230
|
Comments: []
|
|
206
|
-
}
|
|
231
|
+
};
|
|
232
|
+
if (isChoice) {
|
|
233
|
+
valuesOrProperties[valuesOrProperties.length - 1].push(prop);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
valuesOrProperties.push(prop);
|
|
237
|
+
}
|
|
238
|
+
if (this.curToken.Type === Tokens.COMMA) {
|
|
239
|
+
this.nextToken();
|
|
240
|
+
isChoice = false;
|
|
241
|
+
}
|
|
207
242
|
continue;
|
|
208
243
|
}
|
|
209
244
|
/**
|
|
@@ -316,8 +351,8 @@ export default class Parser {
|
|
|
316
351
|
* parse property value
|
|
317
352
|
*/
|
|
318
353
|
const props = this.parseAssignmentValue();
|
|
319
|
-
|
|
320
|
-
if (!isChoice && this.curToken.Type === Tokens.SLASH) {
|
|
354
|
+
let operator = this.isOperator() ? this.parseOperator() : undefined;
|
|
355
|
+
if (!isChoice && this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) {
|
|
321
356
|
this.nextToken();
|
|
322
357
|
const nextType = this.parsePropertyType();
|
|
323
358
|
if (Array.isArray(props)) {
|
|
@@ -326,9 +361,14 @@ export default class Parser {
|
|
|
326
361
|
* of one, e.g. `(float .ge 1.0) / null`
|
|
327
362
|
*/
|
|
328
363
|
props.push(nextType);
|
|
329
|
-
this.
|
|
364
|
+
if (!this.isOperator()) {
|
|
365
|
+
this.nextToken();
|
|
366
|
+
}
|
|
330
367
|
}
|
|
331
368
|
}
|
|
369
|
+
if (this.isOperator()) {
|
|
370
|
+
operator = this.parseOperator();
|
|
371
|
+
}
|
|
332
372
|
if (Array.isArray(props)) {
|
|
333
373
|
/**
|
|
334
374
|
* property has multiple types (e.g. `float / tstr / int`)
|
|
@@ -480,10 +520,6 @@ export default class Parser {
|
|
|
480
520
|
openSegment() {
|
|
481
521
|
if (this.curToken.Type === Tokens.LBRACE) {
|
|
482
522
|
this.nextToken();
|
|
483
|
-
if (this.peekToken.Type === Tokens.LPAREN) {
|
|
484
|
-
this.nextToken();
|
|
485
|
-
return [Tokens.RPAREN, Tokens.RBRACE];
|
|
486
|
-
}
|
|
487
523
|
return [Tokens.RBRACE];
|
|
488
524
|
}
|
|
489
525
|
else if (this.curToken.Type === Tokens.LPAREN) {
|
|
@@ -544,6 +580,13 @@ export default class Parser {
|
|
|
544
580
|
Unwrapped: isUnwrapped
|
|
545
581
|
};
|
|
546
582
|
}
|
|
583
|
+
else if (this.curToken.Literal === Tokens.LBRACE) {
|
|
584
|
+
const val = this.parseAssignmentValue();
|
|
585
|
+
if (Array.isArray(val)) {
|
|
586
|
+
throw new Error('Unexpected array in property type parsing');
|
|
587
|
+
}
|
|
588
|
+
type = val;
|
|
589
|
+
}
|
|
547
590
|
else if (this.curToken.Type === Tokens.IDENT) {
|
|
548
591
|
type = {
|
|
549
592
|
Type: 'group',
|
|
@@ -611,6 +654,9 @@ export default class Parser {
|
|
|
611
654
|
this.nextToken();
|
|
612
655
|
}
|
|
613
656
|
this.nextToken();
|
|
657
|
+
if (!type || (typeof type === 'object' && !('Value' in type))) {
|
|
658
|
+
throw new Error('Invalid type for range definition');
|
|
659
|
+
}
|
|
614
660
|
const Min = typeof type === 'string' || typeof type.Value === 'number'
|
|
615
661
|
? type
|
|
616
662
|
: type.Value;
|
|
@@ -641,7 +687,7 @@ export default class Parser {
|
|
|
641
687
|
* as a grouped range.
|
|
642
688
|
*/
|
|
643
689
|
this.nextToken();
|
|
644
|
-
if (this.
|
|
690
|
+
if (this.isOperator()) {
|
|
645
691
|
isGroupedRange = true;
|
|
646
692
|
}
|
|
647
693
|
}
|
|
@@ -649,6 +695,10 @@ export default class Parser {
|
|
|
649
695
|
this.nextToken(); // eat ")"
|
|
650
696
|
}
|
|
651
697
|
}
|
|
698
|
+
if (!type) {
|
|
699
|
+
const { line, position: column } = this.l.getLocation();
|
|
700
|
+
throw new Error(`Unexpected type: ${this.curToken.Type} at line ${line} column ${column}`);
|
|
701
|
+
}
|
|
652
702
|
return type;
|
|
653
703
|
}
|
|
654
704
|
parseOperator() {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# CDDL Core Concepts: Choices and Maps
|
|
2
|
+
|
|
3
|
+
This guide explains three fundamental concepts in CDDL that are often confused: **Type Choices**, **Group Choices**, and **Maps**.
|
|
4
|
+
|
|
5
|
+
## 1. Type Choice (`/`)
|
|
6
|
+
|
|
7
|
+
A **Type Choice** allows a value to be one of several distinct types. It is represented by the single forward slash `/`.
|
|
8
|
+
|
|
9
|
+
Think of it as an "OR" operator for *values*.
|
|
10
|
+
|
|
11
|
+
### Syntax
|
|
12
|
+
```cddl
|
|
13
|
+
type1 / type2 / type3
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Examples
|
|
17
|
+
|
|
18
|
+
**Simple Value Choice:**
|
|
19
|
+
```cddl
|
|
20
|
+
; MyValue can be an integer OR a text string
|
|
21
|
+
MyValue = int / tstr
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Inside an Array:**
|
|
25
|
+
```cddl
|
|
26
|
+
; An array where the first element is either an int OR a float
|
|
27
|
+
MyArray = [ int / float, tstr ]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Inside a Map Property:**
|
|
31
|
+
```cddl
|
|
32
|
+
; The "score" property can be an integer OR null
|
|
33
|
+
MyMap = {
|
|
34
|
+
"score" => int / null
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. Group Choice (`//`)
|
|
41
|
+
|
|
42
|
+
A **Group Choice** allows a structure (like a map or array) to match one of several *groups* of properties. It is represented by the double forward slash `//`.
|
|
43
|
+
|
|
44
|
+
Think of it as an "OR" operator for *sets of fields/entries*.
|
|
45
|
+
|
|
46
|
+
### Syntax
|
|
47
|
+
```cddl
|
|
48
|
+
(group1) // (group2) // (group3)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Examples
|
|
52
|
+
|
|
53
|
+
**Choice between sets of fields:**
|
|
54
|
+
```cddl
|
|
55
|
+
; A defined group that is either a "street address" OR a "po box"
|
|
56
|
+
Address = (
|
|
57
|
+
street: tstr,
|
|
58
|
+
city: tstr
|
|
59
|
+
) // (
|
|
60
|
+
po-box: int,
|
|
61
|
+
city: tstr
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Usage in Maps:**
|
|
66
|
+
When used inside a map, it means the map must contain *either* the keys from the first group *OR* the keys from the second group.
|
|
67
|
+
|
|
68
|
+
```cddl
|
|
69
|
+
; A user profile must have either a "username" OR an "email" (but not necessarily both, depending on other rules)
|
|
70
|
+
UserProfile = {
|
|
71
|
+
id: int,
|
|
72
|
+
(
|
|
73
|
+
username: tstr
|
|
74
|
+
) // (
|
|
75
|
+
email: tstr
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Key Difference from Type Choice:**
|
|
81
|
+
- `key: int / tstr` -> The key exists, and its value is either int or string.
|
|
82
|
+
- `(key1: int) // (key2: int)` -> Either the key `key1` exists OR the key `key2` exists.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 3. Maps
|
|
87
|
+
|
|
88
|
+
In CDDL, a **Map** is defined using curly braces `{}`. It corresponds to a JSON Object or a dictionary/hash map.
|
|
89
|
+
|
|
90
|
+
Inside a Map, you define **Groups** of properties/pairs using `key => value` or `key: value` syntax.
|
|
91
|
+
|
|
92
|
+
### Syntax
|
|
93
|
+
```cddl
|
|
94
|
+
MyMap = {
|
|
95
|
+
key1 => value1,
|
|
96
|
+
key2: value2,
|
|
97
|
+
optional-key3?: value3
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Deep Dive: Maps vs. Groups
|
|
102
|
+
|
|
103
|
+
- A **Group** is just a sequence of fields (key-value pairs) inside a structure.
|
|
104
|
+
- A **Map** `{ ... }` acts as a container for a Group.
|
|
105
|
+
|
|
106
|
+
**Defining a reusable Group:**
|
|
107
|
+
```cddl
|
|
108
|
+
; "PersonFields" is just a list of fields, NOT a map yet.
|
|
109
|
+
PersonFields = (
|
|
110
|
+
name: tstr,
|
|
111
|
+
age: int
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Using the Group in a Map:**
|
|
116
|
+
```cddl
|
|
117
|
+
; "Student" is a Map that contains the PersonFields
|
|
118
|
+
Student = {
|
|
119
|
+
PersonFields,
|
|
120
|
+
school: tstr
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
; "Teacher" is also a Map that contains the PersonFields
|
|
124
|
+
Teacher = {
|
|
125
|
+
PersonFields,
|
|
126
|
+
subject: tstr
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Mixing Choices in Maps
|
|
131
|
+
|
|
132
|
+
You can combine Maps and Choices to create complex validation logic:
|
|
133
|
+
|
|
134
|
+
```cddl
|
|
135
|
+
Response = {
|
|
136
|
+
"status" => "ok",
|
|
137
|
+
"data" => tstr
|
|
138
|
+
} / {
|
|
139
|
+
"status" => "error",
|
|
140
|
+
"code" => int,
|
|
141
|
+
"message" => tstr
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
*This definition means a Response object must look like EITHER the first block (status="ok", data=...) OR the second block (status="error", code=...).*
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 4. Arrays
|
|
149
|
+
|
|
150
|
+
Arrays in CDDL are defined using square brackets `[]`. They correspond to JSON Arrays.
|
|
151
|
+
|
|
152
|
+
### Syntax
|
|
153
|
+
```cddl
|
|
154
|
+
MyArray = [ type1, type2, ... ]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Examples
|
|
158
|
+
|
|
159
|
+
**Simple Array:**
|
|
160
|
+
```cddl
|
|
161
|
+
; An array of an integer, a string, and a float
|
|
162
|
+
Point3D = [ x: float, y: float, z: float ]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Array with Group Choice:**
|
|
166
|
+
You can use group choices inside arrays to allow different structures.
|
|
167
|
+
|
|
168
|
+
```cddl
|
|
169
|
+
; An item is either an integer OR a string
|
|
170
|
+
Item = [ (int // tstr) ]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 5. Literals
|
|
176
|
+
|
|
177
|
+
CDDL supports various literal values which are useful for exact matching.
|
|
178
|
+
|
|
179
|
+
### Examples
|
|
180
|
+
|
|
181
|
+
```cddl
|
|
182
|
+
LiteralTest = {
|
|
183
|
+
; Exact string match
|
|
184
|
+
type: "widget",
|
|
185
|
+
|
|
186
|
+
; Exact integer match
|
|
187
|
+
version: 1,
|
|
188
|
+
|
|
189
|
+
; Big Integers
|
|
190
|
+
big_id: 9007199254740995,
|
|
191
|
+
|
|
192
|
+
; Null and Booleans
|
|
193
|
+
is_active: true,
|
|
194
|
+
deleted_at: null
|
|
195
|
+
}
|
|
196
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cddl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Concise data definition language (RFC 8610) implementation and JSON validator in Node.js",
|
|
5
5
|
"author": "Christian Bromann <mail@bromann.dev>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"release:patch": "npm run release -- patch",
|
|
31
31
|
"release:minor": "npm run release -- minor",
|
|
32
32
|
"release:major": "npm run release -- major",
|
|
33
|
-
"test": "vitest",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"checks:all": "npm run compile && npm run test",
|
|
34
35
|
"watch": "tsc --watch"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"@types/node": "^22.15.3",
|
|
39
40
|
"@vitest/coverage-v8": "^3.1.2",
|
|
40
41
|
"npm-run-all": "^4.1.5",
|
|
41
|
-
"release-it": "^19.
|
|
42
|
+
"release-it": "^19.2.4",
|
|
42
43
|
"typescript": "^5.8.3",
|
|
43
44
|
"vitest": "^3.1.2"
|
|
44
45
|
},
|