Skip to main content

koda_rs/
lib.rs

1//! # koda-rs
2//!
3//! Rust implementation of **KODA** (Compact Object Data Architecture): a compact,
4//! deterministic format for structured data, with both text and binary representations.
5//!
6//! ## Text format
7//!
8//! Parse and stringify a key-value style text format:
9//!
10//! ```text
11//! name: "my-app"
12//! version: 1
13//! enabled: true
14//! ```
15//!
16//! ## Binary format
17//!
18//! Encode and decode the same data to a binary wire format compatible with
19//! [koda-go](https://github.com/hghukasyan/koda-go): magic `KODA`, version, key
20//! dictionary, then typed values (null, bool, integer, float, string, array, object).
21
22pub mod decoder;
23pub mod encoder;
24pub mod error;
25pub mod lexer;
26pub mod parser;
27pub mod stream;
28pub mod value;
29
30#[cfg(feature = "serde")]
31pub mod serde;
32
33pub use decoder::{decode, decode_with_options, DecodeOptions};
34
35#[cfg(feature = "parallel")]
36pub use decoder::{decode_parallel, decode_parallel_with_options};
37pub use encoder::encode;
38pub use error::{KodaError, Result};
39pub use parser::parse;
40pub use stream::parse_reader;
41pub use value::Value;
42
43#[cfg(feature = "serde")]
44pub use serde::{from_str, to_string};
45
46/// Serializes a `Value` to KODA text format (key: value style).
47///
48/// Produces deterministic output with object keys in sorted order.
49/// Strings are quoted and escaped; numbers and booleans are unquoted.
50pub fn stringify(value: &Value<'_>) -> String {
51    stringify_value(value, 0)
52}
53
54fn stringify_value(v: &Value<'_>, _indent: usize) -> String {
55    match v {
56        Value::Null => "null".to_string(),
57        Value::Bool(b) => b.to_string(),
58        Value::Number(n) => {
59            if n.fract() == 0.0
60                && n.is_finite()
61                && *n >= (i64::MIN as f64)
62                && *n <= (i64::MAX as f64)
63            {
64                format!("{}", *n as i64)
65            } else {
66                let s = n.to_string();
67                if s.contains('.') || s.contains('e') || s.contains('E') {
68                    s
69                } else {
70                    format!("{}.0", s)
71                }
72            }
73        }
74        Value::String(s) => escape_string(s.as_ref()),
75        Value::Array(arr) => {
76            let parts: Vec<String> = arr.iter().map(|x| stringify_value(x, 0)).collect();
77            format!("[{}]", parts.join(", "))
78        }
79        Value::Object(m) => {
80            let parts: Vec<String> = m
81                .iter()
82                .map(|(k, v)| {
83                    let key = if is_bare_ident(k.as_ref()) {
84                        k.to_string()
85                    } else {
86                        escape_string(k.as_ref())
87                    };
88                    format!("{}: {}", key, stringify_value(v, 0))
89                })
90                .collect();
91            format!("{{{}}}", parts.join(", "))
92        }
93    }
94}
95
96fn is_bare_ident(s: &str) -> bool {
97    let mut chars = s.chars();
98    match chars.next() {
99        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
100        _ => return false,
101    }
102    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
103}
104
105fn escape_string(s: &str) -> String {
106    let mut out = String::from('"');
107    for c in s.chars() {
108        match c {
109            '"' => out.push_str("\\\""),
110            '\\' => out.push_str("\\\\"),
111            '\n' => out.push_str("\\n"),
112            '\r' => out.push_str("\\r"),
113            '\t' => out.push_str("\\t"),
114            c => out.push(c),
115        }
116    }
117    out.push('"');
118    out
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use std::borrow::Cow;
125    use std::collections::BTreeMap;
126
127    #[test]
128    fn roundtrip_text() {
129        let text = r#"name: "my-app"
130version: 1
131enabled: true"#;
132        let value = parse(text).unwrap();
133        let out = stringify(&value);
134        let value2 = parse(&out).unwrap();
135        assert_eq!(value, value2);
136    }
137
138    #[test]
139    fn roundtrip_binary() {
140        let mut m = BTreeMap::new();
141        m.insert(
142            Cow::Owned("name".to_string()),
143            Value::String(Cow::Owned("my-app".to_string())),
144        );
145        m.insert(Cow::Owned("version".to_string()), Value::Number(1.0));
146        m.insert(Cow::Owned("enabled".to_string()), Value::Bool(true));
147        let value = Value::Object(m);
148        let bytes = encode(&value).unwrap();
149        let decoded = decode(&bytes).unwrap();
150        assert_eq!(value, decoded);
151    }
152}