#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Integer(i64),
Float(f64),
String(String),
Array(Vec<Value>),
Object(Vec<(String, Value)>),
}
use crate::{KdlDocument, KdlEntry, KdlError, KdlErrorKind, KdlNode, KdlNumber, KdlValue};
pub fn value_to_kdl_document(value: &Value) -> Result<KdlDocument, KdlError> {
match value {
Value::Object(fields) => {
let nodes = fields
.iter()
.map(|(key, val)| value_to_kdl_node(key, val))
.collect::<Result<Vec<_>, _>>()?;
Ok(KdlDocument { nodes })
}
_ => Err(make_conv_error("top-level value must be an Object")),
}
}
pub fn kdl_document_to_value(doc: &KdlDocument) -> Result<Value, KdlError> {
let mut fields = Vec::with_capacity(doc.nodes.len());
for node in &doc.nodes {
let val = kdl_node_to_value(node)?;
fields.push((node.name.clone(), val));
}
Ok(Value::Object(fields))
}
const FLOAT_TYPE: &str = "f64";
fn value_to_kdl_node(key: &str, value: &Value) -> Result<KdlNode, KdlError> {
match value {
Value::Object(fields) => {
let children = fields
.iter()
.map(|(k, v)| value_to_kdl_node(k, v))
.collect::<Result<Vec<_>, _>>()?;
Ok(KdlNode {
ty: None,
name: key.to_string(),
entries: Vec::new(),
children: Some(children),
})
}
Value::Array(items) => {
let entries = items
.iter()
.map(primitive_to_argument)
.collect::<Result<Vec<_>, _>>()?;
Ok(KdlNode {
ty: None,
name: key.to_string(),
entries,
children: None,
})
}
_ => {
let entry = primitive_to_argument(value)?;
Ok(KdlNode {
ty: None,
name: key.to_string(),
entries: vec![entry],
children: None,
})
}
}
}
fn primitive_to_argument(value: &Value) -> Result<KdlEntry, KdlError> {
match value {
Value::Null => Ok(KdlEntry::Argument {
ty: None,
value: KdlValue::Null,
}),
Value::Bool(b) => Ok(KdlEntry::Argument {
ty: None,
value: KdlValue::Bool(*b),
}),
Value::Integer(i) => Ok(KdlEntry::Argument {
ty: None,
value: KdlValue::Number(i64_to_kdl_number(*i)),
}),
Value::Float(f) => Ok(KdlEntry::Argument {
ty: Some(FLOAT_TYPE.to_string()),
value: KdlValue::Number(f64_to_kdl_number(*f)),
}),
Value::String(s) => Ok(KdlEntry::Argument {
ty: None,
value: KdlValue::String(s.clone()),
}),
Value::Array(_) | Value::Object(_) => Err(make_conv_error(
"nested Array/Object inside Array is not supported",
)),
}
}
fn kdl_node_to_value(node: &KdlNode) -> Result<Value, KdlError> {
let args: Vec<&KdlEntry> = node
.entries
.iter()
.filter(|e| matches!(e, KdlEntry::Argument { .. }))
.collect();
let has_children = node
.children
.as_ref()
.map(|c| !c.is_empty())
.unwrap_or(false);
match (args.len(), has_children) {
(0, false) => Ok(Value::Null),
(0, true) => {
let children = node.children.as_ref().unwrap();
let mut fields = Vec::with_capacity(children.len());
for child in children {
let v = kdl_node_to_value(child)?;
fields.push((child.name.clone(), v));
}
Ok(Value::Object(fields))
}
(1, false) => kdl_entry_to_value(args[0]),
(_, false) => {
let items = args
.iter()
.map(|e| kdl_entry_to_value(e))
.collect::<Result<Vec<_>, _>>()?;
Ok(Value::Array(items))
}
(_, true) => Err(make_conv_error(
"node has both positional arguments and children, which is not supported",
)),
}
}
fn kdl_entry_to_value(entry: &KdlEntry) -> Result<Value, KdlError> {
let (ty, kdl_val) = match entry {
KdlEntry::Argument { ty, value } => (ty.as_deref(), value),
KdlEntry::Property { .. } => {
return Err(make_conv_error(
"expected positional argument, got property",
));
}
};
match kdl_val {
KdlValue::Null => Ok(Value::Null),
KdlValue::Bool(b) => Ok(Value::Bool(*b)),
KdlValue::String(s) => Ok(Value::String(s.clone())),
KdlValue::Number(n) => {
if ty == Some(FLOAT_TYPE) {
match n.as_f64 {
Some(f) => Ok(Value::Float(f)),
None => Err(make_conv_error("(f64)-annotated number has no f64 value")),
}
} else {
match n.as_i64 {
Some(i) => Ok(Value::Integer(i)),
None => match n.as_f64 {
Some(f) => Ok(Value::Float(f)),
None => Err(make_conv_error("number has neither i64 nor f64 value")),
},
}
}
}
}
}
fn i64_to_kdl_number(i: i64) -> KdlNumber {
KdlNumber {
raw: i.to_string(),
as_i64: Some(i),
as_f64: Some(i as f64),
}
}
fn f64_to_kdl_number(f: f64) -> KdlNumber {
let raw = format_f64(f);
KdlNumber {
raw,
as_i64: None, as_f64: Some(f),
}
}
fn format_f64(f: f64) -> String {
if f.is_nan() {
return "#nan".to_string();
}
if f.is_infinite() {
return if f > 0.0 {
"#inf".to_string()
} else {
"#-inf".to_string()
};
}
let s = format!("{f}");
if s.contains('.') || s.contains('e') || s.contains('E') || s.starts_with('#') {
s
} else {
format!("{s}.0")
}
}
fn make_conv_error(msg: &str) -> KdlError {
let _ = msg;
KdlError {
line: 0,
col: 0,
kind: KdlErrorKind::UnexpectedEof,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(v: &Value) -> Value {
let doc = value_to_kdl_document(v).expect("encode failed");
kdl_document_to_value(&doc).expect("decode failed")
}
#[test]
fn roundtrip_single_string_node() {
let input = Value::Object(vec![(
"text".to_string(),
Value::String("hello".to_string()),
)]);
let output = roundtrip(&input);
assert_eq!(input, output);
}
#[test]
fn roundtrip_array_node() {
let input = Value::Object(vec![(
"langs".to_string(),
Value::Array(vec![
Value::String("en".to_string()),
Value::String("ja".to_string()),
]),
)]);
let output = roundtrip(&input);
assert_eq!(input, output);
}
#[test]
fn roundtrip_nested_object() {
let root_ref = Value::Object(vec![
("cid".to_string(), Value::String("bafyCID".to_string())),
("uri".to_string(), Value::String("at://x".to_string())),
]);
let input = Value::Object(vec![(
"reply".to_string(),
Value::Object(vec![("root".to_string(), root_ref)]),
)]);
let output = roundtrip(&input);
assert_eq!(input, output);
}
#[test]
fn roundtrip_integer() {
let input = Value::Object(vec![("count".to_string(), Value::Integer(42))]);
let output = roundtrip(&input);
assert_eq!(input, output);
match &output {
Value::Object(fields) => {
assert_eq!(fields[0].1, Value::Integer(42));
}
_ => panic!("expected Object"),
}
}
#[test]
fn roundtrip_float() {
let input = Value::Object(vec![("ratio".to_string(), Value::Float(2.5))]);
let output = roundtrip(&input);
match &output {
Value::Object(fields) => {
if let Value::Float(f) = fields[0].1 {
assert!((f - 2.5_f64).abs() < 1e-10);
} else {
panic!("expected Float, got {:?}", fields[0].1);
}
}
_ => panic!("expected Object"),
}
}
#[test]
fn integer_and_float_are_distinct() {
let int_input = Value::Object(vec![("n".to_string(), Value::Integer(1))]);
let flt_input = Value::Object(vec![("n".to_string(), Value::Float(1.0))]);
let int_out = roundtrip(&int_input);
let flt_out = roundtrip(&flt_input);
assert_ne!(int_out, flt_out);
match &int_out {
Value::Object(f) => assert!(matches!(f[0].1, Value::Integer(_))),
_ => panic!(),
}
match &flt_out {
Value::Object(f) => assert!(matches!(f[0].1, Value::Float(_))),
_ => panic!(),
}
}
#[test]
fn roundtrip_null() {
let input = Value::Object(vec![("deleted".to_string(), Value::Null)]);
let output = roundtrip(&input);
assert_eq!(input, output);
}
#[test]
fn roundtrip_bool() {
let input = Value::Object(vec![
("active".to_string(), Value::Bool(true)),
("deleted".to_string(), Value::Bool(false)),
]);
let output = roundtrip(&input);
assert_eq!(input, output);
}
#[test]
fn roundtrip_negative_integer() {
let input = Value::Object(vec![("offset".to_string(), Value::Integer(-7))]);
let output = roundtrip(&input);
assert_eq!(input, output);
}
#[test]
fn top_level_non_object_is_error() {
let result = value_to_kdl_document(&Value::String("oops".to_string()));
assert!(result.is_err());
}
#[test]
fn roundtrip_via_text() {
use crate::{normalize, parse};
let input = Value::Object(vec![
("name".to_string(), Value::String("Alice".to_string())),
("age".to_string(), Value::Integer(30)),
("score".to_string(), Value::Float(9.5)),
("active".to_string(), Value::Bool(true)),
(
"meta".to_string(),
Value::Object(vec![(
"role".to_string(),
Value::String("admin".to_string()),
)]),
),
]);
let doc = value_to_kdl_document(&input).unwrap();
let text = normalize(&doc);
let doc2 = parse(&text).unwrap();
let output = kdl_document_to_value(&doc2).unwrap();
match (&input, &output) {
(Value::Object(a), Value::Object(b)) => {
assert_eq!(a.len(), b.len());
assert_eq!(a[0], b[0]); assert_eq!(a[1], b[1]); if let (Value::Float(fa), Value::Float(fb)) = (&a[2].1, &b[2].1) {
assert!((fa - fb).abs() < 1e-10);
} else {
panic!("expected Float for score");
}
assert_eq!(a[3], b[3]); assert_eq!(a[4], b[4]); }
_ => panic!("expected Object"),
}
}
#[test]
fn empty_node_decoded_as_null() {
let doc = KdlDocument {
nodes: vec![KdlNode {
ty: None,
name: "empty".to_string(),
entries: vec![],
children: None,
}],
};
let val = kdl_document_to_value(&doc).unwrap();
assert_eq!(val, Value::Object(vec![("empty".to_string(), Value::Null)]));
}
}