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 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 ![Test](https://github.com/christian-bromann/cddl/workflows/Test/badge.svg?branch=master)
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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import parse from '../build/cli.js'
4
+
5
+ if (process.env.NODE_ENV == null) {
6
+ process.env.NODE_ENV = 'test'
7
+ }
8
+
9
+ parse()
package/build/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export default function cli(args?: string[]): Promise<undefined>;
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -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,2 @@
1
+ export declare const pkg: any;
2
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,GAAG,KAAqF,CAAA"}
@@ -0,0 +1,5 @@
1
+ import fs from 'node:fs/promises';
2
+ import url from 'node:url';
3
+ import path from 'node:path';
4
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
5
+ export const pkg = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'));
@@ -0,0 +1,3 @@
1
+ import type { Assignment } from 'cddl';
2
+ export declare function transform(assignments: Assignment[]): string;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }