cddl2ts 0.7.0 → 0.7.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAWH,KAAK,UAAU,EAMlB,MAAM,MAAM,CAAA;AAuBb,MAAM,WAAW,gBAAgB;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,wBAAgB,SAAS,CAAE,WAAW,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,UAwB/E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAWH,KAAK,UAAU,EAMlB,MAAM,MAAM,CAAA;AAyCb,MAAM,WAAW,gBAAgB;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,wBAAgB,SAAS,CAAE,WAAW,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,UAwB/E"}
package/build/index.js CHANGED
@@ -7,17 +7,35 @@ const b = types.builders;
7
7
  const NATIVE_TYPES = {
8
8
  any: b.tsAnyKeyword(),
9
9
  number: b.tsNumberKeyword(),
10
+ integer: b.tsNumberKeyword(),
10
11
  int: b.tsNumberKeyword(),
11
- float: b.tsNumberKeyword(),
12
12
  uint: b.tsNumberKeyword(),
13
+ nint: b.tsNumberKeyword(),
14
+ unsigned: b.tsNumberKeyword(),
15
+ float: b.tsNumberKeyword(),
16
+ float16: b.tsNumberKeyword(),
17
+ float32: b.tsNumberKeyword(),
18
+ float64: b.tsNumberKeyword(),
19
+ 'float16-32': b.tsNumberKeyword(),
20
+ 'float32-64': b.tsNumberKeyword(),
13
21
  bool: b.tsBooleanKeyword(),
22
+ false: b.tsBooleanKeyword(),
23
+ true: b.tsBooleanKeyword(),
24
+ bstr: b.tsTypeReference(b.identifier('Uint8Array')),
25
+ bytes: b.tsTypeReference(b.identifier('Uint8Array')),
14
26
  str: b.tsStringKeyword(),
15
27
  text: b.tsStringKeyword(),
16
28
  tstr: b.tsStringKeyword(),
17
29
  range: b.tsNumberKeyword(),
30
+ undefined: b.tsUndefinedKeyword(),
18
31
  nil: b.tsNullKeyword(),
19
32
  null: b.tsNullKeyword()
20
33
  };
