cddl2ts 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 +62 -0
- package/bin/cddl2ts.js +9 -0
- package/build/cli.d.ts +2 -0
- package/build/cli.d.ts.map +1 -0
- package/build/cli.js +33 -0
- package/build/constants.d.ts +2 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +5 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +200 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Christian Bromann<mail@bromann.dev>
|
|
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,62 @@
|
|
|
1
|
+
CDDL to TypeScript 
|
|
2
|
+
==================
|
|
3
|
+
|
|
4
|
+
> A Node.js package that can generate a TypeScript definition based on a CDDL file
|
|
5
|
+
|
|
6
|
+
CDDL expresses Concise Binary Object Representation (CBOR) data structures ([RFC 7049](https://tools.ietf.org/html/rfc7049)). Its main goal is to provide an easy and unambiguous way to express structures for protocol messages and data formats that use CBOR or JSON. This package allows you to transform a CDDL file into a TypeScript interface that you can use for other TypeScript project.
|
|
7
|
+
|
|
8
|
+
Related projects:
|
|
9
|
+
- [christian-bromann/cddl](https://github.com/christian-bromann/cddl): parses CDDL into an AST
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
To install this package run:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
$ npm install cddl2ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Using this package
|
|
20
|
+
|
|
21
|
+
This package exposes a CLI as well as a programmatic interface for transforming CDDL into TypeScript.
|
|
22
|
+
|
|
23
|
+
### CLI
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx cddl2ts ./path/to/interface.cddl &> ./path/to/interface.ts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Programmatic Interface
|
|
30
|
+
|
|
31
|
+
The module exports a `transform` method that takes an CDDL AST object and returns a TypeScript definition as `string`, e.g.:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { transform } from 'cddl2ts'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* spec.cddl:
|
|
38
|
+
*
|
|
39
|
+
* session.CapabilityRequest = {
|
|
40
|
+
* ?acceptInsecureCerts: bool,
|
|
41
|
+
* ?browserName: text,
|
|
42
|
+
* ?browserVersion: text,
|
|
43
|
+
* ?platformName: text,
|
|
44
|
+
* };
|
|
45
|
+
*/
|
|
46
|
+
const ts = transform('./spec.cddl')
|
|
47
|
+
console.log(ts)
|
|
48
|
+
/**
|
|
49
|
+
* outputs:
|
|
50
|
+
*
|
|
51
|
+
* interface SessionCapabilityRequest {
|
|
52
|
+
* acceptInsecureCerts?: boolean,
|
|
53
|
+
* browserName?: string,
|
|
54
|
+
* browserVersion?: string,
|
|
55
|
+
* platformName?: string,
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
If you are interested in this project, please feel free to contribute ideas or code patches. Have a look at our [contributing guidelines](https://github.com/christian-bromann/cddl2ts/blob/master/CONTRIBUTING.md) to get started.
|
package/bin/cddl2ts.js
ADDED
package/build/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAmBA,wBAA8B,GAAG,CAAE,IAAI,WAAwB,sBAmB9D"}
|
package/build/cli.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from 'cddl';
|
|
4
|
+
import { transform } from './index.js';
|
|
5
|
+
import { pkg } from './constants.js';
|
|
6
|
+
const HELP = `
|
|
7
|
+
${pkg.name}
|
|
8
|
+
${pkg.description}
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
runme2ts ./path/to/spec.cddl &> ./path/to/interface.ts
|
|
12
|
+
|
|
13
|
+
v${pkg.version}
|
|
14
|
+
Copyright ${(new Date()).getFullYear()} ${pkg.author}
|
|
15
|
+
`;
|
|
16
|
+
export default async function cli(args = process.argv.slice(2)) {
|
|
17
|
+
if (args.includes('--help') || args.length === 0) {
|
|
18
|
+
console.log(HELP);
|
|
19
|
+
return process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
22
|
+
console.log(pkg.version);
|
|
23
|
+
return process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
const absoluteFilePath = path.resolve(process.cwd(), args[0]);
|
|
26
|
+
const hasAccess = await fs.access(absoluteFilePath).then(() => true, () => false);
|
|
27
|
+
if (!hasAccess) {
|
|
28
|
+
console.error(`Couldn't find or access source CDDL file at "${absoluteFilePath}"`);
|
|
29
|
+
return process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const ast = parse(absoluteFilePath);
|
|
32
|
+
console.log(transform(ast));
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,GAAG,KAAqF,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAmG,MAAM,MAAM,CAAA;AAqBvI,wBAAgB,SAAS,CAAE,WAAW,EAAE,UAAU,EAAE,UAkBnD"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import camelcase from 'camelcase';
|
|
2
|
+
import { parse, print, types } from 'recast';
|
|
3
|
+
import typescriptParser from 'recast/parsers/typescript.js';
|
|
4
|
+
import { pkg } from './constants.js';
|
|
5
|
+
const b = types.builders;
|
|
6
|
+
const NATIVE_TYPES = {
|
|
7
|
+
number: b.tsNumberKeyword(),
|
|
8
|
+
float: b.tsNumberKeyword(),
|
|
9
|
+
uint: b.tsNumberKeyword(),
|
|
10
|
+
bool: b.tsBooleanKeyword(),
|
|
11
|
+
str: b.tsStringKeyword(),
|
|
12
|
+
text: b.tsStringKeyword(),
|
|
13
|
+
tstr: b.tsStringKeyword(),
|
|
14
|
+
range: b.tsNumberKeyword(),
|
|
15
|
+
nil: b.tsNullKeyword(),
|
|
16
|
+
null: b.tsNullKeyword()
|
|
17
|
+
};
|
|
18
|
+
export function transform(assignments) {
|
|
19
|
+
let ast = parse(`// compiled with https://www.npmjs.com/package/cddl2ts v${pkg.version}`, {
|
|
20
|
+
parser: typescriptParser,
|
|
21
|
+
sourceFileName: 'cddl2Ts.ts',
|
|
22
|
+
sourceRoot: process.cwd()
|
|
23
|
+
});
|
|
24
|
+
for (const assignment of assignments) {
|
|
25
|
+
const statement = parseAssignment(ast, assignment);
|
|
26
|
+
if (!statement) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
ast.program.body.push(statement);
|
|
30
|
+
}
|
|
31
|
+
return print(ast).code;
|
|
32
|
+
}
|
|
33
|
+
function parseAssignment(ast, assignment) {
|
|
34
|
+
if (assignment.Type === 'variable') {
|
|
35
|
+
const propType = Array.isArray(assignment.PropertyType)
|
|
36
|
+
? assignment.PropertyType
|
|
37
|
+
: [assignment.PropertyType];
|
|
38
|
+
const id = b.identifier(camelcase(assignment.Name, { pascalCase: true }));
|
|
39
|
+
let typeParameters;
|
|
40
|
+
// @ts-expect-error e.g. "js-int = -9007199254740991..9007199254740991"
|
|
41
|
+
if (propType.length === 1 && propType[0].Type === 'range') {
|
|
42
|
+
typeParameters = b.tsNumberKeyword();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
typeParameters = b.tsUnionType(propType.map(parsePropertyType));
|
|
46
|
+
}
|
|
47
|
+
const expr = b.tsTypeAliasDeclaration(id, typeParameters);
|
|
48
|
+
expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
|
|
49
|
+
return expr;
|
|
50
|
+
}
|
|
51
|
+
if (assignment.Type === 'group') {
|
|
52
|
+
const id = b.identifier(camelcase(assignment.Name, { pascalCase: true }));
|
|
53
|
+
const objectType = parseObjectType(assignment.Properties);
|
|
54
|
+
const extendInterfaces = assignment.Properties
|
|
55
|
+
.filter((prop) => prop.Name === '')
|
|
56
|
+
.map((prop) => b.tsExpressionWithTypeArguments(b.identifier(camelcase(prop.Type[0].Value, { pascalCase: true }))));
|
|
57
|
+
const expr = b.tsInterfaceDeclaration(id, b.tsInterfaceBody(objectType));
|
|
58
|
+
expr.extends = extendInterfaces;
|
|
59
|
+
expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
|
|
60
|
+
return expr;
|
|
61
|
+
}
|
|
62
|
+
if (assignment.Type === 'array') {
|
|
63
|
+
const id = b.identifier(camelcase(assignment.Name, { pascalCase: true }));
|
|
64
|
+
const firstType = assignment.Values[0].Type;
|
|
65
|
+
const obj = Array.isArray(firstType)
|
|
66
|
+
? firstType.map(parseUnionType)
|
|
67
|
+
: firstType.Values
|
|
68
|
+
? firstType.Values.map((val) => parseUnionType(val.Type[0]))
|
|
69
|
+
// ToDo(Christian): transpile this case correctly
|
|
70
|
+
: [];
|
|
71
|
+
const value = b.tsArrayType(b.tsParenthesizedType(b.tsUnionType(obj)));
|
|
72
|
+
const expr = b.tsTypeAliasDeclaration(id, value);
|
|
73
|
+
expr.comments = assignment.Comments.map((c) => b.commentLine(` ${c.Content}`, true));
|
|
74
|
+
return expr;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Unknown assignment type "${assignment.Type}"`);
|
|
77
|
+
}
|
|
78
|
+
function parsePropertyType(propType) {
|
|
79
|
+
if (typeof propType === 'string') {
|
|
80
|
+
return b.tsStringKeyword();
|
|
81
|
+
}
|
|
82
|
+
if (propType.Type === 'group') {
|
|
83
|
+
return b.tsTypeReference(b.identifier(camelcase(propType.Value.toString(), { pascalCase: true })));
|
|
84
|
+
}
|
|
85
|
+
if (propType.Type === 'literal') {
|
|
86
|
+
return b.tsLiteralType(b.stringLiteral(propType.Value.toString()));
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Couldn't parse property type ${JSON.stringify(propType, null, 4)}`);
|
|
89
|
+
}
|
|
90
|
+
function parseObjectType(props) {
|
|
91
|
+
const propItems = [];
|
|
92
|
+
for (const prop of props) {
|
|
93
|
+
/**
|
|
94
|
+
* Empty groups like
|
|
95
|
+
* {
|
|
96
|
+
* HasCut: false,
|
|
97
|
+
* Occurrence: { n: 1, m: 1 },
|
|
98
|
+
* Name: '',
|
|
99
|
+
* Type: [ { Type: 'group', Value: 'Extensible', Unwrapped: false } ],
|
|
100
|
+
* Comment: ''
|
|
101
|
+
* }
|
|
102
|
+
* are ignored and later added as interface extensions
|
|
103
|
+
*/
|
|
104
|
+
if (prop.Name === '') {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const id = b.identifier(camelcase(prop.Name));
|
|
108
|
+
const cddlType = Array.isArray(prop.Type) ? prop.Type : [prop.Type];
|
|
109
|
+
const comments = prop.Comments.map((c) => ` ${c.Content}`);
|
|
110
|
+
if (prop.Operator && prop.Operator.Type === 'default') {
|
|
111
|
+
const defaultValue = parseDefaultValue(prop.Operator);
|
|
112
|
+
defaultValue && comments.length && comments.push(''); // add empty line if we have previous comments
|
|
113
|
+
defaultValue && comments.push(` @default ${defaultValue}`);
|
|
114
|
+
}
|
|
115
|
+
const type = cddlType.map((t) => {
|
|
116
|
+
const unionType = parseUnionType(t);
|
|
117
|
+
if (unionType) {
|
|
118
|
+
const defaultValue = parseDefaultValue(t.Operator);
|
|
119
|
+
defaultValue && comments.length && comments.push(''); // add empty line if we have previous comments
|
|
120
|
+
defaultValue && comments.push(` @default ${defaultValue}`);
|
|
121
|
+
return unionType;
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Couldn't parse property ${JSON.stringify(t)}`);
|
|
124
|
+
});
|
|
125
|
+
const typeAnnotation = b.tsTypeAnnotation(b.tsUnionType(type));
|
|
126
|
+
const isOptional = prop.Occurrence.n === 0;
|
|
127
|
+
const propSignature = b.tsPropertySignature(id, typeAnnotation, isOptional);
|
|
128
|
+
propSignature.comments = comments.length ? [b.commentBlock(`*\n *${comments.join('\n *')}\n `)] : [];
|
|
129
|
+
propItems.push(propSignature);
|
|
130
|
+
}
|
|
131
|
+
return propItems;
|
|
132
|
+
}
|
|
133
|
+
function parseUnionType(t) {
|
|
134
|
+
if (typeof t === 'string') {
|
|
135
|
+
if (!NATIVE_TYPES[t]) {
|
|
136
|
+
throw new Error(`Unknown native type: "${t}`);
|
|
137
|
+
}
|
|
138
|
+
return NATIVE_TYPES[t];
|
|
139
|
+
}
|
|
140
|
+
else if (NATIVE_TYPES[t.Type]) {
|
|
141
|
+
return NATIVE_TYPES[t.Type];
|
|
142
|
+
}
|
|
143
|
+
else if (t.Value === 'null') {
|
|
144
|
+
return b.tsNullKeyword();
|
|
145
|
+
}
|
|
146
|
+
else if (t.Type === 'group') {
|
|
147
|
+
const value = t.Value;
|
|
148
|
+
/**
|
|
149
|
+
* check if we have
|
|
150
|
+
* ?attributes: {*text => text},
|
|
151
|
+
*/
|
|
152
|
+
if (!value && t.Properties) {
|
|
153
|
+
return b.tsTypeLiteral(parseObjectType(t.Properties));
|
|
154
|
+
}
|
|
155
|
+
return b.tsTypeReference(b.identifier(camelcase(value.toString(), { pascalCase: true })));
|
|
156
|
+
}
|
|
157
|
+
else if (t.Type === 'literal' && typeof t.Value === 'string') {
|
|
158
|
+
return b.tsLiteralType(b.stringLiteral(t.Value));
|
|
159
|
+
}
|
|
160
|
+
else if (t.Type === 'literal' && typeof t.Value === 'number') {
|
|
161
|
+
return b.tsLiteralType(b.numericLiteral(t.Value));
|
|
162
|
+
}
|
|
163
|
+
else if (t.Type === 'array') {
|
|
164
|
+
const types = t.Values[0].Type;
|
|
165
|
+
const typedTypes = (Array.isArray(types) ? types : [types]).map((val) => {
|
|
166
|
+
return typeof val === 'string' && NATIVE_TYPES[val]
|
|
167
|
+
? NATIVE_TYPES[val]
|
|
168
|
+
: b.tsTypeReference(b.identifier(camelcase(val.Value, { pascalCase: true })));
|
|
169
|
+
});
|
|
170
|
+
return b.tsArrayType(typedTypes.length > 1
|
|
171
|
+
? b.tsParenthesizedType(b.tsUnionType(typedTypes))
|
|
172
|
+
: b.tsUnionType(typedTypes));
|
|
173
|
+
}
|
|
174
|
+
else if (typeof t.Type === 'object' && t.Type.Type === 'range') {
|
|
175
|
+
return b.tsNumberKeyword();
|
|
176
|
+
}
|
|
177
|
+
else if (typeof t.Type === 'object' && t.Type.Type === 'group') {
|
|
178
|
+
/**
|
|
179
|
+
* e.g. ?pointerType: input.PointerType .default "mouse"
|
|
180
|
+
*/
|
|
181
|
+
const referenceValue = camelcase(t.Type.Value, { pascalCase: true });
|
|
182
|
+
return b.tsTypeReference(b.identifier(referenceValue));
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`Unknown union type: ${JSON.stringify(t)}`);
|
|
185
|
+
}
|
|
186
|
+
function parseDefaultValue(operator) {
|
|
187
|
+
if (!operator || operator.Type !== 'default') {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const operatorValue = operator.Value;
|
|
191
|
+
if (operator.Value === 'null') {
|
|
192
|
+
return operator.Value;
|
|
193
|
+
}
|
|
194
|
+
if (operatorValue.Type !== 'literal') {
|
|
195
|
+
throw new Error(`Can't parse operator default value of ${JSON.stringify(operator)}`);
|
|
196
|
+
}
|
|
197
|
+
return typeof operatorValue.Value === 'string'
|
|
198
|
+
? `'${operatorValue.Value}'`
|
|
199
|
+
: operatorValue.Value;
|
|
200
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cddl2ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Node.js package that can generate a TypeScript definition based on a CDDL file",
|
|
5
|
+
"author": "Christian Bromann <mail@bromann.dev>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/christian-bromann/cddl2ts#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+ssh://git@github.com/christian-bromann/cddl2ts.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cddl"
|
|
14
|
+
],
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/christian-bromann/cddl2ts/issues"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": "./build/index.js",
|
|
20
|
+
"bin": {
|
|
21
|
+
"cddl2ts": "./bin/cddl2ts.js"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "run-s clean compile",
|
|
25
|
+
"clean": "rm -rf ./build ./coverage",
|
|
26
|
+
"compile": "tsc -p ./tsconfig.json",
|
|
27
|
+
"release": "release-it --github.release",
|
|
28
|
+
"release:ci": "npm run release -- --ci --npm.skipChecks --no-git.requireCleanWorkingDir",
|
|
29
|
+
"release:patch": "npm run release -- patch",
|
|
30
|
+
"release:minor": "npm run release -- minor",
|
|
31
|
+
"release:major": "npm run release -- major",
|
|
32
|
+
"test": "vitest",
|
|
33
|
+
"watch": "tsc --watch"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/jest": "^29.5.1",
|
|
37
|
+
"@types/node": "^20.2.3",
|
|
38
|
+
"@vitest/coverage-c8": "^0.31.1",
|
|
39
|
+
"npm-run-all": "^4.1.5",
|
|
40
|
+
"release-it": "^15.10.3",
|
|
41
|
+
"typescript": "^5.0.4",
|
|
42
|
+
"vitest": "^0.31.1"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@babel/parser": "^7.21.9",
|
|
46
|
+
"camelcase": "^7.0.1",
|
|
47
|
+
"cddl": "^0.8.3",
|
|
48
|
+
"recast": "^0.23.2",
|
|
49
|
+
"yargs": "^17.7.2"
|
|
50
|
+
}
|
|
51
|
+
}
|