cddl2ts 0.3.0 → 0.4.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/src/index.ts ADDED
@@ -0,0 +1,652 @@
1
+ import camelcase from 'camelcase'
2
+ import { parse, print, types } from 'recast'
3
+ import typescriptParser from 'recast/parsers/typescript.js'
4
+
5
+ import type { Assignment, PropertyType, PropertyReference, Property, Array, Operator } from 'cddl'
6
+
7
+ import {
8
+ isCDDLArray,
9
+ isGroup,
10
+ isNamedGroupReference,
11
+ isLiteralWithValue,
12
+ isNativeTypeWithOperator,
13
+ isUnNamedProperty,
14
+ isPropertyReference,
15
+ isRange,
16
+ isVariable,
17
+ pascalCase
18
+ } from './utils.js'
19
+
20
+ import { pkg } from './constants.js'
21
+
22
+ const b = types.builders
23
+ const NATIVE_TYPES: Record<string, any> = {
24
+ any: b.tsAnyKeyword(),
25
+ number: b.tsNumberKeyword(),
26
+ int: b.tsNumberKeyword(),
27
+ float: b.tsNumberKeyword(),
28
+ uint: b.tsNumberKeyword(),
29
+ bool: b.tsBooleanKeyword(),
30
+ str: b.tsStringKeyword(),
31
+ text: b.tsStringKeyword(),
32
+ tstr: b.tsStringKeyword(),
33
+ range: b.tsNumberKeyword(),
34
+ nil: b.tsNullKeyword(),
35
+ null: b.tsNullKeyword()
36
+ }
37
+ type ObjectEntry = types.namedTypes.TSCallSignatureDeclaration | types.namedTypes.TSConstructSignatureDeclaration | types.namedTypes.TSIndexSignature | types.namedTypes.TSMethodSignature | types.namedTypes.TSPropertySignature
38
+ type ObjectBody = ObjectEntry[]
39
+ type TSTypeKind = types.namedTypes.TSAsExpression['typeAnnotation']
40
+
41
+ export interface TransformOptions {
42
+ useUnknown?: boolean
43
+ }
44
+
45
+ export function transform (assignments: Assignment[], options?: TransformOptions) {
46
+ if (options?.useUnknown) {
47
+ NATIVE_TYPES.any = b.tsUnknownKeyword()
48
+ } else {
49
+ NATIVE_TYPES.any = b.tsAnyKeyword()
50
+ }
51
+
52
+ let ast = parse(
53
+ `// compiled with https://www.npmjs.com/package/cddl2ts v${pkg.version}`,
54
+ {
55
+ parser: typescriptParser,
56
+ sourceFileName: 'cddl2Ts.ts',
57
+ sourceRoot: process.cwd()
58
+ }
59
+ ) satisfies types.namedTypes.File
60
+
61
+ for (const assignment of assignments) {
62
+ const statement = parseAssignment(assignment)
63
+ if (!statement) {
64
+ continue
65
+ }
66
+ ast.program.body.push(statement)
67
+ }
68
+ return print(ast).code
69
+ }
70
+
71
+ function parseAssignment (assignment: Assignment) {
72
+ if (isVariable(assignment)) {
73
+ const propType = Array.isArray(assignment.PropertyType)
74
+ ? assignment.PropertyType
75
+ : [assignment.PropertyType]
76
+
77
+ const id = b.identifier(pascalCase(assignment.Name))
78
+
79
+ let typeParameters: any
80
+ // @ts-expect-error e.g. "js-int = -9007199254740991..9007199254740991"
81
+ if (propType.length === 1 && propType[0].Type === 'range') {
82
+ typeParameters = b.tsNumberKeyword()
83
+ } else {
84
+ typeParameters = b.tsUnionType(propType.map(parseUnionType))
85
+ }
86
+
87
+ const expr = b.tsTypeAliasDeclaration(id, typeParameters)
88
+ expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
89
+ return b.exportDeclaration(false, expr)
90
+ }
91
+
92
+ if (isGroup(assignment)) {
93
+ const id = b.identifier(pascalCase(assignment.Name))
94
+
95
+ /**
96
+ * Check if we have choices in the group (arrays of Properties)
97
+ */
98
+ const properties = assignment.Properties
99
+ const hasChoices = properties.some(p => Array.isArray(p))
100
+
101
+ if (hasChoices) {
102
+ // Flatten static properties and collect choices
103
+ const staticProps: Property[] = []
104
+ const intersections: any[] = []
105
+
106
+ for (let i = 0; i < properties.length; i++) {
107
+ const prop = properties[i]
108
+ if (Array.isArray(prop)) {
109
+ // It's a choice (Union)
110
+ // prop is Property[] where each Property is an option
111
+ // CDDL parser appends the last choice element as a subsequent property
112
+ // so we need to grab it and merge it into the union
113
+ const choiceOptions = [...prop]
114
+ const nextProp = properties[i + 1]
115
+
116
+ if (nextProp && !Array.isArray(nextProp)) {
117
+ choiceOptions.push(nextProp)
118
+ i++ // Skip next property
119
+ }
120
+
121
+ const options = choiceOptions.map(p => {
122
+ // If p is a group reference (Name ''), it's a TypeReference
123
+ // e.g. SessionAutodetectProxyConfiguration // SessionDirectProxyConfiguration
124
+ // The parser sometimes wraps it in an array, sometimes not (if inside a choice)
125
+ const typeVal = Array.isArray(p.Type) ? p.Type[0] : p.Type
126
+
127
+ if (isUnNamedProperty(p)) {
128
+ // Handle un-named properties (bare types) in choices.
129
+ // Native types / literals
130
+ if (isNamedGroupReference(typeVal)) {
131
+ return b.tsTypeReference(
132
+ b.identifier(pascalCase(typeVal.Value || typeVal.Type))
133
+ )
134
+ }
135
+ return parseUnionType(typeVal);
136
+ }
137
+ // Otherwise it is an object literal with this property
138
+ return b.tsTypeLiteral(parseObjectType([p]))
139
+ })
140
+ intersections.push(b.tsUnionType(options))
141
+ } else {
142
+ staticProps.push(prop)
143
+ }
144
+ }
145
+
146
+ if (staticProps.length > 0) {
147
+ // Check if we have mixins in static props
148
+ const mixins = staticProps.filter(isUnNamedProperty)
149
+ const ownProps = staticProps.filter(p => !isUnNamedProperty(p))
150
+
151
+ if (ownProps.length > 0) {
152
+ intersections.unshift(b.tsTypeLiteral(parseObjectType(ownProps)))
153
+ }
154
+
155
+ for (const mixin of mixins) {
156
+ if (Array.isArray(mixin.Type) && mixin.Type.length > 1) {
157
+ const options = mixin.Type.map(parseUnionType)
158
+ intersections.push(b.tsUnionType(options))
159
+ } else {
160
+ const typeVal = Array.isArray(mixin.Type) ? mixin.Type[0] : mixin.Type
161
+ if (isNamedGroupReference(typeVal)) {
162
+ intersections.push(b.tsTypeReference(
163
+ b.identifier(pascalCase(typeVal.Value))
164
+ ))
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // If only one intersection element, return it directly
171
+ // If multiple, return Intersection
172
+ let value: any
173
+ if (intersections.length === 0) {
174
+ value = b.tsAnyKeyword() // Should not happen for valid CDDL?
175
+ } else if (intersections.length === 1) {
176
+ value = intersections[0]
177
+ } else {
178
+ value = b.tsIntersectionType(intersections)
179
+ }
180
+
181
+ const expr = b.tsTypeAliasDeclaration(id, value)
182
+ expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
183
+ return b.exportDeclaration(false, expr)
184
+ }
185
+
186
+ const props = properties as Property[]
187
+
188
+ /**
189
+ * transform CDDL groups like `Extensible = (*text => any)`
190
+ */
191
+ if (props.length === 1) {
192
+ const prop = props[0]
193
+ const propType = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
194
+ if (propType.length === 1 && Object.keys(NATIVE_TYPES).includes(prop.Name)) {
195
+ const value = parseUnionType(assignment)
196
+ const expr = b.tsTypeAliasDeclaration(id, value)
197
+ expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
198
+ return b.exportDeclaration(false, expr)
199
+ }
200
+ }
201
+
202
+ // Check if extended interfaces are likely unions or conflicting types
203
+ // In CDDL, including a group (Name '') that is defined elsewhere as a choice (union)
204
+ // is valid, but TypeScript interfaces cannot extend unions.
205
+ // We can't easily know if the referenced group is a union without a symbol table or 2-pass.
206
+ // However, if we simply use type intersection for ALL group inclusions, it is always safe.
207
+ // (Interface extending Interface is same as Type = Interface & Interface)
208
+ // Let's refactor to use Type Alias with Intersection if there are any mixins.
209
+
210
+ const mixins = props.filter(isUnNamedProperty)
211
+
212
+ if (mixins.length > 0) {
213
+ // It has mixins (extensions). Use type alias with intersection to be safe against unions.
214
+ // Type = (Mixin1 & Mixin2 & { OwnProps })
215
+
216
+ const intersections: any[] = []
217
+
218
+ for (const mixin of mixins) {
219
+ // If mixin is a group choice (e.g. `(A // B)`), the parser returns a Group object
220
+ // with Properties containing the choices. We need to extract them.
221
+ if (Array.isArray(mixin.Type) && mixin.Type.length > 1) {
222
+ // Check if it's a choice of types
223
+ const unionOptions: any[] = []
224
+ for (const t of mixin.Type) {
225
+ let refName: string | undefined
226
+ if (typeof t === 'string') refName = t
227
+ else if (isNamedGroupReference(t)) refName = t.Value
228
+ else refName = (t as any).Value || (t as any).Type
229
+
230
+ if (refName) unionOptions.push(b.tsTypeReference(b.identifier(pascalCase(refName))))
231
+ }
232
+ if (unionOptions.length > 0) {
233
+ intersections.push(b.tsParenthesizedType(b.tsUnionType(unionOptions)))
234
+ continue
235
+ }
236
+ }
237
+
238
+ if (isGroup(mixin.Type) && Array.isArray(mixin.Type.Properties)) {
239
+ const group = mixin.Type
240
+ const choices: any[] = []
241
+
242
+ for (const prop of group.Properties) {
243
+ // Choices are wrapped in arrays in the properties
244
+ const options = Array.isArray(prop) ? prop : [prop]
245
+ if (options.length > 1) { // It's a choice within the mixin group
246
+ const unionOptions: any[] = []
247
+ for (const option of options) {
248
+ let refName: string | undefined
249
+ const type = option.Type
250
+ if (typeof type === 'string') refName = type
251
+ else if (isNamedGroupReference(type)) refName = type.Value
252
+ else if (Array.isArray(type) && type[0]) {
253
+ const first = type[0]
254
+ if (isNamedGroupReference(first)) refName = first.Value
255
+ else if (isUnNamedProperty(first)) {
256
+ if (isNamedGroupReference(first.Type)) refName = first.Type.Value
257
+ else if (isGroup(first.Type) && first.Type.Properties && first.Type.Properties.length === 1) {
258
+ // Handle case where group reference is wrapped deeply
259
+ const subProp = first.Type.Properties[0] as Property
260
+ if (isNamedGroupReference(subProp.Type)) refName = subProp.Type.Value
261
+ }
262
+ }
263
+ if (!refName) refName = (first as any).Value || (first as any).Type
264
+ }
265
+
266
+ if (refName) unionOptions.push(b.tsTypeReference(b.identifier(pascalCase(refName))))
267
+ }
268
+ if (unionOptions.length > 0) {
269
+ choices.push(b.tsParenthesizedType(b.tsUnionType(unionOptions)))
270
+ continue
271
+ }
272
+ }
273
+
274
+ for (const option of options) {
275
+ let refName: string | undefined
276
+ const type = option.Type
277
+
278
+ if (typeof type === 'string') {
279
+ refName = type
280
+ } else if (Array.isArray(type)) {
281
+ if (type.length > 1) {
282
+ // console.log('DEBUG: Found array length > 1', JSON.stringify(type, null, 2))
283
+ const unionChoices: any[] = []
284
+ for (const t of type) {
285
+ let name: string | undefined
286
+ if (typeof t === 'string') name = t
287
+ else if (isNamedGroupReference(t)) name = t.Value
288
+ else if (isUnNamedProperty(t)) {
289
+ if (isNamedGroupReference(t.Type)) name = t.Type.Value
290
+ else if (isGroup(t.Type) && t.Type.Properties && t.Type.Properties.length === 1) {
291
+ const subProp = t.Type.Properties[0] as Property
292
+ if (isNamedGroupReference(subProp.Type)) name = subProp.Type.Value
293
+ }
294
+ }
295
+ if (!name) name = (t as any).Value || (t as any).Type
296
+
297
+ if (name) unionChoices.push(b.tsTypeReference(b.identifier(pascalCase(name))))
298
+ }
299
+
300
+ if (unionChoices.length > 0) {
301
+ choices.push(b.tsParenthesizedType(b.tsUnionType(unionChoices)))
302
+ continue
303
+ }
304
+ } else if (type.length === 1 && Array.isArray(type[0])) {
305
+ // Handle nested union e.g. [ [ A, B ] ] which seems common in some parsers for choices
306
+ const nested = type[0]
307
+ if (nested.length > 1) {
308
+ const unionChoices: any[] = []
309
+ for (const t of nested) {
310
+ let name: string | undefined
311
+ if (typeof t === 'string') name = t
312
+ else if (isNamedGroupReference(t)) name = t.Value
313
+ else if (isUnNamedProperty(t)) {
314
+ if (isNamedGroupReference(t.Type)) name = t.Type.Value
315
+ else if (isGroup(t.Type) && t.Type.Properties && t.Type.Properties.length === 1) {
316
+ const subProp = t.Type.Properties[0] as Property
317
+ if (isNamedGroupReference(subProp.Type)) name = subProp.Type.Value
318
+ }
319
+ else {
320
+ // Fallback for wrapped primitive?
321
+ name = (t.Type as any).Value || (t.Type as any).Type
322
+ }
323
+ }
324
+ if (!name) name = (t as any).Value || (t as any).Type
325
+
326
+ if (name) unionChoices.push(b.tsTypeReference(b.identifier(pascalCase(name))))
327
+ }
328
+ if (unionChoices.length > 0) {
329
+ choices.push(b.tsParenthesizedType(b.tsUnionType(unionChoices)))
330
+ continue
331
+ }
332
+ }
333
+ }
334
+
335
+ const first = type[0]
336
+ if (first) {
337
+ if (isNamedGroupReference(first)) {
338
+ refName = first.Value
339
+ } else if (!isGroup(first)) {
340
+ if (isUnNamedProperty(first)) {
341
+ if (isNamedGroupReference(first.Type)) {
342
+ refName = first.Type.Value
343
+ } else {
344
+ refName = (first.Type as any).Value || (first.Type as any).Type
345
+ }
346
+ } else {
347
+ refName = (first as any).Value || (first as any).Type
348
+ }
349
+ } else {
350
+ refName = (first as any).Value || (first as any).Type
351
+ }
352
+ }
353
+ } else if (type && typeof type === 'object') {
354
+ if (isGroup(type) && Array.isArray(type.Properties)) {
355
+ choices.push(b.tsTypeLiteral(parseObjectType(type.Properties as Property[])))
356
+ continue
357
+ }
358
+ refName = isNamedGroupReference(type)
359
+ ? type.Value || type.Type
360
+ : (type as any).Value || (type as any).Type
361
+ }
362
+
363
+ // If we found a refName, push it. Note that if this was a choice we handled above, we skip this
364
+ if (refName) {
365
+ choices.push(
366
+ b.tsTypeReference(b.identifier(pascalCase(refName)))
367
+ )
368
+ }
369
+ }
370
+ }
371
+
372
+ if (choices.length > 0) {
373
+ // Unions inside intersections must be parenthesized to avoid ambiguity
374
+ // e.g. (A | B) & C vs A | (B & C)
375
+ const union = b.tsUnionType(choices)
376
+ intersections.push(b.tsParenthesizedType(union))
377
+ continue
378
+ }
379
+ }
380
+
381
+ const propType = mixin.Type as PropertyType[]
382
+
383
+ if (typeof propType === 'string' && NATIVE_TYPES[propType]) {
384
+ intersections.push(NATIVE_TYPES[propType])
385
+ continue
386
+ }
387
+
388
+ const groupRef = propType[0] as PropertyReference
389
+
390
+ // Handle nested inline groups if any (though usually flat here if name is empty?)
391
+ const value = (groupRef?.Value || (groupRef as any)?.Type) as string
392
+ if (value) {
393
+ intersections.push(
394
+ b.tsTypeReference(b.identifier(pascalCase(value)))
395
+ )
396
+ }
397
+ }
398
+
399
+ const ownProps = props.filter(p => !isUnNamedProperty(p))
400
+ if (ownProps.length > 0) {
401
+ intersections.push(b.tsTypeLiteral(parseObjectType(ownProps)))
402
+ }
403
+
404
+ let value: any
405
+ if (intersections.length === 1) {
406
+ value = intersections[0]
407
+ } else {
408
+ value = b.tsIntersectionType(intersections)
409
+ }
410
+
411
+ const expr = b.tsTypeAliasDeclaration(id, value)
412
+ expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
413
+ return b.exportDeclaration(false, expr)
414
+ }
415
+
416
+ // Fallback to interface if no mixins (pure object)
417
+ const objectType = parseObjectType(props)
418
+
419
+ const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType))
420
+ expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
421
+ return b.exportDeclaration(false, expr)
422
+ }
423
+
424
+ if (isCDDLArray(assignment)) {
425
+ const id = b.identifier(pascalCase(assignment.Name))
426
+
427
+ const assignmentValues = assignment.Values[0]
428
+
429
+ if (Array.isArray(assignmentValues)) {
430
+ // It's a choice/union in the array definition
431
+ // e.g. Foo = [ (A | B) ]
432
+ // assignment.Values[0] is Property[] (the choices)
433
+ // We need to parse each choice.
434
+ const obj = assignmentValues.map((prop) => {
435
+ const t = Array.isArray(prop.Type) ? prop.Type[0] : prop.Type
436
+ return parseUnionType(t)
437
+ })
438
+ const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)))
439
+ const expr = b.tsTypeAliasDeclaration(id, value)
440
+ expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
441
+ return b.exportDeclaration(false, expr)
442
+ }
443
+
444
+ // Standard array
445
+ const firstType = assignmentValues.Type
446
+ const obj = Array.isArray(firstType)
447
+ ? firstType.map(parseUnionType)
448
+ : isCDDLArray(firstType)
449
+ ? firstType.Values.map((val: any) => parseUnionType(Array.isArray(val.Type) ? val.Type[0] : val.Type))
450
+ : [parseUnionType(firstType)]
451
+
452
+ const value = b.tsArrayType(
453
+ obj.length === 1
454
+ ? obj[0]
455
+ : b.tsParenthesizedType(b.tsUnionType(obj))
456
+ )
457
+ const expr = b.tsTypeAliasDeclaration(id, value)
458
+ expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true))
459
+ return b.exportDeclaration(false, expr)
460
+ }
461
+
462
+ throw new Error(`Unknown assignment type "${(assignment as any).Type}"`)
463
+ }
464
+
465
+ function parseObjectType (props: Property[]): ObjectBody {
466
+ const propItems: ObjectBody = []
467
+ for (const prop of props) {
468
+ /**
469
+ * Empty groups like
470
+ * {
471
+ * HasCut: false,
472
+ * Occurrence: { n: 1, m: 1 },
473
+ * Name: '',
474
+ * Type: [ { Type: 'group', Value: 'Extensible', Unwrapped: false } ],
475
+ * Comment: ''
476
+ * }
477
+ * are ignored and later added as interface extensions
478
+ */
479
+ if (isUnNamedProperty(prop)) {
480
+ continue
481
+ }
482
+
483
+ const id = b.identifier(camelcase(prop.Name))
484
+ const cddlType: PropertyType[] = Array.isArray(prop.Type) ? prop.Type : [prop.Type]
485
+ const comments: string[] = prop.Comments.map((c) => ` ${c.Content}`)
486
+
487
+ if (prop.Operator && prop.Operator.Type === 'default') {
488
+ const defaultValue = parseDefaultValue(prop.Operator)
489
+ defaultValue && comments.length && comments.push('') // add empty line if we have previous comments
490
+ defaultValue && comments.push(` @default ${defaultValue}`)
491
+ }
492
+
493
+ const type = cddlType.map((t) => {
494
+ const unionType = parseUnionType(t)
495
+ if (unionType) {
496
+ const defaultValue = parseDefaultValue((t as PropertyReference).Operator)
497
+ defaultValue && comments.length && comments.push('') // add empty line if we have previous comments
498
+ defaultValue && comments.push(` @default ${defaultValue}`)
499
+ return unionType
500
+ }
501
+
502
+
503
+ throw new Error(`Couldn't parse property ${JSON.stringify(t)}`)
504
+ })
505
+
506
+ const typeAnnotation = b.tsTypeAnnotation(b.tsUnionType(type))
507
+ const isOptional = prop.Occurrence.n === 0
508
+ const propSignature = b.tsPropertySignature(id, typeAnnotation, isOptional)
509
+ propSignature.comments = comments.length ? [b.commentBlock(`*\n *${comments.join('\n *')}\n `)] : []
510
+ propItems.push(propSignature)
511
+ }
512
+
513
+ return propItems
514
+ }
515
+
516
+ function parseUnionType (t: PropertyType | Assignment): TSTypeKind {
517
+ if (typeof t === 'string') {
518
+ if (!NATIVE_TYPES[t]) {
519
+ throw new Error(`Unknown native type: "${t}`)
520
+ }
521
+ return NATIVE_TYPES[t]
522
+ } else if ((t as any).Type && typeof (t as any).Type === 'string' && NATIVE_TYPES[(t as any).Type]) {
523
+ return NATIVE_TYPES[(t as any).Type]
524
+ } else if (isNativeTypeWithOperator(t) && NATIVE_TYPES[(t.Type as any).Type]) {
525
+ return NATIVE_TYPES[(t.Type as any).Type]
526
+ } else if (isPropertyReference(t) && t.Value === 'null') {
527
+ return b.tsNullKeyword()
528
+ } else if (isGroup(t)) {
529
+ /**
530
+ * check if we have special groups
531
+ */
532
+ if (isGroup(t) && !isNamedGroupReference(t) && t.Properties) {
533
+ const prop = t.Properties
534
+
535
+ /**
536
+ * Check if we have choices in the group (arrays of Properties)
537
+ */
538
+ if (prop.some(p => Array.isArray(p))) {
539
+ const options: TSTypeKind[] = []
540
+ for (const choice of prop) {
541
+ const subProps = Array.isArray(choice) ? choice : [choice]
542
+
543
+ if (subProps.length === 1 && isUnNamedProperty(subProps[0])) {
544
+ const first = subProps[0]
545
+ const subType = Array.isArray(first.Type) ? first.Type[0] : first.Type
546
+ options.push(parseUnionType(subType as PropertyType))
547
+ continue
548
+ }
549
+
550
+ if (subProps.every(isUnNamedProperty)) {
551
+ const tupleItems = subProps.map((p) => {
552
+ const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type
553
+ return parseUnionType(subType as PropertyType)
554
+ })
555
+ options.push(b.tsTupleType(tupleItems))
556
+ continue
557
+ }
558
+
559
+ options.push(b.tsTypeLiteral(parseObjectType(subProps)))
560
+ }
561
+ return b.tsUnionType(options)
562
+ }
563
+
564
+ if ((prop as Property[]).every(isUnNamedProperty)) {
565
+ const items = (prop as Property[]).map(p => {
566
+ const t = Array.isArray(p.Type) ? p.Type[0] : p.Type
567
+ return parseUnionType(t as PropertyType)
568
+ })
569
+
570
+ if (items.length === 1) return items[0];
571
+ return b.tsTupleType(items);
572
+ }
573
+
574
+ /**
575
+ * {*text => text} which will be transformed to `Record<string, string>`
576
+ */
577
+ if (prop.length === 1 && Object.keys(NATIVE_TYPES).includes((prop[0] as Property).Name)) {
578
+ return b.tsTypeReference(
579
+ b.identifier('Record'),
580
+ b.tsTypeParameterInstantiation([
581
+ NATIVE_TYPES[(prop[0] as Property).Name],
582
+ parseUnionType(((prop[0] as Property).Type as PropertyType[])[0])
583
+ ])
584
+ )
585
+ }
586
+
587
+ /**
588
+ * e.g. ?attributes: {*foo => text},
589
+ */
590
+ return b.tsTypeLiteral(parseObjectType(t.Properties as Property[]))
591
+ } else if (isNamedGroupReference(t)) {
592
+ return b.tsTypeReference(
593
+ b.identifier(pascalCase(t.Value))
594
+ )
595
+ }
596
+ throw new Error(`Unknown group type: ${JSON.stringify(t)}`)
597
+ } else if (isLiteralWithValue(t)) {
598
+ if (typeof t.Value === 'string') return b.tsLiteralType(b.stringLiteral(t.Value))
599
+ if (typeof t.Value === 'number') return b.tsLiteralType(b.numericLiteral(t.Value))
600
+ if (typeof t.Value === 'boolean') return b.tsLiteralType(b.booleanLiteral(t.Value))
601
+ if (typeof t.Value === 'bigint') return b.tsLiteralType(b.bigIntLiteral(t.Value.toString()))
602
+ if (t.Value === null) return b.tsNullKeyword()
603
+ throw new Error(`Unsupported literal type: ${JSON.stringify(t)}`)
604
+ } else if (isCDDLArray(t)) {
605
+ const types = ((t as Array).Values[0] as Property).Type as PropertyType[]
606
+ const typedTypes = (Array.isArray(types) ? types : [types]).map((val) => {
607
+ if (typeof val === 'string' && NATIVE_TYPES[val]) {
608
+ return NATIVE_TYPES[val]
609
+ }
610
+ return b.tsTypeReference(
611
+ b.identifier(pascalCase((val as any).Value as string))
612
+ )
613
+ })
614
+
615
+ if (typedTypes.length > 1) {
616
+ return b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(typedTypes)))
617
+ }
618
+
619
+ if (!typedTypes[0]) {
620
+ console.log('typedTypes[0] is missing!', types, typedTypes);
621
+ }
622
+ return b.tsArrayType(typedTypes[0])
623
+ } else if (isRange(t)) {
624
+ return b.tsNumberKeyword()
625
+ } else if (isNativeTypeWithOperator(t) && isNamedGroupReference(t.Type)) {
626
+ /**
627
+ * e.g. ?pointerType: input.PointerType .default "mouse"
628
+ */
629
+ const referenceValue = pascalCase(t.Type.Value)
630
+ return b.tsTypeReference(b.identifier(referenceValue))
631
+ }
632
+
633
+ throw new Error(`Unknown union type: ${JSON.stringify(t)}`)
634
+ }
635
+
636
+ function parseDefaultValue (operator?: Operator) {
637
+ if (!operator || operator.Type !== 'default') {
638
+ return
639
+ }
640
+
641
+ const operatorValue = operator.Value as PropertyReference
642
+ if (operator.Value === 'null') {
643
+ return operator.Value
644
+ }
645
+
646
+ if (operatorValue.Type !== 'literal') {
647
+ throw new Error(`Can't parse operator default value of ${JSON.stringify(operator)}`)
648
+ }
649
+ return typeof operatorValue.Value === 'string'
650
+ ? `'${operatorValue.Value}'`
651
+ : operatorValue.Value as unknown as string
652
+ }