Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ __Note:__ this is __work in progress__, feel free to have a look at the code or

## Install

This monorepo ships a parser plus a set of language transformers:

- [`cddl`](packages/cddl/README.md) – parser & validator
- [`cddl2ts`](packages/cddl2ts/README.md) – generate TypeScript definitions
- [`cddl2py`](packages/cddl2py/README.md) – generate Python (`TypedDict`/Pydantic) definitions
- [`cddl2java`](packages/cddl2java/README.md) – generate Java classes
- [`cddl2swift`](packages/cddl2swift/README.md) – generate Swift definitions
- [`cddl2kotlin`](packages/cddl2kotlin/README.md) – generate Kotlin definitions

To install one of the packages run:

```sh
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
"compile": "run-s compile:cddl compile:other",
"compile:cddl": "pnpm --filter=cddl exec tsc -p ./tsconfig.json",
"compile:other": "pnpm -r --filter=!cddl --filter=!cddl-monorepo exec tsc -p ./tsconfig.json",
"release:ci:all": "run-s \"release:ci:cddl {@}\" \"release:ci:cddl2ts {@}\" \"release:ci:cddl2java {@}\" \"release:ci:cddl2py {@}\"",
"release:ci:all": "run-s \"release:ci:cddl {@}\" \"release:ci:cddl2ts {@}\" \"release:ci:cddl2java {@}\" \"release:ci:cddl2py {@}\" \"release:ci:cddl2swift {@}\" \"release:ci:cddl2kotlin {@}\"",
"release:ci:cddl": "pnpm run --filter=cddl release:ci",
"release:ci:cddl2ts": "pnpm run --filter=cddl2ts release:ci",
"release:ci:cddl2java": "pnpm run --filter=cddl2java release:ci",
"release:ci:cddl2py": "pnpm run --filter=cddl2py release:ci",
"release:ci:cddl2swift": "pnpm run --filter=cddl2swift release:ci",
"release:ci:cddl2kotlin": "pnpm run --filter=cddl2kotlin release:ci",
"test": "run-p test:*",
"test:typechecks": "tsc -p ./tsconfig.test.json",
"test:unit": "vitest --config vitest.config.ts --run",
Expand Down
10 changes: 10 additions & 0 deletions packages/cddl2kotlin/.release-it.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import baseConfig from '../../.release-it.base';
import type { Config } from 'release-it';

const config: Config = {
...baseConfig('cddl2kotlin'),
};

console.log("Release-it config for cddl2kotlin loaded", config);

export default config;
103 changes: 103 additions & 0 deletions packages/cddl2kotlin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# CDDL to Kotlin

> Generate Kotlin type definitions from CDDL as `data class`es, `typealias`es and `enum`s.

`cddl2kotlin` converts a parsed CDDL schema into Kotlin source code. Groups become
`data class`es, named assignments become `typealias`es, and choices become
`enum class`es or `sealed interface`s.

## Install

Use the CLI:

```sh
npm install cddl2kotlin
```

Use the programmatic API:

```sh
npm install cddl cddl2kotlin
```

## What It Generates

`cddl2kotlin` maps common CDDL constructs into idiomatic Kotlin, including:

- named CDDL assignments to `typealias` declarations
- groups to `data class` declarations (empty groups become a plain `class`)
- optional group fields to nullable types with a `= null` default
- arrays to `List<T>`
- record groups (`*text => T`) to `Map<String, T>`
- string-literal choices to `enum class` declarations
- choices of named types to `sealed interface`s with wrapper `data class`es

It also normalizes names for Kotlin code by turning type names into `PascalCase`,
field names into `camelCase` (escaping Kotlin keywords with backticks) and enum
constants into `UPPER_SNAKE_CASE`.

Because Kotlin has no structural union or intersection types, inline unions of
more than one concrete type fall back to `Any?`, and group mixins (unnamed group
references) are flattened by inlining the referenced group's fields.

## CLI

The CLI reads a CDDL file and writes generated Kotlin code to stdout, so the
normal workflow is to redirect the output into a `.kt` file.

```sh
npx cddl2kotlin ./path/to/schema.cddl > ./Types.kt
```

Show help:

```sh
npx cddl2kotlin --help
```

## Programmatic API

The package exports a single `transform()` function. It accepts the parsed CDDL
AST and returns the generated Kotlin source as a string.

```js
import { parse } from 'cddl'
import { transform } from 'cddl2kotlin'

const ast = parse('./schema.cddl')
const kotlin = transform(ast)

