#[derive(Debug, Clone, PartialEq)]
pub struct KdlDocument {
pub nodes: Vec<KdlNode>,
}
impl KdlDocument {
pub fn nodes(&self) -> &[KdlNode] {
&self.nodes
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct KdlNode {
pub ty: Option<String>,
pub name: String,
pub entries: Vec<KdlEntry>,
pub children: Option<Vec<KdlNode>>,
}
impl KdlNode {
pub fn ty(&self) -> Option<&str> {
self.ty.as_deref()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn entries(&self) -> &[KdlEntry] {
&self.entries
}
pub fn children(&self) -> Option<&[KdlNode]> {
self.children.as_deref()
}
}
impl KdlNode {
pub fn get(&self, key: &str) -> Option<&KdlValue> {
self.entries().iter().find_map(|entry| match entry {
KdlEntry::Property { key: k, value, .. } if k == key => Some(value),
_ => None,
})
}
}
impl KdlNode {
pub fn first_arg(&self) -> Option<&KdlValue> {
self.entries.iter().find_map(|e| match e {
KdlEntry::Argument { value, .. } => Some(value),
_ => None,
})
}
pub fn first_string_arg(&self) -> Option<&str> {
self.first_arg().and_then(KdlValue::as_str)
}
pub fn string_args(&self) -> impl Iterator<Item = &str> {
self.entries.iter().filter_map(|entry| match entry {
KdlEntry::Argument { value, .. } => value.as_str(),
_ => None,
})
}
pub fn arg_values(&self) -> impl Iterator<Item = &KdlValue> {
self.entries.iter().filter_map(|entry| match entry {
KdlEntry::Argument { value, .. } => Some(value),
_ => None,
})
}
pub fn find_child(&self, name: &str) -> Option<&KdlNode> {
self.children.as_deref()?.iter().find(|c| c.name == name)
}
pub fn find_children<'s>(&'s self, name: &'s str) -> impl Iterator<Item = &'s KdlNode> + 's {
self.children
.as_deref()
.into_iter()
.flat_map(|s| s.iter())
.filter(move |c| c.name == name)
}
pub fn string_prop(&self, key: &str) -> Option<&str> {
self.get(key).and_then(KdlValue::as_str)
}
pub fn bool_prop(&self, key: &str) -> Option<bool> {
self.get(key).and_then(KdlValue::as_bool)
}
pub fn int_prop(&self, key: &str) -> Option<i64> {
self.get(key).and_then(KdlValue::as_i64)
}
pub fn string_child_values(&self, child_name: &str) -> Vec<&str> {
self.children
.as_deref()
.into_iter()
.flat_map(|s| s.iter())
.filter(|c| c.name == child_name)
.filter_map(|c| c.first_string_arg())
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum KdlEntry {
Argument {
ty: Option<String>,
value: KdlValue,
},
Property {
key: String,
ty: Option<String>,
value: KdlValue,
},
}
impl KdlEntry {
pub fn value(&self) -> &KdlValue {
match self {
KdlEntry::Argument { value, .. } => value,
KdlEntry::Property { value, .. } => value,
}
}
pub fn ty(&self) -> Option<&str> {
match self {
KdlEntry::Argument { ty, .. } | KdlEntry::Property { ty, .. } => ty.as_deref(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum KdlValue {
String(String),
Number(KdlNumber),
Bool(bool),
Null,
}
impl KdlValue {
pub fn as_str(&self) -> Option<&str> {
match self {
KdlValue::String(s) => Some(s),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
KdlValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
KdlValue::Number(n) => n.as_f64(),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
KdlValue::Number(n) => n.as_i64(),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct KdlNumber {
pub raw: String,
pub as_i64: Option<i64>,
pub as_f64: Option<f64>,
}
impl KdlNumber {
pub fn raw(&self) -> &str {
&self.raw
}
pub fn as_i64(&self) -> Option<i64> {
self.as_i64
}
pub fn as_f64(&self) -> Option<f64> {
self.as_f64
}
}
impl PartialEq for KdlNumber {
fn eq(&self, other: &Self) -> bool {
self.raw == other.raw
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct KdlError {
pub(crate) line: usize,
pub(crate) col: usize,
pub(crate) kind: KdlErrorKind,
}
impl KdlError {
pub fn line(&self) -> usize {
self.line
}
pub fn col(&self) -> usize {
self.col
}
pub fn kind(&self) -> &KdlErrorKind {
&self.kind
}
}
impl core::fmt::Display for KdlError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}:{}: {}", self.line, self.col, self.kind)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum KdlErrorKind {
UnexpectedChar(char),
UnexpectedEof,
InvalidEscape,
InvalidUnicodeEscape,
InvalidNumber,
DisallowedCodePoint(char),
BareKeyword,
UnmatchedBlockCommentEnd,
UnclosedBlockComment,
UnclosedString,
InconsistentIndentation,
InvalidSlashdash,
}
impl core::fmt::Display for KdlErrorKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnexpectedChar(c) => write!(f, "unexpected character: {:?}", c),
Self::UnexpectedEof => write!(f, "unexpected end of input"),
Self::InvalidEscape => write!(f, "invalid escape sequence"),
Self::InvalidUnicodeEscape => write!(f, "invalid unicode escape"),
Self::InvalidNumber => write!(f, "invalid number literal"),
Self::DisallowedCodePoint(c) => {
write!(f, "disallowed code point: U+{:04X}", *c as u32)
}
Self::BareKeyword => write!(f, "bare keyword (use #true, #false, #null, etc.)"),
Self::UnmatchedBlockCommentEnd => write!(f, "unmatched */"),
Self::UnclosedBlockComment => write!(f, "unclosed block comment"),
Self::UnclosedString => write!(f, "unclosed string"),
Self::InconsistentIndentation => write!(f, "inconsistent multiline string indentation"),
Self::InvalidSlashdash => write!(f, "slashdash in invalid position"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_number(raw: &str, i: Option<i64>, f: Option<f64>) -> KdlNumber {
KdlNumber {
raw: raw.to_string(),
as_i64: i,
as_f64: f,
}
}
#[test]
fn as_str_returns_some_for_string() {
let v = KdlValue::String("hello".to_string());
assert_eq!(v.as_str(), Some("hello"));
}
#[test]
fn as_str_returns_none_for_number() {
let v = KdlValue::Number(make_number("42", Some(42), Some(42.0)));
assert_eq!(v.as_str(), None);
}
#[test]
fn as_bool_returns_some_for_bool() {
assert_eq!(KdlValue::Bool(true).as_bool(), Some(true));
assert_eq!(KdlValue::Bool(false).as_bool(), Some(false));
}
#[test]
fn as_bool_returns_none_for_string() {
let v = KdlValue::String("true".to_string());
assert_eq!(v.as_bool(), None);
}
#[test]
fn as_f64_returns_some_for_number() {
let v = KdlValue::Number(make_number("2.5", None, Some(2.5)));
assert_eq!(v.as_f64(), Some(2.5));
}
#[test]
fn as_f64_returns_none_for_bool() {
assert_eq!(KdlValue::Bool(true).as_f64(), None);
}
#[test]
fn as_i64_returns_some_for_integer() {
let v = KdlValue::Number(make_number("42", Some(42), Some(42.0)));
assert_eq!(v.as_i64(), Some(42));
}
#[test]
fn as_i64_returns_none_for_float_only() {
let v = KdlValue::Number(make_number("2.5", None, Some(2.5)));
assert_eq!(v.as_i64(), None);
}
#[test]
fn entry_value_for_argument() {
let entry = KdlEntry::Argument {
ty: None,
value: KdlValue::String("arg".to_string()),
};
assert_eq!(entry.value(), &KdlValue::String("arg".to_string()));
}
#[test]
fn entry_value_for_property() {
let entry = KdlEntry::Property {
key: "key".to_string(),
ty: None,
value: KdlValue::Bool(true),
};
assert_eq!(entry.value(), &KdlValue::Bool(true));
}
#[test]
fn node_get_returns_some_for_existing_key() {
let node = KdlNode {
ty: None,
name: "test".to_string(),
entries: vec![
KdlEntry::Argument {
ty: None,
value: KdlValue::String("positional".to_string()),
},
KdlEntry::Property {
key: "color".to_string(),
ty: None,
value: KdlValue::String("red".to_string()),
},
],
children: None,
};
assert_eq!(
node.get("color"),
Some(&KdlValue::String("red".to_string()))
);
}
#[test]
fn node_get_returns_none_for_missing_key() {
let node = KdlNode {
ty: None,
name: "test".to_string(),
entries: vec![KdlEntry::Argument {
ty: None,
value: KdlValue::String("positional".to_string()),
}],
children: None,
};
assert_eq!(node.get("missing"), None);
}
fn arg(value: KdlValue) -> KdlEntry {
KdlEntry::Argument { ty: None, value }
}
fn prop(key: &str, value: KdlValue) -> KdlEntry {
KdlEntry::Property {
key: key.to_string(),
ty: None,
value,
}
}
fn typed_arg(ty: &str, value: KdlValue) -> KdlEntry {
KdlEntry::Argument {
ty: Some(ty.to_string()),
value,
}
}
fn typed_prop(key: &str, ty: &str, value: KdlValue) -> KdlEntry {
KdlEntry::Property {
key: key.to_string(),
ty: Some(ty.to_string()),
value,
}
}
fn node(name: &str, entries: Vec<KdlEntry>, children: Option<Vec<KdlNode>>) -> KdlNode {
KdlNode {
ty: None,
name: name.to_string(),
entries,
children,
}
}
#[test]
fn first_arg_skips_property() {
let n = node(
"n",
vec![
prop("k", KdlValue::Bool(true)),
arg(KdlValue::String("v".to_string())),
],
None,
);
assert_eq!(n.first_arg(), Some(&KdlValue::String("v".to_string())));
}
#[test]
fn first_arg_returns_none_when_no_args() {
let n = node("n", vec![prop("k", KdlValue::Bool(true))], None);
assert_eq!(n.first_arg(), None);
}
#[test]
fn first_string_arg_for_string() {
let n = node("n", vec![arg(KdlValue::String("hi".to_string()))], None);
assert_eq!(n.first_string_arg(), Some("hi"));
}
#[test]
fn first_string_arg_for_non_string() {
let n = node(
"n",
vec![arg(KdlValue::Number(make_number("1", Some(1), Some(1.0))))],
None,
);
assert_eq!(n.first_string_arg(), None);
}
#[test]
fn find_child_returns_first_match() {
let parent = node(
"p",
vec![],
Some(vec![
node("a", vec![arg(KdlValue::String("first".to_string()))], None),
node("a", vec![arg(KdlValue::String("second".to_string()))], None),
]),
);
assert_eq!(
parent.find_child("a").map(|c| c.first_string_arg()),
Some(Some("first")),
);
}
#[test]
fn find_child_returns_none_when_no_children() {
let n = node("n", vec![], None);
assert!(n.find_child("a").is_none());
}
#[test]
fn find_children_iterates_all_matches() {
let parent = node(
"p",
vec![],
Some(vec![
node("a", vec![arg(KdlValue::String("1".to_string()))], None),
node("b", vec![], None),
node("a", vec![arg(KdlValue::String("2".to_string()))], None),
]),
);
let collected: Vec<_> = parent.find_children("a").map(|c| c.name.clone()).collect();
assert_eq!(collected, vec!["a".to_string(), "a".to_string()]);
}
#[test]
fn find_children_empty_for_no_match() {
let parent = node("p", vec![], Some(vec![node("a", vec![], None)]));
assert_eq!(parent.find_children("z").count(), 0);
}
#[test]
fn string_prop_for_string_value() {
let n = node(
"n",
vec![prop("k", KdlValue::String("hello".to_string()))],
None,
);
assert_eq!(n.string_prop("k"), Some("hello"));
}
#[test]
fn string_prop_for_wrong_type() {
let n = node(
"n",
vec![prop(
"k",
KdlValue::Number(make_number("1", Some(1), Some(1.0))),
)],
None,
);
assert_eq!(n.string_prop("k"), None);
}
#[test]
fn string_prop_missing_key() {
let n = node("n", vec![prop("k", KdlValue::Bool(true))], None);
assert_eq!(n.string_prop("absent"), None);
}
#[test]
fn bool_prop_for_bool_value() {
let n = node("n", vec![prop("flag", KdlValue::Bool(true))], None);
assert_eq!(n.bool_prop("flag"), Some(true));
}
#[test]
fn bool_prop_for_wrong_type() {
let n = node(
"n",
vec![prop("flag", KdlValue::String("true".to_string()))],
None,
);
assert_eq!(n.bool_prop("flag"), None);
}
#[test]
fn bool_prop_missing_key() {
let n = node("n", vec![], None);
assert_eq!(n.bool_prop("absent"), None);
}
#[test]
fn int_prop_for_int_value() {
let n = node(
"n",
vec![prop(
"count",
KdlValue::Number(make_number("42", Some(42), Some(42.0))),
)],
None,
);
assert_eq!(n.int_prop("count"), Some(42));
}
#[test]
fn int_prop_for_float_only() {
let n = node(
"n",
vec![prop(
"x",
KdlValue::Number(make_number("2.5", None, Some(2.5))),
)],
None,
);
assert_eq!(n.int_prop("x"), None);
}
#[test]
fn int_prop_missing_key() {
let n = node("n", vec![], None);
assert_eq!(n.int_prop("absent"), None);
}
#[test]
fn string_child_values_collects_string_args() {
let parent = node(
"p",
vec![],
Some(vec![
node("item", vec![arg(KdlValue::String("a".to_string()))], None),
node("item", vec![arg(KdlValue::String("b".to_string()))], None),
node("other", vec![arg(KdlValue::String("z".to_string()))], None),
]),
);
assert_eq!(parent.string_child_values("item"), vec!["a", "b"]);
}
#[test]
fn string_child_values_skips_non_string() {
let parent = node(
"p",
vec![],
Some(vec![
node("item", vec![arg(KdlValue::String("a".to_string()))], None),
node(
"item",
vec![arg(KdlValue::Number(make_number("1", Some(1), Some(1.0))))],
None,
),
]),
);
assert_eq!(parent.string_child_values("item"), vec!["a"]);
}
#[test]
fn entry_ty_for_typed_argument() {
let e = typed_arg(
"u32",
KdlValue::Number(make_number("1", Some(1), Some(1.0))),
);
assert_eq!(e.ty(), Some("u32"));
}
#[test]
fn entry_ty_for_untyped_argument() {
let e = arg(KdlValue::Bool(true));
assert_eq!(e.ty(), None);
}
#[test]
fn entry_ty_for_typed_property() {
let e = typed_prop(
"k",
"i64",
KdlValue::Number(make_number("7", Some(7), Some(7.0))),
);
assert_eq!(e.ty(), Some("i64"));
}
#[test]
fn entry_ty_for_untyped_property() {
let e = prop("k", KdlValue::Bool(false));
assert_eq!(e.ty(), None);
}
}