cddl2ts 0.7.2 → 0.8.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,EAWH,KAAK,UAAU,EAMlB,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
@@ -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,7 +524,7 @@ function parseObjectType(props) {
498
524
  }
499
525
  return propItems;
500
526
  }
501
- function parseUnionType(t) {
527
+ function parseUnionType(t, options) {
502
528
  if (typeof t === 'string') {
503
529
  if (!NATIVE_TYPES[t]) {
504
530
  throw new Error(`Unknown native type: "${t}`);
@@ -524,31 +550,31 @@ function parseUnionType(t) {
524
550
  * Check if we have choices in the group (arrays of Properties)
525
551
  */
526
552
  if (prop.some(p => Array.isArray(p))) {
527
- const options = [];
553
+ const choices = [];
528
554
  for (const choice of prop) {
529
555
  const subProps = Array.isArray(choice) ? choice : [choice];
530
556
  if (subProps.length === 1 && isUnNamedProperty(subProps[0])) {
531
557
  const first = subProps[0];
532
558
  const subType = Array.isArray(first.Type) ? first.Type[0] : first.Type;
533
- options.push(parseUnionType(subType));
559
+ choices.push(parseUnionType(subType, options));
534
560
  continue;
535
561
  }
536
562
  if (subProps.every(isUnNamedProperty)) {
537
563
  const tupleItems = subProps.map((p) => {
538
564
  const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type;
539
- return parseUnionType(subType);
565
+ return parseUnionType(subType, options);
540
566
  });
541
- options.push(b.tsTupleType(tupleItems));
567
+ choices.push(b.tsTupleType(tupleItems));
542
568
  continue;
543
569
  }
544
- options.push(b.tsTypeLiteral(parseObjectType(subProps)));
570
+ choices.push(b.tsTypeLiteral(parseObjectType(subProps, options)));
545
571
  }
546
- return b.tsUnionType(options);
572
+ return b.tsUnionType(choices);
547
573
  }
548
574
  if (prop.every(isUnNamedProperty)) {
549
575
  const items = prop.map(p => {
550
576
  const t = Array.isArray(p.Type) ? p.Type[0] : p.Type;
551
- return parseUnionType(t);
577
+ return parseUnionType(t, options);
552
578
  });
553
579
  if (items.length === 1)
554
580
  return items[0];
@@ -560,13 +586,13 @@ function parseUnionType(t) {
560
586
  if (prop.length === 1 && RECORD_KEY_TYPES.has(prop[0].Name)) {
561
587
  return b.tsTypeReference(b.identifier('Record'), b.tsTypeParameterInstantiation([
562
588
  NATIVE_TYPES[prop[0].Name],
563
- parseUnionType(prop[0].Type[0])
589
+ parseUnionType(prop[0].Type[0], options)
564
590
  ]));
565
591
  }
566
592
  /**
567
593
  * e.g. ?attributes: {*foo => text},
568
594
  */
569
- return b.tsTypeLiteral(parseObjectType(t.Properties));
595
+ return b.tsTypeLiteral(parseObjectType(t.Properties, options));
570
596
  }
571
597
  else if (isNamedGroupReference(t)) {
572
598
  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.8.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",
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
@@ -59,13 +59,23 @@ const RECORD_KEY_TYPES = new Set([
59
59
  type ObjectEntry = types.namedTypes.TSCallSignatureDeclaration | types.namedTypes.TSConstructSignatureDeclaration | types.namedTypes.TSIndexSignature | types.namedTypes.TSMethodSignature | types.namedTypes.TSPropertySignature
60
60
  type ObjectBody = ObjectEntry[]
61
61
  type TSTypeKind = types.namedTypes.TSAsExpression['typeAnnotation']
62
+ export type FieldCase = 'camel' | 'snake'
63
+ type TransformSettings = Required<TransformOptions>
64
+ const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/
62
65
 
63
66
  export interface TransformOptions {
64
67
  useUnknown?: boolean
68
+ fieldCase?: FieldCase
65
69
  }
66
70
 
67
71
  export function transform (assignments: Assignment[], options?: TransformOptions) {
68
- if (options?.useUnknown) {
72
+ const transformOptions: TransformSettings = {
73
+ useUnknown: false,
74
+ fieldCase: 'camel',
75
+ ...options
76
+ }
77
+
78
+ if (transformOptions.useUnknown) {
69
79
  NATIVE_TYPES.any = b.tsUnknownKeyword()
70
80
  } else {
71
81
  NATIVE_TYPES.any = b.tsAnyKeyword()
@@ -81,7 +91,7 @@ export function transform (assignments: Assignment[], options?: TransformOptions
81
91
  ) satisfies types.namedTypes.File
82
92
 
83
93
  for (const assignment of assignments) {
84
- const statement = parseAssignment(assignment)
94
+ const statement = parseAssignment(assignment, transformOptions)
85
95
  if (!statement) {
86
96
  continue
87
97
  }
@@ -111,7 +121,30 @@ function isExtensibleRecordProperty (prop: Property) {
111
121
  RECORD_KEY_TYPES.has(prop.Name)
112
122
  }
113
123
 
114
- function parseAssignment (assignment: Assignment) {
124
+ function toSnakeCase (name: string) {
125
+ return name
126
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
127
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
128
+ .replace(/[^A-Za-z0-9_$]+/g, '_')
129
+ .replace(/_+/g, '_')
130
+ .replace(/^_+|_+$/g, '')
131
+ .toLowerCase()
132
+ }
133
+
134
+ function formatFieldName (name: string, fieldCase: FieldCase) {
135
+ return fieldCase === 'snake'
136
+ ? toSnakeCase(name)
137
+ : camelcase(name)
138
+ }
139
+
140
+ function createPropertyKey (name: string, options: TransformSettings) {
141
+ const fieldName = formatFieldName(name, options.fieldCase)
142
+ return IDENTIFIER_PATTERN.test(fieldName)
143
+ ? b.identifier(fieldName)
144
+ : b.stringLiteral(fieldName)
145
+ }
146
+
147
+ function parseAssignment (assignment: Assignment, options: TransformSettings) {
115
148
  if (isVariable(assignment)) {
116
149
  const propType = Array.isArray(assignment.PropertyType)
117
150
  ? assignment.PropertyType
@@ -124,7 +157,7 @@ function parseAssignment (assignment: Assignment) {
124
157
  if (propType.length === 1 && propType[0].Type === 'range') {
125
158
  typeParameters = b.tsNumberKeyword()
126
159
  } else {
127
- typeParameters = b.tsUnionType(propType.map(parseUnionType))
160
+ typeParameters = b.tsUnionType(propType.map((prop) => parseUnionType(prop, options)))
128
161
  }
129
162
 
130
163
  const expr = b.tsTypeAliasDeclaration(id, typeParameters)
@@ -161,7 +194,7 @@ function parseAssignment (assignment: Assignment) {
161
194
  i++ // Skip next property
162
195
  }
163
196
 
164
- const options = choiceOptions.map(p => {
197
+ const choiceTypes = choiceOptions.map(p => {
165
198
  // If p is a group reference (Name ''), it's a TypeReference
166
199
  // e.g. SessionAutodetectProxyConfiguration // SessionDirectProxyConfiguration
167
200
  // The parser sometimes wraps it in an array, sometimes not (if inside a choice)
@@ -175,12 +208,12 @@ function parseAssignment (assignment: Assignment) {
175
208
  b.identifier(pascalCase(typeVal.Value || typeVal.Type))
176
209
  )
177
210
  }
178
- return parseUnionType(typeVal);
211
+ return parseUnionType(typeVal, options)
179
212
  }
180
213
  // Otherwise it is an object literal with this property
181
- return b.tsTypeLiteral(parseObjectType([p]))
214
+ return b.tsTypeLiteral(parseObjectType([p], options))
182
215
  })
183
- intersections.push(b.tsUnionType(options))
216
+ intersections.push(b.tsUnionType(choiceTypes))
184
217
  } else {
185
218
  staticProps.push(prop)
186
219
  }
@@ -192,13 +225,13 @@ function parseAssignment (assignment: Assignment) {
192
225
  const ownProps = staticProps.filter(p => !isUnNamedProperty(p))
193
226
 
194
227
  if (ownProps.length > 0) {
195
- intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps)))
228
+ intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps, options)))
196
229
  }
197
230
 
198
231
  for (const mixin of mixins) {
199
232
  if (Array.isArray(mixin.Type) && mixin.Type.length > 1) {
200
- const options = mixin.Type.map(parseUnionType)
201
- intersections.push(b.tsUnionType(options))
233
+ const choices = mixin.Type.map((type) => parseUnionType(type, options))
234
+ intersections.push(b.tsUnionType(choices))
202
235
  } else {
203
236
  const typeVal = Array.isArray(mixin.Type) ? mixin.Type[0] : mixin.Type
204
237
  if (isNamedGroupReference(typeVal)) {
@@ -235,7 +268,7 @@ function parseAssignment (assignment: Assignment) {
235
268
  const prop = props[0]
236
269
  const propType = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
237
270
  if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
238
- const value = parseUnionType(assignment)
271
+ const value = parseUnionType(assignment, options)
239
272
  const expr = b.tsTypeAliasDeclaration(id, value)
240
273
  expr.comments = getAssignmentComments(assignment)
241
274
  return exportWithComments(expr)
@@ -284,10 +317,10 @@ function parseAssignment (assignment: Assignment) {
284
317
 
285
318
  for (const prop of group.Properties) {
286
319
  // 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
320
+ const choiceProps = Array.isArray(prop) ? prop : [prop]
321
+ if (choiceProps.length > 1) { // It's a choice within the mixin group
289
322
  const unionOptions: any[] = []
290
- for (const option of options) {
323
+ for (const option of choiceProps) {
291
324
  let refName: string | undefined
292
325
  const type = option.Type
293
326
  if (typeof type === 'string') refName = type
@@ -314,7 +347,7 @@ function parseAssignment (assignment: Assignment) {
314
347
  }
315
348
  }
316
349
 
317
- for (const option of options) {
350
+ for (const option of choiceProps) {
318
351
  let refName: string | undefined
319
352
  const type = option.Type
320
353
 
@@ -395,7 +428,7 @@ function parseAssignment (assignment: Assignment) {
395
428
  }
396
429
  } else if (type && typeof type === 'object') {
397
430
  if (isGroup(type) && Array.isArray(type.Properties)) {
398
- choices.push(b.tsTypeLiteral(parseObjectType(type.Properties as Property[])))
431
+ choices.push(b.tsTypeLiteral(parseObjectType(type.Properties as Property[], options)))
399
432
  continue
400
433
  }
401
434
  refName = isNamedGroupReference(type)
@@ -441,7 +474,7 @@ function parseAssignment (assignment: Assignment) {
441
474
 
442
475
  const ownProps = props.filter(p => !isUnNamedProperty(p))
443
476
  if (ownProps.length > 0) {
444
- intersections.push(b.tsTypeLiteral(parseObjectType(ownProps)))
477
+ intersections.push(b.tsTypeLiteral(parseObjectType(ownProps, options)))
445
478
  }
446
479
 
447
480
  let value: any
@@ -457,7 +490,7 @@ function parseAssignment (assignment: Assignment) {
457
490
  }
458
491
 
459
492
  // Fallback to interface if no mixins (pure object)
460
- const objectType = parseObjectType(props)
493
+ const objectType = parseObjectType(props, options)
461
494
 
462
495
  const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType))
463
496
  expr.comments = getAssignmentComments(assignment)
@@ -476,7 +509,7 @@ function parseAssignment (assignment: Assignment) {
476
509
  // We need to parse each choice.
477
510
  const obj = assignmentValues.map((prop) => {
478
511
  const t = Array.isArray(prop.Type) ? prop.Type[0] : prop.Type
479
- return parseUnionType(t)
512
+ return parseUnionType(t, options)
480
513
  })
481
514
  const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)))
482
515
  const expr = b.tsTypeAliasDeclaration(id, value)
@@ -487,10 +520,10 @@ function parseAssignment (assignment: Assignment) {
487
520
  // Standard array
488
521
  const firstType = assignmentValues.Type
489
522
  const obj = Array.isArray(firstType)
490
- ? firstType.map(parseUnionType)
523
+ ? firstType.map((type) => parseUnionType(type, options))
491
524
  : isCDDLArray(firstType)
492
- ? firstType.Values.map((val: any) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type))
493
- : [parseUnionType(firstType)]
525
+ ? firstType.Values.map((val: any) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type, options))
526
+ : [parseUnionType(firstType, options)]
494
527
 
495
528
  const value = b.tsArrayType(
496
529
  obj.length === 1
@@ -505,7 +538,7 @@ function parseAssignment (assignment: Assignment) {
505
538
  throw new Error(`Unknown assignment type "${(assignment as any).Type}"`)
506
539
  }
507
540
 
508
- function parseObjectType (props: Property[]): ObjectBody {
541
+ function parseObjectType (props: Property[], options: TransformSettings): ObjectBody {
509
542
  const propItems: ObjectBody = []
510
543
  for (const prop of props) {
511
544
  /**
@@ -534,7 +567,7 @@ function parseObjectType (props: Property[]): ObjectBody {
534
567
  [keyIdentifier],
535
568
  b.tsTypeAnnotation(
536
569
  b.tsUnionType([
537
- ...cddlType.map((t) => parseUnionType(t)),
570
+ ...cddlType.map((t) => parseUnionType(t, options)),
538
571
  b.tsUndefinedKeyword()
539
572
  ])
540
573
  )
@@ -546,7 +579,7 @@ function parseObjectType (props: Property[]): ObjectBody {
546
579
  continue
547
580
  }
548
581
 
549
- const id = b.identifier(camelcase(prop.Name))
582
+ const id = createPropertyKey(prop.Name, options)
550
583
 
551
584
  if (prop.Operator && prop.Operator.Type === 'default') {
552
585
  const defaultValue = parseDefaultValue(prop.Operator)
@@ -555,7 +588,7 @@ function parseObjectType (props: Property[]): ObjectBody {
555
588
  }
556
589
 
557
590
  const type = cddlType.map((t) => {
558
- const unionType = parseUnionType(t)
591
+ const unionType = parseUnionType(t, options)
559
592
  if (unionType) {
560
593
  const defaultValue = parseDefaultValue((t as PropertyReference).Operator)
561
594
  defaultValue && comments.length && comments.push('') // add empty line if we have previous comments
@@ -577,7 +610,7 @@ function parseObjectType (props: Property[]): ObjectBody {
577
610
  return propItems
578
611
  }
579
612
 
580
- function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
613
+ function parseUnionType (t: PropertyType | Assignment, options: TransformSettings): TSTypeKind {
581
614
  if (typeof t === 'string') {
582
615
  if (!NATIVE_TYPES[t]) {
583
616
  throw new Error(`Unknown native type: "${t}`)
@@ -600,35 +633,35 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
600
633
  * Check if we have choices in the group (arrays of Properties)
601
634
  */
602
635
  if (prop.some(p => Array.isArray(p))) {
603
- const options: TSTypeKind[] = []
636
+ const choices: TSTypeKind[] = []
604
637
  for (const choice of prop) {
605
638
  const subProps = Array.isArray(choice) ? choice : [choice]
606
639
 
607
640
  if (subProps.length === 1 && isUnNamedProperty(subProps[0])) {
608
641
  const first = subProps[0]
609
642
  const subType = Array.isArray(first.Type) ? first.Type[0] : first.Type
610
- options.push(parseUnionType(subType as PropertyType))
643
+ choices.push(parseUnionType(subType as PropertyType, options))
611
644
  continue
612
645
  }
613
646
 
614
647
  if (subProps.every(isUnNamedProperty)) {
615
648
  const tupleItems = subProps.map((p) => {
616
649
  const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type
617
- return parseUnionType(subType as PropertyType)
650
+ return parseUnionType(subType as PropertyType, options)
618
651
  })
619
- options.push(b.tsTupleType(tupleItems))
652
+ choices.push(b.tsTupleType(tupleItems))
620
653
  continue
621
654
  }
622
655
 
623
- options.push(b.tsTypeLiteral(parseObjectType(subProps)))
656
+ choices.push(b.tsTypeLiteral(parseObjectType(subProps, options)))
624
657
  }
625
- return b.tsUnionType(options)
658
+ return b.tsUnionType(choices)
626
659
  }
627
660
 
628
661
  if ((prop as Property[]).every(isUnNamedProperty)) {
629
662
  const items = (prop as Property[]).map(p => {
630
663
  const t = Array.isArray(p.Type) ? p.Type[0] : p.Type
631
- return parseUnionType(t as PropertyType)
664
+ return parseUnionType(t as PropertyType, options)
632
665
  })
633
666
 
634
667
  if (items.length === 1) return items[0];
@@ -643,7 +676,7 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
643
676
  b.identifier('Record'),
644
677
  b.tsTypeParameterInstantiation([
645
678
  NATIVE_TYPES[(prop[0] as Property).Name],
646
- parseUnionType(((prop[0] as Property).Type as PropertyType[])[0])
679
+ parseUnionType(((prop[0] as Property).Type as PropertyType[])[0], options)
647
680
  ])
648
681
  )
649
682
  }
@@ -651,7 +684,7 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
651
684
  /**
652
685
  * e.g. ?attributes: {*foo => text},
653
686
  */
654
- return b.tsTypeLiteral(parseObjectType(t.Properties as Property[]))
687
+ return b.tsTypeLiteral(parseObjectType(t.Properties as Property[], options))
655
688
  } else if (isNamedGroupReference(t)) {
656
689
  return b.tsTypeReference(
657
690
  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,35 @@ describe('transform edge cases', () => {
123
123
  expect(output).toContain('export type MaybeValue = unknown;')
124
124
  })
125
125
 
126
+ it('should keep camelCase fields by default', () => {
127
+ const output = transform([
128
+ group('session-capability-request', [
129
+ property('page_ranges', 'tstr')
130
+ ])
131
+ ])
132
+
133
+ expect(output).toContain('export interface SessionCapabilityRequest {')
134
+ expect(output).toContain('pageRanges: string;')
135
+ })
136
+
137
+ it('should support snake_case fields without changing exported type names', () => {
138
+ const output = transform([
139
+ group('session-capability-request', [
140
+ property('acceptInsecureCerts', 'bool'),
141
+ property('pageRanges', 'tstr'),
142
+ property('nestedConfig', group('', [
143
+ property('requestId', 'uint')
144
+ ]))
145
+ ])
146
+ ], { fieldCase: 'snake' })
147
+
148
+ expect(output).toContain('export interface SessionCapabilityRequest {')
149
+ expect(output).toContain('accept_insecure_certs: boolean;')
150
+ expect(output).toContain('page_ranges: string;')
151
+ expect(output).toContain('nested_config: {')
152
+ expect(output).toContain('request_id: number;')
153
+ })
154
+
126
155
  it('should generate intersections for choices with static props and mixins', () => {
127
156
  const output = transform([
128
157
  group('combined', [