#[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,
})
}
}
#[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,
}
}
}
#[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);
}
}