34
+ const RECORD_KEY_TYPES = new Set([
35
+ 'int', 'uint', 'nint', 'integer', 'unsigned', 'number',
36
+ 'float', 'float16', 'float32', 'float64', 'float16-32', 'float32-64',
37
+ 'str', 'text', 'tstr'
38
+ ]);
21
39
  export function transform(assignments, options) {
22
40
  if (options?.useUnknown) {
23
41
  NATIVE_TYPES.any = b.tsUnknownKeyword();
@@ -39,6 +57,21 @@ export function transform(assignments, options) {
39
57
  }
40
58
  return print(ast).code;
41
59
  }
60
+ function getAssignmentComments(assignment) {
61
+ return assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
62
+ }
63
+ function exportWithComments(declaration) {
64
+ const expr = b.exportDeclaration(false, declaration);
65
+ expr.comments = declaration.comments;
66
+ declaration.comments = [];
67
+ return expr;
68
+ }
69
+ function isExtensibleRecordProperty(prop) {
70
+ return !isUnNamedProperty(prop) &&
71
+ prop.Occurrence.m === Infinity &&
72
+ !prop.HasCut &&
73
+ RECORD_KEY_TYPES.has(prop.Name);
74
+ }
42
75
  function parseAssignment(assignment) {
43
76
  if (isVariable(assignment)) {
44
77
  const propType = Array.isArray(assignment.PropertyType)
@@ -54,8 +87,8 @@ function parseAssignment(assignment) {
54
87
  typeParameters = b.tsUnionType(propType.map(parseUnionType));
55
88
  }
56
89
  const expr = b.tsTypeAliasDeclaration(id, typeParameters);
57
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
58
- return b.exportDeclaration(false, expr);
90
+ expr.comments = getAssignmentComments(assignment);
91
+ return exportWithComments(expr);
59
92
  }
60
93
  if (isGroup(assignment)) {
61
94
  const id = b.identifier(pascalCase(assignment.Name));
@@ -136,8 +169,8 @@ function parseAssignment(assignment) {
136
169
  value = b.tsIntersectionType(intersections);
137
170
  }
138
171
  const expr = b.tsTypeAliasDeclaration(id, value);
139
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
140
- return b.exportDeclaration(false, expr);
172
+ expr.comments = getAssignmentComments(assignment);
173
+ return exportWithComments(expr);
141
174
  }
142
175
  const props = properties;
143
176
  /**
@@ -146,11 +179,11 @@ function parseAssignment(assignment) {
146
179
  if (props.length === 1) {
147
180
  const prop = props[0];
148
181
  const propType = Array.isArray(prop.Type) ? prop.Type : [prop.Type];
149
- if (propType.length === 1 && Object.keys(NATIVE_TYPES).includes(prop.Name)) {
182
+ if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
150
183
  const value = parseUnionType(assignment);
151
184
  const expr = b.tsTypeAliasDeclaration(id, value);
152
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
153
- return b.exportDeclaration(false, expr);
185
+ expr.comments = getAssignmentComments(assignment);
186
+ return exportWithComments(expr);
154
187
  }
155
188
  }
156
189
  // Check if extended interfaces are likely unions or conflicting types
@@ -367,14 +400,14 @@ function parseAssignment(assignment) {
367
400
  value = b.tsIntersectionType(intersections);
368
401
  }
369
402
  const expr = b.tsTypeAliasDeclaration(id, value);
370
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
371
- return b.exportDeclaration(false, expr);
403
+ expr.comments = getAssignmentComments(assignment);
404
+ return exportWithComments(expr);
372
405
  }
373
406
  // Fallback to interface if no mixins (pure object)
374
407
  const objectType = parseObjectType(props);
375
408
  const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType));
376
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
377
- return b.exportDeclaration(false, expr);
409
+ expr.comments = getAssignmentComments(assignment);
410
+ return exportWithComments(expr);
378
411
  }
379
412
  if (isCDDLArray(assignment)) {
380
413
  const id = b.identifier(pascalCase(assignment.Name));
@@ -390,8 +423,8 @@ function parseAssignment(assignment) {
390
423
  });
391
424
  const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)));
392
425
  const expr = b.tsTypeAliasDeclaration(id, value);
393
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
394
- return b.exportDeclaration(false, expr);
426
+ expr.comments = getAssignmentComments(assignment);
427
+ return exportWithComments(expr);
395
428
  }
396
429
  // Standard array
397
430
  const firstType = assignmentValues.Type;
@@ -404,8 +437,8 @@ function parseAssignment(assignment) {
404
437
  ? obj[0]
405
438
  : b.tsParenthesizedType(b.tsUnionType(obj)));
406
439
  const expr = b.tsTypeAliasDeclaration(id, value);
407
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
408
- return b.exportDeclaration(false, expr);
440
+ expr.comments = getAssignmentComments(assignment);
441
+ return exportWithComments(expr);
409
442
  }
410
443
  throw new Error(`Unknown assignment type "${assignment.Type}"`);
411
444
  }
@@ -426,9 +459,22 @@ function parseObjectType(props) {
426
459
  if (isUnNamedProperty(prop)) {
427
460
  continue;
428
461
  }
429
- const id = b.identifier(camelcase(prop.Name));
430
462
  const cddlType = Array.isArray(prop.Type) ? prop.Type : [prop.Type];
431
463
  const comments = prop.Comments.map((c) => ` ${c.Content}`);
464
+ if (isExtensibleRecordProperty(prop)) {
465
+ const keyIdentifier = b.identifier('key');
466
+ keyIdentifier.typeAnnotation = b.tsTypeAnnotation(NATIVE_TYPES[prop.Name]);
467
+ const indexSignature = b.tsIndexSignature([keyIdentifier], b.tsTypeAnnotation(b.tsUnionType([
468
+ ...cddlType.map((t) => parseUnionType(t)),
469
+ b.tsUndefinedKeyword()
470
+ ])));
471
+ indexSignature.comments = comments.length
472
+ ? [b.commentBlock(`*\n *${comments.join('\n *')}\n `)]
473
+ : [];
474
+ propItems.push(indexSignature);
475
+ continue;
476
+ }
477
+ const id = b.identifier(camelcase(prop.Name));
432
478
  if (prop.Operator && prop.Operator.Type === 'default') {
433
479
  const defaultValue = parseDefaultValue(prop.Operator);
434
480
  defaultValue && comments.length && comments.push(''); // add empty line if we have previous comments
@@ -511,7 +557,7 @@ function parseUnionType(t) {
511
557
  /**
512
558
  * {*text => text} which will be transformed to `Record<string, string>`
513
559
  */
514
- if (prop.length === 1 && Object.keys(NATIVE_TYPES).includes(prop[0].Name)) {
560
+ if (prop.length === 1 && RECORD_KEY_TYPES.has(prop[0].Name)) {
515
561
  return b.tsTypeReference(b.identifier('Record'), b.tsTypeParameterInstantiation([
516
562
  NATIVE_TYPES[prop[0].Name],
517
563
  parseUnionType(prop[0].Type[0])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cddl2ts",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "A Node.js package that can generate a TypeScript definition based on a CDDL file",
5
5
  "author": "Christian Bromann <mail@bromann.dev>",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "camelcase": "^9.0.0",
33
33
  "recast": "^0.23.11",
34
34
  "yargs": "^18.0.0",
35
- "cddl": "0.19.0"
35
+ "cddl": "0.19.2"
36
36
  },
37
37
  "scripts": {
38
38
  "release": "release-it --config .release-it.ts --VV",
package/src/index.ts CHANGED
@@ -27,17 +27,35 @@ const b = types.builders
27
27
  const NATIVE_TYPES: Record<string, any> = {
28
28
  any: b.tsAnyKeyword(),
29
29
  number: b.tsNumberKeyword(),
30
+ integer: b.tsNumberKeyword(),
30
31
  int: b.tsNumberKeyword(),
31
- float: b.tsNumberKeyword(),
32
32
  uint: b.tsNumberKeyword(),
33
+ nint: b.tsNumberKeyword(),
34
+ unsigned: b.tsNumberKeyword(),
35
+ float: b.tsNumberKeyword(),
36
+ float16: b.tsNumberKeyword(),
37
+ float32: b.tsNumberKeyword(),
38
+ float64: b.tsNumberKeyword(),
39
+ 'float16-32': b.tsNumberKeyword(),
40
+ 'float32-64': b.tsNumberKeyword(),
33
41
  bool: b.tsBooleanKeyword(),
42
+ false: b.tsBooleanKeyword(),
43
+ true: b.tsBooleanKeyword(),
44
+ bstr: b.tsTypeReference(b.identifier('Uint8Array')),
45
+ bytes: b.tsTypeReference(b.identifier('Uint8Array')),
34
46
  str: b.tsStringKeyword(),
35
47
  text: b.tsStringKeyword(),
36
48
  tstr: b.tsStringKeyword(),
37
49
  range: b.tsNumberKeyword(),
50
+ undefined: b.tsUndefinedKeyword(),
38
51
  nil: b.tsNullKeyword(),
39
52
  null: b.tsNullKeyword()
40
53
  }
54
+ const RECORD_KEY_TYPES = new Set([
55
+ 'int', 'uint', 'nint', 'integer', 'unsigned', 'number',
56
+ 'float', 'float16', 'float32', 'float64', 'float16-32', 'float32-64',
57
+ 'str', 'text', 'tstr'
58
+ ])
41
59
  type ObjectEntry = types.namedTypes.TSCallSignatureDeclaration | types.namedTypes.TSConstructSignatureDeclaration | types.namedTypes.TSIndexSignature | types.namedTypes.TSMethodSignature | types.namedTypes.TSPropertySignature
42
60
  type ObjectBody = ObjectEntry[]
43
61
  type TSTypeKind = types.namedTypes.TSAsExpression['typeAnnotation']
@@ -72,6 +90,27 @@ export function transform (assignments: Assignment[], options?: TransformOptions
72
90
  return print(ast).code
73
91
  }
74
92
 
93
+ function getAssignmentComments (assignment: Assignment) {
94
+ return assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
95
+ }
96
+
97
+ function exportWithComments (
98
+ declaration: types.namedTypes.TSTypeAliasDeclaration
99
+ | types.namedTypes.TSInterfaceDeclaration
100
+ ) {
101
+ const expr = b.exportDeclaration(false, declaration)
102
+ expr.comments = declaration.comments
103
+ declaration.comments = []
104
+ return expr
105
+ }
106
+
107
+ function isExtensibleRecordProperty (prop: Property) {
108
+ return !isUnNamedProperty(prop) &&
109
+ prop.Occurrence.m === Infinity &&
110
+ !prop.HasCut &&
111
+ RECORD_KEY_TYPES.has(prop.Name)
112
+ }
113
+
75
114
  function parseAssignment (assignment: Assignment) {
76
115
  if (isVariable(assignment)) {
77
116
  const propType = Array.isArray(assignment.PropertyType)
@@ -89,8 +128,8 @@ function parseAssignment (assignment: Assignment) {
89
128
  }
90
129
 
91
130
  const expr = b.tsTypeAliasDeclaration(id, typeParameters)
92
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
93
- return b.exportDeclaration(false, expr)
131
+ expr.comments = getAssignmentComments(assignment)
132
+ return exportWithComments(expr)
94
133
  }
95
134
 
96
135
  if (isGroup(assignment)) {
@@ -183,8 +222,8 @@ function parseAssignment (assignment: Assignment) {
183
222
  }
184
223
 
185
224
  const expr = b.tsTypeAliasDeclaration(id, value)
186
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
187
- return b.exportDeclaration(false, expr)
225
+ expr.comments = getAssignmentComments(assignment)
226
+ return exportWithComments(expr)
188
227
  }
189
228
 
190
229
  const props = properties as Property[]
@@ -195,11 +234,11 @@ function parseAssignment (assignment: Assignment) {
195
234
  if (props.length === 1) {
196
235
  const prop = props[0]
197
236
  const propType = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
198
- if (propType.length === 1 && Object.keys(NATIVE_TYPES).includes(prop.Name)) {
237
+ if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
199
238
  const value = parseUnionType(assignment)
200
239
  const expr = b.tsTypeAliasDeclaration(id, value)
201
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
202
- return b.exportDeclaration(false, expr)
240
+ expr.comments = getAssignmentComments(assignment)
241
+ return exportWithComments(expr)
203
242
  }
204
243
  }
205
244
 
@@ -413,16 +452,16 @@ function parseAssignment (assignment: Assignment) {
413
452
  }
414
453
 
415
454
  const expr = b.tsTypeAliasDeclaration(id, value)
416
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
417
- return b.exportDeclaration(false, expr)
455
+ expr.comments = getAssignmentComments(assignment)
456
+ return exportWithComments(expr)
418
457
  }
419
458
 
420
459
  // Fallback to interface if no mixins (pure object)
421
460
  const objectType = parseObjectType(props)
422
461
 
423
462
  const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType))
424
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
425
- return b.exportDeclaration(false, expr)
463
+ expr.comments = getAssignmentComments(assignment)
464
+ return exportWithComments(expr)
426
465
  }
427
466
 
428
467
  if (isCDDLArray(assignment)) {
@@ -441,8 +480,8 @@ function parseAssignment (assignment: Assignment) {
441
480
  })
442
481
  const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)))
443
482
  const expr = b.tsTypeAliasDeclaration(id, value)
444
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
445
- return b.exportDeclaration(false, expr)
483
+ expr.comments = getAssignmentComments(assignment)
484
+ return exportWithComments(expr)
446
485
  }
447
486
 
448
487
  // Standard array
@@ -459,8 +498,8 @@ function parseAssignment (assignment: Assignment) {
459
498
  : b.tsParenthesizedType(b.tsUnionType(obj))
460
499
  )
461
500
  const expr = b.tsTypeAliasDeclaration(id, value)
462
- expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
463
- return b.exportDeclaration(false, expr)
501
+ expr.comments = getAssignmentComments(assignment)
502
+ return exportWithComments(expr)
464
503
  }
465
504
 
466
505
  throw new Error(`Unknown assignment type "${(assignment as any).Type}"`)
@@ -484,10 +523,31 @@ function parseObjectType (props: Property[]): ObjectBody {
484
523
  continue
485
524
  }
486
525
 
487
- const id = b.identifier(camelcase(prop.Name))
488
526
  const cddlType: PropertyType[] = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
489
527
  const comments: string[] = prop.Comments.map((c) => ` ${c.Content}`)
490
528
 
529
+ if (isExtensibleRecordProperty(prop)) {
530
+ const keyIdentifier = b.identifier('key')
531
+ keyIdentifier.typeAnnotation = b.tsTypeAnnotation(NATIVE_TYPES[prop.Name])
532
+
533
+ const indexSignature = b.tsIndexSignature(
534
+ [keyIdentifier],
535
+ b.tsTypeAnnotation(
536
+ b.tsUnionType([
537
+ ...cddlType.map((t) => parseUnionType(t)),
538
+ b.tsUndefinedKeyword()
539
+ ])
540
+ )
541
+ )
542
+ indexSignature.comments = comments.length
543
+ ? [b.commentBlock(`*\n *${comments.join('\n *')}\n `)]
544
+ : []
545
+ propItems.push(indexSignature)
546
+ continue
547
+ }
548
+
549
+ const id = b.identifier(camelcase(prop.Name))
550
+
491
551
  if (prop.Operator && prop.Operator.Type === 'default') {
492
552
  const defaultValue = parseDefaultValue(prop.Operator)
493
553
  defaultValue && comments.length && comments.push('') // add empty line if we have previous comments
@@ -578,7 +638,7 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
578
638
  /**
579
639
  * {*text => text} which will be transformed to `Record<string, string>`
580
640
  */
581
- if (prop.length === 1 && Object.keys(NATIVE_TYPES).includes((prop[0] as Property).Name)) {
641
+ if (prop.length === 1 && RECORD_KEY_TYPES.has((prop[0] as Property).Name)) {
582
642
  return b.tsTypeReference(
583
643
  b.identifier('Record'),
584
644
  b.tsTypeParameterInstantiation([
@@ -0,0 +1,16 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`extensible metadata > should render extensible metadata as an interface with an index signature 1`] = `
4
+ "export type MetadataScalar = null | boolean | number | number | string;
5
+
6
+ export interface MessageMetadata {
7
+ provider?: string;
8
+ model?: string;
9
+ modelType?: string;
10
+ runId?: string;
11
+ threadId?: string;
12
+ systemFingerprint?: string;
13
+ serviceTier?: string;
14
+ [key: string]: MetadataScalar | undefined;
15
+ }"
16
+ `;
@@ -12,52 +12,52 @@ export type DirectProxyConfiguration = Extensible & {
12
12
  proxyType: "direct";
13
13
  };
14
14
 
15
- export // 1. Simple Group Choice
16
- type ManualProxyConfiguration = Extensible & {
15
+ // 1. Simple Group Choice
16
+ export type ManualProxyConfiguration = Extensible & {
17
17
  proxyType: "manual";
18
18
  };
19
19
 
20
- export // 2. Nested Group Choice
21
- type SimpleGroupChoice = Int | string;
20
+ // 2. Nested Group Choice
21
+ export type SimpleGroupChoice = Int | string;
22
22
 
23
- export // 3. Group Choice with Multiple Items (Sequence) - interpreted as tuple
24
- type NestedGroupChoice = (Int | Tstr);
23
+ // 3. Group Choice with Multiple Items (Sequence) - interpreted as tuple
24
+ export type NestedGroupChoice = (Int | Tstr);
25
25
 
26
- export // 4. Map Group Choice - interpreted as map (interface) union
27
- type SequenceGroupChoice = [number, string] | number;
26
+ // 4. Map Group Choice - interpreted as map (interface) union
27
+ export type SequenceGroupChoice = [number, string] | number;
28
28
 
29
- export // 5. Type Choice inside Group - should be value union
30
- type MapGroupChoice = {
29
+ // 5. Type Choice inside Group - should be value union
30
+ export type MapGroupChoice = {
31
31
  a: 1;
32
32
  } | {
33
33
  b: 2;
34
34
  };
35
35
 
36
- export // 6. Array with Group Choice - should be array of union?
37
- type TypeChoiceInsideGroup = number | string;
36
+ // 6. Array with Group Choice - should be array of union?
37
+ export type TypeChoiceInsideGroup = number | string;
38
38
 
39
- export // 7. Map with nested group (bare and parens) - should be object like
40
- type ArrayGroupChoice = Int | string[];
39
+ // 7. Map with nested group (bare and parens) - should be object like
40
+ export type ArrayGroupChoice = Int | string[];
41
41
 
42
42
  export type MapWithBareGroup = number;
43
43
 
44
- export // 8. Group wrapped in map with multiple properties
45
- type MapWithParensGroup = number;
44
+ // 8. Group wrapped in map with multiple properties
45
+ export type MapWithParensGroup = number;
46
46
 
47
- export // 9. Property type choice without operators
48
- type MapGroupWrap = ;
47
+ // 9. Property type choice without operators
48
+ export type MapGroupWrap = ;
49
49
 
50
- export // 10. Nested choice with group reference
51
- interface PropChoiceNoOp {
50
+ // 10. Nested choice with group reference
51
+ export interface PropChoiceNoOp {
52
52
  a: number | number;
53
53
  b: string;
54
54
  }
55
55
 
56
- export // 11. Complex type (group) followed by property without comma
57
- type NestedChoiceRef = number | string;
56
+ // 11. Complex type (group) followed by property without comma
57
+ export type NestedChoiceRef = number | string;
58
58
 
59
- export // 12. Multi-item group choice (verifies isChoice persistence)
60
- interface MapNoComma {
59
+ // 12. Multi-item group choice (verifies isChoice persistence)
60
+ export interface MapNoComma {
61
61
  a: number;
62
62
  b: string;
63
63
  }
@@ -95,8 +95,8 @@ export interface SomeGroup {
95
95
  export type ScriptListLocalValue = ScriptLocalValue[];
96
96
  export type ScriptMappingLocalValue = (ScriptLocalValue | ScriptLocalValue)[];
97
97
 
98
- export // some comments here
99
- type Extensible = Record<string, any>;",
98
+ // some comments here
99
+ export type Extensible = Record<string, any>;",
100
100
  ],
101
101
  ]
102
102
  `;
@@ -388,9 +388,9 @@ export interface BrowsingContextPrintParameters {
388
388
  shrinkToFit?: boolean;
389
389
  }
390
390
 
391
- export // Minimum size is 1pt x 1pt. Conversion follows from
391
+ // Minimum size is 1pt x 1pt. Conversion follows from
392
392
  // https://www.w3.org/TR/css3-values/#absolute-lengths
393
- interface BrowsingContextPrintMarginParameters {
393
+ export interface BrowsingContextPrintMarginParameters {
394
394
  /**
395
395
  * @default 1
396
396
  */
@@ -0,0 +1,49 @@
1
+ import url from 'node:url'
2
+ import path from 'node:path'
3
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
4
+
5
+ import cli from '../src/cli.js'
6
+
7
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
8
+ const cddlFile = path.join(__dirname, '..', '..', '..', 'examples', 'commons', 'extensible_metadata.cddl')
9
+
10
+ vi.mock('../src/constants', () => ({
11
+ pkg: {
12
+ name: 'cddl2ts',
13
+ version: '0.0.0'
14
+ }
15
+ }))
16
+
17
+ describe('extensible metadata', () => {
18
+ let exitOrig = process.exit
19
+ let logOrig = console.log
20
+ let errorOrig = console.error
21
+
22
+ beforeEach(() => {
23
+ process.exit = vi.fn() as any
24
+ console.log = vi.fn()
25
+ console.error = vi.fn()
26
+ })
27
+
28
+ afterEach(() => {
29
+ process.exit = exitOrig
30
+ console.log = logOrig
31
+ console.error = errorOrig
32
+ })
33
+
34
+ it('should render extensible metadata as an interface with an index signature', async () => {
35
+ await cli([cddlFile])
36
+
37
+ expect(process.exit).not.toHaveBeenCalledWith(1)
38
+ expect(console.error).not.toHaveBeenCalled()
39
+
40
+ const output = vi.mocked(console.log).mock.calls.flat().join('\n')
41
+
42
+ expect(output).toContain('export interface MessageMetadata {')
43
+ expect(output).toContain('provider?: string;')
44
+ expect(output).toContain('modelType?: string;')
45
+ expect(output).toContain('[key: string]: MetadataScalar | undefined;')
46
+ expect(output).not.toContain('text?: MetadataScalar;')
47
+ expect(output).toMatchSnapshot()
48
+ })
49
+ })
@@ -39,8 +39,8 @@ describe('named group choice', () => {
39
39
 
40
40
  const output = vi.mocked(console.log).mock.calls.flat().join('\n')
41
41
 
42
- // The export keyword is separated from the type definition by comments
43
- expect(output).toMatch(/export\s+(\/\/.*\n)+\s*type Choice = OptionA \| OptionB/)
42
+ // Leading comments should render before the exported declaration.
43
+ expect(output).toMatch(/(\/\/.*\n)+export type Choice = OptionA \| OptionB/)
44
44
  expect(output).toContain('export interface OptionA {')
45
45
  expect(output).toContain('export interface OptionB {')
46
46
  })
@@ -1,21 +1,84 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import { transform } from '../src/index.js'
3
- import type { Variable } from 'cddl'
3
+ import type { Group, Property, Variable } from 'cddl'
4
+
5
+ function variable(name: string, propertyType: Variable['PropertyType']): Variable {
6
+ return {
7
+ Type: 'variable',
8
+ Name: name,
9
+ PropertyType: propertyType,
10
+ Comments: [],
11
+ IsChoiceAddition: false
12
+ }
13
+ }
14
+
15
+ function property(name: string, type: Property['Type']): Property {
16
+ return {
17
+ HasCut: false,
18
+ Occurrence: { n: 1, m: 1 },
19
+ Name: name,
20
+ Type: type,
21
+ Comments: []
22
+ }
23
+ }
24
+
25
+ function group(name: string, properties: Group['Properties']): Group {
26
+ return {
27
+ Type: 'group',
28
+ Name: name,
29
+ IsChoiceAddition: false,
30
+ Properties: properties,
31
+ Comments: []
32
+ }
33
+ }
4
34
 
5
35
  describe('literal transformation direct', () => {
6
36
  it('should transform bigint literals correctly', () => {
7
- const assignment: Variable = {
8
- Type: 'variable',
9
- Name: 'MyBigInt',
10
- PropertyType: {
11
- Type: 'literal',
12
- Value: 9007199254740995n
13
- } as any,
14
- Comments: [],
15
- IsChoiceAddition: false
16
- }
37
+ const assignment = variable('MyBigInt', {
38
+ Type: 'literal',
39
+ Value: 9007199254740995n
40
+ } as any)
17
41
 
18
42
  const output = transform([assignment])
19
43
  expect(output).toContain('export type MyBigInt = 9007199254740995n;')
20
44
  })
45
+
46
+ it('should transform float32 aliases correctly', () => {
47
+ const assignment = variable('score', 'float32')
48
+
49
+ const output = transform([assignment])
50
+ expect(output).toContain('export type Score = number;')
51
+ })
52
+
53
+ it.each([
54
+ ['integer-value', 'integer', 'number'],
55
+ ['negative-value', 'nint', 'number'],
56
+ ['unsigned-value', 'unsigned', 'number'],
57
+ ['half-float', 'float16', 'number'],
58
+ ['double-float', 'float64', 'number'],
59
+ ['float-window', 'float16-32', 'number'],
60
+ ['float-range', 'float32-64', 'number'],
61
+ ['binary-payload', 'bytes', 'Uint8Array'],
62
+ ['binary-blob', 'bstr', 'Uint8Array'],
63
+ ['missing-value', 'undefined', 'undefined']
64
+ ] as const)('should map %s (%s) to %s', (name, propertyType, expectedType) => {
65
+ const output = transform([variable(name, propertyType)])
66
+ expect(output).toContain(`export type ${name.split('-').map((part) => `${part[0]!.toUpperCase()}${part.slice(1)}`).join('')} = ${expectedType};`)
67
+ })
68
+
69
+ it('should keep bytes fields as object properties instead of record aliases', () => {
70
+ const output = transform([
71
+ group('network-get-data-result', [
72
+ property('bytes', {
73
+ Type: 'group',
74
+ Value: 'network.BytesValue',
75
+ Unwrapped: false
76
+ } as any)
77
+ ])
78
+ ])
79
+
80
+ expect(output).toContain('export interface NetworkGetDataResult {')
81
+ expect(output).toContain('bytes: NetworkBytesValue;')
82
+ expect(output).not.toContain('export type NetworkGetDataResult = Record')
83
+ })
21
84
  })
@@ -14,11 +14,11 @@ import { transform } from '../src/index.js'
14
14
 
15
15
  const COMMENTS: Comment[] = []
16
16
 
17
- function comment (content: string): Comment {
17
+ function comment (content: string, leading = false): Comment {
18
18
  return {
19
19
  Type: 'comment',
20
20
  Content: content,
21
- Leading: false
21
+ Leading: leading
22
22
  }
23
23
  }
24
24
 
@@ -240,6 +240,40 @@ describe('transform edge cases', () => {
240
240
  expect(output).toContain('enabled?: boolean')
241
241
  })
242
242
 
243
+ it('should place leading comments before exported declarations', () => {
244
+ const output = transform([
245
+ variable('metadata-scalar', ['null', 'bool', 'int', 'float', 'text'], [
246
+ comment('Flat scalar value used by concise metadata bags.', true)
247
+ ])
248
+ ])
249
+
250
+ expect(output).toContain(`// Flat scalar value used by concise metadata bags.\nexport type MetadataScalar = null | boolean | number | number | string;`)
251
+ expect(output).not.toContain('export // Flat scalar value used by concise metadata bags.')
252
+ })
253
+
254
+ it('should emit extensible object properties as index signatures', () => {
255
+ const output = transform([
256
+ variable('metadata-scalar', ['null', 'bool', 'int', 'float', 'text']),
257
+ group('message-metadata', [
258
+ property('provider', 'text', {
259
+ Occurrence: { n: 0, m: 1 }
260
+ }),
261
+ property('model', 'text', {
262
+ Occurrence: { n: 0, m: 1 }
263
+ }),
264
+ property('text', groupRef('metadata-scalar'), {
265
+ Occurrence: { n: 0, m: Infinity }
266
+ })
267
+ ])
268
+ ])
269
+
270
+ expect(output).toContain('export interface MessageMetadata {')
271
+ expect(output).toContain('provider?: string;')
272
+ expect(output).toContain('model?: string;')
273
+ expect(output).toContain('[key: string]: MetadataScalar | undefined;')
274
+ expect(output).not.toContain('text?: MetadataScalar;')
275
+ })
276
+
243
277
  it('should throw clear errors for unsupported inputs', () => {
244
278
  expect(() => transform([
245
279
  variable('unknown-native', 'nope')