1pub 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
46pub 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}