Initial serde-kdl crate
Serde serialization/deserialization for KDL, built on kdl 6.5 (KDL v2 spec).
Public API:
- from_str / from_doc: deserialize from KDL string or KdlDocument
- to_string / to_string_pretty / to_doc: serialize to KDL
Mapping rules:
- Struct fields <-> nodes (field_name value)
- Nested structs <-> children blocks
- Vec<primitive> <-> multiple arguments on one node
- Vec<struct> <-> repeated nodes with same name
- Tuples <-> multiple arguments
- Option::None <-> absent node
- Enums: unit variants as strings, struct/newtype/tuple via child nodes
- HashMap/BTreeMap <-> same as structs
- Dash (-) children convention supported for deserialization
31 integration tests + 1 doctest, all passing.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb5a316
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/.ramekin/Dockerfile b/.ramekin/Dockerfile
new file mode 100644
index 0000000..92b1a5e
--- /dev/null
+++ b/.ramekin/Dockerfile
@@ -0,0 +1,21 @@
+FROM ramekin-agent
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ bc \
+ && rm -rf /var/lib/apt/lists/*
+
+# Rust nightly (edition 2024)
+ENV RUSTUP_HOME=/usr/local/rustup \
+ CARGO_HOME=/usr/local/cargo \
+ PATH="/usr/local/cargo/bin:$PATH"
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
+ | sh -s -- -y --default-toolchain nightly --profile minimal \
+ && rustup component add llvm-tools-preview \
+ && cargo install grcov
+
+
+ENV RANGER_DEFAULT_BACKLOG=ranger
+
+WORKDIR /workspace
+
+ENTRYPOINT ["pi"]
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..2ad3f03
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,224 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "kdl"
+version = "6.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e"
+dependencies = [
+ "miette",
+ "num",
+ "winnow",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "miette"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
+dependencies = [
+ "cfg-if",
+ "unicode-width",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-kdl"
+version = "0.1.0"
+dependencies = [
+ "kdl",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "winnow"
+version = "0.6.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
+dependencies = [
+ "memchr",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..28b8fac
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "serde-kdl"
+version = "0.1.0"
+edition = "2021"
+description = "Serde integration for KDL (KDL Document Language)"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/kdl-org/kdl-rs"
+keywords = ["kdl", "serde", "serialization", "config"]
+categories = ["config", "encoding"]
+
+[dependencies]
+kdl = "6.5"
+serde = { version = "1", features = ["derive"] }
+thiserror = "2"
+
+[dev-dependencies]
+serde = { version = "1", features = ["derive"] }
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..83577c2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,166 @@
+# serde-kdl
+
+[Serde](https://serde.rs) integration for [KDL](https://kdl.dev) (KDL Document Language).
+
+Built on top of [`kdl`](https://crates.io/crates/kdl) v6.5 (KDL v2 spec).
+
+## Quick Start
+
+```rust
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Config {
+ title: String,
+ count: i32,
+ enabled: bool,
+}
+
+let kdl_input = r#"
+title "My App"
+count 42
+enabled #true
+"#;
+
+// Deserialize
+let config: Config = serde_kdl::from_str(kdl_input).unwrap();
+
+// Serialize
+let output = serde_kdl::to_string(&config).unwrap();
+
+// Roundtrip
+let roundtrip: Config = serde_kdl::from_str(&output).unwrap();
+assert_eq!(config, roundtrip);
+```
+
+## Mapping Rules
+
+### Structs → Nodes
+
+Each struct field becomes a KDL node. The node name is the field name, and the
+value is the first argument.
+
+```kdl
+title "My App"
+count 42
+enabled #true
+```
+
+### Nested Structs → Children Blocks
+
+```kdl
+server {
+ host "localhost"
+ port 8080
+}
+```
+
+### Vec of Primitives → Multiple Arguments
+
+```kdl
+tags "web" "rust" "config"
+```
+
+### Vec of Structs → Repeated Nodes
+
+```kdl
+server {
+ host "localhost"
+ port 8080
+}
+server {
+ host "example.com"
+ port 443
+}
+```
+
+### Dash Children Convention
+
+For deserialization, the `-` (dash) node name convention is supported:
+
+```kdl
+items {
+ - 1
+ - 2
+ - 3
+}
+```
+
+### Tuples → Multiple Arguments
+
+```kdl
+point 1.0 2.0 3.0
+```
+
+### Option
+
+`None` is represented by the absence of a node. Serialization omits `None`
+fields entirely. A `#null` argument also deserializes as `None`.
+
+### Enums
+
+**Unit variants** serialize as strings:
+
+```kdl
+color "Red"
+```
+
+**Newtype variants** use the variant name as a child node name:
+
+```kdl
+value {
+ Text "hello"
+}
+```
+
+**Struct variants** use the variant name as a child node with a children block:
+
+```kdl
+shape {
+ Circle {
+ radius 5.0
+ }
+}
+```
+
+**Tuple variants** use the variant name as a child node with multiple arguments:
+
+```kdl
+data {
+ Point 1.0 2.0 3.0
+}
+```
+
+### HashMap / BTreeMap
+
+Maps serialize identically to structs — each key becomes a node name:
+
+```kdl
+settings {
+ key1 "value1"
+ key2 "value2"
+}
+```
+
+## API
+
+```rust
+// Deserialize from string
+let config: Config = serde_kdl::from_str(kdl_str)?;
+
+// Deserialize from KdlDocument
+let config: Config = serde_kdl::from_doc(&doc)?;
+
+// Serialize to string
+let s: String = serde_kdl::to_string(&config)?;
+
+// Serialize to string (auto-formatted)
+let s: String = serde_kdl::to_string_pretty(&config)?;
+
+// Serialize to KdlDocument
+let doc: kdl::KdlDocument = serde_kdl::to_doc(&config)?;
+```
+
+## License
+
+MIT OR Apache-2.0
diff --git a/src/de.rs b/src/de.rs
new file mode 100644
index 0000000..d38eae3
--- /dev/null
+++ b/src/de.rs
@@ -0,0 +1,1258 @@
+//! Deserializer implementation for KDL.
+//!
+//! # Mapping Rules
+//!
+//! - **Struct/Map** ↔ KDL document (or children block). Each node name is a key.
+//! - **Primitives** ↔ A node's first argument: `name "value"` or `count 42`.
+//! - **Nested struct** ↔ A node's children block: `nested { field "val" }`.
+//! - **Vec/sequence** ↔ Either multiple nodes with the same name, or a single
+//! node with multiple arguments, or `-` children convention.
+//! - **Option** ↔ Absent node = `None`, present node = `Some`.
+//! - **Enum (unit variant)** ↔ String argument: `color "Red"`.
+//! - **Enum (newtype/tuple/struct)** ↔ Child node named after variant:
+//! `shape { Circle { radius 5.0 } }`.
+
+use kdl::{KdlDocument, KdlNode, KdlValue};
+use serde::de::{self, DeserializeSeed, IntoDeserializer, MapAccess, SeqAccess, Visitor};
+use serde::Deserialize;
+
+use crate::error::{Error, Result};
+
+/// Deserialize a type from a KDL string.
+pub fn from_str<'de, T: Deserialize<'de>>(s: &'de str) -> Result<T> {
+ let doc: KdlDocument = s.parse()?;
+ from_doc(&doc)
+}
+
+/// Deserialize a type from a [`KdlDocument`].
+pub fn from_doc<'de, T: Deserialize<'de>>(doc: &KdlDocument) -> Result<T> {
+ let de = DocumentDeserializer::new(doc);
+ T::deserialize(de)
+}
+
+// ---------------------------------------------------------------------------
+// DocumentDeserializer: deserializes a KdlDocument as a struct/map
+// ---------------------------------------------------------------------------
+
+struct DocumentDeserializer<'a> {
+ doc: &'a KdlDocument,
+}
+
+impl<'a> DocumentDeserializer<'a> {
+ fn new(doc: &'a KdlDocument) -> Self {
+ Self { doc }
+ }
+}
+
+impl<'de, 'a> de::Deserializer<'de> for DocumentDeserializer<'a> {
+ type Error = Error;
+
+ fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_map(visitor)
+ }
+
+ fn deserialize_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value> {
+ visitor.visit_map(DocumentMapAccess::new(self.doc))
+ }
+
+ fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_map(DocumentMapAccess::new(self.doc))
+ }
+
+ fn deserialize_newtype_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value> {
+ visitor.visit_newtype_struct(self)
+ }
+
+ fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+
+ fn deserialize_unit_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+
+ fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+
+ serde::forward_to_deserialize_any! {
+ bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64
+ char str string bytes byte_buf option
+ seq tuple tuple_struct enum identifier
+ }
+}
+
+// ---------------------------------------------------------------------------
+// DocumentMapAccess: iterates over unique node names in a document
+// ---------------------------------------------------------------------------
+
+struct DocumentMapAccess<'a> {
+ doc: &'a KdlDocument,
+ /// Unique node names, in order of first appearance
+ keys: Vec<&'a str>,
+ index: usize,
+}
+
+impl<'a> DocumentMapAccess<'a> {
+ fn new(doc: &'a KdlDocument) -> Self {
+ let mut keys = Vec::new();
+ let mut seen = std::collections::HashSet::new();
+ for node in doc.nodes() {
+ let name = node.name().value();
+ if seen.insert(name) {
+ keys.push(name);
+ }
+ }
+ Self {
+ doc,
+ keys,
+ index: 0,
+ }
+ }
+}
+
+impl<'de, 'a> MapAccess<'de> for DocumentMapAccess<'a> {
+ type Error = Error;
+
+ fn next_key_seed<K: DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
+ if self.index >= self.keys.len() {
+ return Ok(None);
+ }
+ let key = self.keys[self.index];
+ seed.deserialize(key.into_deserializer()).map(Some)
+ }
+
+ fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
+ let key = self.keys[self.index];
+ self.index += 1;
+
+ // Collect all nodes with this name
+ let nodes: Vec<&KdlNode> = self
+ .doc
+ .nodes()
+ .iter()
+ .filter(|n| n.name().value() == key)
+ .collect();
+
+ seed.deserialize(FieldDeserializer::new(nodes))
+ }
+}
+
+// ---------------------------------------------------------------------------
+// FieldDeserializer: deserializes all nodes matching a field name
+// ---------------------------------------------------------------------------
+
+struct FieldDeserializer<'a> {
+ nodes: Vec<&'a KdlNode>,
+}
+
+impl<'a> FieldDeserializer<'a> {
+ fn new(nodes: Vec<&'a KdlNode>) -> Self {
+ Self { nodes }
+ }
+
+ fn first_node(&self) -> Result<&'a KdlNode> {
+ self.nodes
+ .first()
+ .copied()
+ .ok_or_else(|| Error::Message("expected at least one node".into()))
+ }
+
+ fn first_arg(&self) -> Result<&'a KdlValue> {
+ let node = self.first_node()?;
+ first_arg_of(node)
+ }
+}
+
+fn first_arg_of<'a>(node: &'a KdlNode) -> Result<&'a KdlValue> {
+ node.get(0).ok_or_else(|| {
+ Error::TypeMismatch {
+ expected: "a value argument",
+ got: format!("node '{}' with no arguments", node.name().value()),
+ }
+ })
+}
+
+/// Helper: get all positional arguments from a node (entries with no name).
+fn node_args(node: &KdlNode) -> Vec<&KdlValue> {
+ node.entries()
+ .iter()
+ .filter(|e| e.name().is_none())
+ .map(|e| e.value())
+ .collect()
+}
+
+/// Helper: get all property entries from a node (entries with a name).
+fn node_props(node: &KdlNode) -> Vec<(&str, &KdlValue)> {
+ node.entries()
+ .iter()
+ .filter_map(|e| e.name().map(|n| (n.value(), e.value())))
+ .collect()
+}
+
+impl<'de, 'a> de::Deserializer<'de> for FieldDeserializer<'a> {
+ type Error = Error;
+
+ fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ let node = self.first_node()?;
+ let args = node_args(node);
+ let props = node_props(node);
+
+ if node.children().is_some() {
+ // Has children → treat as struct/map
+ self.deserialize_map(visitor)
+ } else if !props.is_empty() {
+ // Has properties → treat as map
+ self.deserialize_map(visitor)
+ } else if args.len() == 1 {
+ // Single argument → deserialize as value
+ ValueDeserializer::new(args[0]).deserialize_any(visitor)
+ } else if args.len() > 1 {
+ // Multiple arguments → deserialize as seq
+ self.deserialize_seq(visitor)
+ } else {
+ // No args, no children, no props → null/unit
+ visitor.visit_unit()
+ }
+ }
+
+ // -- Primitives: delegate to first argument --
+
+ fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_bool(visitor)
+ }
+
+ fn deserialize_i8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_i8(visitor)
+ }
+
+ fn deserialize_i16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_i16(visitor)
+ }
+
+ fn deserialize_i32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_i32(visitor)
+ }
+
+ fn deserialize_i64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_i64(visitor)
+ }
+
+ fn deserialize_i128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_i128(visitor)
+ }
+
+ fn deserialize_u8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_u8(visitor)
+ }
+
+ fn deserialize_u16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_u16(visitor)
+ }
+
+ fn deserialize_u32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_u32(visitor)
+ }
+
+ fn deserialize_u64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_u64(visitor)
+ }
+
+ fn deserialize_u128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_u128(visitor)
+ }
+
+ fn deserialize_f32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_f32(visitor)
+ }
+
+ fn deserialize_f64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_f64(visitor)
+ }
+
+ fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_char(visitor)
+ }
+
+ fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_str(visitor)
+ }
+
+ fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(self.first_arg()?).deserialize_string(visitor)
+ }
+
+ fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // If we got here, the node exists, so it's Some.
+ // None is handled at the map level by the absence of a node.
+ let node = self.first_node()?;
+ let args = node_args(node);
+ // If there's a single argument and it's null, treat as None
+ if args.len() == 1 && args[0].is_null() && node.children().is_none() {
+ visitor.visit_none()
+ } else {
+ visitor.visit_some(self)
+ }
+ }
+
+ fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+
+ fn deserialize_unit_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+
+ fn deserialize_newtype_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value> {
+ visitor.visit_newtype_struct(self)
+ }
+
+ fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // Strategy:
+ // 1. Multiple nodes with same name → each node is an element
+ // 2. Single node with `-` children → each `-` child is an element
+ // 3. Single node with multiple args → each arg is an element
+ // 4. Single node with non-`-` children → children nodes are elements
+
+ if self.nodes.len() > 1 {
+ // Multiple nodes → each node contributes one element
+ visitor.visit_seq(MultiNodeSeqAccess {
+ nodes: self.nodes,
+ index: 0,
+ })
+ } else {
+ let node = self.first_node()?;
+
+ // Check for `-` children convention
+ if let Some(children) = node.children() {
+ let dash_nodes: Vec<&KdlNode> = children
+ .nodes()
+ .iter()
+ .filter(|n| n.name().value() == "-")
+ .collect();
+ if !dash_nodes.is_empty() {
+ return visitor.visit_seq(MultiNodeSeqAccess {
+ nodes: dash_nodes,
+ index: 0,
+ });
+ }
+ // Non-dash children: treat each child as an element
+ let child_nodes: Vec<&KdlNode> = children.nodes().iter().collect();
+ if !child_nodes.is_empty() {
+ return visitor.visit_seq(ChildNodeSeqAccess {
+ nodes: child_nodes,
+ index: 0,
+ });
+ }
+ }
+
+ // Arguments as elements
+ let args = node_args(node);
+ visitor.visit_seq(ArgsSeqAccess {
+ args,
+ index: 0,
+ })
+ }
+ }
+
+ fn deserialize_tuple<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_tuple_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _len: usize,
+ visitor: V,
+ ) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ let node = self.first_node()?;
+ // If node has children, deserialize from children document
+ if let Some(children) = node.children() {
+ return visitor.visit_map(DocumentMapAccess::new(children));
+ }
+ // If node has properties, deserialize from properties
+ let props = node_props(node);
+ if !props.is_empty() {
+ return visitor.visit_map(PropsMapAccess {
+ props,
+ index: 0,
+ });
+ }
+ // Empty map
+ visitor.visit_map(PropsMapAccess {
+ props: vec![],
+ index: 0,
+ })
+ }
+
+ fn deserialize_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value> {
+ let node = self.first_node()?;
+
+ // If node has children, deserialize struct from children
+ if let Some(children) = node.children() {
+ return visitor.visit_map(DocumentMapAccess::new(children));
+ }
+
+ // If node has properties, use them as struct fields
+ let props = node_props(node);
+ if !props.is_empty() {
+ return visitor.visit_map(PropsMapAccess {
+ props,
+ index: 0,
+ });
+ }
+
+ // If struct has a single field and node has a single argument,
+ // try to match argument to the sole field
+ let args = node_args(node);
+ if fields.len() == 1 && args.len() == 1 {
+ return visitor.visit_map(SingleArgStructAccess {
+ field_name: fields[0],
+ value: args[0],
+ done: false,
+ });
+ }
+
+ // Empty
+ visitor.visit_map(PropsMapAccess {
+ props: vec![],
+ index: 0,
+ })
+ }
+
+ fn deserialize_enum<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value> {
+ let node = self.first_node()?;
+ let args = node_args(node);
+
+ // Unit variant: first argument is variant name string
+ // e.g., `color "Red"`
+ if !args.is_empty() && node.children().is_none() {
+ if let Some(s) = args[0].as_string() {
+ return visitor.visit_enum(EnumUnitAccess {
+ variant: s,
+ node,
+ arg_offset: 1,
+ });
+ }
+ }
+
+ // Complex variant: child node named after variant
+ // e.g., `shape { Circle { radius 5.0 } }`
+ if let Some(children) = node.children() {
+ let child_nodes = children.nodes();
+ if child_nodes.len() == 1 {
+ let variant_node = &child_nodes[0];
+ return visitor.visit_enum(EnumComplexAccess {
+ variant_name: variant_node.name().value(),
+ variant_node,
+ });
+ }
+ }
+
+ Err(Error::Message(format!(
+ "cannot deserialize enum from node '{}'",
+ node.name().value()
+ )))
+ }
+
+ fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_str(visitor)
+ }
+
+ fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+}
+
+// ---------------------------------------------------------------------------
+// SeqAccess implementations
+// ---------------------------------------------------------------------------
+
+/// Seq from multiple nodes with the same name. Each node is deserialized as an element.
+struct MultiNodeSeqAccess<'a> {
+ nodes: Vec<&'a KdlNode>,
+ index: usize,
+}
+
+impl<'de, 'a> SeqAccess<'de> for MultiNodeSeqAccess<'a> {
+ type Error = Error;
+
+ fn next_element_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<Option<T::Value>> {
+ if self.index >= self.nodes.len() {
+ return Ok(None);
+ }
+ let node = self.nodes[self.index];
+ self.index += 1;
+ seed.deserialize(NodeContentDeserializer::new(node))
+ .map(Some)
+ }
+}
+
+/// Seq from child nodes (non-dash). Each child node is an element deserialized
+/// by its content (children or first arg).
+struct ChildNodeSeqAccess<'a> {
+ nodes: Vec<&'a KdlNode>,
+ index: usize,
+}
+
+impl<'de, 'a> SeqAccess<'de> for ChildNodeSeqAccess<'a> {
+ type Error = Error;
+
+ fn next_element_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<Option<T::Value>> {
+ if self.index >= self.nodes.len() {
+ return Ok(None);
+ }
+ let node = self.nodes[self.index];
+ self.index += 1;
+ seed.deserialize(NodeContentDeserializer::new(node))
+ .map(Some)
+ }
+}
+
+/// Seq from a node's positional arguments.
+struct ArgsSeqAccess<'a> {
+ args: Vec<&'a KdlValue>,
+ index: usize,
+}
+
+impl<'de, 'a> SeqAccess<'de> for ArgsSeqAccess<'a> {
+ type Error = Error;
+
+ fn next_element_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<Option<T::Value>> {
+ if self.index >= self.args.len() {
+ return Ok(None);
+ }
+ let val = self.args[self.index];
+ self.index += 1;
+ seed.deserialize(ValueDeserializer::new(val)).map(Some)
+ }
+}
+
+// ---------------------------------------------------------------------------
+// PropsMapAccess: iterate over node properties as key-value pairs
+// ---------------------------------------------------------------------------
+
+struct PropsMapAccess<'a> {
+ props: Vec<(&'a str, &'a KdlValue)>,
+ index: usize,
+}
+
+impl<'de, 'a> MapAccess<'de> for PropsMapAccess<'a> {
+ type Error = Error;
+
+ fn next_key_seed<K: DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
+ if self.index >= self.props.len() {
+ return Ok(None);
+ }
+ let (key, _) = self.props[self.index];
+ seed.deserialize(key.into_deserializer()).map(Some)
+ }
+
+ fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
+ let (_, val) = self.props[self.index];
+ self.index += 1;
+ seed.deserialize(ValueDeserializer::new(val))
+ }
+}
+
+// ---------------------------------------------------------------------------
+// SingleArgStructAccess: a struct with one field from a node's single arg
+// ---------------------------------------------------------------------------
+
+struct SingleArgStructAccess<'a> {
+ field_name: &'static str,
+ value: &'a KdlValue,
+ done: bool,
+}
+
+impl<'de, 'a> MapAccess<'de> for SingleArgStructAccess<'a> {
+ type Error = Error;
+
+ fn next_key_seed<K: DeserializeSeed<'de>>(&mut self, seed: K) -> Result<Option<K::Value>> {
+ if self.done {
+ return Ok(None);
+ }
+ seed.deserialize(self.field_name.into_deserializer())
+ .map(Some)
+ }
+
+ fn next_value_seed<V: DeserializeSeed<'de>>(&mut self, seed: V) -> Result<V::Value> {
+ self.done = true;
+ seed.deserialize(ValueDeserializer::new(self.value))
+ }
+}
+
+// ---------------------------------------------------------------------------
+// NodeContentDeserializer: deserializes from a single node's content
+// ---------------------------------------------------------------------------
+
+/// Deserializes a single node's "content" - its arguments, properties, and children.
+/// Used when a node represents a single value in a sequence.
+struct NodeContentDeserializer<'a> {
+ node: &'a KdlNode,
+}
+
+impl<'a> NodeContentDeserializer<'a> {
+ fn new(node: &'a KdlNode) -> Self {
+ Self { node }
+ }
+}
+
+impl<'de, 'a> de::Deserializer<'de> for NodeContentDeserializer<'a> {
+ type Error = Error;
+
+ fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ let args = node_args(self.node);
+ let props = node_props(self.node);
+
+ if self.node.children().is_some() {
+ self.deserialize_map(visitor)
+ } else if !props.is_empty() {
+ self.deserialize_map(visitor)
+ } else if args.len() == 1 {
+ ValueDeserializer::new(args[0]).deserialize_any(visitor)
+ } else if args.len() > 1 {
+ self.deserialize_seq(visitor)
+ } else {
+ visitor.visit_unit()
+ }
+ }
+
+ fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_bool(visitor)
+ }
+
+ fn deserialize_i8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i8(visitor)
+ }
+
+ fn deserialize_i16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i16(visitor)
+ }
+
+ fn deserialize_i32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i32(visitor)
+ }
+
+ fn deserialize_i64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i64(visitor)
+ }
+
+ fn deserialize_i128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_i128(visitor)
+ }
+
+ fn deserialize_u8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u8(visitor)
+ }
+
+ fn deserialize_u16<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u16(visitor)
+ }
+
+ fn deserialize_u32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u32(visitor)
+ }
+
+ fn deserialize_u64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u64(visitor)
+ }
+
+ fn deserialize_u128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_u128(visitor)
+ }
+
+ fn deserialize_f32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_f32(visitor)
+ }
+
+ fn deserialize_f64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_f64(visitor)
+ }
+
+ fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_char(visitor)
+ }
+
+ fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_str(visitor)
+ }
+
+ fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ ValueDeserializer::new(first_arg_of(self.node)?).deserialize_string(visitor)
+ }
+
+ fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ let args = node_args(self.node);
+ if args.len() == 1 && args[0].is_null() && self.node.children().is_none() {
+ visitor.visit_none()
+ } else {
+ visitor.visit_some(self)
+ }
+ }
+
+ fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+
+ fn deserialize_unit_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+
+ fn deserialize_newtype_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value> {
+ visitor.visit_newtype_struct(self)
+ }
+
+ fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ // Check for `-` children convention first
+ if let Some(children) = self.node.children() {
+ let dash_nodes: Vec<&KdlNode> = children
+ .nodes()
+ .iter()
+ .filter(|n| n.name().value() == "-")
+ .collect();
+ if !dash_nodes.is_empty() {
+ return visitor.visit_seq(MultiNodeSeqAccess {
+ nodes: dash_nodes,
+ index: 0,
+ });
+ }
+ // Non-dash children
+ let child_nodes: Vec<&KdlNode> = children.nodes().iter().collect();
+ if !child_nodes.is_empty() {
+ return visitor.visit_seq(ChildNodeSeqAccess {
+ nodes: child_nodes,
+ index: 0,
+ });
+ }
+ }
+ // Fall back to arguments
+ let args = node_args(self.node);
+ visitor.visit_seq(ArgsSeqAccess { args, index: 0 })
+ }
+
+ fn deserialize_tuple<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_tuple_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _len: usize,
+ visitor: V,
+ ) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ if let Some(children) = self.node.children() {
+ return visitor.visit_map(DocumentMapAccess::new(children));
+ }
+ let props = node_props(self.node);
+ visitor.visit_map(PropsMapAccess {
+ props,
+ index: 0,
+ })
+ }
+
+ fn deserialize_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value> {
+ if let Some(children) = self.node.children() {
+ return visitor.visit_map(DocumentMapAccess::new(children));
+ }
+ let props = node_props(self.node);
+ if !props.is_empty() {
+ return visitor.visit_map(PropsMapAccess {
+ props,
+ index: 0,
+ });
+ }
+ // Single-arg struct
+ let args = node_args(self.node);
+ if fields.len() == 1 && args.len() == 1 {
+ return visitor.visit_map(SingleArgStructAccess {
+ field_name: fields[0],
+ value: args[0],
+ done: false,
+ });
+ }
+ visitor.visit_map(PropsMapAccess {
+ props: vec![],
+ index: 0,
+ })
+ }
+
+ fn deserialize_enum<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value> {
+ let args = node_args(self.node);
+
+ // Unit variant: first argument is variant name
+ if !args.is_empty() && self.node.children().is_none() {
+ if let Some(s) = args[0].as_string() {
+ return visitor.visit_enum(EnumUnitAccess {
+ variant: s,
+ node: self.node,
+ arg_offset: 1,
+ });
+ }
+ }
+
+ // Complex variant: child node named after variant
+ if let Some(children) = self.node.children() {
+ let child_nodes = children.nodes();
+ if child_nodes.len() == 1 {
+ let variant_node = &child_nodes[0];
+ return visitor.visit_enum(EnumComplexAccess {
+ variant_name: variant_node.name().value(),
+ variant_node,
+ });
+ }
+ }
+
+ Err(Error::Message(format!(
+ "cannot deserialize enum from node '{}'",
+ self.node.name().value()
+ )))
+ }
+
+ fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_str(visitor)
+ }
+
+ fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+}
+
+// ---------------------------------------------------------------------------
+// ValueDeserializer: deserializes a KdlValue as a primitive
+// ---------------------------------------------------------------------------
+
+struct ValueDeserializer<'a> {
+ value: &'a KdlValue,
+}
+
+impl<'a> ValueDeserializer<'a> {
+ fn new(value: &'a KdlValue) -> Self {
+ Self { value }
+ }
+
+ fn to_i128(&self) -> Result<i128> {
+ match self.value {
+ KdlValue::Integer(i) => Ok(*i),
+ KdlValue::Float(f) => Ok(*f as i128),
+ other => Err(Error::TypeMismatch {
+ expected: "integer",
+ got: format!("{other:?}"),
+ }),
+ }
+ }
+
+ fn to_f64(&self) -> Result<f64> {
+ match self.value {
+ KdlValue::Float(f) => Ok(*f),
+ KdlValue::Integer(i) => Ok(*i as f64),
+ other => Err(Error::TypeMismatch {
+ expected: "float",
+ got: format!("{other:?}"),
+ }),
+ }
+ }
+}
+
+macro_rules! deserialize_integer {
+ ($method:ident, $visit:ident, $ty:ty) => {
+ fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ let i = self.to_i128()?;
+ let val: $ty = i.try_into().map_err(|_| Error::IntegerOutOfRange(i))?;
+ visitor.$visit(val)
+ }
+ };
+}
+
+impl<'de, 'a> de::Deserializer<'de> for ValueDeserializer<'a> {
+ type Error = Error;
+
+ fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ match self.value {
+ KdlValue::String(s) => visitor.visit_str(s),
+ KdlValue::Integer(i) => visitor.visit_i128(*i),
+ KdlValue::Float(f) => visitor.visit_f64(*f),
+ KdlValue::Bool(b) => visitor.visit_bool(*b),
+ KdlValue::Null => visitor.visit_none(),
+ }
+ }
+
+ fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ match self.value {
+ KdlValue::Bool(b) => visitor.visit_bool(*b),
+ other => Err(Error::TypeMismatch {
+ expected: "bool",
+ got: format!("{other:?}"),
+ }),
+ }
+ }
+
+ deserialize_integer!(deserialize_i8, visit_i8, i8);
+ deserialize_integer!(deserialize_i16, visit_i16, i16);
+ deserialize_integer!(deserialize_i32, visit_i32, i32);
+ deserialize_integer!(deserialize_i64, visit_i64, i64);
+ deserialize_integer!(deserialize_u8, visit_u8, u8);
+ deserialize_integer!(deserialize_u16, visit_u16, u16);
+ deserialize_integer!(deserialize_u32, visit_u32, u32);
+ deserialize_integer!(deserialize_u64, visit_u64, u64);
+
+ fn deserialize_i128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_i128(self.to_i128()?)
+ }
+
+ fn deserialize_u128<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ let i = self.to_i128()?;
+ let val: u128 = i.try_into().map_err(|_| Error::IntegerOutOfRange(i))?;
+ visitor.visit_u128(val)
+ }
+
+ fn deserialize_f32<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_f32(self.to_f64()? as f32)
+ }
+
+ fn deserialize_f64<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_f64(self.to_f64()?)
+ }
+
+ fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ match self.value {
+ KdlValue::String(s) if s.len() == 1 => {
+ visitor.visit_char(s.chars().next().unwrap())
+ }
+ other => Err(Error::TypeMismatch {
+ expected: "single character",
+ got: format!("{other:?}"),
+ }),
+ }
+ }
+
+ fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ match self.value {
+ KdlValue::String(s) => visitor.visit_str(s),
+ other => Err(Error::TypeMismatch {
+ expected: "string",
+ got: format!("{other:?}"),
+ }),
+ }
+ }
+
+ fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_str(visitor)
+ }
+
+ fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ match self.value {
+ KdlValue::String(s) => visitor.visit_bytes(s.as_bytes()),
+ other => Err(Error::TypeMismatch {
+ expected: "string (for bytes)",
+ got: format!("{other:?}"),
+ }),
+ }
+ }
+
+ fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_bytes(visitor)
+ }
+
+ fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ if self.value.is_null() {
+ visitor.visit_none()
+ } else {
+ visitor.visit_some(self)
+ }
+ }
+
+ fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ if self.value.is_null() {
+ visitor.visit_unit()
+ } else {
+ Err(Error::TypeMismatch {
+ expected: "null",
+ got: format!("{:?}", self.value),
+ })
+ }
+ }
+
+ fn deserialize_unit_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value> {
+ self.deserialize_unit(visitor)
+ }
+
+ fn deserialize_newtype_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ visitor: V,
+ ) -> Result<V::Value> {
+ visitor.visit_newtype_struct(self)
+ }
+
+ fn deserialize_seq<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value> {
+ Err(Error::TypeMismatch {
+ expected: "sequence",
+ got: format!("scalar value {:?}", self.value),
+ })
+ }
+
+ fn deserialize_tuple<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_tuple_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _len: usize,
+ visitor: V,
+ ) -> Result<V::Value> {
+ self.deserialize_seq(visitor)
+ }
+
+ fn deserialize_map<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value> {
+ Err(Error::TypeMismatch {
+ expected: "map",
+ got: format!("scalar value {:?}", self.value),
+ })
+ }
+
+ fn deserialize_struct<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _fields: &'static [&'static str],
+ _visitor: V,
+ ) -> Result<V::Value> {
+ Err(Error::TypeMismatch {
+ expected: "struct",
+ got: format!("scalar value {:?}", self.value),
+ })
+ }
+
+ fn deserialize_enum<V: Visitor<'de>>(
+ self,
+ _name: &'static str,
+ _variants: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value> {
+ // For a bare value, it must be a string naming a unit variant
+ match self.value {
+ KdlValue::String(s) => visitor.visit_enum(s.as_str().into_deserializer()),
+ other => Err(Error::TypeMismatch {
+ expected: "string (enum variant name)",
+ got: format!("{other:?}"),
+ }),
+ }
+ }
+
+ fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ self.deserialize_str(visitor)
+ }
+
+ fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+ visitor.visit_unit()
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Enum deserialization
+// ---------------------------------------------------------------------------
+
+/// For unit and newtype variants where the variant name is a string argument.
+/// e.g., `color "Red"` or `wrapper "SomeVariant" 42`
+struct EnumUnitAccess<'a> {
+ variant: &'a str,
+ node: &'a KdlNode,
+ arg_offset: usize,
+}
+
+impl<'de, 'a> de::EnumAccess<'de> for EnumUnitAccess<'a> {
+ type Error = Error;
+ type Variant = EnumUnitVariantAccess<'a>;
+
+ fn variant_seed<V: DeserializeSeed<'de>>(self, seed: V) -> Result<(V::Value, Self::Variant)> {
+ let val = seed.deserialize::<serde::de::value::StrDeserializer<'_, Error>>(
+ self.variant.into_deserializer(),
+ )?;
+ Ok((
+ val,
+ EnumUnitVariantAccess {
+ node: self.node,
+ arg_offset: self.arg_offset,
+ },
+ ))
+ }
+}
+
+struct EnumUnitVariantAccess<'a> {
+ node: &'a KdlNode,
+ arg_offset: usize,
+}
+
+impl<'de, 'a> de::VariantAccess<'de> for EnumUnitVariantAccess<'a> {
+ type Error = Error;
+
+ fn unit_variant(self) -> Result<()> {
+ Ok(())
+ }
+
+ fn newtype_variant_seed<T: DeserializeSeed<'de>>(self, seed: T) -> Result<T::Value> {
+ let args = node_args(self.node);
+ if self.arg_offset < args.len() {
+ seed.deserialize(ValueDeserializer::new(args[self.arg_offset]))
+ } else {
+ Err(Error::Message(
+ "expected a value after variant name for newtype variant".into(),
+ ))
+ }
+ }
+
+ fn tuple_variant<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+ let args = node_args(self.node);
+ let remaining: Vec<&KdlValue> = args[self.arg_offset..].to_vec();
+ visitor.visit_seq(ArgsSeqAccess {
+ args: remaining,
+ index: 0,
+ })
+ }
+
+ fn struct_variant<V: Visitor<'de>>(
+ self,
+ _fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value> {
+ // Properties of the node are the struct fields
+ let props = node_props(self.node);
+ visitor.visit_map(PropsMapAccess { props, index: 0 })
+ }
+}
+
+/// For complex variants where a child node is named after the variant.
+/// e.g., `shape { Circle { radius 5.0 } }`
+struct EnumComplexAccess<'a> {
+ variant_name: &'a str,
+ variant_node: &'a KdlNode,
+}
+
+impl<'de, 'a> de::EnumAccess<'de> for EnumComplexAccess<'a> {
+ type Error = Error;
+ type Variant = EnumComplexVariantAccess<'a>;
+
+ fn variant_seed<V: DeserializeSeed<'de>>(self, seed: V) -> Result<(V::Value, Self::Variant)> {
+ let val = seed.deserialize::<serde::de::value::StrDeserializer<'_, Error>>(
+ self.variant_name.into_deserializer(),
+ )?;
+ Ok((
+ val,
+ EnumComplexVariantAccess {
+ node: self.variant_node,
+ },
+ ))
+ }
+}
+
+struct EnumComplexVariantAccess<'a> {
+ node: &'a KdlNode,
+}
+
+impl<'de, 'a> de::VariantAccess<'de> for EnumComplexVariantAccess<'a> {
+ type Error = Error;
+
+ fn unit_variant(self) -> Result<()> {
+ Ok(())
+ }
+
+ fn newtype_variant_seed<T: DeserializeSeed<'de>>(self, seed: T) -> Result<T::Value> {
+ seed.deserialize(NodeContentDeserializer::new(self.node))
+ }
+
+ fn tuple_variant<V: Visitor<'de>>(self, _len: usize, visitor: V) -> Result<V::Value> {
+ let args = node_args(self.node);
+ visitor.visit_seq(ArgsSeqAccess { args, index: 0 })
+ }
+
+ fn struct_variant<V: Visitor<'de>>(
+ self,
+ _fields: &'static [&'static str],
+ visitor: V,
+ ) -> Result<V::Value> {
+ if let Some(children) = self.node.children() {
+ visitor.visit_map(DocumentMapAccess::new(children))
+ } else {
+ let props = node_props(self.node);
+ visitor.visit_map(PropsMapAccess { props, index: 0 })
+ }
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..f03272c
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,54 @@
+use std::fmt;
+
+/// Error type for serde-kdl serialization and deserialization.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// A KDL parsing error.
+ #[error("KDL parse error: {0}")]
+ Parse(#[from] kdl::KdlError),
+
+ /// A serde (de)serialization error.
+ #[error("{0}")]
+ Message(String),
+
+ /// The top-level type must be a struct or map.
+ #[error("top-level type must be a struct or map")]
+ TopLevelNotStruct,
+
+ /// Expected a specific type but got something else.
+ #[error("type mismatch: expected {expected}, got {got}")]
+ TypeMismatch {
+ expected: &'static str,
+ got: String,
+ },
+
+ /// A required field was missing.
+ #[error("missing field: {0}")]
+ MissingField(String),
+
+ /// Integer overflow during conversion.
+ #[error("integer {0} out of range for target type")]
+ IntegerOutOfRange(i128),
+
+ /// Enum variant not found or ambiguous.
+ #[error("unknown enum variant: {0}")]
+ UnknownVariant(String),
+
+ /// Unsupported operation.
+ #[error("unsupported: {0}")]
+ Unsupported(String),
+}
+
+impl serde::de::Error for Error {
+ fn custom<T: fmt::Display>(msg: T) -> Self {
+ Error::Message(msg.to_string())
+ }
+}
+
+impl serde::ser::Error for Error {
+ fn custom<T: fmt::Display>(msg: T) -> Self {
+ Error::Message(msg.to_string())
+ }
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..b8d56dd
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,123 @@
+//! # serde-kdl
+//!
+//! [Serde](https://serde.rs) integration for [KDL](https://kdl.dev), the
+//! KDL Document Language.
+//!
+//! This crate provides `serialize` and `deserialize` support for KDL documents
+//! using the [`kdl`](https://crates.io/crates/kdl) crate (v6, KDL v2 spec).
+//!
+//! ## Quick Start
+//!
+//! ```rust
+//! use serde::{Deserialize, Serialize};
+//!
+//! #[derive(Debug, Serialize, Deserialize, PartialEq)]
+//! struct Config {
+//! title: String,
+//! count: i32,
+//! enabled: bool,
+//! }
+//!
+//! let kdl_input = r#"
+//! title "My App"
+//! count 42
+//! enabled #true
+//! "#;
+//!
+//! // Deserialize
+//! let config: Config = serde_kdl::from_str(kdl_input).unwrap();
+//! assert_eq!(config.title, "My App");
+//! assert_eq!(config.count, 42);
+//! assert_eq!(config.enabled, true);
+//!
+//! // Serialize
+//! let output = serde_kdl::to_string(&config).unwrap();
+//! let roundtrip: Config = serde_kdl::from_str(&output).unwrap();
+//! assert_eq!(config, roundtrip);
+//! ```
+//!
+//! ## Mapping Rules
+//!
+//! ### Structs and Maps
+//!
+//! Struct fields map to node names. Each field becomes a node whose name is
+//! the field name and whose first argument is the value.
+//!
+//! ```kdl
+//! title "My App"
+//! count 42
+//! enabled #true
+//! ```
+//!
+//! ### Nested Structs
+//!
+//! Nested structs use children blocks:
+//!
+//! ```kdl
+//! server {
+//! host "localhost"
+//! port 8080
+//! }
+//! ```
+//!
+//! ### Sequences
+//!
+//! Sequences of primitives use multiple arguments on a single node:
+//!
+//! ```kdl
+//! tags "web" "rust" "config"
+//! ```
+//!
+//! Sequences of structs use repeated nodes with the same name:
+//!
+//! ```kdl
+//! server {
+//! host "localhost"
+//! port 8080
+//! }
+//! server {
+//! host "example.com"
+//! port 443
+//! }
+//! ```
+//!
+//! The `-` (dash) children convention is also supported for deserialization:
+//!
+//! ```kdl
+//! items {
+//! - 1
+//! - 2
+//! - 3
+//! }
+//! ```
+//!
+//! ### Option
+//!
+//! `None` is represented by the absence of a node. `Some(value)` serializes
+//! the inner value normally. `#null` arguments also deserialize as `None`.
+//!
+//! ### Enums
+//!
+//! Unit variants serialize as strings:
+//!
+//! ```kdl
+//! color "Red"
+//! ```
+//!
+//! Newtype, tuple, and struct variants use the variant name as a child node:
+//!
+//! ```kdl
+//! shape {
+//! Circle {
+//! radius 5.0
+//! }
+//! }
+//! ```
+
+pub mod de;
+pub mod error;
+pub mod ser;
+
+pub use de::{from_doc, from_str};
+pub use error::Error;
+pub use ser::{to_doc, to_string, to_string_pretty};
diff --git a/src/ser.rs b/src/ser.rs
new file mode 100644
index 0000000..f715388
--- /dev/null
+++ b/src/ser.rs
@@ -0,0 +1,543 @@
+//! Serializer implementation for KDL.
+//!
+//! # Mapping Rules
+//!
+//! - **Struct/Map** → KDL document. Each field becomes a node.
+//! - **Primitives** → Node argument: `name "value"`, `count 42`.
+//! - **Nested struct** → Node with children block: `nested { field "val" }`.
+//! - **Vec of primitives** → Single node, multiple arguments: `tags "a" "b" "c"`.
+//! - **Vec of structs** → Multiple nodes with the same name.
+//! - **Option::None** → Node omitted. **Option::Some** → Serialize inner value.
+//! - **Enum (unit variant)** → String: `"VariantName"`.
+//! - **Enum (newtype)** → Child node: `VariantName value`.
+//! - **Enum (struct)** → Child node with children: `VariantName { field val }`.
+
+use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
+use serde::ser::{self, Serialize};
+
+use crate::error::{Error, Result};
+
+/// Serialize a value to a KDL string.
+pub fn to_string<T: Serialize>(value: &T) -> Result<String> {
+ let doc = to_doc(value)?;
+ Ok(doc.to_string())
+}
+
+/// Serialize a value to a KDL string with auto-formatting.
+pub fn to_string_pretty<T: Serialize>(value: &T) -> Result<String> {
+ let mut doc = to_doc(value)?;
+ doc.autoformat();
+ Ok(doc.to_string())
+}
+
+/// Serialize a value to a [`KdlDocument`].
+pub fn to_doc<T: Serialize>(value: &T) -> Result<KdlDocument> {
+ let v = value.serialize(ValueSerializer)?;
+ value_to_doc(v)
+}
+
+// ---------------------------------------------------------------------------
+// Intermediate value representation
+// ---------------------------------------------------------------------------
+
+/// An intermediate representation that bridges serde's data model and KDL's
+/// tree structure. We serialize into this first, then convert to KDL.
+#[derive(Debug, Clone)]
+enum Value {
+ Null,
+ Bool(bool),
+ Integer(i128),
+ Float(f64),
+ String(String),
+ Seq(Vec<Value>),
+ Map(Vec<(String, Value)>),
+}
+
+impl Value {
+ fn is_primitive(&self) -> bool {
+ matches!(
+ self,
+ Value::Null | Value::Bool(_) | Value::Integer(_) | Value::Float(_) | Value::String(_)
+ )
+ }
+
+ fn to_kdl_value(&self) -> Option<KdlValue> {
+ match self {
+ Value::Null => Some(KdlValue::Null),
+ Value::Bool(b) => Some(KdlValue::Bool(*b)),
+ Value::Integer(i) => Some(KdlValue::Integer(*i)),
+ Value::Float(f) => Some(KdlValue::Float(*f)),
+ Value::String(s) => Some(KdlValue::String(s.clone())),
+ _ => None,
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Value → KDL conversion
+// ---------------------------------------------------------------------------
+
+fn value_to_doc(value: Value) -> Result<KdlDocument> {
+ match value {
+ Value::Map(entries) => {
+ let mut doc = KdlDocument::new();
+ for (key, val) in entries {
+ let nodes = value_to_nodes(&key, val)?;
+ for node in nodes {
+ doc.nodes_mut().push(node);
+ }
+ }
+ Ok(doc)
+ }
+ _ => Err(Error::TopLevelNotStruct),
+ }
+}
+
+/// Convert a key-value pair to one or more KDL nodes.
+fn value_to_nodes(name: &str, value: Value) -> Result<Vec<KdlNode>> {
+ match value {
+ // Primitives → single node with one argument
+ Value::Null => {
+ let mut node = KdlNode::new(name);
+ node.push(KdlEntry::new(KdlValue::Null));
+ Ok(vec![node])
+ }
+ Value::Bool(b) => {
+ let mut node = KdlNode::new(name);
+ node.push(KdlEntry::new(KdlValue::Bool(b)));
+ Ok(vec![node])
+ }
+ Value::Integer(i) => {
+ let mut node = KdlNode::new(name);
+ node.push(KdlEntry::new(KdlValue::Integer(i)));
+ Ok(vec![node])
+ }
+ Value::Float(f) => {
+ let mut node = KdlNode::new(name);
+ node.push(KdlEntry::new(KdlValue::Float(f)));
+ Ok(vec![node])
+ }
+ Value::String(s) => {
+ let mut node = KdlNode::new(name);
+ node.push(KdlEntry::new(KdlValue::String(s)));
+ Ok(vec![node])
+ }
+
+ // Seq: if all elements are primitive → one node with multiple args
+ // if elements are maps → multiple nodes with same name
+ // mixed → use `-` children
+ Value::Seq(elements) => {
+ if elements.is_empty() {
+ // Empty sequence → node with children block but no children
+ let mut node = KdlNode::new(name);
+ node.set_children(KdlDocument::new());
+ return Ok(vec![node]);
+ }
+
+ let all_primitive = elements.iter().all(|e| e.is_primitive());
+ let all_maps = elements.iter().all(|e| matches!(e, Value::Map(_)));
+
+ if all_primitive {
+ // Single node, multiple arguments
+ let mut node = KdlNode::new(name);
+ for elem in &elements {
+ if let Some(kv) = elem.to_kdl_value() {
+ node.push(KdlEntry::new(kv));
+ }
+ }
+ Ok(vec![node])
+ } else if all_maps {
+ // Multiple nodes with the same name
+ let mut nodes = Vec::new();
+ for elem in elements {
+ if let Value::Map(entries) = elem {
+ let mut node = KdlNode::new(name);
+ let children_doc = map_entries_to_doc(entries)?;
+ node.set_children(children_doc);
+ nodes.push(node);
+ }
+ }
+ Ok(nodes)
+ } else {
+ // Mixed or nested sequences → use `-` children
+ let mut node = KdlNode::new(name);
+ let mut children = KdlDocument::new();
+ for elem in elements {
+ let child_nodes = value_to_nodes("-", elem)?;
+ for cn in child_nodes {
+ children.nodes_mut().push(cn);
+ }
+ }
+ node.set_children(children);
+ Ok(vec![node])
+ }
+ }
+
+ // Map → node with children
+ Value::Map(entries) => {
+ let mut node = KdlNode::new(name);
+ let children_doc = map_entries_to_doc(entries)?;
+ node.set_children(children_doc);
+ Ok(vec![node])
+ }
+ }
+}
+
+fn map_entries_to_doc(entries: Vec<(String, Value)>) -> Result<KdlDocument> {
+ let mut doc = KdlDocument::new();
+ for (k, v) in entries {
+ let nodes = value_to_nodes(&k, v)?;
+ for node in nodes {
+ doc.nodes_mut().push(node);
+ }
+ }
+ Ok(doc)
+}
+
+// ---------------------------------------------------------------------------
+// ValueSerializer: serde Serializer that produces a Value
+// ---------------------------------------------------------------------------
+
+struct ValueSerializer;
+
+impl ser::Serializer for ValueSerializer {
+ type Ok = Value;
+ type Error = Error;
+
+ type SerializeSeq = SerializeSeq;
+ type SerializeTuple = SerializeSeq;
+ type SerializeTupleStruct = SerializeSeq;
+ type SerializeTupleVariant = SerializeTupleVariant;
+ type SerializeMap = SerializeMap;
+ type SerializeStruct = SerializeMap;
+ type SerializeStructVariant = SerializeStructVariant;
+
+ fn serialize_bool(self, v: bool) -> Result<Value> {
+ Ok(Value::Bool(v))
+ }
+
+ fn serialize_i8(self, v: i8) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_i16(self, v: i16) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_i32(self, v: i32) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_i64(self, v: i64) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_i128(self, v: i128) -> Result<Value> {
+ Ok(Value::Integer(v))
+ }
+
+ fn serialize_u8(self, v: u8) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_u16(self, v: u16) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_u32(self, v: u32) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_u64(self, v: u64) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_u128(self, v: u128) -> Result<Value> {
+ Ok(Value::Integer(v as i128))
+ }
+
+ fn serialize_f32(self, v: f32) -> Result<Value> {
+ Ok(Value::Float(v as f64))
+ }
+
+ fn serialize_f64(self, v: f64) -> Result<Value> {
+ Ok(Value::Float(v))
+ }
+
+ fn serialize_char(self, v: char) -> Result<Value> {
+ Ok(Value::String(v.to_string()))
+ }
+
+ fn serialize_str(self, v: &str) -> Result<Value> {
+ Ok(Value::String(v.to_string()))
+ }
+
+ fn serialize_bytes(self, v: &[u8]) -> Result<Value> {
+ Ok(Value::Seq(
+ v.iter().map(|b| Value::Integer(*b as i128)).collect(),
+ ))
+ }
+
+ fn serialize_none(self) -> Result<Value> {
+ Ok(Value::Null)
+ }
+
+ fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Value> {
+ value.serialize(self)
+ }
+
+ fn serialize_unit(self) -> Result<Value> {
+ Ok(Value::Null)
+ }
+
+ fn serialize_unit_struct(self, _name: &'static str) -> Result<Value> {
+ Ok(Value::Null)
+ }
+
+ fn serialize_unit_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ ) -> Result<Value> {
+ Ok(Value::String(variant.to_string()))
+ }
+
+ fn serialize_newtype_struct<T: ?Sized + Serialize>(
+ self,
+ _name: &'static str,
+ value: &T,
+ ) -> Result<Value> {
+ value.serialize(self)
+ }
+
+ fn serialize_newtype_variant<T: ?Sized + Serialize>(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ value: &T,
+ ) -> Result<Value> {
+ let inner = value.serialize(ValueSerializer)?;
+ Ok(Value::Map(vec![(variant.to_string(), inner)]))
+ }
+
+ fn serialize_seq(self, len: Option<usize>) -> Result<SerializeSeq> {
+ Ok(SerializeSeq {
+ elements: Vec::with_capacity(len.unwrap_or(0)),
+ })
+ }
+
+ fn serialize_tuple(self, len: usize) -> Result<SerializeSeq> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_struct(
+ self,
+ _name: &'static str,
+ len: usize,
+ ) -> Result<SerializeSeq> {
+ self.serialize_seq(Some(len))
+ }
+
+ fn serialize_tuple_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ len: usize,
+ ) -> Result<SerializeTupleVariant> {
+ Ok(SerializeTupleVariant {
+ variant: variant.to_string(),
+ elements: Vec::with_capacity(len),
+ })
+ }
+
+ fn serialize_map(self, len: Option<usize>) -> Result<SerializeMap> {
+ Ok(SerializeMap {
+ entries: Vec::with_capacity(len.unwrap_or(0)),
+ current_key: None,
+ })
+ }
+
+ fn serialize_struct(self, _name: &'static str, len: usize) -> Result<SerializeMap> {
+ self.serialize_map(Some(len))
+ }
+
+ fn serialize_struct_variant(
+ self,
+ _name: &'static str,
+ _variant_index: u32,
+ variant: &'static str,
+ len: usize,
+ ) -> Result<SerializeStructVariant> {
+ Ok(SerializeStructVariant {
+ variant: variant.to_string(),
+ entries: Vec::with_capacity(len),
+ })
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Compound serializer types
+// ---------------------------------------------------------------------------
+
+struct SerializeSeq {
+ elements: Vec<Value>,
+}
+
+impl ser::SerializeSeq for SerializeSeq {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+ self.elements.push(value.serialize(ValueSerializer)?);
+ Ok(())
+ }
+
+ fn end(self) -> Result<Value> {
+ Ok(Value::Seq(self.elements))
+ }
+}
+
+impl ser::SerializeTuple for SerializeSeq {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+ ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Value> {
+ ser::SerializeSeq::end(self)
+ }
+}
+
+impl ser::SerializeTupleStruct for SerializeSeq {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+ ser::SerializeSeq::serialize_element(self, value)
+ }
+
+ fn end(self) -> Result<Value> {
+ ser::SerializeSeq::end(self)
+ }
+}
+
+struct SerializeTupleVariant {
+ variant: String,
+ elements: Vec<Value>,
+}
+
+impl ser::SerializeTupleVariant for SerializeTupleVariant {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+ self.elements.push(value.serialize(ValueSerializer)?);
+ Ok(())
+ }
+
+ fn end(self) -> Result<Value> {
+ Ok(Value::Map(vec![(
+ self.variant,
+ Value::Seq(self.elements),
+ )]))
+ }
+}
+
+struct SerializeMap {
+ entries: Vec<(String, Value)>,
+ current_key: Option<String>,
+}
+
+impl ser::SerializeMap for SerializeMap {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<()> {
+ let key_val = key.serialize(ValueSerializer)?;
+ let key_str = match key_val {
+ Value::String(s) => s,
+ Value::Integer(i) => i.to_string(),
+ Value::Bool(b) => b.to_string(),
+ other => {
+ return Err(Error::Unsupported(format!(
+ "map key must be a string, got {other:?}"
+ )))
+ }
+ };
+ self.current_key = Some(key_str);
+ Ok(())
+ }
+
+ fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
+ let key = self
+ .current_key
+ .take()
+ .ok_or_else(|| Error::Message("serialize_value called before serialize_key".into()))?;
+ let val = value.serialize(ValueSerializer)?;
+ // Skip None/Null values (they represent absent optional fields)
+ if !matches!(val, Value::Null) {
+ self.entries.push((key, val));
+ }
+ Ok(())
+ }
+
+ fn end(self) -> Result<Value> {
+ Ok(Value::Map(self.entries))
+ }
+}
+
+impl ser::SerializeStruct for SerializeMap {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized + Serialize>(
+ &mut self,
+ key: &'static str,
+ value: &T,
+ ) -> Result<()> {
+ let val = value.serialize(ValueSerializer)?;
+ // Skip None/Null values for struct fields (Option::None)
+ if !matches!(val, Value::Null) {
+ self.entries.push((key.to_string(), val));
+ }
+ Ok(())
+ }
+
+ fn end(self) -> Result<Value> {
+ Ok(Value::Map(self.entries))
+ }
+}
+
+struct SerializeStructVariant {
+ variant: String,
+ entries: Vec<(String, Value)>,
+}
+
+impl ser::SerializeStructVariant for SerializeStructVariant {
+ type Ok = Value;
+ type Error = Error;
+
+ fn serialize_field<T: ?Sized + Serialize>(
+ &mut self,
+ key: &'static str,
+ value: &T,
+ ) -> Result<()> {
+ let val = value.serialize(ValueSerializer)?;
+ if !matches!(val, Value::Null) {
+ self.entries.push((key.to_string(), val));
+ }
+ Ok(())
+ }
+
+ fn end(self) -> Result<Value> {
+ Ok(Value::Map(vec![(
+ self.variant,
+ Value::Map(self.entries),
+ )]))
+ }
+}
diff --git a/tests/integration.rs b/tests/integration.rs
new file mode 100644
index 0000000..323e536
--- /dev/null
+++ b/tests/integration.rs
@@ -0,0 +1,579 @@
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+// ── Basic struct ───────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct SimpleConfig {
+ title: String,
+ count: i32,
+ enabled: bool,
+ ratio: f64,
+}
+
+#[test]
+fn deserialize_simple_struct() {
+ let input = r#"
+title "My App"
+count 42
+enabled #true
+ratio 3.14
+"#;
+ let config: SimpleConfig = serde_kdl::from_str(input).unwrap();
+ assert_eq!(config.title, "My App");
+ assert_eq!(config.count, 42);
+ assert_eq!(config.enabled, true);
+ assert_eq!(config.ratio, 3.14);
+}
+
+#[test]
+fn serialize_simple_struct() {
+ let config = SimpleConfig {
+ title: "My App".into(),
+ count: 42,
+ enabled: true,
+ ratio: 3.14,
+ };
+ let output = serde_kdl::to_string(&config).unwrap();
+ let roundtrip: SimpleConfig = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(config, roundtrip);
+}
+
+// ── Nested struct ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Server {
+ host: String,
+ port: u16,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct AppConfig {
+ name: String,
+ server: Server,
+}
+
+#[test]
+fn deserialize_nested_struct() {
+ let input = r#"
+name "webapp"
+server {
+ host "localhost"
+ port 8080
+}
+"#;
+ let config: AppConfig = serde_kdl::from_str(input).unwrap();
+ assert_eq!(config.name, "webapp");
+ assert_eq!(config.server.host, "localhost");
+ assert_eq!(config.server.port, 8080);
+}
+
+#[test]
+fn serialize_nested_struct() {
+ let config = AppConfig {
+ name: "webapp".into(),
+ server: Server {
+ host: "localhost".into(),
+ port: 8080,
+ },
+ };
+ let output = serde_kdl::to_string(&config).unwrap();
+ let roundtrip: AppConfig = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(config, roundtrip);
+}
+
+// ── Vec of primitives ──────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Tagged {
+ name: String,
+ tags: Vec<String>,
+}
+
+#[test]
+fn deserialize_vec_primitives() {
+ let input = r#"
+name "project"
+tags "web" "rust" "config"
+"#;
+ let tagged: Tagged = serde_kdl::from_str(input).unwrap();
+ assert_eq!(tagged.name, "project");
+ assert_eq!(tagged.tags, vec!["web", "rust", "config"]);
+}
+
+#[test]
+fn serialize_vec_primitives() {
+ let tagged = Tagged {
+ name: "project".into(),
+ tags: vec!["web".into(), "rust".into(), "config".into()],
+ };
+ let output = serde_kdl::to_string(&tagged).unwrap();
+ let roundtrip: Tagged = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(tagged, roundtrip);
+}
+
+// ── Vec of structs (repeated nodes) ────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Cluster {
+ server: Vec<Server>,
+}
+
+#[test]
+fn deserialize_vec_structs() {
+ let input = r#"
+server {
+ host "localhost"
+ port 8080
+}
+server {
+ host "example.com"
+ port 443
+}
+"#;
+ let cluster: Cluster = serde_kdl::from_str(input).unwrap();
+ assert_eq!(cluster.server.len(), 2);
+ assert_eq!(cluster.server[0].host, "localhost");
+ assert_eq!(cluster.server[0].port, 8080);
+ assert_eq!(cluster.server[1].host, "example.com");
+ assert_eq!(cluster.server[1].port, 443);
+}
+
+#[test]
+fn serialize_vec_structs() {
+ let cluster = Cluster {
+ server: vec![
+ Server {
+ host: "localhost".into(),
+ port: 8080,
+ },
+ Server {
+ host: "example.com".into(),
+ port: 443,
+ },
+ ],
+ };
+ let output = serde_kdl::to_string(&cluster).unwrap();
+ let roundtrip: Cluster = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(cluster, roundtrip);
+}
+
+// ── Dash children convention ───────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct DashList {
+ items: Vec<i32>,
+}
+
+#[test]
+fn deserialize_dash_children() {
+ let input = r#"
+items {
+ - 1
+ - 2
+ - 3
+}
+"#;
+ let list: DashList = serde_kdl::from_str(input).unwrap();
+ assert_eq!(list.items, vec![1, 2, 3]);
+}
+
+// ── Option fields ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct OptionalFields {
+ required: String,
+ optional: Option<String>,
+}
+
+#[test]
+fn deserialize_option_present() {
+ let input = r#"
+required "hello"
+optional "world"
+"#;
+ let val: OptionalFields = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.required, "hello");
+ assert_eq!(val.optional, Some("world".into()));
+}
+
+#[test]
+fn deserialize_option_absent() {
+ let input = r#"
+required "hello"
+"#;
+ let val: OptionalFields = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.required, "hello");
+ assert_eq!(val.optional, None);
+}
+
+#[test]
+fn deserialize_option_null() {
+ let input = r#"
+required "hello"
+optional #null
+"#;
+ let val: OptionalFields = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.required, "hello");
+ assert_eq!(val.optional, None);
+}
+
+#[test]
+fn serialize_option() {
+ let with = OptionalFields {
+ required: "hello".into(),
+ optional: Some("world".into()),
+ };
+ let output = serde_kdl::to_string(&with).unwrap();
+ assert!(output.contains("optional"));
+ let roundtrip: OptionalFields = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(with, roundtrip);
+
+ let without = OptionalFields {
+ required: "hello".into(),
+ optional: None,
+ };
+ let output = serde_kdl::to_string(&without).unwrap();
+ // None fields should be omitted
+ assert!(!output.contains("optional"));
+ let roundtrip: OptionalFields = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(without, roundtrip);
+}
+
+// ── Enum variants ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Color {
+ Red,
+ Green,
+ Blue,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Colored {
+ name: String,
+ color: Color,
+}
+
+#[test]
+fn deserialize_unit_variant() {
+ let input = r#"
+name "widget"
+color "Red"
+"#;
+ let val: Colored = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.color, Color::Red);
+}
+
+#[test]
+fn serialize_unit_variant() {
+ let val = Colored {
+ name: "widget".into(),
+ color: Color::Green,
+ };
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: Colored = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── Struct variant enum ────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Shape {
+ Circle { radius: f64 },
+ Rectangle { width: f64, height: f64 },
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Drawing {
+ name: String,
+ shape: Shape,
+}
+
+#[test]
+fn deserialize_struct_variant() {
+ let input = r#"
+name "my drawing"
+shape {
+ Circle {
+ radius 5.0
+ }
+}
+"#;
+ let val: Drawing = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.name, "my drawing");
+ assert_eq!(val.shape, Shape::Circle { radius: 5.0 });
+}
+
+#[test]
+fn serialize_struct_variant() {
+ let val = Drawing {
+ name: "my drawing".into(),
+ shape: Shape::Rectangle {
+ width: 10.0,
+ height: 20.0,
+ },
+ };
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: Drawing = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── Newtype variant enum ───────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+enum Wrapper {
+ Text(String),
+ Number(i64),
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Wrapped {
+ value: Wrapper,
+}
+
+#[test]
+fn deserialize_newtype_variant() {
+ let input = r#"
+value {
+ Text "hello"
+}
+"#;
+ let val: Wrapped = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.value, Wrapper::Text("hello".into()));
+}
+
+#[test]
+fn serialize_newtype_variant() {
+ let val = Wrapped {
+ value: Wrapper::Text("hello".into()),
+ };
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: Wrapped = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── HashMap ────────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithMap {
+ settings: HashMap<String, String>,
+}
+
+#[test]
+fn deserialize_hashmap() {
+ let input = r#"
+settings {
+ key1 "value1"
+ key2 "value2"
+}
+"#;
+ let val: WithMap = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.settings.get("key1"), Some(&"value1".into()));
+ assert_eq!(val.settings.get("key2"), Some(&"value2".into()));
+}
+
+#[test]
+fn serialize_hashmap() {
+ let mut settings = HashMap::new();
+ settings.insert("key1".into(), "value1".into());
+ let val = WithMap { settings };
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: WithMap = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── Various integer types ──────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct IntTypes {
+ a: u8,
+ b: u16,
+ c: u32,
+ d: u64,
+ e: i8,
+ f: i16,
+ g: i32,
+ h: i64,
+}
+
+#[test]
+fn roundtrip_int_types() {
+ let val = IntTypes {
+ a: 1,
+ b: 2,
+ c: 3,
+ d: 4,
+ e: -1,
+ f: -2,
+ g: -3,
+ h: -4,
+ };
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: IntTypes = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── Tuple ──────────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithTuple {
+ point: (f64, f64, f64),
+}
+
+#[test]
+fn deserialize_tuple() {
+ let input = r#"
+point 1.0 2.0 3.0
+"#;
+ let val: WithTuple = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.point, (1.0, 2.0, 3.0));
+}
+
+#[test]
+fn roundtrip_tuple() {
+ let val = WithTuple {
+ point: (1.0, 2.0, 3.0),
+ };
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: WithTuple = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── Deeply nested ──────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level3 {
+ value: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level2 {
+ inner: Level3,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Level1 {
+ middle: Level2,
+}
+
+#[test]
+fn deeply_nested() {
+ let input = r#"
+middle {
+ inner {
+ value "deep"
+ }
+}
+"#;
+ let val: Level1 = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.middle.inner.value, "deep");
+
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: Level1 = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── Boolean values ─────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Booleans {
+ yes: bool,
+ no: bool,
+}
+
+#[test]
+fn booleans() {
+ let input = r#"
+yes #true
+no #false
+"#;
+ let val: Booleans = serde_kdl::from_str(input).unwrap();
+ assert_eq!(val.yes, true);
+ assert_eq!(val.no, false);
+
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: Booleans = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── Empty vec ──────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithVec {
+ items: Vec<String>,
+}
+
+#[test]
+fn serialize_empty_vec() {
+ let val = WithVec { items: vec![] };
+ let output = serde_kdl::to_string(&val).unwrap();
+ // Empty vec gets an empty children block
+ let roundtrip: WithVec = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(roundtrip.items, Vec::<String>::new());
+}
+
+// ── to_string_pretty ───────────────────────────────────────────────────
+
+#[test]
+fn pretty_print() {
+ let config = AppConfig {
+ name: "webapp".into(),
+ server: Server {
+ host: "localhost".into(),
+ port: 8080,
+ },
+ };
+ let pretty = serde_kdl::to_string_pretty(&config).unwrap();
+ // Should still roundtrip
+ let roundtrip: AppConfig = serde_kdl::from_str(&pretty).unwrap();
+ assert_eq!(config, roundtrip);
+}
+
+// ── to_doc / from_doc ──────────────────────────────────────────────────
+
+#[test]
+fn doc_api() {
+ let config = SimpleConfig {
+ title: "Test".into(),
+ count: 1,
+ enabled: false,
+ ratio: 0.5,
+ };
+ let doc = serde_kdl::to_doc(&config).unwrap();
+ assert!(doc.get("title").is_some());
+ let roundtrip: SimpleConfig = serde_kdl::from_doc(&doc).unwrap();
+ assert_eq!(config, roundtrip);
+}
+
+// ── Vec of integers ────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct Numbers {
+ values: Vec<i32>,
+}
+
+#[test]
+fn roundtrip_vec_ints() {
+ let val = Numbers {
+ values: vec![1, 2, 3, 4, 5],
+ };
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: Numbers = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}
+
+// ── Char field ─────────────────────────────────────────────────────────
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+struct WithChar {
+ letter: char,
+}
+
+#[test]
+fn roundtrip_char() {
+ let val = WithChar { letter: 'X' };
+ let output = serde_kdl::to_string(&val).unwrap();
+ let roundtrip: WithChar = serde_kdl::from_str(&output).unwrap();
+ assert_eq!(val, roundtrip);
+}