aeon/
serializer.rs

1use crate::document::{AeonDocument, AeonMacro, AeonProperty};
2use crate::value::AeonValue;
3
4macro_rules! serialize_arg(
5    ($s:ident, $idx:ident, $val:expr) => {
6        if $idx == 0 {
7            $s.push_str($val);
8        } else {
9            $s.push(',');
10            $s.push(' ');
11            $s.push_str($val);
12        }
13    }
14);
15
16pub trait AeonFormatter {
17    fn serialize_aeon(obj: &AeonDocument) -> String;
18    fn serialize_macro(&mut self, mac: &AeonMacro, s: &mut String);
19    fn serialize_property(&mut self, obj: &AeonDocument, property: &AeonProperty, s: &mut String);
20    fn serialize_value(&mut self, obj: &AeonDocument, value: &AeonValue, s: &mut String);
21}
22
23pub struct PrettySerializer {
24    indent: i8,
25    indent_skip: bool,
26}
27
28impl PrettySerializer {
29    pub fn new() -> Self {
30        Self {
31            indent: 0,
32            indent_skip: false,
33        }
34    }
35}
36
37impl AeonFormatter for PrettySerializer {
38    fn serialize_aeon(obj: &AeonDocument) -> String {
39        let mut ser = PrettySerializer::new();
40        let mut s = String::with_capacity(50);
41        for mac in obj.macros.values() {
42            ser.serialize_macro(mac, &mut s);
43        }
44        if !obj.macros.is_empty() {
45            s.push('\n');
46        }
47        for prop in obj.properties.values() {
48            ser.serialize_property(obj, prop, &mut s);
49            s.push('\n');
50            s.push('\n');
51        }
52        s
53    }
54
55    fn serialize_macro(&mut self, mac: &AeonMacro, s: &mut String) {
56        s.push('@');
57        s.push_str(mac.name.as_str());
58        s.push('(');
59        for arg in 0..mac.args.len() {
60            serialize_arg!(s, arg, &mac.args[arg]);
61        }
62        s.push(')');
63        s.push('\n');
64    }
65
66    fn serialize_property(&mut self, obj: &AeonDocument, property: &AeonProperty, s: &mut String) {
67        s.push_str(property.name.as_str());
68        s.push(':');
69        s.push(' ');
70        self.serialize_value(obj, &property.value, s);
71    }
72
73    fn serialize_value(&mut self, obj: &AeonDocument, value: &AeonValue, s: &mut String) {
74        macro_rules! indent_me {
75            ($self:expr, $s:expr) => {
76                if !$self.indent_skip {
77                    for _ in 0..$self.indent {
78                        $s.push(' ');
79                    }
80                } else {
81                    $self.indent_skip = false;
82                }
83            };
84        }
85        match value {
86            AeonValue::Nil => {
87                indent_me!(self, s);
88                s.push_str("nil");
89            }
90            AeonValue::Bool(v) => {
91                indent_me!(self, s);
92                s.push_str(if *v { "true" } else { "false" }); // could probably just use v.to_string() here
93            }
94            AeonValue::String(v) => {
95                indent_me!(self, s);
96                s.push('"');
97                for x in v.chars() {
98                    match x {
99                        '\\' => {
100                            s.push('\\');
101                            s.push('\\');
102                        }
103                        '\r' => {
104                            s.push('\\');
105                            s.push('r');
106                        }
107                        '\n' => {
108                            s.push('\\');
109                            s.push('n');
110                        }
111                        '\t' => {
112                            s.push('\\');
113                            s.push('t');
114                        }
115                        '"' => {
116                            s.push('\\');
117                            s.push('"');
118                        }
119                        _ => {
120                            s.push(x);
121                        }
122                    }
123                }
124                s.push('"');
125            }
126            AeonValue::Integer(v) => {
127                indent_me!(self, s);
128                s.push_str(&v.to_string());
129            }
130            AeonValue::Double(v) => {
131                indent_me!(self, s);
132                s.push_str(&format!("{:?}", v));
133            }
134            AeonValue::List(v) => {
135                indent_me!(self, s);
136                s.push('[');
137
138                self.indent += 4;
139                for (i, item) in v.iter().enumerate() {
140                    if i != 0 {
141                        s.push(',');
142                    }
143                    s.push('\n');
144                    self.serialize_value(obj, item, s);
145                }
146                self.indent -= 4;
147                if !v.is_empty() {
148                    s.push('\n');
149                    indent_me!(self, s);
150                }
151                s.push(']');
152            }
153            AeonValue::Object(v) => {
154                indent_me!(self, s);
155                if let Some(m) = obj.try_get_macro(v) {
156                    // first check if a macro exists for this map
157                    s.push_str(&m.name);
158                    s.push('(');
159                    self.indent += 4;
160                    if v.iter().any(|(_, v)| matches!(v, AeonValue::List(_))) {
161                        // map contains a list
162                        self.indent_skip = true;
163                        for i in 0..m.args.len() {
164                            if i != 0 {
165                                s.push(',');
166                                s.push('\n');
167                            }
168                            self.serialize_value(obj, &v[&m.args[i]], s);
169                        }
170                        self.indent -= 4;
171                        if !v.is_empty() {
172                            s.push('\n');
173                            indent_me!(self, s);
174                        }
175                    } else {
176                        // map contains no list
177                        for i in 0..m.args.len() {
178                            self.indent_skip = true;
179                            if i != 0 {
180                                s.push(',');
181                                s.push(' ');
182                            }
183                            self.serialize_value(obj, &v[&m.args[i]], s);
184                        }
185                        self.indent -= 4;
186                    }
187                    s.push(')');
188                } else {
189                    // if not, serialize as a regular map
190                    s.push('{');
191                    let mut f = true;
192                    for (k, v) in v.iter() {
193                        if f {
194                            f = false;
195                        } else {
196                            s.push(',');
197                            s.push(' ');
198                        }
199                        s.push('\n');
200                        if !is_valid_identifier(k.as_str()) {
201                            s.push('"');
202                            s.push_str(k.as_str());
203                            s.push('"');
204                        } else {
205                            s.push_str(k.as_str());
206                        }
207                        s.push(':');
208                        s.push(' ');
209                        self.indent_skip = true;
210                        self.serialize_value(obj, v, s);
211                    }
212                    if !v.is_empty() {
213                        s.push('\n');
214                        indent_me!(self, s);
215                    }
216                    s.push('}');
217                }
218            }
219        }
220    }
221}
222
223fn is_valid_identifier(s: &str) -> bool {
224    let start_valid = match s.get(0..=0) {
225        None => false,
226        Some(first) => first
227            .chars()
228            .next()
229            .is_some_and(|it| it.is_ascii_alphabetic()),
230    };
231    if !start_valid {
232        return false;
233    }
234    // keywords are not identifiers
235    if s == "nil" || s == "true" || s == "false" {
236        return false;
237    }
238    for c in s.chars() {
239        match c {
240            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => (),
241            _ => return false,
242        }
243    }
244    true
245}
246#[cfg(test)]
247mod tests {
248    use crate::document::{AeonDocument, AeonMacro};
249    use crate::map;
250    use crate::serializer::{AeonFormatter, PrettySerializer};
251    use crate::value::AeonValue;
252
253    #[test]
254    pub fn serialize_using_macros() {
255        let mut aeon = AeonDocument::new();
256        aeon.add_macro(AeonMacro::new(
257            "character".into(),
258            vec![
259                "name".into(),
260                "world".into(),
261                "double".into(),
262                "or_nothing".into(),
263            ],
264        ));
265        aeon.add_property(
266            "char",
267            AeonValue::Object(map![
268               "name".into() => AeonValue::String("erki".into()),
269               "world".into() => AeonValue::Integer(1),
270               "double".into() => AeonValue::Double(139.3567),
271               "or_nothing".into() => AeonValue::Nil,
272            ]),
273        );
274        let serialized = PrettySerializer::serialize_aeon(&aeon);
275        assert_eq!("@character(name, world, double, or_nothing)\n\nchar: character(\"erki\", 1, 139.3567, nil)\n\n", serialized);
276    }
277
278    #[test]
279    pub fn serialize_using_nested_macros() {
280        let mut aeon = AeonDocument::new();
281        aeon.add_macro(AeonMacro::new(
282            "character".into(),
283            vec![
284                "name".into(),
285                "world".into(),
286                "double".into(),
287                "or_nothing".into(),
288            ],
289        ));
290        aeon.add_property(
291            "char",
292            AeonValue::Object(map![
293               "name".into() => AeonValue::String("erki".into()),
294               "world".into() => AeonValue::Integer(1),
295               "double".into() => AeonValue::Double(139.3567),
296               "or_nothing".into() => AeonValue::Object(map![
297                   "name".into() => AeonValue::String("unused".into()),
298                   "world".into() => AeonValue::Integer(-53),
299                   "double".into() => AeonValue::Double(-11.38),
300                   "or_nothing".into() => AeonValue::Nil,
301               ]),
302            ]),
303        );
304        let serialized = PrettySerializer::serialize_aeon(&aeon);
305        assert_eq!("@character(name, world, double, or_nothing)\n\nchar: character(\"erki\", 1, 139.3567, character(\"unused\", -53, -11.38, nil))\n\n", serialized);
306    }
307
308    #[test]
309    pub fn serialize_map_property() {
310        let mut aeon = AeonDocument::new();
311        aeon.add_property(
312            "character",
313            AeonValue::Object(map![
314               "name".into() => AeonValue::String("erki".into()),
315               "world".into() => AeonValue::Integer(1),
316               "double".into() => AeonValue::Double(139.3567),
317               "or_nothing".into() => AeonValue::Nil,
318            ]),
319        );
320        let serialized = PrettySerializer::serialize_aeon(&aeon);
321        // TODO: regex or rewrite serialize implementation to be more testable
322        // or just don't test the entire serialization and instead its parts
323        assert!(serialized.starts_with("character: {\n"));
324        assert!(serialized.ends_with("}\n\n"));
325        assert!(serialized.contains(r#"name: "erki""#));
326        assert!(serialized.contains(r#"world: 1"#));
327        assert!(serialized.contains(r#"double: 139.3567"#));
328        assert!(serialized.contains(r#"or_nothing: nil"#));
329        assert!(serialized.contains(','));
330    }
331
332    #[test]
333    pub fn serialize_list_of_strings_property() {
334        let mut aeon = AeonDocument::new();
335        aeon.add_property(
336            "characters",
337            AeonValue::List(vec![
338                AeonValue::String("erki".into()),
339                AeonValue::String("persiko".into()),
340                AeonValue::String("frukt".into()),
341                AeonValue::String("152436.13999".into()),
342            ]),
343        );
344        let serialized = PrettySerializer::serialize_aeon(&aeon);
345        assert_eq!("characters: [\n    \"erki\",\n    \"persiko\",\n    \"frukt\",\n    \"152436.13999\"\n]\n\n", serialized);
346    }
347
348    #[test]
349    pub fn serialize_string_property() {
350        let mut aeon = AeonDocument::new();
351        aeon.add_property("character", AeonValue::String("erki".into()));
352        let ser = PrettySerializer::serialize_aeon(&aeon);
353        assert_eq!("character: \"erki\"\n\n", ser);
354    }
355
356    #[test]
357    pub fn serialize_string_property_with_escape_char() {
358        let mut aeon = AeonDocument::new();
359        aeon.add_property(
360            "testing",
361            AeonValue::String("C:\\Path\\Is\\Escaped\"oh quote\nline\ttab".into()),
362        );
363        let ser = PrettySerializer::serialize_aeon(&aeon);
364        assert_eq!(
365            "testing: \"C:\\\\Path\\\\Is\\\\Escaped\\\"oh quote\\nline\\ttab\"\n\n",
366            ser
367        );
368    }
369
370    #[test]
371    pub fn serialize_double_with_no_decimals() {
372        let mut aeon = AeonDocument::new();
373        aeon.add_property(
374            "nodecimals",
375            AeonValue::Double(85.0),
376        );
377        let ser = PrettySerializer::serialize_aeon(&aeon);
378        assert_eq!(
379            "nodecimals: 85.0\n\n",
380            ser
381        );
382    }
383
384    #[test]
385    pub fn serialize_macros() {
386        let mut aeon = AeonDocument::new();
387        aeon.add_macro(AeonMacro::new(
388            "character".into(),
389            vec!["name".into(), "world".into()],
390        ));
391        let ser = PrettySerializer::serialize_aeon(&aeon);
392        assert_eq!("@character(name, world)\n\n", ser);
393    }
394}