cddl 0.12.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 +54 -9
- 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
|
/**
|
|
@@ -317,7 +352,7 @@ export default class Parser {
|
|
|
317
352
|
*/
|
|
318
353
|
const props = this.parseAssignmentValue();
|
|
319
354
|
let operator = this.isOperator() ? this.parseOperator() : undefined;
|
|
320
|
-
if (!isChoice && this.curToken.Type === Tokens.SLASH) {
|
|
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)) {
|
|
@@ -485,10 +520,6 @@ export default class Parser {
|
|
|
485
520
|
openSegment() {
|
|
486
521
|
if (this.curToken.Type === Tokens.LBRACE) {
|
|
487
522
|
this.nextToken();
|
|
488
|
-
if (this.peekToken.Type === Tokens.LPAREN) {
|
|
489
|
-
this.nextToken();
|
|
490
|
-
return [Tokens.RPAREN, Tokens.RBRACE];
|
|
491
|
-
}
|
|
492
523
|
return [Tokens.RBRACE];
|
|
493
524
|
}
|
|
494
525
|
else if (this.curToken.Type === Tokens.LPAREN) {
|
|
@@ -549,6 +580,13 @@ export default class Parser {
|
|
|
549
580
|
Unwrapped: isUnwrapped
|
|
550
581
|
};
|
|
551
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
|
+
}
|
|
552
590
|
else if (this.curToken.Type === Tokens.IDENT) {
|
|
553
591
|
type = {
|
|
554
592
|
Type: 'group',
|
|
@@ -616,6 +654,9 @@ export default class Parser {
|
|
|
616
654
|
this.nextToken();
|
|
617
655
|
}
|
|
618
656
|
this.nextToken();
|
|
657
|
+
if (!type || (typeof type === 'object' && !('Value' in type))) {
|
|
658
|
+
throw new Error('Invalid type for range definition');
|
|
659
|
+
}
|
|
619
660
|
const Min = typeof type === 'string' || typeof type.Value === 'number'
|
|
620
661
|
? type
|
|
621
662
|
: type.Value;
|
|
@@ -654,6 +695,10 @@ export default class Parser {
|
|
|
654
695
|
this.nextToken(); // eat ")"
|
|
655
696
|
}
|
|
656
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
|
+
}
|
|
657
702
|
return type;
|
|
658
703
|
}
|
|
659
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.12.
|
|
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
|
},
|