cddl2ts 0.7.2 → 0.9.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.
package/README.md CHANGED
@@ -23,6 +23,12 @@ This package exposes a CLI as well as a programmatic interface for transforming
23
23
  npx cddl2ts ./path/to/interface.cddl &> ./path/to/interface.ts
24
24
  ```
25
25
 
26
+ Generated interface fields default to `camelCase`. Pass `--field-case snake` to emit `snake_case` fields while keeping exported interface and type names unchanged.
27
+
28
+ ```sh
29
+ npx cddl2ts ./path/to/interface.cddl --field-case snake &> ./path/to/interface.ts
30
+ ```
31
+
26
32
  ### Programmatic Interface
27
33
 
28
34
  The module exports a `transform` method that takes a CDDL AST object and returns a TypeScript definition as `string`, e.g.:
@@ -41,16 +47,16 @@ import { parse, transform } from 'cddl'
41
47
  * };
42
48
  */
43
49
  const ast = parse('./spec.cddl')
44
- const ts = transform(ast)
50
+ const ts = transform(ast, { fieldCase: 'snake' })
45
51
  console.log(ts)
46
52
  /**
47
53
  * outputs:
48
54
  *
49
55
  * interface SessionCapabilityRequest {
50
- * acceptInsecureCerts?: boolean,
51
- * browserName?: string,
52
- * browserVersion?: string,
53
- * platformName?: string,
56
+ * accept_insecure_certs?: boolean,
57
+ * browser_name?: string,
58
+ * browser_version?: string,
59
+ * platform_name?: string,
54
60
  * }
55
61
  */
56
62
  ```
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AASA,wBAA8B,GAAG,CAAE,IAAI,WAAwB,sBAgC9D"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAWA,wBAA8B,GAAG,CAAE,IAAI,WAAwB,sBAyC9D"}
package/build/cli.js CHANGED
@@ -4,6 +4,7 @@ import yargs from 'yargs';
4
4
  import { parse } from 'cddl';
5
5
  import { transform } from './index.js';
6
6
  import { pkg } from './constants.js';
7
+ const FIELD_CASE_CHOICES = ['camel', 'snake'];
7
8
  export default async function cli(argv = process.argv.slice(2)) {
8
9
  const parser = yargs(argv)
9
10
  .usage(`${pkg.name}\n${pkg.description}\n\nUsage:\nrunme2ts ./path/to/spec.cddl &> ./path/to/interface.ts`)
@@ -14,6 +15,12 @@ export default async function cli(argv = process.argv.slice(2)) {
14
15
  type: 'boolean',
15
16
  description: 'Use unknown instead of any',
16
17
  default: false
18
+ })
19
+ .option('field-case', {
20
+ choices: FIELD_CASE_CHOICES,
21
+ type: 'string',
22
+ description: 'Case for generated interface fields',
23
+ default: 'camel'
17
24
  })
18
25
  .help('help')
19
26
  .alias('h', 'help')
@@ -30,5 +37,8 @@ export default async function cli(argv = process.argv.slice(2)) {
30
37
  return process.exit(1);
31
38
  }
32
39
  const ast = parse(absoluteFilePath);
33
- console.log(transform(ast, { useUnknown: args.u }));
40
+ console.log(transform(ast, {
41
+ useUnknown: args.u,
42
+ fieldCase: args.fieldCase
43
+ }));
34
44
  }
package/build/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { type Assignment } from 'cddl';
2
+ export type FieldCase = 'camel' | 'snake';
2
3
  export interface TransformOptions {
3
4
  useUnknown?: boolean;
5
+ fieldCase?: FieldCase;
4
6
  }
5
7
  export declare function transform(assignments: Assignment[], options?: TransformOptions): string;
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -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;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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAYH,KAAK,UAAU,EAOlB,MAAM,MAAM,CAAA;AAwCb,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAIzC,MAAM,WAAW,gBAAgB;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,SAAS,CAAA;CACxB;AAED,wBAAgB,SAAS,CAAE,WAAW,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,UA8B/E"}
package/build/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import camelcase from 'camelcase';
2
2
  import { parse, print, types } from 'recast';
3
3
  import typescriptParser from 'recast/parsers/typescript.js';
4
- import { isCDDLArray, isGroup, isNamedGroupReference, isLiteralWithValue, isNativeTypeWithOperator, isUnNamedProperty, isPropertyReference, isRange, isVariable, pascalCase } from 'cddl';
4
+ import { getRegexpPattern, isCDDLArray, isGroup, isNamedGroupReference, isLiteralWithValue, isNativeTypeWithOperator, isUnNamedProperty, isPropertyReference, isRange, isVariable, pascalCase } from 'cddl';
5
5
  import { pkg } from './constants.js';
6
6
  const b = types.builders;
