From d60c55766d41c97ad4d0a23750ca754169432002 Mon Sep 17 00:00:00 2001 From: Sean Proctor Date: Thu, 4 Jun 2026 11:27:49 -0400 Subject: [PATCH 1/3] Add Claude agent file --- CLAUDE.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6a9d6a6 --- /dev/null +++ b/CLAUDE.md @@ -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`. From c73a8849f7ff6d9c5f834dfb7e4267d61b49c4ca Mon Sep 17 00:00:00 2001 From: Sean Proctor Date: Thu, 4 Jun 2026 16:38:09 -0400 Subject: [PATCH 2/3] move dependencies into versions catalog --- benchmark/build.gradle.kts | 31 ++++++++++++------------ build.gradle.kts | 17 ++++++------- core/build.gradle.kts | 21 +++++++--------- gradle.properties | 14 +---------- gradle/libs.versions.toml | 49 ++++++++++++++++++++++++++++++++++++++ settings.gradle.kts | 19 --------------- 6 files changed, 83 insertions(+), 68 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index a1cd29e..08e7008 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -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 { diff --git a/build.gradle.kts b/build.gradle.kts index baf0c29..faf4551 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 72dab52..412eb34 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -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 @@ -55,9 +55,6 @@ kotlin { } sourceSets { - val serializationVersion: String by rootProject - val datetimeVersion: String by rootProject - applyDefaultHierarchyTemplate() all { @@ -75,7 +72,7 @@ kotlin { val commonMain by getting { dependencies { - api("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") + api(libs.kotlinx.serialization.core) } } @@ -89,9 +86,9 @@ 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) } } @@ -99,7 +96,7 @@ kotlin { dependsOn(commonMain) dependencies { - api("org.jetbrains.kotlinx:kotlinx-datetime:$datetimeVersion") + api(libs.kotlinx.datetime) } } diff --git a/gradle.properties b/gradle.properties index 36e4fe4..1a39dac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,16 +17,4 @@ 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 +# Dependency and plugin versions are managed in gradle/libs.versions.toml. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..1d746ed --- /dev/null +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index a4b2ec7..e8112b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") From 34dc93d16f62dba8dbe137f378acad76ece93f0e Mon Sep 17 00:00:00 2001 From: Sean Proctor Date: Thu, 4 Jun 2026 16:43:48 -0400 Subject: [PATCH 3/3] remove comment about version move --- gradle.properties | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1a39dac..73ad64c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,5 +16,3 @@ org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn = true group = dev.eav.tomlkt archivesName = tomlkt version = 0.6.0 - -# Dependency and plugin versions are managed in gradle/libs.versions.toml.