cddl 0.12.0 → 0.13.0

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.
@@ -3,5 +3,5 @@ export declare const command = "repl";
3
3
  export declare const desc = "Run CDDL repl";
4
4
  export declare const builder: (yargs: Argv<{}>) => Argv<{}>;
5
5
  export declare const handler: () => void;
6
- export declare function evaluate(evalCmd: string, _: any, file: string, callback: (err: Error | null, result: any) => void): void;
6
+ export declare function evaluate(evalCmd: string, _: any, _file: string, callback: (err: Error | null, result: any) => void): void;
7
7
  //# sourceMappingURL=repl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/repl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAMjC,eAAO,MAAM,OAAO,SAAS,CAAA;AAC7B,eAAO,MAAM,IAAI,kBAAkB,CAAA;AACnC,eAAO,MAAM,OAAO,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC,aAItC,CAAA;AAED,eAAO,MAAM,OAAO,YAKnB,CAAA;AAED,wBAAgB,QAAQ,CAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,QAUlH"}
1
+ {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/repl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAMjC,eAAO,MAAM,OAAO,SAAS,CAAA;AAC7B,eAAO,MAAM,IAAI,kBAAkB,CAAA;AACnC,eAAO,MAAM,OAAO,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC,aAItC,CAAA;AAED,eAAO,MAAM,OAAO,YAKnB,CAAA;AAED,wBAAgB,QAAQ,CAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,QAUnH"}
@@ -10,12 +10,12 @@ export const builder = (yargs) => {
10
10
  .help();
11
11
  };
12
12
  export const handler = () => {
13
- const r = repl.start({
13
+ repl.start({
14
14
  prompt: '> ',
15
15
  eval: evaluate
16
16
  });
17
17
  };
18
- export function evaluate(evalCmd, _, file, callback) {
18
+ export function evaluate(evalCmd, _, _file, callback) {
19
19
  if (!evalCmd) {
20
20
  return callback(new Error('No input'), null);
21
21
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lexer.d.ts","sourceRoot":"","sources":["../src/lexer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAU,MAAM,aAAa,CAAC;AAI5C,MAAM,CAAC,OAAO,OAAO,KAAK;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAI;IACpB,YAAY,EAAE,MAAM,CAAI;IACxB,EAAE,EAAE,MAAM,CAAI;gBAED,MAAM,EAAE,MAAM;IAM3B,OAAO,CAAC,QAAQ;IAUhB,WAAW;;;;IAqBX,OAAO,CAAE,UAAU,EAAE,MAAM;IAI3B,eAAe;IASf,SAAS,IAAK,KAAK;IA+FnB,OAAO,CAAC,cAAc;IA2BtB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,cAAc;CAKzB"}
1
+ {"version":3,"file":"lexer.d.ts","sourceRoot":"","sources":["../src/lexer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAU,MAAM,aAAa,CAAC;AAI5C,MAAM,CAAC,OAAO,OAAO,KAAK;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAI;IACpB,YAAY,EAAE,MAAM,CAAI;IACxB,EAAE,EAAE,MAAM,CAAI;gBAED,MAAM,EAAE,MAAM;IAM3B,OAAO,CAAC,QAAQ;IAUhB,WAAW;;;;IAoBX,OAAO,CAAE,UAAU,EAAE,MAAM;IAI3B,eAAe;IASf,SAAS,IAAK,KAAK;IA+FnB,OAAO,CAAC,cAAc;IA2BtB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,cAAc;CAKzB"}
package/build/lexer.js CHANGED
@@ -27,7 +27,6 @@ export default class Lexer {
27
27
  let i = 0;
28
28
  for (const [line, lineLength] of Object.entries(sourceLineLength)) {
29
29
  i += lineLength + 1;
30
- const lineBegin = i - lineLength;
31
30
  if (i > position) {
32
31
  const lineBegin = i - lineLength;
33
32
  return {
package/build/parser.d.ts CHANGED
@@ -6,6 +6,7 @@ export default class Parser {
6
6
  l: Lexer;
7
7
  curToken: Token;
8
8
  peekToken: Token;
9
+ peekBelowToken: Token;
9
10
  constructor(filePath: string);
10
11
  private nextToken;
11
12
  private parseAssignments;
@@ -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;gBAEhB,QAAQ,EAAE,MAAM;IAQ7B,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,gBAAgB;IA2CxB,OAAO,CAAC,oBAAoB;IA0b5B,OAAO,CAAC,wBAAwB;IAahC;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,iBAAiB;IAqJzB,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"}
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.l.nextToken();
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.length && this.peekToken.Type === Tokens.SLASH) {
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
- valuesOrProperties.push({
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.0",
3
+ "version": "0.13.0",
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",
@@ -12,6 +12,10 @@
12
12
  "keywords": [
13
13
  "cddl"
14
14
  ],
15
+ "engines": {
16
+ "node": ">=20.0.0",
17
+ "npm": ">=9.0.0"
18
+ },
15
19
  "bugs": {
16
20
  "url": "https://github.com/christian-bromann/cddl/issues"
17
21
  },
@@ -30,19 +34,20 @@
30
34
  "release:patch": "npm run release -- patch",
31
35
  "release:minor": "npm run release -- minor",
32
36
  "release:major": "npm run release -- major",
33
- "test": "vitest",
37
+ "test": "vitest run",
38
+ "checks:all": "npm run compile && npm run test",
34
39
  "watch": "tsc --watch"
35
40
  },
36
41
  "devDependencies": {
37
- "@types/jest": "^29.5.14",
38
- "@types/node": "^22.15.3",
39
- "@vitest/coverage-v8": "^3.1.2",
42
+ "@types/node": "^24.12.0",
43
+ "@types/yargs": "^17.0.35",
44
+ "@vitest/coverage-v8": "^4.1.0",
40
45
  "npm-run-all": "^4.1.5",
41
- "release-it": "^19.0.1",
42
- "typescript": "^5.8.3",
43
- "vitest": "^3.1.2"
46
+ "release-it": "^19.2.4",
47
+ "typescript": "^5.9.3",
48
+ "vitest": "^4.1.0"
44
49
  },
45
50
  "dependencies": {
46
- "yargs": "^17.7.2"
51
+ "yargs": "^18.0.0"
47
52
  }
48
53
  }