Compare commits

...

4 Commits

Author SHA1 Message Date
Kat Marchán 1799c064ad update docs 2026-05-28 21:45:38 -07:00
Kat Marchán 4397ae8cd7 update se.rs too 2026-05-28 21:20:11 -07:00
Kat Marchán 78ee15c9fb etc 2026-05-28 20:55:16 -07:00
Kat Marchán 65dc03b3dd IT LIVES 2026-05-28 18:44:33 -07:00
6 changed files with 256 additions and 52 deletions

View File

@ -28,7 +28,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust: [1.96, stable]
rust: [1.95, stable]
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:

View File

@ -8,7 +8,7 @@ readme = "README.md"
homepage = "https://kdl.dev"
repository = "https://github.com/kdl-org/kdl-rs"
keywords = ["kdl", "document", "serialization", "config"]
rust-version = "1.96"
rust-version = "1.95"
edition = "2021"
[features]

View File

@ -1 +1 @@
msrv = "1.96"
msrv = "1.95"

248
src/de.rs
View File

@ -2,6 +2,16 @@
//!
//! This module provides [`from_str`] for deserializing Rust types from KDL text.
//!
//! Due to the fact that `serde` was developed with JSON in mind, and the incompatibility
//! between KDL and JSON's data model, not all `serde` concepts apply smoothly to KDL. This
//! leads to the fact that some KDL concepts are inexpressible in terms of `serde` derives
//! and may require manual deserialization.
//!
//! The most notable restriction is the ability to distinguish between *arguments*,
//! *properties* and *child nodes*, as JSON does not have such conception.
//!
//! Due to that the mapping is performed in a best effort manner.
//!
//! # KDL → Serde Data Model Mapping
//!
//! KDL documents are mapped to serde's data model as follows:
@ -29,13 +39,97 @@
//! }
//!
//! let kdl = r#"
//! name "my-app"
//! name my-app
//! port 8080
//! "#;
//!
//! let config: Config = kdl::de::from_str(kdl).unwrap();
//! assert_eq!(config, Config { name: "my-app".into(), port: 8080 });
//! ```
//!
//! ## Arguments mapping
//!
//! This library supports two kinds of special renaming for arguments - `#{n}` and `#args`:
//!
//! - `#{n}`: This is used when you want to access argument at a specific
//! position. The field name should start with `#`, then follow by the position
//! of the argument in numerical order starting from 0.
//!
//! ```rust
//! use serde::Deserialize;
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Config {
//! server: Server,
//! }
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Server {
//! #[serde(rename = "#0")]
//! name: String,
//! #[serde(rename = "#1")]
//! port: u16,
//! }
//!
//! let kdl = r#"
//! server my-app 8080
//! "#;
//!
//! let config: Config = kdl::de::from_str(kdl).unwrap();
//! assert_eq!(config, Config { server: Server { name: "my-app".into(), port: 8080 }});
//! ```
//!
//! - `#args`: This is used when you want to collect *all* the arguments of the node.
//!
//! ```rust
//! use serde::Deserialize;
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Config {
//! server: Server,
//! }
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Server {
//! #[serde(rename = "#args")]
//! info: Vec<String>,
//! }
//!
//! let kdl = r#"
//! server my-app "https://example.com"
//! "#;
//!
//! let config: Config = kdl::de::from_str(kdl).unwrap();
//! assert_eq!(config, Config { server: Server { info: Vec::from(["my-app".into(), "https://example.com".into()]) }});
//! ```
//!
//! ## Properties mapping
//!
//! You can use `#@field-name` on a field that will be used for a property.
//!
//! ```rust
//! use serde::Deserialize;
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Config {
//! server: Server,
//! }
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Server {
//! #[serde(rename = "#0")]
//! name: String,
//! #[serde(rename = "#@port")]
//! port: u16,
//! }
//!
//! let kdl = r#"
//! server my-app port=8080
//! "#;
//!
//! let config: Config = kdl::de::from_str(kdl).unwrap();
//! assert_eq!(config, Config { server: Server { name: "my-app".into(), port: 8080 }});
//! ```
use std::borrow::Cow;
use std::fmt;
@ -44,7 +138,7 @@ use serde::de::value::StringDeserializer;
use serde::de::{self, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
use serde::Deserialize;
use crate::{KdlDocument, KdlEntry, KdlNode, KdlValue};
use crate::{KdlDocument, KdlEntry, KdlIdentifier, KdlNode, KdlValue};
/// Errors that can occur during KDL deserialization.
#[derive(Debug, Clone)]
@ -95,7 +189,7 @@ fn str_deserializer(s: &str) -> de::value::StrDeserializer<'_, Error> {
/// }
///
/// let kdl = r#"
/// name "my-app"
/// name my-app
/// port 8080
/// "#;
///
@ -111,6 +205,51 @@ where
T::deserialize(de)
}
struct IdentDeserializer<'a> {
ident: &'a KdlIdentifier,
}
impl<'de, 'a> de::Deserializer<'de> for IdentDeserializer<'a> {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_str(self.ident.value())
}
fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_string(self.ident.value().into())
}
fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_str(self.ident.value())
}
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error> {
visitor.visit_newtype_struct(self)
}
fn deserialize_enum<V: Visitor<'de>>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
dbg!(_name);
dbg!(_variants);
visitor.visit_enum(str_deserializer(self.ident.value()))
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char
bytes byte_buf unit unit_struct seq tuple
tuple_struct map struct option identifier ignored_any
}
}
struct ValueDeserializer<'a> {
value: &'a KdlValue,
}
@ -157,6 +296,8 @@ impl<'de, 'a> de::Deserializer<'de> for ValueDeserializer<'a> {
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
dbg!(_name);
dbg!(_variants);
match self.value {
KdlValue::String(s) => visitor.visit_enum(str_deserializer(s.as_str())),
_ => Err(de::Error::custom("expected a string for unit enum variant")),
@ -248,6 +389,8 @@ impl<'de, 'a> de::Deserializer<'de> for DocumentDeserializer<'a> {
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
dbg!(_name);
dbg!(_variants);
// For enums at document level, treat as a single-node document where
// the node name is the variant.
let nodes = self.doc.nodes();
@ -554,6 +697,8 @@ impl<'de, 'a> de::Deserializer<'de> for NodeDeserializer<'a> {
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error> {
dbg!(_name);
dbg!(_variants);
// If the node is a scalar string, treat it as a unit variant name.
if self.is_scalar() {
if let KdlValue::String(s) = self.args()[0].value() {
@ -698,6 +843,7 @@ struct NodeMapAccess<'a> {
}
enum NodeMapValue<'a> {
Ident(&'a KdlIdentifier),
Arg(&'a KdlValue),
Args(Vec<&'a KdlEntry>),
SingleNode(&'a KdlNode),
@ -708,6 +854,10 @@ impl<'a> NodeMapAccess<'a> {
fn new(node: &'a KdlNode) -> Self {
let mut entries: Vec<(Cow<'a, str>, NodeMapValue<'a>)> = Vec::new();
entries.push(("#name".into(), NodeMapValue::Ident(node.name())));
if let Some(ty) = node.ty() {
entries.push(("#type".into(), NodeMapValue::Ident(ty)))
}
let args: Vec<_> = node
.entries()
.iter()
@ -720,19 +870,25 @@ impl<'a> NodeMapAccess<'a> {
.collect();
for (i, arg) in args.iter().enumerate() {
entries.push((
format!("$argument{}", i + 1).into(),
NodeMapValue::Arg(arg.value()),
));
entries.push((format!("#{}", i).into(), NodeMapValue::Arg(arg.value())));
if let Some(ty) = arg.ty() {
entries.push((format!("#{}#type", i).into(), NodeMapValue::Ident(ty)));
}
}
if !args.is_empty() {
entries.push(("$arguments".into(), NodeMapValue::Args(args.clone())));
entries.push(("#args".into(), NodeMapValue::Args(args.clone())));
}
for prop in &props {
let name = prop.name().unwrap().value();
entries.push((format!("@{}", name).into(), NodeMapValue::Arg(prop.value())));
entries.push((
format!("#@{}", name).into(),
NodeMapValue::Arg(prop.value()),
));
if let Some(ty) = prop.ty() {
entries.push((format!("#@{}#type", name).into(), NodeMapValue::Ident(ty)));
}
entries.push((name.into(), NodeMapValue::Arg(prop.value())));
}
@ -786,6 +942,7 @@ impl<'de, 'a> MapAccess<'de> for NodeMapAccess<'a> {
let (_, ref value) = self.entries[self.idx];
self.idx += 1;
match value {
NodeMapValue::Ident(ident) => seed.deserialize(IdentDeserializer { ident }),
NodeMapValue::Arg(v) => seed.deserialize(ValueDeserializer { value: v }),
NodeMapValue::Args(entries) => seed.deserialize(ArgsSeqDeserializer {
entries: entries.clone(),
@ -1346,9 +1503,9 @@ h 8
fn rename_props() {
#[derive(Deserialize, Debug, PartialEq)]
struct Server {
#[serde(rename = "@host")]
#[serde(rename = "#@host")]
host: String,
#[serde(rename = "@port")]
#[serde(rename = "#@port")]
port: u16,
}
@ -1372,13 +1529,26 @@ h 8
#[test]
fn rename_args() {
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
enum HostType {
IpAddr,
Hostname,
}
#[derive(Deserialize, Debug, PartialEq)]
struct Server {
#[serde(rename = "$argument1")]
#[serde(rename = "#type")]
server_type: String,
#[serde(rename = "#0#type")]
host_type: HostType,
#[serde(rename = "#0")]
host: String,
#[serde(rename = "@port")]
#[serde(rename = "#@port#type")]
port_type: String,
#[serde(rename = "#@port")]
port: u16,
#[serde(rename = "$arguments")]
#[serde(rename = "#args")]
all: Vec<String>,
}
@ -1387,13 +1557,16 @@ h 8
server: Server,
}
let kdl = "server localhost port=8080 remote";
let kdl = "(linux-debian-arm64)server (hostname)localhost port=(number)8080 remote";
let config: Config = from_str(kdl).unwrap();
assert_eq!(
config,
Config {
server: Server {
server_type: "linux-debian-arm64".into(),
host_type: HostType::Hostname,
host: "localhost".into(),
port_type: "number".into(),
port: 8080,
all: Vec::from(["localhost".into(), "remote".into()])
},
@ -1405,9 +1578,9 @@ h 8
fn rename_all_args() {
#[derive(Deserialize, Debug, PartialEq)]
struct Command {
#[serde(rename = "@name")]
#[serde(rename = "#@name")]
name: String,
#[serde(rename = "$arguments")]
#[serde(rename = "#args")]
args: Vec<String>,
}
@ -1433,9 +1606,9 @@ h 8
fn rename_children_args() {
#[derive(Deserialize, Debug, PartialEq)]
struct Server {
#[serde(rename = "@host")]
#[serde(rename = "#@host")]
host: String,
#[serde(rename = "$arguments")]
#[serde(rename = "#args")]
ports: Vec<u16>,
}
@ -1456,4 +1629,41 @@ h 8
}
);
}
// TODO(@zkat): Can't figure out how to do internally tagged stuff just yet...
// #[test]
// fn internal_tag_rename() {
// #[derive(Deserialize, Debug, PartialEq)]
// #[serde(tag = "#0", rename_all = "kebab-case")]
// enum Upstream {
// Git {
// #[serde(rename = "#1")]
// url: String,
// hash: Option<String>,
// },
// Svn {
// #[serde(rename = "#1")]
// url: String,
// rev: Option<String>,
// },
// }
// #[derive(Deserialize, Debug, PartialEq)]
// struct Config {
// #[serde(rename = "upstream")]
// upstreams: Vec<Upstream>,
// }
// let kdl = r#"upstream git "https://codeberg.org/kdl/kdl-rs""#;
// let config: Config = from_str(kdl).unwrap();
// assert_eq!(
// config,
// Config {
// upstreams: vec![Upstream::Git {
// url: "https://codeberg.org/kdl/kdl-rs".into(),
// hash: None,
// }]
// }
// );
// }
}

View File

@ -140,7 +140,7 @@
//!
//! ## Minimum Supported Rust Version (MSRV)
//!
//! You must be at least `1.96` tall to get on this ride.
//! You must be at least `1.95` tall to get on this ride.
//!
//! ## License
//!

View File

@ -28,13 +28,15 @@
//!
//! let config = Config { name: "my-app".into(), port: 8080 };
//! let kdl = kdl::se::to_string(&config).unwrap();
//! assert_eq!(kdl, "name \"my-app\"\nport 8080\n");
//! assert_eq!(kdl, "name my-app\nport 8080\n");
//! ```
//! You can refer to the [crate::de] module to further customize the
//! (de)serialization model.
use serde::ser::{self, Serialize};
use std::fmt;
use crate::{KdlDocument, KdlEntry, KdlEntryFormat, KdlNode, KdlValue};
use crate::{KdlDocument, KdlEntry, KdlNode, KdlValue};
/// Errors that can occur during KDL serialization.
#[derive(Debug)]
@ -73,7 +75,7 @@ impl ser::Error for Error {
///
/// let config = Config { name: "my-app".into(), port: 8080 };
/// let kdl = kdl::se::to_string(&config).unwrap();
/// assert_eq!(kdl, "name \"my-app\"\nport 8080\n");
/// assert_eq!(kdl, "name my-app\nport 8080\n");
/// ```
pub fn to_string<T: Serialize>(value: &T) -> Result<String, Error> {
let doc = to_document(value)?;
@ -89,7 +91,7 @@ pub fn to_document<T: Serialize>(value: &T) -> Result<KdlDocument, Error> {
doc: KdlDocument::new(),
};
value.serialize(&mut ser)?;
Ok(ser.doc)
Ok(dbg!(ser.doc))
}
struct DocumentSerializer {
@ -160,12 +162,7 @@ impl<'a> ser::Serializer for &'a mut DocumentSerializer {
}
fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
let mut entry = KdlEntry::new(KdlValue::String(v.to_string()));
entry.set_format(KdlEntryFormat {
value_repr: format!("\"{}\"", v.escape_default()),
leading: " ".to_string(),
..Default::default()
});
let entry = KdlEntry::new(KdlValue::String(v.to_string()));
let mut node = KdlNode::new("-");
node.entries_mut().push(entry);
self.doc.nodes_mut().push(node);
@ -515,12 +512,7 @@ impl<'a> ser::Serializer for &'a mut NodeValueSerializer<'a> {
}
fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
let mut entry = KdlEntry::new(KdlValue::String(v.to_string()));
entry.set_format(KdlEntryFormat {
value_repr: format!("\"{}\"", v.escape_default()),
leading: " ".to_string(),
..Default::default()
});
let entry = KdlEntry::new(KdlValue::String(v.to_string()));
self.node.entries_mut().push(entry);
Ok(())
}
@ -761,15 +753,17 @@ impl<'a> ser::SerializeStruct for NodeChildMapSerializer<'a> {
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
if let Some(attr_name) = key.strip_prefix('@') {
if let Some(attr_name) = key.strip_prefix("#@") {
let kdl_val = to_kdl_value(value)?;
self.node
.entries_mut()
.push(KdlEntry::new_prop(attr_name, kdl_val));
} else if key == "$arguments" {
} else if key == "#args" {
let mut ser = ArgsSerializer { node: self.node };
value.serialize(&mut ser)?;
} else if key.starts_with("$argument") {
} else if key.starts_with("#") {
// TODO(@zkat): How do we get the ordering here?... This will just
// insert stuff as we discover it.
let kdl_val = to_kdl_value(value)?;
self.node.entries_mut().push(KdlEntry::new(kdl_val));
} else {
@ -1430,7 +1424,7 @@ mod tests {
port: 8080,
};
let kdl = to_string(&config).unwrap();
assert_eq!(kdl, "name \"my-app\"\nport 8080\n");
assert_eq!(kdl, "name my-app\nport 8080\n");
}
#[test]
@ -1455,7 +1449,7 @@ mod tests {
let kdl = to_string(&config).unwrap();
assert!(kdl.contains("server"));
assert!(kdl.contains("host \"localhost\""));
assert!(kdl.contains("host localhost"));
assert!(kdl.contains("port 8080"));
}
@ -1487,7 +1481,7 @@ mod tests {
name: Some("hello".into()),
};
let kdl = to_string(&config).unwrap();
assert!(kdl.contains("name \"hello\""));
assert!(kdl.contains("name hello"));
}
#[test]
@ -1505,7 +1499,7 @@ mod tests {
let config = Config { color: Color::Red };
let kdl = to_string(&config).unwrap();
dbg!(&kdl);
assert!(kdl.contains("color \"Red\""));
assert!(kdl.contains("color Red"));
}
#[test]
@ -1570,9 +1564,9 @@ mod tests {
fn rename_props() {
#[derive(Serialize)]
struct Server {
#[serde(rename = "@host")]
#[serde(rename = "#@host")]
host: String,
#[serde(rename = "@port")]
#[serde(rename = "#@port")]
port: u16,
}
@ -1597,9 +1591,9 @@ mod tests {
fn rename_args() {
#[derive(Serialize)]
struct Server {
#[serde(rename = "$argument1")]
#[serde(rename = "#0")]
host: String,
#[serde(rename = "@port")]
#[serde(rename = "#@port")]
port: u16,
}
@ -1624,9 +1618,9 @@ mod tests {
fn rename_all_args() {
#[derive(Serialize)]
struct Command {
#[serde(rename = "@name")]
#[serde(rename = "#@name")]
name: String,
#[serde(rename = "$arguments")]
#[serde(rename = "#args")]
args: Vec<String>,
}