cddl2py 0.0.1 → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WebdriverIO Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # CDDL to Python
2
+
3
+ > Generate Python type definitions from CDDL as `TypedDict` classes or Pydantic models.
4
+
5
+ `cddl2py` converts a parsed CDDL schema into Python source code. By default it emits
6
+ `TypedDict`-based definitions that work well for static typing. With the `--pydantic`
7
+ flag, it emits `BaseModel` classes instead.
8
+
9
+ ## Install
10
+
11
+ Use the CLI:
12
+
13
+ ```sh
14
+ npm install cddl2py
15
+ ```
16
+
17
+ Use the programmatic API:
18
+
19
+ ```sh
20
+ npm install cddl cddl2py
21
+ ```
22
+
23
+ ## What It Generates
24
+
25
+ `cddl2py` currently maps common CDDL constructs into Python-friendly types, including:
26
+
27
+ - named CDDL assignments to Python aliases or classes
28
+ - groups to `TypedDict` classes
29
+ - optional group fields to `NotRequired[...]`
30
+ - arrays to `list[...]`
31
+ - unions to `Union[...]`
32
+ - literals to `Literal[...]`
33
+ - an optional Pydantic mode that emits `BaseModel` classes and `Field(default=...)`
34
+
35
+ It also normalizes names for Python code by turning type names into `PascalCase` and
36
+ field names into `snake_case`.
37
+
38
+ ## CLI
39
+
40
+ The CLI reads a CDDL file and writes generated Python code to stdout, so the normal
41
+ workflow is to redirect the output into a `.py` file.
42
+
43
+ Generate `TypedDict` output:
44
+
45
+ ```sh
46
+ npx cddl2py ./path/to/schema.cddl > ./types.py
47
+ ```
48
+
49
+ Generate Pydantic models:
50
+
51
+ ```sh
52
+ npx cddl2py --pydantic ./path/to/schema.cddl > ./models.py
53
+ ```
54
+
55
+ Show help:
56
+
57
+ ```sh
58
+ npx cddl2py --help
59
+ ```
60
+
61
+ ## Programmatic API
62
+
63
+ The package exports a single `transform()` function. It accepts the parsed CDDL AST
64
+ and returns the generated Python source as a string.
65
+
66
+ ```js
67
+ import { parse } from 'cddl'
68
+ import { transform } from 'cddl2py'
69
+
70
+ const ast = parse('./schema.cddl')
71
+ const python = transform(ast)
72
+
73
+ console.log(python)
74
+ ```
75
+
76
+ To generate Pydantic models instead:
77
+
78
+ ```js
79
+ import { parse } from 'cddl'
80
+ import { transform } from 'cddl2py'
81
+
82
+ const ast = parse('./schema.cddl')
83
+ const python = transform(ast, { pydantic: true })
84
+
85
+ console.log(python)
86
+ ```
87
+
88
+ ## Example
89
+
90
+ Input CDDL:
91
+
92
+ ```cddl
93
+ person = {
94
+ name: tstr,
95
+ age: uint,
96
+ ?nickname: tstr,
97
+ }
98
+ ```
99
+
100
+ Generated Python (`transform(ast)`):
101
+
102
+ ```python
103
+ from __future__ import annotations
104
+
105
+ from typing_extensions import NotRequired, TypedDict
106
+
107
+ class Person(TypedDict):
108
+ name: str
109
+ age: int
110
+ nickname: NotRequired[str]
111
+ ```
112
+
113
+ Generated Python (`transform(ast, { pydantic: true })`):
114
+
115
+ ```python
116
+ from __future__ import annotations
117
+
118
+ from typing import Optional
119
+ from pydantic import BaseModel
120
+
121
+ class Person(BaseModel):
122
+ name: str
123
+ age: int
124
+ nickname: Optional[str] = None
125
+ ```
126
+
127
+ ## Notes
128
+
129
+ - Generated files include a header comment with the `cddl2py` version used.
130
+ - Pydantic output imports from `pydantic`, so your Python environment should have it installed if you use `--pydantic`.
131
+ - The CLI validates that the input file exists before attempting to parse it.
132
+
133
+ ---
134
+
135
+ If you want to contribute fixes or improvements, see the repository
136
+ [contributing guide](https://github.com/webdriverio/cddl/blob/main/CONTRIBUTING.md).
package/bin/cddl2py.js CHANGED
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,UAAU,EAGlB,MAAM,MAAM,CAAA;AAKb,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AASD,wBAAgB,SAAS,CAAE,WAAW,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAkBxF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,UAAU,EAGlB,MAAM,MAAM,CAAA;AAKb,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAgBD,wBAAgB,SAAS,CAAE,WAAW,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAuBxF"}
package/build/index.js CHANGED
@@ -7,13 +7,18 @@ export function transform(assignments, options) {
7
7
  typingImports: new Set(),
8
8
  typingExtensionsImports: new Set(),
9
9
  pydanticImports: new Set(),
10
+ definedTypeNames: new Set(),
11
+ assignmentsByName: new Map(assignments.map((assignment) => [pascalCase(assignment.Name), assignment])),
12
+ aliasUnionTypesByName: new Map(),
10
13
  };
11
14
  const blocks = [];
12
- for (const assignment of assignments) {
15
+ const orderedAssignments = orderAssignments(assignments);
16
+ for (const assignment of orderedAssignments) {
13
17
  const block = generateAssignment(assignment, ctx);
14
18
  if (block) {
15
19
  blocks.push(block);
16
20
  }
21
+ ctx.definedTypeNames.add(pascalCase(assignment.Name));
17
22
  }
18
23
  return renderOutput(ctx, blocks);
19
24
  }
@@ -63,11 +68,12 @@ function generateVariable(v, ctx) {
63
68
  if (propTypes.length === 1 && isRange(propTypes[0])) {
64
69
  return `${comments}${name} = int`;
65
70
  }
66
- const types = propTypes.map(t => resolveType(t, ctx));
71
+ const types = propTypes.map(t => resolveType(t, ctx, { quoteForwardReferences: true }));
67
72
  if (types.length === 1) {
68
73
  return `${comments}${name} = ${types[0]}`;
69
74
  }
70
75
  ctx.typingImports.add('Union');
76
+ ctx.aliasUnionTypesByName.set(name, types);
71
77
  return `${comments}${name} = Union[${types.join(', ')}]`;
72
78
  }
73
79
  // ---------------------------------------------------------------------------
@@ -105,8 +111,14 @@ function generateGroup(group, ctx) {
105
111
  }
106
112
  else {
107
113
  const typeVal = Array.isArray(mixin.Type) ? mixin.Type[0] : mixin.Type;
108
- if (isNamedGroupReference(typeVal)) {
109
- simpleMixinBases.push(pascalCase(typeVal.Value));
114
+ const mixinTarget = resolveMixinTarget(typeVal, ctx);
115
+ if (mixinTarget) {
116
+ if (mixinTarget.kind === 'union') {
117
+ unionMixinGroups.push(mixinTarget.types);
118
+ }
119
+ else {
120
+ simpleMixinBases.push(mixinTarget.type);
121
+ }
110
122
  }
111
123
  else if (isGroup(typeVal) && !isNamedGroupReference(typeVal) && typeVal.Properties) {
112
124
  const inlineGroup = typeVal;
@@ -186,6 +198,7 @@ function generateGroupWithChoices(name, properties, ctx) {
186
198
  }
187
199
  else {
188
200
  ctx.typingImports.add('Union');
201
+ ctx.aliasUnionTypesByName.set(name, unionTypes);
189
202
  blocks.push(`${name} = Union[${unionTypes.join(', ')}]`);
190
203
  }
191
204
  return blocks.join('\n\n');
@@ -229,6 +242,7 @@ function generateGroupWithUnionMixins(name, simpleBases, unionGroups, ownProps,
229
242
  return blocks.join('\n\n');
230
243
  }
231
244
  ctx.typingImports.add('Union');
245
+ ctx.aliasUnionTypesByName.set(name, variantNames);
232
246
  blocks.push(`${name} = Union[${variantNames.join(', ')}]`);
233
247
  return blocks.join('\n\n');
234
248
  }
@@ -247,7 +261,7 @@ function generateArrayAssignment(arr, ctx) {
247
261
  if (Array.isArray(firstVal)) {
248
262
  const options = firstVal.map(p => {
249
263
  const t = Array.isArray(p.Type) ? p.Type[0] : p.Type;
250
- return resolveType(t, ctx);
264
+ return resolveType(t, ctx, { quoteForwardReferences: true });
251
265
  });
252
266
  if (options.length === 1) {
253
267
  return `${comments}${name} = list[${options[0]}]`;
@@ -261,14 +275,14 @@ function generateArrayAssignment(arr, ctx) {
261
275
  const innerArr = types[0];
262
276
  const innerVal = innerArr.Values[0];
263
277
  const innerTypes = Array.isArray(innerVal.Type) ? innerVal.Type : [innerVal.Type];
264
- const typeStrs = innerTypes.map(v => resolveType(v, ctx));
278
+ const typeStrs = innerTypes.map(v => resolveType(v, ctx, { quoteForwardReferences: true }));
265
279
  if (typeStrs.length === 1) {
266
280
  return `${comments}${name} = list[${typeStrs[0]}]`;
267
281
  }
268
282
  ctx.typingImports.add('Union');
269
283
  return `${comments}${name} = list[Union[${typeStrs.join(', ')}]]`;
270
284
  }
271
- const typeStrs = types.map(t => resolveType(t, ctx));
285
+ const typeStrs = types.map(t => resolveType(t, ctx, { quoteForwardReferences: true }));
272
286
  if (typeStrs.length === 1) {
273
287
  return `${comments}${name} = list[${typeStrs[0]}]`;
274
288
  }
@@ -283,8 +297,9 @@ function generateClass(name, bases, props, ctx) {
283
297
  let classDecl;
284
298
  if (ctx.pydantic) {
285
299
  ctx.pydanticImports.add('BaseModel');
286
- if (bases.length > 0) {
287
- classDecl = `class ${name}(${bases.join(', ')}):`;
300
+ const pydanticBases = bases.filter((base) => isModelCompatibleBase(base, ctx));
301
+ if (pydanticBases.length > 0) {
302
+ classDecl = `class ${name}(${pydanticBases.join(', ')}):`;
288
303
  }
289
304
  else {
290
305
  classDecl = `class ${name}(BaseModel):`;
@@ -292,8 +307,9 @@ function generateClass(name, bases, props, ctx) {
292
307
  }
293
308
  else {
294
309
  ctx.typingExtensionsImports.add('TypedDict');
295
- if (bases.length > 0) {
296
- classDecl = `class ${name}(${bases.join(', ')}):`;
310
+ const typedDictBases = bases.filter((base) => isModelCompatibleBase(base, ctx));
311
+ if (typedDictBases.length > 0) {
312
+ classDecl = `class ${name}(${typedDictBases.join(', ')}):`;
297
313
  }
298
314
  else {
299
315
  classDecl = `class ${name}(TypedDict):`;
@@ -332,7 +348,7 @@ function generateField(prop, ctx) {
332
348
  typeStr = `Union[${types.join(', ')}]`;
333
349
  }
334
350
  const inlineComment = prop.Comments
335
- .filter(c => !c.Leading)
351
+ .filter((c) => Boolean(c) && !c.Leading)
336
352
  .map(c => c.Content.trim())
337
353
  .join('; ');
338
354
  const commentSuffix = inlineComment ? ` # ${inlineComment}` : '';
@@ -372,7 +388,7 @@ function generateField(prop, ctx) {
372
388
  // ---------------------------------------------------------------------------
373
389
  // Type resolution
374
390
  // ---------------------------------------------------------------------------
375
- function resolveType(t, ctx) {
391
+ function resolveType(t, ctx, options = {}) {
376
392
  if (typeof t === 'string') {
377
393
  const mapped = NATIVE_TYPE_MAP[t];
378
394
  if (mapped) {
@@ -402,42 +418,42 @@ function resolveType(t, ctx) {
402
418
  }
403
419
  if (isGroup(t)) {
404
420
  if (isNamedGroupReference(t)) {
405
- return pascalCase(t.Value);
421
+ return formatTypeReference(pascalCase(t.Value), ctx, options);
406
422
  }
407
423
  const group = t;
408
424
  if (group.Properties) {
409
425
  const props = group.Properties;
410
426
  if (props.some(p => Array.isArray(p))) {
411
- const options = [];
427
+ const choiceTypes = [];
412
428
  for (const choice of props) {
413
429
  const subProps = Array.isArray(choice) ? choice : [choice];
414
430
  if (subProps.length === 1 && isUnNamedProperty(subProps[0])) {
415
431
  const subType = Array.isArray(subProps[0].Type) ? subProps[0].Type[0] : subProps[0].Type;
416
- options.push(resolveType(subType, ctx));
432
+ choiceTypes.push(resolveType(subType, ctx, options));
417
433
  continue;
418
434
  }
419
435
  if (subProps.every(isUnNamedProperty)) {
420
436
  const tupleItems = subProps.map(p => {
421
437
  const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type;
422
- return resolveType(subType, ctx);
438
+ return resolveType(subType, ctx, options);
423
439
  });
424
440
  ctx.typingImports.add('Tuple');
425
- options.push(`Tuple[${tupleItems.join(', ')}]`);
441
+ choiceTypes.push(`Tuple[${tupleItems.join(', ')}]`);
426
442
  continue;
427
443
  }
428
444
  }
429
- if (options.length > 1) {
445
+ if (choiceTypes.length > 1) {
430
446
  ctx.typingImports.add('Union');
431
- return `Union[${options.join(', ')}]`;
447
+ return `Union[${choiceTypes.join(', ')}]`;
432
448
  }
433
- if (options.length === 1) {
434
- return options[0];
449
+ if (choiceTypes.length === 1) {
450
+ return choiceTypes[0];
435
451
  }
436
452
  }
437
453
  if (props.every(isUnNamedProperty)) {
438
454
  const items = props.map(p => {
439
455
  const subType = Array.isArray(p.Type) ? p.Type[0] : p.Type;
440
- return resolveType(subType, ctx);
456
+ return resolveType(subType, ctx, options);
441
457
  });
442
458
  if (items.length === 1) {
443
459
  return items[0];
@@ -450,7 +466,7 @@ function resolveType(t, ctx) {
450
466
  if (keyType === 'Any') {
451
467
  ctx.typingImports.add('Any');
452
468
  }
453
- const valType = resolveType(props[0].Type[0], ctx);
469
+ const valType = resolveType(props[0].Type[0], ctx, options);
454
470
  return `dict[${keyType}, ${valType}]`;
455
471
  }
456
472
  ctx.typingImports.add('Any');
@@ -485,7 +501,7 @@ function resolveType(t, ctx) {
485
501
  }
486
502
  const firstVal = arrValues[0];
487
503
  const innerTypes = Array.isArray(firstVal.Type) ? firstVal.Type : [firstVal.Type];
488
- const typeStrs = innerTypes.map(v => resolveType(v, ctx));
504
+ const typeStrs = innerTypes.map(v => resolveType(v, ctx, options));
489
505
  if (typeStrs.length === 1) {
490
506
  return `list[${typeStrs[0]}]`;
491
507
  }
@@ -499,12 +515,12 @@ function resolveType(t, ctx) {
499
515
  return 'int';
500
516
  }
501
517
  if (isNativeTypeWithOperator(t) && isNamedGroupReference(t.Type)) {
502
- return pascalCase(t.Type.Value);
518
+ return formatTypeReference(pascalCase(t.Type.Value), ctx, options);
503
519
  }
504
520
  if (isPropertyReference(t)) {
505
521
  const ref = t;
506
522
  if (ref.Type === 'group_array' && typeof ref.Value === 'string') {
507
- return `list[${pascalCase(ref.Value)}]`;
523
+ return `list[${formatTypeReference(pascalCase(ref.Value), ctx, options)}]`;
508
524
  }
509
525
  if (ref.Type === 'tag') {
510
526
  const tag = ref.Value;
@@ -515,7 +531,7 @@ function resolveType(t, ctx) {
515
531
  }
516
532
  return mapped;
517
533
  }
518
- return pascalCase(tag.TypePart);
534
+ return formatTypeReference(pascalCase(tag.TypePart), ctx, options);
519
535
  }
520
536
  }
521
537
  throw new Error(`Unknown type: ${JSON.stringify(t)}`);
@@ -523,13 +539,19 @@ function resolveType(t, ctx) {
523
539
  // ---------------------------------------------------------------------------
524
540
  // Helpers
525
541
  // ---------------------------------------------------------------------------
526
- function formatLeadingComments(comments) {
527
- const leading = comments.filter(c => c.Leading);
542
+ function formatLeadingComments(comments = []) {
543
+ const leading = comments.filter((c) => c !== null && c !== undefined && c.Leading);
528
544
  if (leading.length === 0) {
529
545
  return '';
530
546
  }
531
547
  return leading.map(c => `# ${c.Content}`).join('\n') + '\n';
532
548
  }
549
+ function formatTypeReference(typeName, ctx, options) {
550
+ if (!options.quoteForwardReferences || ctx.definedTypeNames.has(typeName)) {
551
+ return typeName;
552
+ }
553
+ return `"${typeName}"`;
554
+ }
533
555
  function formatDefaultValue(operator) {
534
556
  if (operator.Type !== 'default') {
535
557
  return '';
@@ -552,3 +574,197 @@ function formatDefaultValue(operator) {
552
574
  }
553
575
  return '';
554
576
  }
577
+ function orderAssignments(assignments) {
578
+ const assignmentsByName = new Map(assignments.map((assignment) => [pascalCase(assignment.Name), assignment]));
579
+ const ordered = [];
580
+ const visited = new Set();
581
+ const visiting = new Set();
582
+ function visit(assignment) {
583
+ const name = pascalCase(assignment.Name);
584
+ if (visited.has(name) || visiting.has(name)) {
585
+ return;
586
+ }
587
+ visiting.add(name);
588
+ for (const dependencyName of getHardDependencies(assignment, assignmentsByName)) {
589
+ const dependency = assignmentsByName.get(dependencyName);
590
+ if (dependency) {
591
+ visit(dependency);
592
+ }
593
+ }
594
+ visiting.delete(name);
595
+ visited.add(name);
596
+ ordered.push(assignment);
597
+ }
598
+ for (const assignment of assignments) {
599
+ visit(assignment);
600
+ }
601
+ return ordered;
602
+ }
603
+ function getHardDependencies(assignment, assignmentsByName) {
604
+ if (!isGroup(assignment)) {
605
+ return [];
606
+ }
607
+ const deps = new Set();
608
+ for (const propertyOrChoice of assignment.Properties) {
609
+ const properties = Array.isArray(propertyOrChoice) ? propertyOrChoice : [propertyOrChoice];
610
+ for (const property of properties) {
611
+ if (!isUnNamedProperty(property)) {
612
+ continue;
613
+ }
614
+ for (const dependency of getMixinDependencies(property.Type, assignmentsByName)) {
615
+ deps.add(dependency);
616
+ }
617
+ }
618
+ }
619
+ return [...deps];
620
+ }
621
+ function getMixinDependencies(type, assignmentsByName) {
622
+ const deps = new Set();
623
+ const values = Array.isArray(type) ? type : [type];
624
+ for (const value of values) {
625
+ if (isNamedGroupReference(value)) {
626
+ for (const dependency of getNamedMixinDependencies(pascalCase(value.Value), assignmentsByName)) {
627
+ deps.add(dependency);
628
+ }
629
+ continue;
630
+ }
631
+ if (isNativeTypeWithOperator(value) && isNamedGroupReference(value.Type)) {
632
+ for (const dependency of getNamedMixinDependencies(pascalCase(value.Type.Value), assignmentsByName)) {
633
+ deps.add(dependency);
634
+ }
635
+ continue;
636
+ }
637
+ if (isGroup(value) && !isNamedGroupReference(value) && value.Properties) {
638
+ for (const property of value.Properties) {
639
+ if (Array.isArray(property)) {
640
+ for (const choice of property) {
641
+ if (isUnNamedProperty(choice)) {
642
+ for (const dependency of getMixinDependencies(choice.Type, assignmentsByName)) {
643
+ deps.add(dependency);
644
+ }
645
+ }
646
+ }
647
+ continue;
648
+ }
649
+ if (!isUnNamedProperty(property)) {
650
+ continue;
651
+ }
652
+ for (const dependency of getMixinDependencies(property.Type, assignmentsByName)) {
653
+ deps.add(dependency);
654
+ }
655
+ }
656
+ }
657
+ }
658
+ return [...deps];
659
+ }
660
+ function getNamedMixinDependencies(name, assignmentsByName) {
661
+ const assignment = assignmentsByName.get(name);
662
+ if (!assignment || !isVariable(assignment)) {
663
+ return [name];
664
+ }
665
+ const propertyTypes = Array.isArray(assignment.PropertyType) ? assignment.PropertyType : [assignment.PropertyType];
666
+ const deps = new Set();
667
+ for (const propertyType of propertyTypes) {
668
+ const referencedName = getReferencedMixinName(propertyType);
669
+ if (referencedName) {
670
+ for (const dependency of getNamedMixinDependencies(referencedName, assignmentsByName)) {
671
+ deps.add(dependency);
672
+ }
673
+ continue;
674
+ }
675
+ }
676
+ return deps.size > 0 ? [...deps] : [name];
677
+ }
678
+ function getReferencedMixinName(propertyType) {
679
+ if (isNamedGroupReference(propertyType)) {
680
+ return pascalCase(propertyType.Value);
681
+ }
682
+ if (isNativeTypeWithOperator(propertyType) && isNamedGroupReference(propertyType.Type)) {
683
+ return pascalCase(propertyType.Type.Value);
684
+ }
685
+ return undefined;
686
+ }
687
+ function resolveMixinTarget(propertyType, ctx) {
688
+ const name = getReferencedMixinName(propertyType);
689
+ if (!name) {
690
+ return null;
691
+ }
692
+ const unionTypes = ctx.aliasUnionTypesByName.get(name);
693
+ if (unionTypes && unionTypes.length > 1) {
694
+ return { kind: 'union', types: expandMixinUnionTypes(unionTypes, ctx) };
695
+ }
696
+ const assignment = ctx.assignmentsByName.get(name);
697
+ if (!assignment || !isVariable(assignment)) {
698
+ return { kind: 'single', type: name };
699
+ }
700
+ const propertyTypes = Array.isArray(assignment.PropertyType) ? assignment.PropertyType : [assignment.PropertyType];
701
+ if (propertyTypes.length > 1) {
702
+ return { kind: 'union', types: expandMixinUnionTypes(propertyTypes.map((type) => resolveType(type, ctx)), ctx) };
703
+ }
704
+ const referencedName = getReferencedMixinName(propertyTypes[0]);
705
+ if (referencedName) {
706
+ return { kind: 'single', type: referencedName };
707
+ }
708
+ return { kind: 'single', type: name };
709
+ }
710
+ function expandMixinUnionTypes(types, ctx, seen = new Set()) {
711
+ const expanded = [];
712
+ for (const type of types) {
713
+ if (seen.has(type)) {
714
+ expanded.push(type);
715
+ continue;
716
+ }
717
+ const nested = ctx.aliasUnionTypesByName.get(type);
718
+ if (nested && nested.length > 1) {
719
+ seen.add(type);
720
+ expanded.push(...expandMixinUnionTypes(nested, ctx, seen));
721
+ seen.delete(type);
722
+ continue;
723
+ }
724
+ expanded.push(type);
725
+ }
726
+ return [...new Set(expanded)];
727
+ }
728
+ function isModelCompatibleBase(base, ctx, seen = new Set()) {
729
+ if (base.startsWith('_')) {
730
+ return true;
731
+ }
732
+ if (seen.has(base)) {
733
+ return false;
734
+ }
735
+ const assignment = ctx.assignmentsByName.get(base);
736
+ if (!assignment) {
737
+ return false;
738
+ }
739
+ if (isGroup(assignment)) {
740
+ return isConcreteGroupBase(assignment);
741
+ }
742
+ if (isCDDLArray(assignment)) {
743
+ return false;
744
+ }
745
+ if (!isVariable(assignment)) {
746
+ return false;
747
+ }
748
+ const propertyTypes = Array.isArray(assignment.PropertyType) ? assignment.PropertyType : [assignment.PropertyType];
749
+ if (propertyTypes.length !== 1) {
750
+ return false;
751
+ }
752
+ const referencedName = getReferencedMixinName(propertyTypes[0]);
753
+ if (!referencedName) {
754
+ return false;
755
+ }
756
+ seen.add(base);
757
+ const isCompatible = isModelCompatibleBase(referencedName, ctx, seen);
758
+ seen.delete(base);
759
+ return isCompatible;
760
+ }
761
+ function isConcreteGroupBase(group) {
762
+ if (group.Properties.some((property) => Array.isArray(property))) {
763
+ return false;
764
+ }
765
+ const properties = group.Properties;
766
+ if (properties.length === 1 && Object.keys(NATIVE_TYPE_MAP).includes(properties[0].Name)) {
767
+ return false;
768
+ }
769
+ return true;
770
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cddl2py",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "A Node.js package that can generate Python type definitions (with optional Pydantic support) based on a CDDL file",
5
5
  "author": "Christian Bromann <mail@bromann.dev>",
6
6
  "license": "MIT",
@@ -18,22 +18,26 @@
18
18
  "bugs": {
19
19
  "url": "https://github.com/webdriverio/cddl/issues"
20
20
  },
21
+ "files": [
22
+ "build",
23
+ "bin"
24
+ ],
21
25
  "type": "module",
22
26
  "exports": "./build/index.js",
23
27
  "types": "./build/index.d.ts",
24
28
  "bin": {
25
29
  "cddl2py": "./bin/cddl2py.js"
26
30
  },
27
- "scripts": {
28
- "release": "release-it --config .release-it.ts --VV",
29
- "release:ci": "pnpm release --ci --npm.skipChecks"
30
- },
31
31
  "devDependencies": {
32
32
  "@types/yargs": "^17.0.35",
33
33
  "@types/node": "^25.5.0"
34
34
  },
35
35
  "dependencies": {
36
- "cddl": "^0.15.0",
37
- "yargs": "^18.0.0"
36
+ "yargs": "^18.0.0",
37
+ "cddl": "0.19.1"
38
+ },
39
+ "scripts": {
40
+ "release": "release-it --config .release-it.ts --VV",
41
+ "release:ci": "pnpm release --ci --npm.skipChecks"
38
42
  }
39
- }
43
+ }
package/.release-it.ts DELETED
@@ -1,11 +0,0 @@
1
- import baseConfig from '../../.release-it.base';
2
- import type { Config } from 'release-it';
3
-
4
- const config: Config = {
5
- ...baseConfig('cddl2py'),
6
- };
7
-
8
- console.log("Release-it config for cddl2py loaded", config);
9
-
10
- export default config;
11
-