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 +11 -5
- package/build/cli.d.ts.map +1 -1
- package/build/cli.js +11 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +113 -38
- package/cli-examples/remote.ts +2 -2
- package/package.json +2 -2
- package/src/cli.ts +13 -2
- package/src/index.ts +130 -37
- package/tests/mod.test.ts +17 -0
- package/tests/transform_edge_cases.test.ts +85 -0
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
56
|
+
* accept_insecure_certs?: boolean,
|
|
57
|
+
* browser_name?: string,
|
|
58
|
+
* browser_version?: string,
|
|
59
|
+
* platform_name?: string,
|
|
54
60
|
* }
|
|
55
61
|
*/
|
|
56
62
|
```
|
package/build/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
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, {
|
|
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
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
149
|
-
intersections.push(b.tsUnionType(
|
|
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
|
|
229
|
-
if (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
616
|
+
choices.push(b.tsTupleType(tupleItems));
|
|
542
617
|
continue;
|
|
543
618
|
}
|
|
544
|
-
|
|
619
|
+
choices.push(b.tsTypeLiteral(parseObjectType(subProps, options)));
|
|
545
620
|
}
|
|
546
|
-
return b.tsUnionType(
|
|
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)));
|
package/cli-examples/remote.ts
CHANGED
|
@@ -385,9 +385,9 @@ export interface BrowsingContextPrintParameters {
|
|
|
385
385
|
shrinkToFit?: boolean;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
|
|
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.
|
|
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.
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
201
|
-
intersections.push(b.tsUnionType(
|
|
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
|
|
288
|
-
if (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
712
|
+
choices.push(b.tsTupleType(tupleItems))
|
|
620
713
|
continue
|
|
621
714
|
}
|
|
622
715
|
|
|
623
|
-
|
|
716
|
+
choices.push(b.tsTypeLiteral(parseObjectType(subProps, options)))
|
|
624
717
|
}
|
|
625
|
-
return b.tsUnionType(
|
|
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', [
|