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" }); }
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 s.push_str(&m.name);
158 s.push('(');
159 self.indent += 4;
160 if v.iter().any(|(_, v)| matches!(v, AeonValue::List(_))) {
161 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 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 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 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 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}