cddl2py 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +136 -0
- package/bin/cddl2py.js +0 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +246 -30
- package/package.json +12 -8
- package/.release-it.ts +0 -11
- package/src/cli.ts +0 -42
- package/src/constants.ts +0 -32
- package/src/index.ts +0 -658
- package/src/utils.ts +0 -12
- package/tests/__snapshots__/complex_types.test.ts.snap +0 -81
- package/tests/__snapshots__/group_choice.test.ts.snap +0 -127
- package/tests/__snapshots__/literals.test.ts.snap +0 -15
- package/tests/__snapshots__/mixin_union.test.ts.snap +0 -65
- package/tests/__snapshots__/mod.test.ts.snap +0 -145
- package/tests/__snapshots__/named_group_choice.test.ts.snap +0 -37
- package/tests/__snapshots__/transform.test.ts.snap +0 -137
- package/tests/__snapshots__/webdriver_local.test.ts.snap +0 -921
- package/tests/__snapshots__/webdriver_remote.test.ts.snap +0 -1249
- package/tests/complex_types.test.ts +0 -92
- package/tests/group_choice.test.ts +0 -88
- package/tests/literals.test.ts +0 -63
- package/tests/mixin_union.test.ts +0 -80
- package/tests/mod.test.ts +0 -106
- package/tests/named_group_choice.test.ts +0 -82
- package/tests/transform.test.ts +0 -149
- package/tests/transform_edge_cases.test.ts +0 -265
- package/tests/unknown.test.ts +0 -72
- package/tests/webdriver_local.test.ts +0 -64
- package/tests/webdriver_remote.test.ts +0 -64
- package/tsconfig.json +0 -11
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
|
package/build/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
441
|
+
choiceTypes.push(`Tuple[${tupleItems.join(', ')}]`);
|
|
426
442
|
continue;
|
|
427
443
|
}
|
|
428
444
|
}
|
|
429
|
-
if (
|
|
445
|
+
if (choiceTypes.length > 1) {
|
|
430
446
|
ctx.typingImports.add('Union');
|
|
431
|
-
return `Union[${
|
|
447
|
+
return `Union[${choiceTypes.join(', ')}]`;
|
|
432
448
|
}
|
|
433
|
-
if (
|
|
434
|
-
return
|
|
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.
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"yargs": "^18.0.0",
|
|
37
|
+
"cddl": "0.19.0"
|
|
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