notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

serializer.rs (5433B)


      1 use crate::ast::*;
      2 use std::fmt::Write;
      3 
      4 /// Serialize a Space back to s-expression format.
      5 pub fn serialize(space: &Space) -> String {
      6     serialize_from(space, space.root)
      7 }
      8 
      9 /// Serialize a subtree starting from a specific cell.
     10 pub fn serialize_from(space: &Space, root: CellId) -> String {
     11     let mut out = String::new();
     12     write_cell(space, root, 0, &mut out);
     13     out
     14 }
     15 
     16 fn format_number(n: f64) -> String {
     17     if n == n.floor() && n.abs() < i64::MAX as f64 {
     18         format!("{}", n as i64)
     19     } else {
     20         // Round to 4 decimal places to avoid f32→f64 noise
     21         // (e.g. -0.20000000298023224 → -0.2)
     22         let rounded = (n * 10000.0).round() / 10000.0;
     23         let s = format!("{:.4}", rounded);
     24         let s = s.trim_end_matches('0');
     25         let s = s.trim_end_matches('.');
     26         s.to_string()
     27     }
     28 }
     29 
     30 fn write_cell(space: &Space, id: CellId, indent: usize, out: &mut String) {
     31     let cell = space.cell(id);
     32     let pad = "  ".repeat(indent);
     33     let inner_pad = "  ".repeat(indent + 1);
     34 
     35     out.push('(');
     36     out.push_str(&cell.cell_type.to_string());
     37 
     38     // Attributes
     39     let attrs = space.attrs(id);
     40     for attr in attrs {
     41         let _ = write!(out, "\n{}", inner_pad);
     42         write_attr(attr, out);
     43     }
     44 
     45     // Children
     46     let children = space.children(id);
     47     for &child_id in children {
     48         let _ = write!(out, "\n{}", inner_pad);
     49         write_cell(space, child_id, indent + 1, out);
     50     }
     51 
     52     // Closing paren on same line if no attrs/children, else on new line
     53     if !attrs.is_empty() || !children.is_empty() {
     54         // For readability, close on the last line
     55         out.push(')');
     56     } else {
     57         out.push(')');
     58     }
     59 
     60     let _ = pad; // used above via inner_pad derivation
     61 }
     62 
     63 fn write_attr(attr: &Attribute, out: &mut String) {
     64     match attr {
     65         Attribute::Shape(s) => {
     66             let _ = write!(out, "(shape {})", s);
     67         }
     68         Attribute::Id(s) => {
     69             let _ = write!(out, "(id {})", s);
     70         }
     71         Attribute::Name(s) => {
     72             let _ = write!(out, "(name \"{}\")", s);
     73         }
     74         Attribute::Material(s) => {
     75             let _ = write!(out, "(material \"{}\")", s);
     76         }
     77         Attribute::Condition(s) => {
     78             let _ = write!(out, "(condition \"{}\")", s);
     79         }
     80         Attribute::Location(loc) => {
     81             let _ = write!(out, "(location {})", loc);
     82         }
     83         Attribute::State(s) => {
     84             let _ = write!(out, "(state {})", s);
     85         }
     86         Attribute::Type(s) => {
     87             let _ = write!(out, "(type {})", s);
     88         }
     89         Attribute::Width(n) => {
     90             let _ = write!(out, "(width {})", format_number(*n));
     91         }
     92         Attribute::Height(n) => {
     93             let _ = write!(out, "(height {})", format_number(*n));
     94         }
     95         Attribute::Depth(n) => {
     96             let _ = write!(out, "(depth {})", format_number(*n));
     97         }
     98         Attribute::Position(x, y, z) => {
     99             let _ = write!(
    100                 out,
    101                 "(position {} {} {})",
    102                 format_number(*x),
    103                 format_number(*y),
    104                 format_number(*z)
    105             );
    106         }
    107         Attribute::Rotation(x, y, z) => {
    108             let _ = write!(
    109                 out,
    110                 "(rotation {} {} {})",
    111                 format_number(*x),
    112                 format_number(*y),
    113                 format_number(*z)
    114             );
    115         }
    116         Attribute::ModelUrl(s) => {
    117             let _ = write!(out, "(model-url \"{}\")", s);
    118         }
    119         Attribute::Tileset(names) => {
    120             out.push_str("(tileset");
    121             for name in names {
    122                 let _ = write!(out, " \"{}\"", name);
    123             }
    124             out.push(')');
    125         }
    126         Attribute::Data(s) => {
    127             let _ = write!(out, "(data \"{}\")", s);
    128         }
    129     }
    130 }
    131 
    132 #[cfg(test)]
    133 mod tests {
    134     use super::*;
    135     use crate::parser::parse;
    136 
    137     #[test]
    138     fn test_serialize_simple() {
    139         let space = parse("(room (name \"Test\") (width 10))").unwrap();
    140         let output = serialize(&space);
    141         assert!(output.contains("(room"));
    142         assert!(output.contains("(name \"Test\")"));
    143         assert!(output.contains("(width 10)"));
    144     }
    145 
    146     #[test]
    147     fn test_tilemap_roundtrip() {
    148         let input =
    149             r#"(tilemap (width 10) (height 10) (tileset "grass" "stone") (data "0 0 1 1"))"#;
    150         let space = parse(input).unwrap();
    151         let serialized = serialize(&space);
    152         let reparsed = parse(&serialized).unwrap();
    153         assert_eq!(reparsed.cell(reparsed.root).cell_type, CellType::Tilemap);
    154         assert_eq!(
    155             reparsed.tileset(reparsed.root),
    156             Some(&vec!["grass".to_string(), "stone".to_string()])
    157         );
    158         assert_eq!(reparsed.data(reparsed.root), Some("0 0 1 1"));
    159     }
    160 
    161     #[test]
    162     fn test_format_number_strips_float_noise() {
    163         // Integers
    164         assert_eq!(format_number(10.0), "10");
    165         assert_eq!(format_number(-3.0), "-3");
    166         assert_eq!(format_number(0.0), "0");
    167 
    168         // Clean decimals
    169         assert_eq!(format_number(1.5), "1.5");
    170         assert_eq!(format_number(-0.2), "-0.2");
    171 
    172         // f32→f64 noise: -0.2f32 as f64 == -0.20000000298023224
    173         assert_eq!(format_number(-0.2_f32 as f64), "-0.2");
    174         assert_eq!(format_number(0.5_f32 as f64), "0.5");
    175         assert_eq!(format_number(1.125_f32 as f64), "1.125");
    176     }
    177 }