Skip to content
Open
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
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

tomlkt is a [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) format plugin for TOML 1.0.0, published to Maven Central as `dev.eav.tomlkt:tomlkt`. It is a Kotlin Multiplatform library targeting JVM, JS (IR), Wasm/JS, and a wide range of Kotlin/Native targets (mingw, macos, ios, linux). The public package is `dev.eav.tomlkt`.

## Common Commands

Build and test run through the Gradle wrapper. The publishable code lives in the `:core` module (`:benchmark` is JMH-only).

```bash
./gradlew core:build # compile + test all targets
./gradlew core:check # full CI check (tests + detekt) — what CI runs
./gradlew core:allTests # run tests on all targets
./gradlew core:jvmTest # run JVM tests only (fastest iteration loop)
./gradlew core:jsTest core:wasmJsTest # JS / Wasm tests
./gradlew core:detektMetadataMain # static analysis (uses format/detekt.yml)
```

Run a single test class/method via the JUnit platform filter (JVM target):

```bash
./gradlew core:jvmTest --tests "dev.eav.tomlkt.IntegerTest"
./gradlew core:jvmTest --tests "dev.eav.tomlkt.IntegerTest.testNegativeNumber"
```

Most tests live in `commonTest` and run on every target; `jvmTest` additionally covers stream I/O (`StreamTest`). When iterating, prefer `core:jvmTest` for speed, but run `core:allTests` before considering a change complete since platform-specific `actual` implementations differ.

Docs are generated with Dokka (`./gradlew core:dokkaGenerate`, output in `docs/`).

## Architecture

The central design is that everything flows through an intermediate representation, `TomlElement`, rather than converting models directly to/from text:

- **Encoding:** Model → `TomlElementEncoder` → `TomlElement` → `TomlElementEmitter` → text
- **Decoding:** text → `TomlElementParser` → `TomlElement` → `TomlElementDecoder` → Model

`Toml.kt` is the public entry point (`Toml`, `Toml { }` factory, `encodeToString`/`decodeFromString`/`parseToTomlTable`). It wires the four internal stages together. Understanding any encode/decode behavior usually means reading the relevant stage in `core/src/commonMain/kotlin/dev/eav/tomlkt/internal/`:

- `parser/TomlElementParser.kt` — hand-written TOML lexer/parser; builds a tree of `TreeNode` (`KeyNode`/`ArrayNode`/`ValueNode`) then a `TomlTable`. Character-class constraints (e.g. `BareKeyRegex`, `DecimalConstraints`) live in `internal/StringUtils.kt`.
- `encoder/` — `AbstractTomlEncoder` + `TomlElementEncoder` implement the kotlinx.serialization `Encoder`/`CompositeEncoder` SPI, turning a serializable model into a `TomlElement`.
- `decoder/` — `AbstractTomlDecoder` + `TomlElementDecoder` implement the `Decoder` SPI, turning a `TomlElement` into a model.
- `emitter/TomlElementEmitter.kt` — renders a `TomlElement` to TOML text via the `TomlWriter` abstraction.

`TomlElement.kt` (the largest file) defines the sealed hierarchy: `TomlNull`, `TomlLiteral`, `TomlArray`, `TomlTable`, plus conversion/accessor extensions (e.g. `TomlTable["a", "b"]` path access, `toTomlLiteral()`). `TomlElementBuilders.kt` provides the `buildTomlTable { }` DSL. These are the only types meant to be (de)serialized directly, and only via `Toml`.

### Annotations drive formatting

User-facing formatting is controlled by annotations in `Annotations.kt` applied to `@Serializable` properties: `@TomlComment`, `@TomlMultilineString`, `@TomlLiteralString`, `@TomlInline`, `@TomlBlockArray`, `@TomlInteger` (base/representation). These are metadata consumed only by the encoder/emitter — **the parser ignores them**, so a parse→emit round trip does not preserve annotation-driven formatting.

### Configuration

`TomlConfig.kt` defines the knobs settable in `Toml { }`: `serializersModule`, `explicitNulls`, `classDiscriminator`, `indentation` (`TomlIndentation`), `itemsPerLineInBlockArray`, `uppercaseInteger`, `ignoreUnknownKeys`.

### Platform split: `expect`/`actual` and source-set hierarchy

The source-set graph is non-default. There is an intermediate `kotlinxMain` set that `dependsOn(commonMain)` and adds a dependency on `kotlinx-datetime`; **every target except JVM** (`jsMain`, all native, `wasmJsMain`) depends on `kotlinxMain`. JVM instead uses `java.time`. This is how date-time `expect`/`actual` types are backed differently per platform:

- `NativeDateTime.common.kt` declares `expect` typealiases (`NativeLocalDateTime`, `NativeOffsetDateTime`, `NativeLocalDate`, `NativeLocalTime`).
- `jvmMain/NativeDateTime.jvm.kt` maps them to `java.time.*`.
- `kotlinxMain/NativeDateTime.kotlinx.kt` maps them to `kotlinx.datetime.*` (note `TomlOffsetDateTime` ↔ `Instant`).

`TomlDateTime.kt` exposes the public `TomlLocalDateTime`/etc. as the serializable intermediate, with `TomlLiteral(...)` ↔ `toLocalDateTime()` conversions.

Stream/reader/writer I/O is also platform-aware: `TomlReader`/`TomlWriter` are common abstractions; `jvmMain` adds `TomlStreams.kt`, `TomlNativeReader.kt`, `TomlNativeWriter.kt` for `InputStream`/`OutputStream` support.

## Conventions

