cddl2ts 0.7.1 → 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 +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 +104 -50
- package/cli-examples/remote.ts +2 -2
- package/package.json +2 -2
- package/src/cli.ts +13 -2
- package/src/index.ts +125 -50
- package/tests/__snapshots__/extensible_metadata.test.ts.snap +16 -0
- package/tests/__snapshots__/group_choice.test.ts.snap +24 -24
- package/tests/__snapshots__/mod.test.ts.snap +2 -2
- package/tests/__snapshots__/webdriver_remote.test.ts.snap +2 -2
- package/tests/extensible_metadata.test.ts +49 -0
- package/tests/mod.test.ts +17 -0
- package/tests/named_group_choice.test.ts +2 -2
- package/tests/transform_edge_cases.test.ts +65 -2
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,EAWH,KAAK,UAAU,EAMlB,MAAM,MAAM,CAAA;
|
|
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
|
-
|
|
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
|
}
|
|
@@ -57,7 +63,42 @@ export function transform(assignments, options) {
|
|
|
57
63
|
}
|
|
58
64
|
return print(ast).code;
|
|
59
65
|
}
|
|
60
|
-
function
|
|
66
|
+
function getAssignmentComments(assignment) {
|
|
67
|
+
return assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
|
|
68
|
+
}
|
|
69
|
+
function exportWithComments(declaration) {
|
|
70
|
+
const expr = b.exportDeclaration(false, declaration);
|
|
71
|
+
expr.comments = declaration.comments;
|
|
72
|
+
declaration.comments = [];
|
|
73
|
+
return expr;
|
|
74
|
+
}
|
|
75
|
+
function isExtensibleRecordProperty(prop) {
|
|
76
|
+
return !isUnNamedProperty(prop) &&
|
|
77
|
+
prop.Occurrence.m === Infinity &&
|
|
78
|
+
!prop.HasCut &&
|
|
79
|
+
RECORD_KEY_TYPES.has(prop.Name);
|
|
80
|
+
}
|
|
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) {
|
|
61
102
|
if (isVariable(assignment)) {
|
|
62
103
|
const propType = Array.isArray(assignment.PropertyType)
|
|
63
104
|
? assignment.PropertyType
|
|
@@ -69,11 +110,11 @@ function parseAssignment(assignment) {
|
|
|
69
110
|
typeParameters = b.tsNumberKeyword();
|
|
70
111
|
}
|
|
71
112
|
else {
|
|
72
|
-
typeParameters = b.tsUnionType(propType.map(parseUnionType));
|
|
113
|
+
typeParameters = b.tsUnionType(propType.map((prop) => parseUnionType(prop, options)));
|
|
73
114
|
}
|
|
74
115
|
const expr = b.tsTypeAliasDeclaration(id, typeParameters);
|
|
75
|
-
expr.comments = assignment
|
|
76
|
-
return
|
|
116
|
+
expr.comments = getAssignmentComments(assignment);
|
|
117
|
+
return exportWithComments(expr);
|
|
77
118
|
}
|
|
78
119
|
if (isGroup(assignment)) {
|
|
79
120
|
const id = b.identifier(pascalCase(assignment.Name));
|
|
@@ -99,7 +140,7 @@ function parseAssignment(assignment) {
|
|
|
99
140
|
choiceOptions.push(nextProp);
|
|
100
141
|
i++; // Skip next property
|
|
101
142
|
}
|
|
102
|
-
const
|
|
143
|
+
const choiceTypes = choiceOptions.map(p => {
|
|
103
144
|
// If p is a group reference (Name ''), it's a TypeReference
|
|
104
145
|
// e.g. SessionAutodetectProxyConfiguration // SessionDirectProxyConfiguration
|
|
105
146
|
// The parser sometimes wraps it in an array, sometimes not (if inside a choice)
|
|
@@ -110,12 +151,12 @@ function parseAssignment(assignment) {
|
|
|
110
151
|
if (isNamedGroupReference(typeVal)) {
|
|
111
152
|
return b.tsTypeReference(b.identifier(pascalCase(typeVal.Value || typeVal.Type)));
|
|
112
153
|
}
|
|
113
|
-
return parseUnionType(typeVal);
|
|
154
|
+
return parseUnionType(typeVal, options);
|
|
114
155
|
}
|
|
115
156
|
// Otherwise it is an object literal with this property
|
|
116
|
-
return b.tsTypeLiteral(parseObjectType([p]));
|
|
157
|
+
return b.tsTypeLiteral(parseObjectType([p], options));
|
|
117
158
|
});
|
|
118
|
-
intersections.push(b.tsUnionType(
|
|
159
|
+
intersections.push(b.tsUnionType(choiceTypes));
|
|
119
160
|
}
|
|
120
161
|
else {
|
|
121
162
|
staticProps.push(prop);
|
|
@@ -126,12 +167,12 @@ function parseAssignment(assignment) {
|
|
|
126
167
|
const mixins = staticProps.filter(isUnNamedProperty);
|
|
127
168
|
const ownProps = staticProps.filter(p => !isUnNamedProperty(p));
|
|
128
169
|
if (ownProps.length > 0) {
|
|
129
|
-
intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps)));
|
|
170
|
+
intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps, options)));
|
|
130
171
|
}
|
|
131
172
|
for (const mixin of mixins) {
|
|
132
173
|
if (Array.isArray(mixin.Type) && mixin.Type.length > 1) {
|
|
133
|
-
const
|
|
134
|
-
intersections.push(b.tsUnionType(
|
|
174
|
+
const choices = mixin.Type.map((type) => parseUnionType(type, options));
|
|
175
|
+
intersections.push(b.tsUnionType(choices));
|
|
135
176
|
}
|
|
136
177
|
else {
|
|
137
178
|
const typeVal = Array.isArray(mixin.Type) ? mixin.Type[0] : mixin.Type;
|
|
@@ -154,8 +195,8 @@ function parseAssignment(assignment) {
|
|
|
154
195
|
value = b.tsIntersectionType(intersections);
|
|
155
196
|
}
|
|
156
197
|
const expr = b.tsTypeAliasDeclaration(id, value);
|
|
157
|
-
expr.comments = assignment
|
|
158
|
-
return
|
|
198
|
+
expr.comments = getAssignmentComments(assignment);
|
|
199
|
+
return exportWithComments(expr);
|
|
159
200
|
}
|
|
160
201
|
const props = properties;
|
|
161
202
|
/**
|
|
@@ -165,10 +206,10 @@ function parseAssignment(assignment) {
|
|
|
165
206
|
const prop = props[0];
|
|
166
207
|
const propType = Array.isArray(prop.Type) ? prop.Type : [prop.Type];
|
|
167
208
|
if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
|
|
168
|
-
const value = parseUnionType(assignment);
|
|
209
|
+
const value = parseUnionType(assignment, options);
|
|
169
210
|
const expr = b.tsTypeAliasDeclaration(id, value);
|
|
170
|
-
expr.comments = assignment
|
|
171
|
-
return
|
|
211
|
+
expr.comments = getAssignmentComments(assignment);
|
|
212
|
+
return exportWithComments(expr);
|
|
172
213
|
}
|
|
173
214
|
}
|
|
174
215
|
// Check if extended interfaces are likely unions or conflicting types
|
|
@@ -210,10 +251,10 @@ function parseAssignment(assignment) {
|
|
|
210
251
|
const choices = [];
|
|
211
252
|
for (const prop of group.Properties) {
|
|
212
253
|
// Choices are wrapped in arrays in the properties
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
254
|
+
const choiceProps = Array.isArray(prop) ? prop : [prop];
|
|
255
|
+
if (choiceProps.length > 1) { // It's a choice within the mixin group
|
|
215
256
|
const unionOptions = [];
|
|
216
|
-
for (const option of
|
|
257
|
+
for (const option of choiceProps) {
|
|
217
258
|
let refName;
|
|
218
259
|
const type = option.Type;
|
|
219
260
|
if (typeof type === 'string')
|
|
@@ -245,7 +286,7 @@ function parseAssignment(assignment) {
|
|
|
245
286
|
continue;
|
|
246
287
|
}
|
|
247
288
|
}
|
|
248
|
-
for (const option of
|
|
289
|
+
for (const option of choiceProps) {
|
|
249
290
|
let refName;
|
|
250
291
|
const type = option.Type;
|
|
251
292
|
if (typeof type === 'string') {
|
|
@@ -340,7 +381,7 @@ function parseAssignment(assignment) {
|
|
|
340
381
|
}
|
|
341
382
|
else if (type && typeof type === 'object') {
|
|
342
383
|
if (isGroup(type) && Array.isArray(type.Properties)) {
|
|
343
|
-
choices.push(b.tsTypeLiteral(parseObjectType(type.Properties)));
|
|
384
|
+
choices.push(b.tsTypeLiteral(parseObjectType(type.Properties, options)));
|
|
344
385
|
continue;
|
|
345
386
|
}
|
|
346
387
|
refName = isNamedGroupReference(type)
|
|
@@ -375,7 +416,7 @@ function parseAssignment(assignment) {
|
|
|
375
416
|
}
|
|
376
417
|
const ownProps = props.filter(p => !isUnNamedProperty(p));
|
|
377
418
|
if (ownProps.length > 0) {
|
|
378
|
-
intersections.push(b.tsTypeLiteral(parseObjectType(ownProps)));
|
|
419
|
+
intersections.push(b.tsTypeLiteral(parseObjectType(ownProps, options)));
|
|
379
420
|
}
|
|
380
421
|
let value;
|
|
381
422
|
if (intersections.length === 1) {
|
|
@@ -385,14 +426,14 @@ function parseAssignment(assignment) {
|
|
|
385
426
|
value = b.tsIntersectionType(intersections);
|
|
386
427
|
}
|
|
387
428
|
const expr = b.tsTypeAliasDeclaration(id, value);
|
|
388
|
-
expr.comments = assignment
|
|
389
|
-
return
|
|
429
|
+
expr.comments = getAssignmentComments(assignment);
|
|
430
|
+
return exportWithComments(expr);
|
|
390
431
|
}
|
|
391
432
|
// Fallback to interface if no mixins (pure object)
|
|
392
|
-
const objectType = parseObjectType(props);
|
|
433
|
+
const objectType = parseObjectType(props, options);
|
|
393
434
|
const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType));
|
|
394
|
-
expr.comments = assignment
|
|
395
|
-
return
|
|
435
|
+
expr.comments = getAssignmentComments(assignment);
|
|
436
|
+
return exportWithComments(expr);
|
|
396
437
|
}
|
|
397
438
|
if (isCDDLArray(assignment)) {
|
|
398
439
|
const id = b.identifier(pascalCase(assignment.Name));
|
|
@@ -404,30 +445,30 @@ function parseAssignment(assignment) {
|
|
|
404
445
|
// We need to parse each choice.
|
|
405
446
|
const obj = assignmentValues.map((prop) => {
|
|
406
447
|
const t = Array.isArray(prop.Type) ? prop.Type[0] : prop.Type;
|
|
407
|
-
return parseUnionType(t);
|
|
448
|
+
return parseUnionType(t, options);
|
|
408
449
|
});
|
|
409
450
|
const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)));
|
|
410
451
|
const expr = b.tsTypeAliasDeclaration(id, value);
|
|
411
|
-
expr.comments = assignment
|
|
412
|
-
return
|
|
452
|
+
expr.comments = getAssignmentComments(assignment);
|
|
453
|
+
return exportWithComments(expr);
|
|
413
454
|
}
|
|
414
455
|
// Standard array
|
|
415
456
|
const firstType = assignmentValues.Type;
|
|
416
457
|
const obj = Array.isArray(firstType)
|
|
417
|
-
? firstType.map(parseUnionType)
|
|
458
|
+
? firstType.map((type) => parseUnionType(type, options))
|
|
418
459
|
: isCDDLArray(firstType)
|
|
419
|
-
? firstType.Values.map((val) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type))
|
|
420
|
-
: [parseUnionType(firstType)];
|
|
460
|
+
? firstType.Values.map((val) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type, options))
|
|
461
|
+
: [parseUnionType(firstType, options)];
|
|
421
462
|
const value = b.tsArrayType(obj.length === 1
|
|
422
463
|
? obj[0]
|
|
423
464
|
: b.tsParenthesizedType(b.tsUnionType(obj)));
|
|
424
465
|
const expr = b.tsTypeAliasDeclaration(id, value);
|
|
425
|
-
expr.comments = assignment
|
|
426
|
-
return
|
|
466
|
+
expr.comments = getAssignmentComments(assignment);
|
|
467
|
+
return exportWithComments(expr);
|
|
427
468
|
}
|
|
428
469
|
throw new Error(`Unknown assignment type "${assignment.Type}"`);
|
|
429
470
|
}
|
|
430
|
-
function parseObjectType(props) {
|
|
471
|
+
function parseObjectType(props, options) {
|
|
431
472
|
const propItems = [];
|
|
432
473
|
for (const prop of props) {
|
|
433
474
|
/**
|
|
@@ -444,16 +485,29 @@ function parseObjectType(props) {
|
|
|
444
485
|
if (isUnNamedProperty(prop)) {
|
|
445
486
|
continue;
|
|
446
487
|
}
|
|
447
|
-
const id = b.identifier(camelcase(prop.Name));
|
|
448
488
|
const cddlType = Array.isArray(prop.Type) ? prop.Type : [prop.Type];
|
|
449
489
|
const comments = prop.Comments.map((c) => ` ${c.Content}`);
|
|
490
|
+
if (isExtensibleRecordProperty(prop)) {
|
|
491
|
+
const keyIdentifier = b.identifier('key');
|
|
492
|
+
keyIdentifier.typeAnnotation = b.tsTypeAnnotation(NATIVE_TYPES[prop.Name]);
|
|
493
|
+
const indexSignature = b.tsIndexSignature([keyIdentifier], b.tsTypeAnnotation(b.tsUnionType([
|
|
494
|
+
...cddlType.map((t) => parseUnionType(t, options)),
|
|
495
|
+
b.tsUndefinedKeyword()
|
|
496
|
+
])));
|
|
497
|
+
indexSignature.comments = comments.length
|
|
498
|
+
? [b.commentBlock(`*\n *${comments.join('\n *')}\n `)]
|
|
499
|
+
: [];
|
|
500
|
+
propItems.push(indexSignature);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const id = createPropertyKey(prop.Name, options);
|
|
450
504
|
if (prop.Operator && prop.Operator.Type === 'default') {
|
|
451
505
|
const defaultValue = parseDefaultValue(prop.Operator);
|
|
452
506
|
defaultValue && comments.length && comments.push(''); // add empty line if we have previous comments
|
|
453
507
|
defaultValue && comments.push(` @default ${defaultValue}`);
|
|
454
508
|
}
|
|
455
509
|
const type = cddlType.map((t) => {
|
|
456
|
-
const unionType = parseUnionType(t);
|
|
510
|
+
const unionType = parseUnionType(t, options);
|
|
457
511
|
if (unionType) {
|
|
458
512
|
const defaultValue = parseDefaultValue(t.Operator);
|
|
459
513
|
defaultValue && comments.length && comments.push(''); // add empty line if we have previous comments
|
|
@@ -470,7 +524,7 @@ function parseObjectType(props) {
|
|
|
470
524
|
}
|
|
471
525
|
return propItems;
|
|
472
526
|
}
|
|
473
|
-
function parseUnionType(t) {
|
|
527
|
+
function parseUnionType(t, options) {
|
|
474
528
|
if (typeof t === 'string') {
|
|
475
529
|
if (!NATIVE_TYPES[t]) {
|
|
476
530
|
throw new Error(`Unknown native type: "${t}`);
|
|
@@ -496,31 +550,31 @@ function parseUnionType(t) {
|
|
|
496
550
|
* Check if we have choices in the group (arrays of Properties)
|
|
497
551
|
*/
|
|
498
552
|
if (prop.some(p => Array.isArray(p))) {
|
|
499
|
-
const
|
|
553
|
+
const choices = [];
|
|
500
554
|
for (const choice of prop) {
|
|
501
555
|
const subProps = Array.isArray(choice) ? choice : [choice];
|
|
502
556
|
if (subProps.length === 1 && isUnNamedProperty(subProps[0])) {
|
|
503
557
|
const first = subProps[0];
|
|
504
558
|
const subType = Array.isArray(first.Type) ? first.Type[0] : first.Type;
|
|
505
|
-
|
|
559
|
+
choices.push(parseUnionType(subType, options));
|
|
506
560
|
continue;
|
|
507
561
|
}
|
|
508
562
|
if (subProps.every(isUnNamedProperty)) {
|
|
509
563
|
const tupleItems = subProps.map((p) => {
|
|
510
564
|
const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type;
|
|
511
|
-
return parseUnionType(subType);
|
|
565
|
+
return parseUnionType(subType, options);
|
|
512
566
|
});
|
|
513
|
-
|
|
567
|
+
choices.push(b.tsTupleType(tupleItems));
|
|
514
568
|
continue;
|
|
515
569
|
}
|
|
516
|
-
|
|
570
|
+
choices.push(b.tsTypeLiteral(parseObjectType(subProps, options)));
|
|
517
571
|
}
|
|
518
|
-
return b.tsUnionType(
|
|
572
|
+
return b.tsUnionType(choices);
|
|
519
573
|
}
|
|
520
574
|
if (prop.every(isUnNamedProperty)) {
|
|
521
575
|
const items = prop.map(p => {
|
|
522
576
|
const t = Array.isArray(p.Type) ? p.Type[0] : p.Type;
|
|
523
|
-
return parseUnionType(t);
|
|
577
|
+
return parseUnionType(t, options);
|
|
524
578
|
});
|
|
525
579
|
if (items.length === 1)
|
|
526
580
|
return items[0];
|
|
@@ -532,13 +586,13 @@ function parseUnionType(t) {
|
|
|
532
586
|
if (prop.length === 1 && RECORD_KEY_TYPES.has(prop[0].Name)) {
|
|
533
587
|
return b.tsTypeReference(b.identifier('Record'), b.tsTypeParameterInstantiation([
|
|
534
588
|
NATIVE_TYPES[prop[0].Name],
|
|
535
|
-
parseUnionType(prop[0].Type[0])
|
|
589
|
+
parseUnionType(prop[0].Type[0], options)
|
|
536
590
|
]));
|
|
537
591
|
}
|
|
538
592
|
/**
|
|
539
593
|
* e.g. ?attributes: {*foo => text},
|
|
540
594
|
*/
|
|
541
|
-
return b.tsTypeLiteral(parseObjectType(t.Properties));
|
|
595
|
+
return b.tsTypeLiteral(parseObjectType(t.Properties, options));
|
|
542
596
|
}
|
|
543
597
|
else if (isNamedGroupReference(t)) {
|
|
544
598
|
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.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",
|
|
@@ -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.
|
|
35
|
+
"cddl": "0.19.2"
|
|
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
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -90,7 +100,51 @@ export function transform (assignments: Assignment[], options?: TransformOptions
|
|
|
90
100
|
return print(ast).code
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
function
|
|
103
|
+
function getAssignmentComments (assignment: Assignment) {
|
|
104
|
+
return assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function exportWithComments (
|
|
108
|
+
declaration: types.namedTypes.TSTypeAliasDeclaration
|
|
109
|
+
| types.namedTypes.TSInterfaceDeclaration
|
|
110
|
+
) {
|
|
111
|
+
const expr = b.exportDeclaration(false, declaration)
|
|
112
|
+
expr.comments = declaration.comments
|
|
113
|
+
declaration.comments = []
|
|
114
|
+
return expr
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isExtensibleRecordProperty (prop: Property) {
|
|
118
|
+
return !isUnNamedProperty(prop) &&
|
|
119
|
+
prop.Occurrence.m === Infinity &&
|
|
120
|
+
!prop.HasCut &&
|
|
121
|
+
RECORD_KEY_TYPES.has(prop.Name)
|
|
122
|
+
}
|
|
123
|
+
|
|
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) {
|
|
94
148
|
if (isVariable(assignment)) {
|
|
95
149
|
const propType = Array.isArray(assignment.PropertyType)
|
|
96
150
|
? assignment.PropertyType
|
|
@@ -103,12 +157,12 @@ function parseAssignment (assignment: Assignment) {
|
|
|
103
157
|
if (propType.length === 1 && propType[0].Type === 'range') {
|
|
104
158
|
typeParameters = b.tsNumberKeyword()
|
|
105
159
|
} else {
|
|
106
|
-
typeParameters = b.tsUnionType(propType.map(parseUnionType))
|
|
160
|
+
typeParameters = b.tsUnionType(propType.map((prop) => parseUnionType(prop, options)))
|
|
107
161
|
}
|
|
108
162
|
|
|
109
163
|
const expr = b.tsTypeAliasDeclaration(id, typeParameters)
|
|
110
|
-
expr.comments = assignment
|
|
111
|
-
return
|
|
164
|
+
expr.comments = getAssignmentComments(assignment)
|
|
165
|
+
return exportWithComments(expr)
|
|
112
166
|
}
|
|
113
167
|
|
|
114
168
|
if (isGroup(assignment)) {
|
|
@@ -140,7 +194,7 @@ function parseAssignment (assignment: Assignment) {
|
|
|
140
194
|
i++ // Skip next property
|
|
141
195
|
}
|
|
142
196
|
|
|
143
|
-
const
|
|
197
|
+
const choiceTypes = choiceOptions.map(p => {
|
|
144
198
|
// If p is a group reference (Name ''), it's a TypeReference
|
|
145
199
|
// e.g. SessionAutodetectProxyConfiguration // SessionDirectProxyConfiguration
|
|
146
200
|
// The parser sometimes wraps it in an array, sometimes not (if inside a choice)
|
|
@@ -154,12 +208,12 @@ function parseAssignment (assignment: Assignment) {
|
|
|
154
208
|
b.identifier(pascalCase(typeVal.Value || typeVal.Type))
|
|
155
209
|
)
|
|
156
210
|
}
|
|
157
|
-
return parseUnionType(typeVal)
|
|
211
|
+
return parseUnionType(typeVal, options)
|
|
158
212
|
}
|
|
159
213
|
// Otherwise it is an object literal with this property
|
|
160
|
-
return b.tsTypeLiteral(parseObjectType([p]))
|
|
214
|
+
return b.tsTypeLiteral(parseObjectType([p], options))
|
|
161
215
|
})
|
|
162
|
-
intersections.push(b.tsUnionType(
|
|
216
|
+
intersections.push(b.tsUnionType(choiceTypes))
|
|
163
217
|
} else {
|
|
164
218
|
staticProps.push(prop)
|
|
165
219
|
}
|
|
@@ -171,13 +225,13 @@ function parseAssignment (assignment: Assignment) {
|
|
|
171
225
|
const ownProps = staticProps.filter(p => !isUnNamedProperty(p))
|
|
172
226
|
|
|
173
227
|
if (ownProps.length > 0) {
|
|
174
|
-
intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps)))
|
|
228
|
+
intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps, options)))
|
|
175
229
|
}
|
|
176
230
|
|
|
177
231
|
for (const mixin of mixins) {
|
|
178
232
|
if (Array.isArray(mixin.Type) && mixin.Type.length > 1) {
|
|
179
|
-
const
|
|
180
|
-
intersections.push(b.tsUnionType(
|
|
233
|
+
const choices = mixin.Type.map((type) => parseUnionType(type, options))
|
|
234
|
+
intersections.push(b.tsUnionType(choices))
|
|
181
235
|
} else {
|
|
182
236
|
const typeVal = Array.isArray(mixin.Type) ? mixin.Type[0] : mixin.Type
|
|
183
237
|
if (isNamedGroupReference(typeVal)) {
|
|
@@ -201,8 +255,8 @@ function parseAssignment (assignment: Assignment) {
|
|
|
201
255
|
}
|
|
202
256
|
|
|
203
257
|
const expr = b.tsTypeAliasDeclaration(id, value)
|
|
204
|
-
expr.comments = assignment
|
|
205
|
-
return
|
|
258
|
+
expr.comments = getAssignmentComments(assignment)
|
|
259
|
+
return exportWithComments(expr)
|
|
206
260
|
}
|
|
207
261
|
|
|
208
262
|
const props = properties as Property[]
|
|
@@ -214,10 +268,10 @@ function parseAssignment (assignment: Assignment) {
|
|
|
214
268
|
const prop = props[0]
|
|
215
269
|
const propType = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
|
|
216
270
|
if (propType.length === 1 && RECORD_KEY_TYPES.has(prop.Name)) {
|
|
217
|
-
const value = parseUnionType(assignment)
|
|
271
|
+
const value = parseUnionType(assignment, options)
|
|
218
272
|
const expr = b.tsTypeAliasDeclaration(id, value)
|
|
219
|
-
expr.comments = assignment
|
|
220
|
-
return
|
|
273
|
+
expr.comments = getAssignmentComments(assignment)
|
|
274
|
+
return exportWithComments(expr)
|
|
221
275
|
}
|
|
222
276
|
}
|
|
223
277
|
|
|
@@ -263,10 +317,10 @@ function parseAssignment (assignment: Assignment) {
|
|
|
263
317
|
|
|
264
318
|
for (const prop of group.Properties) {
|
|
265
319
|
// Choices are wrapped in arrays in the properties
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
320
|
+
const choiceProps = Array.isArray(prop) ? prop : [prop]
|
|
321
|
+
if (choiceProps.length > 1) { // It's a choice within the mixin group
|
|
268
322
|
const unionOptions: any[] = []
|
|
269
|
-
for (const option of
|
|
323
|
+
for (const option of choiceProps) {
|
|
270
324
|
let refName: string | undefined
|
|
271
325
|
const type = option.Type
|
|
272
326
|
if (typeof type === 'string') refName = type
|
|
@@ -293,7 +347,7 @@ function parseAssignment (assignment: Assignment) {
|
|
|
293
347
|
}
|
|
294
348
|
}
|
|
295
349
|
|
|
296
|
-
for (const option of
|
|
350
|
+
for (const option of choiceProps) {
|
|
297
351
|
let refName: string | undefined
|
|
298
352
|
const type = option.Type
|
|
299
353
|
|
|
@@ -374,7 +428,7 @@ function parseAssignment (assignment: Assignment) {
|
|
|
374
428
|
}
|
|
375
429
|
} else if (type && typeof type === 'object') {
|
|
376
430
|
if (isGroup(type) && Array.isArray(type.Properties)) {
|
|
377
|
-
choices.push(b.tsTypeLiteral(parseObjectType(type.Properties as Property[])))
|
|
431
|
+
choices.push(b.tsTypeLiteral(parseObjectType(type.Properties as Property[], options)))
|
|
378
432
|
continue
|
|
379
433
|
}
|
|
380
434
|
refName = isNamedGroupReference(type)
|
|
@@ -420,7 +474,7 @@ function parseAssignment (assignment: Assignment) {
|
|
|
420
474
|
|
|
421
475
|
const ownProps = props.filter(p => !isUnNamedProperty(p))
|
|
422
476
|
if (ownProps.length > 0) {
|
|
423
|
-
intersections.push(b.tsTypeLiteral(parseObjectType(ownProps)))
|
|
477
|
+
intersections.push(b.tsTypeLiteral(parseObjectType(ownProps, options)))
|
|
424
478
|
}
|
|
425
479
|
|
|
426
480
|
let value: any
|
|
@@ -431,16 +485,16 @@ function parseAssignment (assignment: Assignment) {
|
|
|
431
485
|
}
|
|
432
486
|
|
|
433
487
|
const expr = b.tsTypeAliasDeclaration(id, value)
|
|
434
|
-
expr.comments = assignment
|
|
435
|
-
return
|
|
488
|
+
expr.comments = getAssignmentComments(assignment)
|
|
489
|
+
return exportWithComments(expr)
|
|
436
490
|
}
|
|
437
491
|
|
|
438
492
|
// Fallback to interface if no mixins (pure object)
|
|
439
|
-
const objectType = parseObjectType(props)
|
|
493
|
+
const objectType = parseObjectType(props, options)
|
|
440
494
|
|
|
441
495
|
const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType))
|
|
442
|
-
expr.comments = assignment
|
|
443
|
-
return
|
|
496
|
+
expr.comments = getAssignmentComments(assignment)
|
|
497
|
+
return exportWithComments(expr)
|
|
444
498
|
}
|
|
445
499
|
|
|
446
500
|
if (isCDDLArray(assignment)) {
|
|
@@ -455,21 +509,21 @@ function parseAssignment (assignment: Assignment) {
|
|
|
455
509
|
// We need to parse each choice.
|
|
456
510
|
const obj = assignmentValues.map((prop) => {
|
|
457
511
|
const t = Array.isArray(prop.Type) ? prop.Type[0] : prop.Type
|
|
458
|
-
return parseUnionType(t)
|
|
512
|
+
return parseUnionType(t, options)
|
|
459
513
|
})
|
|
460
514
|
const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)))
|
|
461
515
|
const expr = b.tsTypeAliasDeclaration(id, value)
|
|
462
|
-
expr.comments = assignment
|
|
463
|
-
return
|
|
516
|
+
expr.comments = getAssignmentComments(assignment)
|
|
517
|
+
return exportWithComments(expr)
|
|
464
518
|
}
|
|
465
519
|
|
|
466
520
|
// Standard array
|
|
467
521
|
const firstType = assignmentValues.Type
|
|
468
522
|
const obj = Array.isArray(firstType)
|
|
469
|
-
? firstType.map(parseUnionType)
|
|
523
|
+
? firstType.map((type) => parseUnionType(type, options))
|
|
470
524
|
: isCDDLArray(firstType)
|
|
471
|
-
? firstType.Values.map((val: any) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type))
|
|
472
|
-
: [parseUnionType(firstType)]
|
|
525
|
+
? firstType.Values.map((val: any) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type, options))
|
|
526
|
+
: [parseUnionType(firstType, options)]
|
|
473
527
|
|
|
474
528
|
const value = b.tsArrayType(
|
|
475
529
|
obj.length === 1
|
|
@@ -477,14 +531,14 @@ function parseAssignment (assignment: Assignment) {
|
|
|
477
531
|
: b.tsParenthesizedType(b.tsUnionType(obj))
|
|
478
532
|
)
|
|
479
533
|
const expr = b.tsTypeAliasDeclaration(id, value)
|
|
480
|
-
expr.comments = assignment
|
|
481
|
-
return
|
|
534
|
+
expr.comments = getAssignmentComments(assignment)
|
|
535
|
+
return exportWithComments(expr)
|
|
482
536
|
}
|
|
483
537
|
|
|
484
538
|
throw new Error(`Unknown assignment type "${(assignment as any).Type}"`)
|
|
485
539
|
}
|
|
486
540
|
|
|
487
|
-
function parseObjectType (props: Property[]): ObjectBody {
|
|
541
|
+
function parseObjectType (props: Property[], options: TransformSettings): ObjectBody {
|
|
488
542
|
const propItems: ObjectBody = []
|
|
489
543
|
for (const prop of props) {
|
|
490
544
|
/**
|
|
@@ -502,10 +556,31 @@ function parseObjectType (props: Property[]): ObjectBody {
|
|
|
502
556
|
continue
|
|
503
557
|
}
|
|
504
558
|
|
|
505
|
-
const id = b.identifier(camelcase(prop.Name))
|
|
506
559
|
const cddlType: PropertyType[] = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
|
|
507
560
|
const comments: string[] = prop.Comments.map((c) => ` ${c.Content}`)
|
|
508
561
|
|
|
562
|
+
if (isExtensibleRecordProperty(prop)) {
|
|
563
|
+
const keyIdentifier = b.identifier('key')
|
|
564
|
+
keyIdentifier.typeAnnotation = b.tsTypeAnnotation(NATIVE_TYPES[prop.Name])
|
|
565
|
+
|
|
566
|
+
const indexSignature = b.tsIndexSignature(
|
|
567
|
+
[keyIdentifier],
|
|
568
|
+
b.tsTypeAnnotation(
|
|
569
|
+
b.tsUnionType([
|
|
570
|
+
...cddlType.map((t) => parseUnionType(t, options)),
|
|
571
|
+
b.tsUndefinedKeyword()
|
|
572
|
+
])
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
indexSignature.comments = comments.length
|
|
576
|
+
? [b.commentBlock(`*\n *${comments.join('\n *')}\n `)]
|
|
577
|
+
: []
|
|
578
|
+
propItems.push(indexSignature)
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const id = createPropertyKey(prop.Name, options)
|
|
583
|
+
|
|
509
584
|
if (prop.Operator && prop.Operator.Type === 'default') {
|
|
510
585
|
const defaultValue = parseDefaultValue(prop.Operator)
|
|
511
586
|
defaultValue && comments.length && comments.push('') // add empty line if we have previous comments
|
|
@@ -513,7 +588,7 @@ function parseObjectType (props: Property[]): ObjectBody {
|
|
|
513
588
|
}
|
|
514
589
|
|
|
515
590
|
const type = cddlType.map((t) => {
|
|
516
|
-
const unionType = parseUnionType(t)
|
|
591
|
+
const unionType = parseUnionType(t, options)
|
|
517
592
|
if (unionType) {
|
|
518
593
|
const defaultValue = parseDefaultValue((t as PropertyReference).Operator)
|
|
519
594
|
defaultValue && comments.length && comments.push('') // add empty line if we have previous comments
|
|
@@ -535,7 +610,7 @@ function parseObjectType (props: Property[]): ObjectBody {
|
|
|
535
610
|
return propItems
|
|
536
611
|
}
|
|
537
612
|
|
|
538
|
-
function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
|
|
613
|
+
function parseUnionType (t: PropertyType | Assignment, options: TransformSettings): TSTypeKind {
|
|
539
614
|
if (typeof t === 'string') {
|
|
540
615
|
if (!NATIVE_TYPES[t]) {
|
|
541
616
|
throw new Error(`Unknown native type: "${t}`)
|
|
@@ -558,35 +633,35 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
|
|
|
558
633
|
* Check if we have choices in the group (arrays of Properties)
|
|
559
634
|
*/
|
|
560
635
|
if (prop.some(p => Array.isArray(p))) {
|
|
561
|
-
const
|
|
636
|
+
const choices: TSTypeKind[] = []
|
|
562
637
|
for (const choice of prop) {
|
|
563
638
|
const subProps = Array.isArray(choice) ? choice : [choice]
|
|
564
639
|
|
|
565
640
|
if (subProps.length === 1 && isUnNamedProperty(subProps[0])) {
|
|
566
641
|
const first = subProps[0]
|
|
567
642
|
const subType = Array.isArray(first.Type) ? first.Type[0] : first.Type
|
|
568
|
-
|
|
643
|
+
choices.push(parseUnionType(subType as PropertyType, options))
|
|
569
644
|
continue
|
|
570
645
|
}
|
|
571
646
|
|
|
572
647
|
if (subProps.every(isUnNamedProperty)) {
|
|
573
648
|
const tupleItems = subProps.map((p) => {
|
|
574
649
|
const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type
|
|
575
|
-
return parseUnionType(subType as PropertyType)
|
|
650
|
+
return parseUnionType(subType as PropertyType, options)
|
|
576
651
|
})
|
|
577
|
-
|
|
652
|
+
choices.push(b.tsTupleType(tupleItems))
|
|
578
653
|
continue
|
|
579
654
|
}
|
|
580
655
|
|
|
581
|
-
|
|
656
|
+
choices.push(b.tsTypeLiteral(parseObjectType(subProps, options)))
|
|
582
657
|
}
|
|
583
|
-
return b.tsUnionType(
|
|
658
|
+
return b.tsUnionType(choices)
|
|
584
659
|
}
|
|
585
660
|
|
|
586
661
|
if ((prop as Property[]).every(isUnNamedProperty)) {
|
|
587
662
|
const items = (prop as Property[]).map(p => {
|
|
588
663
|
const t = Array.isArray(p.Type) ? p.Type[0] : p.Type
|
|
589
|
-
return parseUnionType(t as PropertyType)
|
|
664
|
+
return parseUnionType(t as PropertyType, options)
|
|
590
665
|
})
|
|
591
666
|
|
|
592
667
|
if (items.length === 1) return items[0];
|
|
@@ -601,7 +676,7 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
|
|
|
601
676
|
b.identifier('Record'),
|
|
602
677
|
b.tsTypeParameterInstantiation([
|
|
603
678
|
NATIVE_TYPES[(prop[0] as Property).Name],
|
|
604
|
-
parseUnionType(((prop[0] as Property).Type as PropertyType[])[0])
|
|
679
|
+
parseUnionType(((prop[0] as Property).Type as PropertyType[])[0], options)
|
|
605
680
|
])
|
|
606
681
|
)
|
|
607
682
|
}
|
|
@@ -609,7 +684,7 @@ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
|
|
|
609
684
|
/**
|
|
610
685
|
* e.g. ?attributes: {*foo => text},
|
|
611
686
|
*/
|
|
612
|
-
return b.tsTypeLiteral(parseObjectType(t.Properties as Property[]))
|
|
687
|
+
return b.tsTypeLiteral(parseObjectType(t.Properties as Property[], options))
|
|
613
688
|
} else if (isNamedGroupReference(t)) {
|
|
614
689
|
return b.tsTypeReference(
|
|
615
690
|
b.identifier(pascalCase(t.Value))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`extensible metadata > should render extensible metadata as an interface with an index signature 1`] = `
|
|
4
|
+
"export type MetadataScalar = null | boolean | number | number | string;
|
|
5
|
+
|
|
6
|
+
export interface MessageMetadata {
|
|
7
|
+
provider?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
modelType?: string;
|
|
10
|
+
runId?: string;
|
|
11
|
+
threadId?: string;
|
|
12
|
+
systemFingerprint?: string;
|
|
13
|
+
serviceTier?: string;
|
|
14
|
+
[key: string]: MetadataScalar | undefined;
|
|
15
|
+
}"
|
|
16
|
+
`;
|
|
@@ -12,52 +12,52 @@ export type DirectProxyConfiguration = Extensible & {
|
|
|
12
12
|
proxyType: "direct";
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
type ManualProxyConfiguration = Extensible & {
|
|
15
|
+
// 1. Simple Group Choice
|
|
16
|
+
export type ManualProxyConfiguration = Extensible & {
|
|
17
17
|
proxyType: "manual";
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
type SimpleGroupChoice = Int | string;
|
|
20
|
+
// 2. Nested Group Choice
|
|
21
|
+
export type SimpleGroupChoice = Int | string;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
type NestedGroupChoice = (Int | Tstr);
|
|
23
|
+
// 3. Group Choice with Multiple Items (Sequence) - interpreted as tuple
|
|
24
|
+
export type NestedGroupChoice = (Int | Tstr);
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
type SequenceGroupChoice = [number, string] | number;
|
|
26
|
+
// 4. Map Group Choice - interpreted as map (interface) union
|
|
27
|
+
export type SequenceGroupChoice = [number, string] | number;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
type MapGroupChoice = {
|
|
29
|
+
// 5. Type Choice inside Group - should be value union
|
|
30
|
+
export type MapGroupChoice = {
|
|
31
31
|
a: 1;
|
|
32
32
|
} | {
|
|
33
33
|
b: 2;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
type TypeChoiceInsideGroup = number | string;
|
|
36
|
+
// 6. Array with Group Choice - should be array of union?
|
|
37
|
+
export type TypeChoiceInsideGroup = number | string;
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
type ArrayGroupChoice = Int | string[];
|
|
39
|
+
// 7. Map with nested group (bare and parens) - should be object like
|
|
40
|
+
export type ArrayGroupChoice = Int | string[];
|
|
41
41
|
|
|
42
42
|
export type MapWithBareGroup = number;
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
type MapWithParensGroup = number;
|
|
44
|
+
// 8. Group wrapped in map with multiple properties
|
|
45
|
+
export type MapWithParensGroup = number;
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
type MapGroupWrap = ;
|
|
47
|
+
// 9. Property type choice without operators
|
|
48
|
+
export type MapGroupWrap = ;
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
interface PropChoiceNoOp {
|
|
50
|
+
// 10. Nested choice with group reference
|
|
51
|
+
export interface PropChoiceNoOp {
|
|
52
52
|
a: number | number;
|
|
53
53
|
b: string;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
type NestedChoiceRef = number | string;
|
|
56
|
+
// 11. Complex type (group) followed by property without comma
|
|
57
|
+
export type NestedChoiceRef = number | string;
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
interface MapNoComma {
|
|
59
|
+
// 12. Multi-item group choice (verifies isChoice persistence)
|
|
60
|
+
export interface MapNoComma {
|
|
61
61
|
a: number;
|
|
62
62
|
b: string;
|
|
63
63
|
}
|
|
@@ -95,8 +95,8 @@ export interface SomeGroup {
|
|
|
95
95
|
export type ScriptListLocalValue = ScriptLocalValue[];
|
|
96
96
|
export type ScriptMappingLocalValue = (ScriptLocalValue | ScriptLocalValue)[];
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
type Extensible = Record<string, any>;",
|
|
98
|
+
// some comments here
|
|
99
|
+
export type Extensible = Record<string, any>;",
|
|
100
100
|
],
|
|
101
101
|
]
|
|
102
102
|
`;
|
|
@@ -388,9 +388,9 @@ export interface BrowsingContextPrintParameters {
|
|
|
388
388
|
shrinkToFit?: boolean;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
// Minimum size is 1pt x 1pt. Conversion follows from
|
|
392
392
|
// https://www.w3.org/TR/css3-values/#absolute-lengths
|
|
393
|
-
interface BrowsingContextPrintMarginParameters {
|
|
393
|
+
export interface BrowsingContextPrintMarginParameters {
|
|
394
394
|
/**
|
|
395
395
|
* @default 1
|
|
396
396
|
*/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import url from 'node:url'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import cli from '../src/cli.js'
|
|
6
|
+
|
|
7
|
+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
|
8
|
+
const cddlFile = path.join(__dirname, '..', '..', '..', 'examples', 'commons', 'extensible_metadata.cddl')
|
|
9
|
+
|
|
10
|
+
vi.mock('../src/constants', () => ({
|
|
11
|
+
pkg: {
|
|
12
|
+
name: 'cddl2ts',
|
|
13
|
+
version: '0.0.0'
|
|
14
|
+
}
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
describe('extensible metadata', () => {
|
|
18
|
+
let exitOrig = process.exit
|
|
19
|
+
let logOrig = console.log
|
|
20
|
+
let errorOrig = console.error
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
process.exit = vi.fn() as any
|
|
24
|
+
console.log = vi.fn()
|
|
25
|
+
console.error = vi.fn()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
process.exit = exitOrig
|
|
30
|
+
console.log = logOrig
|
|
31
|
+
console.error = errorOrig
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should render extensible metadata as an interface with an index signature', async () => {
|
|
35
|
+
await cli([cddlFile])
|
|
36
|
+
|
|
37
|
+
expect(process.exit).not.toHaveBeenCalledWith(1)
|
|
38
|
+
expect(console.error).not.toHaveBeenCalled()
|
|
39
|
+
|
|
40
|
+
const output = vi.mocked(console.log).mock.calls.flat().join('\n')
|
|
41
|
+
|
|
42
|
+
expect(output).toContain('export interface MessageMetadata {')
|
|
43
|
+
expect(output).toContain('provider?: string;')
|
|
44
|
+
expect(output).toContain('modelType?: string;')
|
|
45
|
+
expect(output).toContain('[key: string]: MetadataScalar | undefined;')
|
|
46
|
+
expect(output).not.toContain('text?: MetadataScalar;')
|
|
47
|
+
expect(output).toMatchSnapshot()
|
|
48
|
+
})
|
|
49
|
+
})
|
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
|
|
@@ -39,8 +39,8 @@ describe('named group choice', () => {
|
|
|
39
39
|
|
|
40
40
|
const output = vi.mocked(console.log).mock.calls.flat().join('\n')
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
expect(output).toMatch(/
|
|
42
|
+
// Leading comments should render before the exported declaration.
|
|
43
|
+
expect(output).toMatch(/(\/\/.*\n)+export type Choice = OptionA \| OptionB/)
|
|
44
44
|
expect(output).toContain('export interface OptionA {')
|
|
45
45
|
expect(output).toContain('export interface OptionB {')
|
|
46
46
|
})
|
|
@@ -14,11 +14,11 @@ import { transform } from '../src/index.js'
|
|
|
14
14
|
|
|
15
15
|
const COMMENTS: Comment[] = []
|
|
16
16
|
|
|
17
|
-
function comment (content: string): Comment {
|
|
17
|
+
function comment (content: string, leading = false): Comment {
|
|
18
18
|
return {
|
|
19
19
|
Type: 'comment',
|
|
20
20
|
Content: content,
|
|
21
|
-
Leading:
|
|
21
|
+
Leading: leading
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -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', [
|
|
@@ -240,6 +269,40 @@ describe('transform edge cases', () => {
|
|
|
240
269
|
expect(output).toContain('enabled?: boolean')
|
|
241
270
|
})
|
|
242
271
|
|
|
272
|
+
it('should place leading comments before exported declarations', () => {
|
|
273
|
+
const output = transform([
|
|
274
|
+
variable('metadata-scalar', ['null', 'bool', 'int', 'float', 'text'], [
|
|
275
|
+
comment('Flat scalar value used by concise metadata bags.', true)
|
|
276
|
+
])
|
|
277
|
+
])
|
|
278
|
+
|
|
279
|
+
expect(output).toContain(`// Flat scalar value used by concise metadata bags.\nexport type MetadataScalar = null | boolean | number | number | string;`)
|
|
280
|
+
expect(output).not.toContain('export // Flat scalar value used by concise metadata bags.')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should emit extensible object properties as index signatures', () => {
|
|
284
|
+
const output = transform([
|
|
285
|
+
variable('metadata-scalar', ['null', 'bool', 'int', 'float', 'text']),
|
|
286
|
+
group('message-metadata', [
|
|
287
|
+
property('provider', 'text', {
|
|
288
|
+
Occurrence: { n: 0, m: 1 }
|
|
289
|
+
}),
|
|
290
|
+
property('model', 'text', {
|
|
291
|
+
Occurrence: { n: 0, m: 1 }
|
|
292
|
+
}),
|
|
293
|
+
property('text', groupRef('metadata-scalar'), {
|
|
294
|
+
Occurrence: { n: 0, m: Infinity }
|
|
295
|
+
})
|
|
296
|
+
])
|
|
297
|
+
])
|
|
298
|
+
|
|
299
|
+
expect(output).toContain('export interface MessageMetadata {')
|
|
300
|
+
expect(output).toContain('provider?: string;')
|
|
301
|
+
expect(output).toContain('model?: string;')
|
|
302
|
+
expect(output).toContain('[key: string]: MetadataScalar | undefined;')
|
|
303
|
+
expect(output).not.toContain('text?: MetadataScalar;')
|
|
304
|
+
})
|
|
305
|
+
|
|
243
306
|
it('should throw clear errors for unsupported inputs', () => {
|
|
244
307
|
expect(() => transform([
|
|
245
308
|
variable('unknown-native', 'nope')
|