console.log(kotlin)
```

## Example

Input CDDL:

```cddl
person = {
name: tstr,
age: uint,
?nickname: tstr,
}
```

Generated Kotlin:

```kotlin
data class Person(
val name: String,
val age: Long,
val nickname: String? = null
)
```

## Notes

- Generated files include a header comment with the `cddl2kotlin` version used.
- The CLI validates that the input file exists before attempting to parse it.

---

If you want to contribute fixes or improvements, see the repository
[contributing guide](https://github.com/webdriverio/cddl/blob/main/CONTRIBUTING.md).
5 changes: 5 additions & 0 deletions packages/cddl2kotlin/bin/cddl2kotlin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import parse from '../build/cli.js'

parse()
43 changes: 43 additions & 0 deletions packages/cddl2kotlin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "cddl2kotlin",
"version": "0.1.0",
"description": "A Node.js package that can generate Kotlin type definitions based on a CDDL file",
"author": "Christian Bromann <mail@bromann.dev>",
"license": "MIT",
"homepage": "https://github.com/webdriverio/cddl/blob/main/packages/cddl2kotlin/README.md",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/webdriverio/cddl.git"
},
"keywords": [
"cddl",
"kotlin",
"codegen"
],
"bugs": {
"url": "https://github.com/webdriverio/cddl/issues"
},
"files": [
"build",
"bin"
],
"type": "module",
"exports": "./build/index.js",
"types": "./build/index.d.ts",
"bin": {
"cddl2kotlin": "./bin/cddl2kotlin.js"
},
"scripts": {
"release": "release-it --config .release-it.ts --VV",
"release:ci": "pnpm release --ci --npm.skipChecks"
},
"devDependencies": {
"@types/yargs": "^17.0.35",
"@types/node": "^25.5.0"
},
"dependencies": {
"camelcase": "^9.0.0",
"cddl": "workspace:*",
"yargs": "^18.0.0"
}
}
36 changes: 36 additions & 0 deletions packages/cddl2kotlin/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import yargs from 'yargs'

import { parse } from 'cddl'

import { transform } from './index.js'
import { pkg } from './constants.js'

export default async function cli (argv = process.argv.slice(2)) {
const parser = yargs(argv)
.usage(`${pkg.name}\n${pkg.description}\n\nUsage:\ncddl2kotlin ./path/to/spec.cddl > ./path/to/Types.kt`)
.epilog(`v${pkg.version}\nCopyright ${(new Date()).getFullYear()} ${pkg.author}`)
.version(pkg.version)
.help('help')
.alias('h', 'help')
.alias('v', 'version')

const args = await parser.argv

if (args._.length === 0) {
parser.showHelp()
return process.exit(0)
}

const absoluteFilePath = path.resolve(process.cwd(), args._[0] as string)
const hasAccess = await fs.access(absoluteFilePath).then(() => true, () => false)

if (!hasAccess) {
console.error(`Couldn't find or access source CDDL file at "${absoluteFilePath}"`)
return process.exit(1)
}

const ast = parse(absoluteFilePath)
console.log(transform(ast))
}
51 changes: 51 additions & 0 deletions packages/cddl2kotlin/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fs from 'node:fs/promises'
import url from 'node:url'
import path from 'node:path'

const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
export const pkg = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'))

/**
* Sentinel returned by the type resolver when it encounters a CDDL `null`/`nil`
* type. Kotlin has no standalone null type, so consumers turn the surrounding
* declaration into a nullable type instead.
*/
export const NULL_TYPE = '__cddl_null__'

/**
* Fallback used whenever a CDDL construct cannot be expressed as a concrete
* Kotlin type (e.g. inline unions of more than one non-null type). `Any?` is
* used because CDDL `any` may also be null.
*/
export const ANY_TYPE = 'Any?'

export const CDDL_PARSE_ERROR_MESSAGE = 'Failed to transform CDDL into Kotlin: %s'

export const NATIVE_TYPE_MAP: Record<string, string> = {
any: ANY_TYPE,
number: 'Double',
int: 'Int',
uint: 'Long',
nint: 'Int',
float: 'Double',
float16: 'Double',
float32: 'Double',
float64: 'Double',
bool: 'Boolean',
bstr: 'ByteArray',
bytes: 'ByteArray',
tstr: 'String',
text: 'String',
str: 'String',
}

/**
* Kotlin hard keywords that cannot be used bare as identifiers. When a CDDL
* property name collides with one of these it is escaped with backticks.
*/
export const KOTLIN_RESERVED_WORDS = new Set([
'as', 'break', 'class', 'continue', 'do', 'else', 'false', 'for', 'fun',
'if', 'in', 'interface', 'is', 'null', 'object', 'package', 'return',
'super', 'this', 'throw', 'true', 'try', 'typealias', 'typeof', 'val',
'var', 'when', 'while',
])
Loading