- The module uses `explicitApi()` — every public declaration needs an explicit visibility modifier and the compiler enforces it.
- Numerous opt-ins are enabled project-wide in `core/build.gradle.kts` (contracts, `ExperimentalSerializationApi`, `InternalSerializationApi`, the internal `@TomlSpecific` marker). When you hit an opt-in error, prefer adding to the existing `languageSettings` block over scattering `@OptIn`.
- Copyright header (Apache-2.0, "Copyright 2026 Loney Chou") is present on every source file — keep it on new files.
- Version, group, and dependency versions are centralized in `gradle.properties`.
31 changes: 15 additions & 16 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,35 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm")
kotlin("plugin.serialization")
kotlin("plugin.allopen")
kotlin("kapt")
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.allopen)
alias(libs.plugins.kotlin.kapt)

id("me.champeau.jmh")
alias(libs.plugins.jmh)
}

dependencies {
jmh("org.openjdk.jmh:jmh-core:1.36")
kaptJmh("org.openjdk.jmh:jmh-generator-annprocess:1.36")
jmh(libs.jmh.core)
kaptJmh(libs.jmh.generator.annprocess)

// tomlkt
jmh(project(":core"))
// toml4j
jmh("com.moandjiezana.toml:toml4j:0.7.2")
jmh(libs.toml4j)
// ktoml
jmh("com.akuleshov7:ktoml-core:0.5.0")
jmh(libs.ktoml.core)
// jackson
jmh("com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.15.1")
jmh("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.1")
jmh("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.1")
jmh(libs.jackson.dataformat.toml)
jmh(libs.jackson.module.kotlin)
jmh(libs.jackson.datatype.jsr310)
// night config
jmh("com.electronwill.night-config:toml:3.6.0")
jmh(libs.night.config.toml)
// tomlj
jmh("org.tomlj:tomlj:1.1.0")
jmh(libs.tomlj)

// official JSON
val serializationVersion: String by rootProject
jmh("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
jmh(libs.kotlinx.serialization.json)
}

jmh {
Expand Down
17 changes: 9 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
plugins {
kotlin("multiplatform") apply false
kotlin("plugin.serialization") apply false
kotlin("plugin.allopen") apply false
kotlin("kapt") apply false
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.allopen) apply false
alias(libs.plugins.kotlin.kapt) apply false

id("org.jetbrains.dokka") apply false
alias(libs.plugins.dokka) apply false

id("io.gitlab.arturbosch.detekt") apply false
id("me.champeau.jmh") apply false
alias(libs.plugins.detekt) apply false
alias(libs.plugins.jmh) apply false

id("com.vanniktech.maven.publish") apply false
alias(libs.plugins.maven.publish) apply false
}

allprojects {
Expand Down
21 changes: 9 additions & 12 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import java.net.URI
// Plugins

plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("org.jetbrains.dokka")
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.dokka)

id("io.gitlab.arturbosch.detekt")
alias(libs.plugins.detekt)

id("com.vanniktech.maven.publish")
alias(libs.plugins.maven.publish)
}

// Archives Metadata
Expand Down Expand Up @@ -55,9 +55,6 @@ kotlin {
}

sourceSets {
val serializationVersion: String by rootProject
val datetimeVersion: String by rootProject

applyDefaultHierarchyTemplate()

all {
Expand All @@ -75,7 +72,7 @@ kotlin {

val commonMain by getting {
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
api(libs.kotlinx.serialization.core)
}
}

Expand All @@ -89,17 +86,17 @@ kotlin {
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit5"))
implementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
implementation(libs.junit.jupiter.api)

runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
runtimeOnly(libs.junit.jupiter.engine)
}
}

val kotlinxMain by creating {
dependsOn(commonMain)

dependencies {
api("org.jetbrains.kotlinx:kotlinx-datetime:$datetimeVersion")
api(libs.kotlinx.datetime)
}
}

Expand Down
14 changes: 0 additions & 14 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,3 @@ org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn = true
group = dev.eav.tomlkt
archivesName = tomlkt
version = 0.6.0

# Dependencies

kotlinVersion = 2.2.0

serializationVersion = 1.9.0
datetimeVersion = 0.7.0

detektVersion = 1.23.8
jmhVersion = 0.7.2

dokkaVersion = 2.0.0

deployerVersion = 0.33.0
49 changes: 49 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[versions]
kotlin = "2.2.0"
kotlinxSerialization = "1.9.0"
kotlinxDatetime = "0.7.0"
junit = "5.6.0"

# Benchmark-only libraries
jmh = "1.36"
toml4j = "0.7.2"
ktoml = "0.5.0"
jackson = "2.15.1"
nightConfig = "3.6.0"
tomlj = "1.1.0"

# Plugins
dokka = "2.0.0"
detekt = "1.23.8"
jmhPlugin = "0.7.2"
mavenPublish = "0.33.0"

[libraries]
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }

junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }

# Benchmark-only libraries
jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
toml4j = { module = "com.moandjiezana.toml:toml4j", version.ref = "toml4j" }
ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" }
jackson-dataformat-toml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-toml", version.ref = "jackson" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
night-config-toml = { module = "com.electronwill.night-config:toml", version.ref = "nightConfig" }
tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
jmh = { id = "me.champeau.jmh", version.ref = "jmhPlugin" }
maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
19 changes: 0 additions & 19 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,6 @@ pluginManagement {
gradlePluginPortal()
mavenCentral()
}

plugins {
val kotlinVersion: String by settings
kotlin("multiplatform") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
kotlin("plugin.allopen") version kotlinVersion
kotlin("kapt") version kotlinVersion

val dokkaVersion: String by settings
id("org.jetbrains.dokka") version dokkaVersion

val detektVersion: String by settings
id("io.gitlab.arturbosch.detekt") version detektVersion
val jmhVersion: String by settings
id("me.champeau.jmh") version jmhVersion

val deployerVersion: String by settings
id("com.vanniktech.maven.publish") version deployerVersion
}
}

include(":core")
Expand Down