7
7
  const NATIVE_TYPES = {
@@ -36,8 +36,14 @@ const RECORD_KEY_TYPES = new Set([
36
36
  'float', 'float16', 'float32', 'float64', 'float16-32', 'float32-64',
37
37
  'str', 'text', 'tstr'
38
38
  ]);
39
+ const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
39
40
  export function transform(assignments, options) {
40
- if (options?.useUnknown) {
41
+ const transformOptions = {
42
+ useUnknown: false,
43
+ fieldCase: 'camel',
44
+ ...options
45
+ };
46
+ if (transformOptions.useUnknown) {
41
47
  NATIVE_TYPES.any = b.tsUnknownKeyword();
42
48
  }
43
49
  else {
@@ -49,7 +55,7 @@ export function transform(assignments, options) {
49
55
  sourceRoot: process.cwd()
50
56
  });
51
57
  for (const assignment of assignments) {
52
- const statement = parseAssignment(assignment);
58
+ const statement = parseAssignment(assignment, transformOptions);
53
59
  if (!statement) {
54
60
  continue;
55
61
  }
@@ -72,7 +78,27 @@ function isExtensibleRecordProperty(prop) {
72
78
  !prop.HasCut &&
73
79
  RECORD_KEY_TYPES.has(prop.Name);
74
80
  }
75
- function parseAssignment(assignment) {
81
+ function toSnakeCase(name) {
82
+ return name
83
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
84
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
85
+ .replace(/[^A-Za-z0-9_$]+/g, '_')
86
+ .replace(/_+/g, '_')
87
+ .replace(/^_+|_+$/g, '')
88
+ .toLowerCase();
89
+ }
90
+ function formatFieldName(name, fieldCase) {
91
+ return fieldCase === 'snake'
92
+ ? toSnakeCase(name)
93
+ : camelcase(name);
94
+ }
95
+ function createPropertyKey(name, options) {
96
+ const fieldName = formatFieldName(name, options.fieldCase);
97
+ return IDENTIFIER_PATTERN.test(fieldName)
98
+ ? b.identifier(fieldName)
99
+ : b.stringLiteral(fieldName);
100
+ }
101
+ function parseAssignment(assignment, options) {
76
102
  if (isVariable(assignment)) {
77
103
  const propType = Array.isArray(assignment.PropertyType)
78
104
  ? assignment.PropertyType
@@ -84,7 +110,7 @@ function parseAssignment(assignment) {
84
110
  typeParameters = b.tsNumberKeyword();
85
111
  }
86
112
  else {
87
- typeParameters = b.tsUnionType(propType.map(parseUnionType));
113
+ typeParameters = b.tsUnionType(propType.map((prop) => parseUnionType(prop, options)));
88
114
  }
89
115
  const expr = b.tsTypeAliasDeclaration(id, typeParameters);
90
116
  expr.comments = getAssignmentComments(assignment);
@@ -114,7 +140,7 @@ function parseAssignment(assignment) {
114
140
  choiceOptions.push(nextProp);
115
141
  i++; // Skip next property
116
142
  }
117
- const options = choiceOptions.map(p => {
143
+ const choiceTypes = choiceOptions.map(p => {
118
144
  // If p is a group reference (Name ''), it's a TypeReference
119
145
  // e.g. SessionAutodetectProxyConfiguration // SessionDirectProxyConfiguration
120
146
  // The parser sometimes wraps it in an array, sometimes not (if inside a choice)
@@ -125,12 +151,12 @@ function parseAssignment(assignment) {
125
151
  if (isNamedGroupReference(typeVal)) {
126
152
  return b.tsTypeReference(b.identifier(pascalCase(typeVal.Value || typeVal.Type)));
127
153
  }
128
- return parseUnionType(typeVal);
154
+ return parseUnionType(typeVal, options);
129
155
  }
130
156
  // Otherwise it is an object literal with this property
131
- return b.tsTypeLiteral(parseObjectType([p]));
157
+ return b.tsTypeLiteral(parseObjectType([p], options));
132
158
  });
133
- intersections.push(b.tsUnionType(options));
159
+ intersections.push(b.tsUnionType(choiceTypes));
134
160
  }
135
161
  else {
136
162
  staticProps.push(prop);
@@ -141,12 +167,12 @@ function parseAssignment(assignment) {
141
167
  const mixins = staticProps.filter(isUnNamedProperty);
142
168
  const ownProps = staticProps.filter(p => !isUnNamedProperty(p));
143
169
  if (ownProps.length > 0) {
144
- intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps)));
170
+ intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps, options)));
145
171
  }
146
172
  for (const mixin of mixins) {
147
173
  if (Array.isArray(mixin.Type) && mixin.Type.length > 1) {
148
- const options = mixin.Type.map(parseUnionType);
149
- intersections.push(b.tsUnionType(options));
174
+ const choices = mixin.Type.map((type) => parseUnionType(type, options));
175
+ intersections.push(b.tsUnionType(choices));
150
176
  }
151
177
  else {
152
178
  const typeVal = Array.isArray(mixin.Type) ? mixin.Type[0] : mixin.Type;
@@ -180,7 +206,7 @@ function parseAssignment(assignment) {
180
206
  const prop = props[0];
181
207
  const propType = Array.isArray(prop.Type) ? prop.Type : [prop.Type];
182
208
  if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
183
- const value = parseUnionType(assignment);
209
+ const value = parseUnionType(assignment, options);
184
210
  const expr = b.tsTypeAliasDeclaration(id, value);
185
211
  expr.comments = getAssignmentComments(assignment);
186
212
  return exportWithComments(expr);
@@ -225,10 +251,10 @@ function parseAssignment(assignment) {
225
251
  const choices = [];
226
252
  for (const prop of group.Properties) {
227
253
  // Choices are wrapped in arrays in the properties
228
- const options = Array.isArray(prop) ? prop : [prop];
229
- if (options.length > 1) { // It's a choice within the mixin group
254
+ const choiceProps = Array.isArray(prop) ? prop : [prop];
255
+ if (choiceProps.length > 1) { // It's a choice within the mixin group
230
256
  const unionOptions = [];
231
- for (const option of options) {
257
+ for (const option of choiceProps) {
232
258
  let refName;
233
259
  const type = option.Type;
234
260
  if (typeof type === 'string')
@@ -260,7 +286,7 @@ function parseAssignment(assignment) {
260
286
  continue;
261
287
  }
262
288
  }
263
- for (const option of options) {
289
+ for (const option of choiceProps) {
264
290
  let refName;
265
291
  const type = option.Type;
266
292
  if (typeof type === 'string') {
@@ -355,7 +381,7 @@ function parseAssignment(assignment) {
355
381
  }
356
382
  else if (type && typeof type === 'object') {
357
383
  if (isGroup(type) && Array.isArray(type.Properties)) {
358
- choices.push(b.tsTypeLiteral(parseObjectType(type.Properties)));
384
+ choices.push(b.tsTypeLiteral(parseObjectType(type.Properties, options)));
359
385
  continue;
360
386
  }
361
387
  refName = isNamedGroupReference(type)
@@ -390,7 +416,7 @@ function parseAssignment(assignment) {
390
416
  }
391
417
  const ownProps = props.filter(p => !isUnNamedProperty(p));
392
418
  if (ownProps.length > 0) {
393
- intersections.push(b.tsTypeLiteral(parseObjectType(ownProps)));
419
+ intersections.push(b.tsTypeLiteral(parseObjectType(ownProps, options)));
394
420
  }
395
421
  let value;
396
422
  if (intersections.length === 1) {
@@ -404,7 +430,7 @@ function parseAssignment(assignment) {
404
430
  return exportWithComments(expr);
405
431
  }
406
432
  // Fallback to interface if no mixins (pure object)
407
- const objectType = parseObjectType(props);
433
+ const objectType = parseObjectType(props, options);
408
434
  const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType));
409
435
  expr.comments = getAssignmentComments(assignment);
410
436
  return exportWithComments(expr);
@@ -419,7 +445,7 @@ function parseAssignment(assignment) {
419
445
  // We need to parse each choice.
420
446
  const obj = assignmentValues.map((prop) => {
421
447
  const t = Array.isArray(prop.Type) ? prop.Type[0] : prop.Type;
422
- return parseUnionType(t);
448
+ return parseUnionType(t, options);
423
449
  });
424
450
  const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)));
425
451
  const expr = b.tsTypeAliasDeclaration(id, value);
@@ -429,10 +455,10 @@ function parseAssignment(assignment) {
429
455
  // Standard array
430
456
  const firstType = assignmentValues.Type;
431
457
  const obj = Array.isArray(firstType)
432
- ? firstType.map(parseUnionType)
458
+ ? firstType.map((type) => parseUnionType(type, options))
433
459
  : isCDDLArray(firstType)
434
- ? firstType.Values.map((val) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type))
435
- : [parseUnionType(firstType)];
460
+ ? firstType.Values.map((val) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type, options))
461
+ : [parseUnionType(firstType, options)];
436
462
  const value = b.tsArrayType(obj.length === 1
437
463
  ? obj[0]
438
464
  : b.tsParenthesizedType(b.tsUnionType(obj)));
@@ -442,7 +468,7 @@ function parseAssignment(assignment) {
442
468
  }
443
469
  throw new Error(`Unknown assignment type "${assignment.Type}"`);
444
470
  }
445
- function parseObjectType(props) {
471
+ function parseObjectType(props, options) {
446
472
  const propItems = [];
447
473
  for (const prop of props) {
448
474
  /**
@@ -465,7 +491,7 @@ function parseObjectType(props) {
465
491
  const keyIdentifier = b.identifier('key');
466
492
  keyIdentifier.typeAnnotation = b.tsTypeAnnotation(NATIVE_TYPES[prop.Name]);
467
493
  const indexSignature = b.tsIndexSignature([keyIdentifier], b.tsTypeAnnotation(b.tsUnionType([
468
- ...cddlType.map((t) => parseUnionType(t)),
494
+ ...cddlType.map((t) => parseUnionType(t, options)),
469
495
  b.tsUndefinedKeyword()
470
496
  ])));
471
497
  indexSignature.comments = comments.length
@@ -474,14 +500,14 @@ function parseObjectType(props) {
474
500
  propItems.push(indexSignature);
475
501
  continue;
476
502
  }
477
- const id = b.identifier(camelcase(prop.Name));
503
+ const id = createPropertyKey(prop.Name, options);
478
504
  if (prop.Operator && prop.Operator.Type === 'default') {
479
505
  const defaultValue = parseDefaultValue(prop.Operator);
480
506
  defaultValue && comments.length && comments.push(''); // add empty line if we have previous comments
481
507
  defaultValue && comments.push(` @default ${defaultValue}`);
482
508
  }
483
509
  const type = cddlType.map((t) => {
484
- const unionType = parseUnionType(t);
510
+ const unionType = parseUnionType(t, options);
485
511
  if (unionType) {
486
512
  const defaultValue = parseDefaultValue(t.Operator);
487
513
  defaultValue && comments.length && comments.push(''); // add empty line if we have previous comments
@@ -498,13 +524,62 @@ function parseObjectType(props) {
498
524
  }
499
525
  return propItems;
500
526
  }
501
- function parseUnionType(t) {
527
+ function parseTemplateLiteralType(template) {
528
+ const ast = parse(`type __CDDLTemplate = ${template};`, {
529
+ parser: typescriptParser,
530
+ sourceFileName: 'cddl2Ts.ts',
531
+ sourceRoot: process.cwd()
532
+ });
533
+ return ast.program.body[0].typeAnnotation;
534
+ }
535
+ function escapeTemplateLiteralSegment(segment) {
536
+ return segment
537
+ .replace(/\\/g, '\\\\')
538
+ .replace(/`/g, '\\`')
539
+ .replace(/\$\{/g, '\\${');
540
+ }
541
+ function regexpPatternToTemplateLiteral(pattern) {
542
+ const normalized = pattern.startsWith('^') && pattern.endsWith('$')
543
+ ? pattern.slice(1, -1)
544
+ : pattern;
545
+ if (!normalized.includes('.+')) {
546
+ return;
547
+ }
548
+ const wildcardOnlyPattern = normalized.replace(/(\.\+)+/g, '');
549
+ if (wildcardOnlyPattern.includes('(') || wildcardOnlyPattern.includes('[') || wildcardOnlyPattern.includes('\\')) {
550
+ return;
551
+ }
552
+ const segments = normalized.split(/(?:\.\+)+/g);
553
+ if (segments.length <= 1) {
554
+ return;
555
+ }
556
+ return `\`${segments.map(escapeTemplateLiteralSegment).join('${string}')}\``;
557
+ }
558
+ function parseNativeTypeWithOperator(t) {
559
+ if (typeof t.Type !== 'string') {
560
+ return;
561
+ }
562
+ const regexpPattern = getRegexpPattern(t);
563
+ const templateLiteral = regexpPattern && regexpPatternToTemplateLiteral(regexpPattern);
564
+ if (templateLiteral) {
565
+ return parseTemplateLiteralType(templateLiteral);
566
+ }
567
+ return NATIVE_TYPES[t.Type];
568
+ }
569
+ function parseUnionType(t, options) {
502
570
  if (typeof t === 'string') {
503
571
  if (!NATIVE_TYPES[t]) {
504
572
  throw new Error(`Unknown native type: "${t}`);
505
573
  }
506
574
  return NATIVE_TYPES[t];
507
575
  }
576
+ else if (isNativeTypeWithOperator(t) && typeof t.Type === 'string') {
577
+ const nativeType = parseNativeTypeWithOperator(t);
578
+ if (!nativeType) {
579
+ throw new Error(`Unknown native type with operator: ${JSON.stringify(t)}`);
580
+ }
581
+ return nativeType;
582
+ }
508
583
  else if (t.Type && typeof t.Type === 'string' && NATIVE_TYPES[t.Type]) {
509
584
  return NATIVE_TYPES[t.Type];
510
585
  }
@@ -524,31 +599,31 @@ function parseUnionType(t) {
524
599
  * Check if we have choices in the group (arrays of Properties)
525
600
  */
526
601
  if (prop.some(p => Array.isArray(p))) {
527
- const options = [];
602
+ const choices = [];
528
603
  for (const choice of prop) {
529
604
  const subProps = Array.isArray(choice) ? choice : [choice];
530
605
  if (subProps.length === 1 && isUnNamedProperty(subProps[0])) {
531
606
  const first = subProps[0];
532
607
  const subType = Array.isArray(first.Type) ? first.Type[0] : first.Type;
533
- options.push(parseUnionType(subType));
608
+ choices.push(parseUnionType(subType, options));
534
609
  continue;
535
610
  }
536
611
  if (subProps.every(isUnNamedProperty)) {
537
612
  const tupleItems = subProps.map((p) => {
538
613
  const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type;
539
- return parseUnionType(subType);
614
+ return parseUnionType(subType, options);
540
615
  });
541
- options.push(b.tsTupleType(tupleItems));
616
+ choices.push(b.tsTupleType(tupleItems));
542
617
  continue;
543
618
  }
544
- options.push(b.tsTypeLiteral(parseObjectType(subProps)));
619
+ choices.push(b.tsTypeLiteral(parseObjectType(subProps, options)));
545
620
  }
546
- return b.tsUnionType(options);
621
+ return b.tsUnionType(choices);
547
622
  }
548
623
  if (prop.every(isUnNamedProperty)) {
549
624
  const items = prop.map(p => {
550
625
  const t = Array.isArray(p.Type) ? p.Type[0] : p.Type;
551
- return parseUnionType(t);
626
+ return parseUnionType(t, options);
552
627
  });
553
628
  if (items.length === 1)
554
629
  return items[0];
@@ -560,13 +635,13 @@ function parseUnionType(t) {
560
635
  if (prop.length === 1 && RECORD_KEY_TYPES.has(prop[0].Name)) {
561
636
  return b.tsTypeReference(b.identifier('Record'), b.tsTypeParameterInstantiation([
562
637
  NATIVE_TYPES[prop[0].Name],
563
- parseUnionType(prop[0].Type[0])
638
+ parseUnionType(prop[0].Type[0], options)
564
639
  ]));
565
640
  }
566
641
  /**
567
642
  * e.g. ?attributes: {*foo => text},
568
643
  */
569
- return b.tsTypeLiteral(parseObjectType(t.Properties));
644
+ return b.tsTypeLiteral(parseObjectType(t.Properties, options));
570
645
  }
571
646
  else if (isNamedGroupReference(t)) {
572
647
  return b.tsTypeReference(b.identifier(pascalCase(t.Value)));
@@ -385,9 +385,9 @@ export interface BrowsingContextPrintParameters {
385
385
  shrinkToFit?: boolean;
386
386
  }
387
387
 
388
- export // Minimum size is 1pt x 1pt. Conversion follows from
388
+ // Minimum size is 1pt x 1pt. Conversion follows from
389
389
  // https://www.w3.org/TR/css3-values/#absolute-lengths
390
- interface BrowsingContextPrintMarginParameters {
390
+ export interface BrowsingContextPrintMarginParameters {
391
391
  /**
392
392
  * @default 1
393
393
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cddl2ts",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
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.2"
35
+ "cddl": "0.20.0"
36
36
  },
37
37
  "scripts": {
38
38
  "release": "release-it --config .release-it.ts --VV",
package/src/cli.ts CHANGED
@@ -4,9 +4,11 @@ import yargs from 'yargs'
4
4
 
5
5
  import { parse } from 'cddl'
6
6
 
7
- import { transform } from './index.js'
7
+ import { transform, type FieldCase } from './index.js'
8
8
  import { pkg } from './constants.js'
9
9
 
10
+ const FIELD_CASE_CHOICES = ['camel', 'snake'] as const satisfies readonly FieldCase[]
11
+
10
12
  export default async function cli (argv = process.argv.slice(2)) {
11
13
  const parser = yargs(argv)
12
14
  .usage(`${pkg.name}\n${pkg.description}\n\nUsage:\nrunme2ts ./path/to/spec.cddl &> ./path/to/interface.ts`)
@@ -18,6 +20,12 @@ export default async function cli (argv = process.argv.slice(2)) {
18
20
  description: 'Use unknown instead of any',
19
21
  default: false
20
22
  })
23
+ .option('field-case', {
24
+ choices: FIELD_CASE_CHOICES,
25
+ type: 'string',
26
+ description: 'Case for generated interface fields',
27
+ default: 'camel'
28
+ })
21
29
  .help('help')
22
30
  .alias('h', 'help')
23
31
  .alias('v', 'version')
@@ -38,5 +46,8 @@ export default async function cli (argv = process.argv.slice(2)) {
38
46
  }
39
47
 
40
48
  const ast = parse(absoluteFilePath)
41
- console.log(transform(ast, { useUnknown: args.u as boolean }))
49
+ console.log(transform(ast, {
50
+ useUnknown: args.u as boolean,
51
+ fieldCase: args.fieldCase as FieldCase
52
+ }))
42
53
  }
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { parse, print, types } from 'recast'
3
3
  import typescriptParser from 'recast/parsers/typescript.js'
4
4
 
5
5
  import {
6
+ getRegexpPattern,
6
7
  isCDDLArray,
7
8
  isGroup,
8
9
  isNamedGroupReference,
@@ -14,6 +15,7 @@ import {
14
15
  isVariable,
15
16
  pascalCase,
16
17
  type Assignment,
18
+ type NativeTypeWithOperator,
17
19
  type PropertyType,
18
20
  type PropertyReference,
19
21
  type Property,
@@ -59,13 +61,23 @@ const RECORD_KEY_TYPES = new Set([
59
61
  type ObjectEntry = types.namedTypes.TSCallSignatureDeclaration | types.namedTypes.TSConstructSignatureDeclaration | types.namedTypes.TSIndexSignature | types.namedTypes.TSMethodSignature | types.namedTypes.TSPropertySignature
60
62
  type ObjectBody = ObjectEntry[]
61
63
  type TSTypeKind = types.namedTypes.TSAsExpression['typeAnnotation']
64
+ export type FieldCase = 'camel' | 'snake'
65
+ type TransformSettings = Required<TransformOptions>
66
+ const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/
62
67
 
63
68
  export interface TransformOptions {
64
69
  useUnknown?: boolean
70
+ fieldCase?: FieldCase
65
71
  }
66
72
 
67
73
  export function transform (assignments: Assignment[], options?: TransformOptions) {
68
- if (options?.useUnknown) {
74
+ const transformOptions: TransformSettings = {
75
+ useUnknown: false,
76
+ fieldCase: 'camel',
77
+ ...options
78
+ }
79
+
80
+ if (transformOptions.useUnknown) {
69
81
  NATIVE_TYPES.any = b.tsUnknownKeyword()
70
82
  } else {
71
83
  NATIVE_TYPES.any = b.tsAnyKeyword()
@@ -81,7 +93,7 @@ export function transform (assignments: Assignment[], options?: TransformOptions
81
93
  ) satisfies types.namedTypes.File
82
94
 
83
95
  for (const assignment of assignments) {
84
- const statement = parseAssignment(assignment)
96
+ const statement = parseAssignment(assignment, transformOptions)
85
97
  if (!statement) {
86
98
  continue
87
99
  }
@@ -111,7 +123,30 @@ function isExtensibleRecordProperty (prop: Property) {
111
123
  RECORD_KEY_TYPES.has(prop.Name)
112
124
  }
113
125
 
114
- function parseAssignment (assignment: Assignment) {
126
+ function toSnakeCase (name: string) {
127
+ return name
128
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
129
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
130
+ .replace(/[^A-Za-z0-9_$]+/g, '_')
131
+ .replace(/_+/g, '_')
132
+ .replace(/^_+|_+$/g, '')
133
+ .toLowerCase()
134
+ }
135
+
136
+ function formatFieldName (name: string, fieldCase: FieldCase) {
137
+ return fieldCase === 'snake'
138
+ ? toSnakeCase(name)
139
+ : camelcase(name)
140
+ }
141
+
142
+ function createPropertyKey (name: string, options: TransformSettings) {
143
+ const fieldName = formatFieldName(name, options.fieldCase)
144
+ return IDENTIFIER_PATTERN.test(fieldName)
145
+ ? b.identifier(fieldName)
146
+ : b.stringLiteral(fieldName)
147
+ }
148
+
149
+ function parseAssignment (assignment: Assignment, options: TransformSettings) {
115
150
  if (isVariable(assignment)) {
116
151
  const propType = Array.isArray(assignment.PropertyType)
117
152
  ? assignment.PropertyType
@@ -124,7 +159,7 @@ function parseAssignment (assignment: Assignment) {
124
159
  if (propType.length === 1 && propType[0].Type === 'range') {
125
160
  typeParameters = b.tsNumberKeyword()
126
161
  } else {
127
- typeParameters = b.tsUnionType(propType.map(parseUnionType))
162
+ typeParameters = b.tsUnionType(propType.map((prop) => parseUnionType(prop, options)))
128
163
  }
129
164
 
130
165
  const expr = b.tsTypeAliasDeclaration(id, typeParameters)
@@ -161,7 +196,7 @@ function parseAssignment (assignment: Assignment) {
161
196
  i++ // Skip next property
162
197
  }
163
198
 
164
- const options = choiceOptions.map(p => {
199
+ const choiceTypes = choiceOptions.map(p => {
165
200
  // If p is a group reference (Name ''), it's a TypeReference
166
201
  // e.g. SessionAutodetectProxyConfiguration // SessionDirectProxyConfiguration
167
202
  // The parser sometimes wraps it in an array, sometimes not (if inside a choice)
@@ -175,12 +210,12 @@ function parseAssignment (assignment: Assignment) {
175
210
  b.identifier(pascalCase(typeVal.Value || typeVal.Type))
176
211
  )
177
212
  }
178
- return parseUnionType(typeVal);
213
+ return parseUnionType(typeVal, options)
179
214
  }
180
215
  // Otherwise it is an object literal with this property
181
- return b.tsTypeLiteral(parseObjectType([p]))
216
+ return b.tsTypeLiteral(parseObjectType([p], options))
182
217
  })
183
- intersections.push(b.tsUnionType(options))
218
+ intersections.push(b.tsUnionType(choiceTypes))
184
219
  } else {
185
220
  staticProps.push(prop)
186
221
  }
@@ -192,13 +227,13 @@ function parseAssignment (assignment: Assignment) {
192
227
  const ownProps = staticProps.filter(p => !isUnNamedProperty(p))
193
228
 
194
229
  if (ownProps.length > 0) {
195
- intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps)))
230
+ intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps, options)))
196
231
  }
197
232
 
198
233
  for (const mixin of mixins) {
199
234
  if (Array.isArray(mixin.Type) && mixin.Type.length > 1) {
200
- const options = mixin.Type.map(parseUnionType)
201
- intersections.push(b.tsUnionType(options))
235
+ const choices = mixin.Type.map((type) => parseUnionType(type, options))
236
+ intersections.push(b.tsUnionType(choices))
202
237
  } else {
203
238
  const typeVal = Array.isArray(mixin.Type) ? mixin.Type[0] : mixin.Type
204
239
  if (isNamedGroupReference(typeVal)) {
@@ -235,7 +270,7 @@ function parseAssignment (assignment: Assignment) {
235
270
  const prop = props[0]
236
271
  const propType = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
237
272
  if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
238
- const value = parseUnionType(assignment)
273
+ const value = parseUnionType(assignment, options)
239
274
  const expr = b.tsTypeAliasDeclaration(id, value)
240
275
  expr.comments = getAssignmentComments(assignment)
241
276
  return exportWithComments(expr)
@@ -284,10 +319,10 @@ function parseAssignment (assignment: Assignment) {
284
319
 
285
320
  for (const prop of group.Properties) {
286
321
  // Choices are wrapped in arrays in the properties
287
- const options = Array.isArray(prop) ? prop : [prop]
288
- if (options.length > 1) { // It's a choice within the mixin group
322
+ const choiceProps = Array.isArray(prop) ? prop : [prop]
323
+ if (choiceProps.length > 1) { // It's a choice within the mixin group
289
324
  const unionOptions: any[] = []
290
- for (const option of options) {
325
+ for (const option of choiceProps) {
291
326
  let refName: string | undefined
292
327
  const type = option.Type
293
328
  if (typeof type === 'string') refName = type
@@ -314,7 +349,7 @@ function parseAssignment (assignment: Assignment) {
314
349
  }
315
350
  }
316
351
 
317
- for (const option of options) {
352
+ for (const option of choiceProps) {
318
353
  let refName: string | undefined
319
354
  const type = option.Type
320
355
 
@@ -395,7 +430,7 @@ function parseAssignment (assignment: Assignment) {
395
430
  }
396
431
  } else if (type && typeof type === 'object') {
397
432
  if (isGroup(type) && Array.isArray(type.Properties)) {
398
- choices.push(b.tsTypeLiteral(parseObjectType(type.Properties as Property[])))
433
+ choices.push(b.tsTypeLiteral(parseObjectType(type.Properties as Property[], options)))
399
434
  continue
400
435
  }
401
436
  refName = isNamedGroupReference(type)
@@ -441,7 +476,7 @@ function parseAssignment (assignment: Assignment) {
441
476
 
442
477
  const ownProps = props.filter(p => !isUnNamedProperty(p))
443
478
  if (ownProps.length > 0) {
444
- intersections.push(b.tsTypeLiteral(parseObjectType(ownProps)))
479
+ intersections.push(b.tsTypeLiteral(parseObjectType(ownProps, options)))
445
480
  }
446
481
 
447
482
  let value: any
@@ -457,7 +492,7 @@ function parseAssignment (assignment: Assignment) {
457
492
  }
458
493
 
459
494
  // Fallback to interface if no mixins (pure object)
460
- const objectType = parseObjectType(props)
495
+ const objectType = parseObjectType(props, options)
461
496
 
462
497
  const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType))
463
498
  expr.comments = getAssignmentComments(assignment)
@@ -476,7 +511,7 @@ function parseAssignment (assignment: Assignment) {
476
511
  // We need to parse each choice.
477
512
  const obj = assignmentValues.map((prop) => {
478
513
  const t = Array.isArray(prop.Type) ? prop.Type[0] : prop.Type
479
- return parseUnionType(t)
514
+ return parseUnionType(t, options)
480
515
  })
481
516
  const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)))
482
517
  const expr = b.tsTypeAliasDeclaration(id, value)
@@ -487,10 +522,10 @@ function parseAssignment (assignment: Assignment) {
487
522
  // Standard array
488
523
  const firstType = assignmentValues.Type
489
524
  const obj = Array.isArray(firstType)
490
- ? firstType.map(parseUnionType)
525
+ ? firstType.map((type) => parseUnionType(type, options))
491
526
  : isCDDLArray(firstType)
492
- ? firstType.Values.map((val: any) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type))
493
- : [parseUnionType(firstType)]
527
+ ? firstType.Values.map((val: any) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type, options))
528
+ : [parseUnionType(firstType, options)]
494
529
 
495
530
  const value = b.tsArrayType(
496
531
  obj.length === 1
@@ -505,7 +540,7 @@ function parseAssignment (assignment: Assignment) {
505
540
  throw new Error(`Unknown assignment type "${(assignment as any).Type}"`)
506
541
  }
507
542
 
508
- function parseObjectType (props: Property[]): ObjectBody {
543
+ function parseObjectType (props: Property[], options: TransformSettings): ObjectBody {
509
544
  const propItems: ObjectBody = []
510
545
  for (const prop of props) {
511
546
  /**
@@ -534,7 +569,7 @@ function parseObjectType (props: Property[]): ObjectBody {
534
569
  [keyIdentifier],
535
570
  b.tsTypeAnnotation(
536
571
  b.tsUnionType([
537
- ...cddlType.map((t) => parseUnionType(t)),
572
+ ...cddlType.map((t) => parseUnionType(t, options)),
538
573
  b.tsUndefinedKeyword()
539
574
  ])
540
575
  )
@@ -546,7 +581,7 @@ function parseObjectType (props: Property[]): ObjectBody {
546
581
  continue
547
582
  }
548
583
 
549
- const id = b.identifier(camelcase(prop.Name))
584
+ const id = createPropertyKey(prop.Name, options)
550
585
 
551
586
  if (prop.Operator && prop.Operator.Type === 'default') {
552
587
  const defaultValue = parseDefaultValue(prop.Operator)
@@ -555,7 +590,7 @@ function parseObjectType (props: Property[]): ObjectBody {
555
590
  }
556
591
 
557
592
  const type = cddlType.map((t) => {
558
- const unionType = parseUnionType(t)
593
+ const unionType = parseUnionType(t, options)
559
594
  if (unionType) {
560
595
  const defaultValue = parseDefaultValue((t as PropertyReference).Operator)
561
596
  defaultValue && comments.length && comments.push('') // add empty line if we have previous comments
@@ -577,12 +612,70 @@ function parseObjectType (props: Property[]): ObjectBody {
577
612
  return propItems
578
613
  }
579
614
 
580
- function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
615
+ function parseTemplateLiteralType (template: string): TSTypeKind {
616
+ const ast = parse(`type __CDDLTemplate = ${template};`, {
617
+ parser: typescriptParser,
618
+ sourceFileName: 'cddl2Ts.ts',
619
+ sourceRoot: process.cwd()
620
+ }) satisfies types.namedTypes.File
621
+ return (ast.program.body[0] as types.namedTypes.TSTypeAliasDeclaration).typeAnnotation
622
+ }
623
+
624
+ function escapeTemplateLiteralSegment (segment: string): string {
625
+ return segment
626
+ .replace(/\\/g, '\\\\')
627
+ .replace(/`/g, '\\`')
628
+ .replace(/\$\{/g, '\\${')
629
+ }
630
+
631
+ function regexpPatternToTemplateLiteral (pattern: string): string | undefined {
632
+ const normalized = pattern.startsWith('^') && pattern.endsWith('$')
633
+ ? pattern.slice(1, -1)
634
+ : pattern
635
+
636
+ if (!normalized.includes('.+')) {
637
+ return
638
+ }
639
+
640
+ const wildcardOnlyPattern = normalized.replace(/(\.\+)+/g, '')
641
+ if (wildcardOnlyPattern.includes('(') || wildcardOnlyPattern.includes('[') || wildcardOnlyPattern.includes('\\')) {
642
+ return
643
+ }
644
+
645
+ const segments = normalized.split(/(?:\.\+)+/g)
646
+ if (segments.length <= 1) {
647
+ return
648
+ }
649
+
650
+ return `\`${segments.map(escapeTemplateLiteralSegment).join('${string}')}\``
651
+ }
652
+
653
+ function parseNativeTypeWithOperator (t: NativeTypeWithOperator): TSTypeKind | undefined {
654
+ if (typeof t.Type !== 'string') {
655
+ return
656
+ }
657
+
658
+ const regexpPattern = getRegexpPattern(t)
659
+ const templateLiteral = regexpPattern && regexpPatternToTemplateLiteral(regexpPattern)
660
+ if (templateLiteral) {
661
+ return parseTemplateLiteralType(templateLiteral)
662
+ }
663
+
664
+ return NATIVE_TYPES[t.Type]
665
+ }
666
+
667
+ function parseUnionType (t: PropertyType | Assignment, options: TransformSettings): TSTypeKind {
581
668
  if (typeof t === 'string') {
582
669
  if (!NATIVE_TYPES[t]) {
583
670
  throw new Error(`Unknown native type: "${t}`)
584
671
  }
585
672
  return NATIVE_TYPES[t]
673
+ } else if (isNativeTypeWithOperator(t) && typeof t.Type === 'string') {
674
+ const nativeType = parseNativeTypeWithOperator(t)
675
+ if (!nativeType) {
676
+ throw new Error(`Unknown native type with operator: ${JSON.stringify(t)}`)
677
+ }
678
+ return nativeType
586
679
  } else if ((t as any).Type && typeof (t as any).Type === 'string' && NATIVE_TYPES[(t as any).Type]) {
587
680
  return NATIVE_TYPES[(t as any).Type]
588
681
  } else if (isNativeTypeWithOperator(t) && NATIVE_TYPES[(t.Type as any).Type]) {
@@ -600,35 +693,35 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
600
693
  * Check if we have choices in the group (arrays of Properties)
601
694
  */
602
695
  if (prop.some(p => Array.isArray(p))) {
603
- const options: TSTypeKind[] = []
696
+ const choices: TSTypeKind[] = []
604
697
  for (const choice of prop) {
605
698
  const subProps = Array.isArray(choice) ? choice : [choice]
606
699
 
607
700
  if (subProps.length === 1 && isUnNamedProperty(subProps[0])) {
608
701
  const first = subProps[0]
609
702
  const subType = Array.isArray(first.Type) ? first.Type[0] : first.Type
610
- options.push(parseUnionType(subType as PropertyType))
703
+ choices.push(parseUnionType(subType as PropertyType, options))
611
704
  continue
612
705
  }
613
706
 
614
707
  if (subProps.every(isUnNamedProperty)) {
615
708
  const tupleItems = subProps.map((p) => {
616
709
  const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type
617
- return parseUnionType(subType as PropertyType)
710
+ return parseUnionType(subType as PropertyType, options)
618
711
  })
619
- options.push(b.tsTupleType(tupleItems))
712
+ choices.push(b.tsTupleType(tupleItems))
620
713
  continue
621
714
  }
622
715
 
623
- options.push(b.tsTypeLiteral(parseObjectType(subProps)))
716
+ choices.push(b.tsTypeLiteral(parseObjectType(subProps, options)))
624
717
  }
625
- return b.tsUnionType(options)
718
+ return b.tsUnionType(choices)
626
719
  }
627
720
 
628
721
  if ((prop as Property[]).every(isUnNamedProperty)) {
629
722
  const items = (prop as Property[]).map(p => {
630
723
  const t = Array.isArray(p.Type) ? p.Type[0] : p.Type
631
- return parseUnionType(t as PropertyType)
724
+ return parseUnionType(t as PropertyType, options)
632
725
  })
633
726
 
634
727
  if (items.length === 1) return items[0];
@@ -643,7 +736,7 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
643
736
  b.identifier('Record'),
644
737
  b.tsTypeParameterInstantiation([
645
738
  NATIVE_TYPES[(prop[0] as Property).Name],
646
- parseUnionType(((prop[0] as Property).Type as PropertyType[])[0])
739
+ parseUnionType(((prop[0] as Property).Type as PropertyType[])[0], options)
647
740
  ])
648
741
  )
649
742
  }
@@ -651,7 +744,7 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
651
744
  /**
652
745
  * e.g. ?attributes: {*foo => text},
653
746
  */
654
- return b.tsTypeLiteral(parseObjectType(t.Properties as Property[]))
747
+ return b.tsTypeLiteral(parseObjectType(t.Properties as Property[], options))
655
748
  } else if (isNamedGroupReference(t)) {
656
749
  return b.tsTypeReference(
657
750
  b.identifier(pascalCase(t.Value))
package/tests/mod.test.ts CHANGED
@@ -66,6 +66,23 @@ describe('cddl2ts', () => {
66
66
  expect(process.exit).toHaveBeenCalledTimes(0)
67
67
  })
68
68
 
69
+ it('should allow configuring snake_case fields from the CLI', async () => {
70
+ await cli([
71
+ path.join(__dirname, '..', '..', '..', 'examples', 'commons', 'test.cddl'),
72
+ '--field-case',
73
+ 'snake'
74
+ ])
75
+
76
+ expect(console.log).toHaveBeenCalledTimes(1)
77
+ const output = vi.mocked(console.log).mock.calls[0]?.[0] as string
78
+ expect(output).toContain('export interface SessionCapabilitiesRequest {')
79
+ expect(output).toContain('always_match?: SessionCapabilityRequest;')
80
+ expect(output).toContain('first_match?: SessionCapabilityRequest[];')
81
+ expect(output).toContain('pointer_type?: InputPointerType;')
82
+ expect(output).not.toContain('alwaysMatch?: SessionCapabilityRequest;')
83
+ expect(process.exit).toHaveBeenCalledTimes(0)
84
+ })
85
+
69
86
  afterEach(() => {
70
87
  process.exit = exitOrig
71
88
  console.log = logOrig
@@ -123,6 +123,91 @@ describe('transform edge cases', () => {
123
123
  expect(output).toContain('export type MaybeValue = unknown;')
124
124
  })
125
125
 
126
+ it('should map simple wildcard regexp strings to template literal types', () => {
127
+ const output = transform([
128
+ variable('channel', {
129
+ Type: 'tstr',
130
+ Operator: {
131
+ Type: 'regexp',
132
+ Value: literal('custom:.+')
133
+ }
134
+ } as any),
135
+ group('event-envelope', [
136
+ property('channel', {
137
+ Type: 'tstr',
138
+ Operator: {
139
+ Type: 'regexp',
140
+ Value: literal('custom:.+')
141
+ }
142
+ } as any)
143
+ ]),
144
+ variable('email-address', {
145
+ Type: 'tstr',
146
+ Operator: {
147
+ Type: 'regexp',
148
+ Value: literal('[^@]+@[^@]+')
149
+ }
150
+ } as any),
151
+ variable('prefixed-name', {
152
+ Type: 'tstr',
153
+ Operator: {
154
+ Type: 'regexp',
155
+ Value: literal('foo_.+')
156
+ }
157
+ } as any),
158
+ variable('wrapped-name', {
159
+ Type: 'tstr',
160
+ Operator: {
161
+ Type: 'regexp',
162
+ Value: literal('some_.+_name')
163
+ }
164
+ } as any),
165
+ variable('double-wildcard', {
166
+ Type: 'tstr',
167
+ Operator: {
168
+ Type: 'regexp',
169
+ Value: literal('some_.+_middle_.+')
170
+ }
171
+ } as any)
172
+ ])
173
+
174
+ expect(output).toContain('export type Channel = `custom:${string}`;')
175
+ expect(output).toContain('channel: `custom:${string}`;')
176
+ expect(output).toContain('export type EmailAddress = string;')
177
+ expect(output).toContain('export type PrefixedName = `foo_${string}`;')
178
+ expect(output).toContain('export type WrappedName = `some_${string}_name`;')
179
+ expect(output).toContain('export type DoubleWildcard = `some_${string}_middle_${string}`;')
180
+ })
181
+
182
+ it('should keep camelCase fields by default', () => {
183
+ const output = transform([
184
+ group('session-capability-request', [
185
+ property('page_ranges', 'tstr')
186
+ ])
187
+ ])
188
+
189
+ expect(output).toContain('export interface SessionCapabilityRequest {')
190
+ expect(output).toContain('pageRanges: string;')
191
+ })
192
+
193
+ it('should support snake_case fields without changing exported type names', () => {
194
+ const output = transform([
195
+ group('session-capability-request', [
196
+ property('acceptInsecureCerts', 'bool'),
197
+ property('pageRanges', 'tstr'),
198
+ property('nestedConfig', group('', [
199
+ property('requestId', 'uint')
200
+ ]))
201
+ ])
202
+ ], { fieldCase: 'snake' })
203
+
204
+ expect(output).toContain('export interface SessionCapabilityRequest {')
205
+ expect(output).toContain('accept_insecure_certs: boolean;')
206
+ expect(output).toContain('page_ranges: string;')
207
+ expect(output).toContain('nested_config: {')
208
+ expect(output).toContain('request_id: number;')
209
+ })
210
+
126
211
  it('should generate intersections for choices with static props and mixins', () => {
127
212
  const output = transform([
128
213
  group('combined', [