use serde_json::{Value, Map};
pub struct Toon;
impl Toon {
pub fn serialize(value: &Value) -> String {
match value {
Value::Object(map) => Self::serialize_object(map, 0),
Value::Array(arr) => Self::serialize_array(arr, 0),
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => if *b { "T" } else { "F" }.to_string(),
Value::Null => "~".to_string(),
}
}
fn serialize_object(map: &Map<String, Value>, indent: usize) -> String {
let mut out = String::new();
let pad = " ".repeat(indent);
for (k, v) in map {
match v {
Value::Object(child_map) => {
out.push_str(&format!("{}{}:\n{}", pad, k, Self::serialize_object(child_map, indent + 1)));
}
Value::Array(arr) => {
out.push_str(&format!("{}{}[{}]:\n{}", pad, k, arr.len(), Self::serialize_array(arr, indent + 1)));
}
_ => {
out.push_str(&format!("{}{}: {}\n", pad, k, Self::serialize(v)));
}
}
}
out
}
fn serialize_array(arr: &[Value], indent: usize) -> String {
if arr.is_empty() {
return "[]".to_string();
}
if let Some(first) = arr.first() {
if let Value::Object(first_map) = first {
let keys: Vec<String> = first_map.keys().cloned().collect();
let pad = " ".repeat(indent);
let mut out = format!("{}{{{}}}:\n", pad, keys.join(","));
for item in arr {
if let Value::Object(item_map) = item {
let values: Vec<String> = keys.iter()
.map(|k| item_map.get(k).map(|v| Self::serialize_flat(v)).unwrap_or_else(|| "~".to_string()))
.collect();
out.push_str(&format!("{}{}\n", pad, values.join(",")));
}
}
return out;
}
}
let mut out = String::new();
let pad = " ".repeat(indent);
for v in arr {
out.push_str(&format!("{}- {}\n", pad, Self::serialize(v).trim()));
}
out
}
fn serialize_flat(value: &Value) -> String {
match value {
Value::String(s) => s.replace(',', "\\,").to_string(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => if *b { "T" } else { "F" }.to_string(),
_ => ".".to_string(),
}
}
pub fn deserialize(input: &str) -> Result<Value, String> {
let lines: Vec<&str> = input.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.is_empty() {
return Ok(Value::Null);
}
Self::parse_level(&lines, 0).map(|(v, _)| v)
}
fn parse_level(lines: &[&str], start_idx: usize) -> Result<(Value, usize), String> {
if start_idx >= lines.len() {
return Ok((Value::Null, start_idx));
}
let first_line = lines[start_idx];
let indent = first_line.chars().take_while(|c| c.is_whitespace()).count();
let trimmed = first_line.trim();
if trimmed.starts_with('{') && trimmed.contains("}:") {
return Self::parse_tabular(lines, start_idx, indent);
}
if trimmed.starts_with("- ") {
return Self::parse_list(lines, start_idx, indent);
}
let mut map = Map::new();
let mut idx = start_idx;
while idx < lines.len() {
let line = lines[idx];
let current_indent = line.chars().take_while(|c| c.is_whitespace()).count();
if current_indent < indent {
break;
}
if current_indent > indent {
idx += 1;
continue;
}
let line_trimmed = line.trim();
if let Some(colon_idx) = line_trimmed.find(':') {
let mut key = line_trimmed[..colon_idx].trim().to_string();
if let Some(bracket_idx) = key.find('[') {
if key.ends_with(']') {
key = key[..bracket_idx].to_string();
}
}
let val_part = line_trimmed[colon_idx + 1..].trim();
if val_part.is_empty() && idx + 1 < lines.len() {
let next_indent = lines[idx + 1].chars().take_while(|c| c.is_whitespace()).count();
if next_indent > current_indent {
let (child_val, next_idx) = Self::parse_level(lines, idx + 1)?;
map.insert(key, child_val);
idx = next_idx;
continue;
}
}
map.insert(key, Self::parse_primitive(val_part));
idx += 1;
} else {
idx += 1;
}
}
Ok((Value::Object(map), idx))
}
fn parse_tabular(lines: &[&str], start_idx: usize, base_indent: usize) -> Result<(Value, usize), String> {
let header = lines[start_idx].trim();
let keys_str = header.trim_start_matches('{').trim_end_matches("}:");
let keys: Vec<&str> = keys_str.split(',').map(|k| k.trim()).collect();
let mut arr = Vec::new();
let mut idx = start_idx + 1;
while idx < lines.len() {
let line = lines[idx];
let current_indent = line.chars().take_while(|c| c.is_whitespace()).count();
if current_indent <= base_indent && !line.trim().is_empty() && idx != (start_idx + 1) {
if current_indent < base_indent { break; }
}
let row_trimmed = line.trim();
if row_trimmed.is_empty() {
idx += 1;
continue;
}
let values: Vec<Value> = row_trimmed.split(',')
.map(|v| Self::parse_primitive(v.trim()))
.collect();
let mut obj = Map::new();
for (i, key) in keys.iter().enumerate() {
let val = values.get(i).cloned().unwrap_or(Value::Null);
obj.insert(key.to_string(), val);
}
arr.push(Value::Object(obj));
idx += 1;
}
Ok((Value::Array(arr), idx))
}
fn parse_list(lines: &[&str], start_idx: usize, base_indent: usize) -> Result<(Value, usize), String> {
let mut arr = Vec::new();
let mut idx = start_idx;
while idx < lines.len() {
let line = lines[idx];
let current_indent = line.chars().take_while(|c| c.is_whitespace()).count();
if current_indent < base_indent {
break;
}
let trimmed = line.trim();
if trimmed.starts_with("- ") {
arr.push(Self::parse_primitive(&trimmed[2..]));
}
idx += 1;
}
Ok((Value::Array(arr), idx))
}
fn parse_primitive(s: &str) -> Value {
match s {
"~" => Value::Null,
"T" => Value::Bool(true),
"F" => Value::Bool(false),
_ => {
if let Ok(n) = s.parse::<i64>() {
Value::Number(n.into())
} else if let Ok(f) = s.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(f) {
Value::Number(n)
} else {
Value::String(s.to_string())
}
} else {
Value::String(s.replace("\\,", ",").to_string())
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_toon_tabular() {
let data = json!([
{"id": 1, "name": "Apple", "price": 10},
{"id": 2, "name": "Banana", "price": 5}
]);
let toon = Toon::serialize(&data);
assert!(toon.contains("{id,name,price}"));
assert!(toon.contains("1,Apple,10"));
}
#[test]
fn test_toon_object() {
let data = json!({
"user": "admin",
"meta": { "last_login": "2024-01-01" }
});
let toon = Toon::serialize(&data);
assert!(toon.contains("user: admin"));
assert!(toon.contains("meta:"));
}
#[test]
fn test_toon_roundtrip() {
let original = json!({
"project": "Aether",
"active": true,
"version": 1,
"null_val": null,
"tags": ["ai", "rust", "security"],
"files": [
{"name": "main.rs", "size": 1024},
{"name": "lib.rs", "size": 2048}
]
});
let serialized = Toon::serialize(&original);
println!("Serialized TOON:\n{}", serialized);
let deserialized = Toon::deserialize(&serialized).unwrap();
assert_eq!(original["project"], deserialized["project"]);
assert_eq!(deserialized["active"], json!(true));
assert_eq!(deserialized["null_val"], Value::Null);
assert_eq!(deserialized["tags"].as_array().unwrap().len(), 3);
assert_eq!(deserialized["files"].as_array().unwrap().len(), 2);
}
}