toonify_core/
encoder.rs

1use std::borrow::Cow;
2use std::str::FromStr;
3
4use bigdecimal::{BigDecimal, Zero};
5use serde_json::{Map, Number, Value};
6
7use crate::error::ToonifyError;
8use crate::options::{Delimiter, EncoderOptions, KeyFoldingMode};
9use crate::quoting::{encode_key, encode_string, is_identifier_segment};
10
11pub fn encode_value(value: &Value, options: &EncoderOptions) -> Result<String, ToonifyError> {
12    let mut encoder = Encoder::new(options);
13    encoder.encode_root(value)?;
14    Ok(encoder.finish())
15}
16
17struct Encoder<'a> {
18    options: &'a EncoderOptions,
19    lines: Vec<String>,
20}
21
22impl<'a> Encoder<'a> {
23    fn new(options: &'a EncoderOptions) -> Self {
24        Self {
25            options,
26            lines: Vec::new(),
27        }
28    }
29
30    fn finish(self) -> String {
31        self.lines.join("\n")
32    }
33
34    fn encode_root(&mut self, value: &Value) -> Result<(), ToonifyError> {
35        match value {
36            Value::Object(map) => {
37                if map.is_empty() {
38                    Ok(())
39                } else {
40                    self.encode_object_fields(map, 0)
41                }
42            }
43            Value::Array(items) => {
44                self.encode_array(None, items, ArrayContext::Normal { depth: 0 })
45            }
46            primitive => {
47                let rendered =
48                    self.stringify_primitive(primitive, Some(self.options.document_delimiter))?;
49                self.lines.push(rendered);
50                Ok(())
51            }
52        }
53    }
54
55    fn encode_object_fields(
56        &mut self,
57        map: &Map<String, Value>,
58        depth: usize,
59    ) -> Result<(), ToonifyError> {
60        for (key, value) in map {
61            let FoldResult { key, value } = self.fold_key(key, value, map);
62            self.encode_named_value(&key, value, depth)?;
63        }
64        Ok(())
65    }
66
67    fn encode_named_value(
68        &mut self,
69        key: &str,
70        value: &Value,
71        depth: usize,
72    ) -> Result<(), ToonifyError> {
73        match value {
74            Value::Object(map) => {
75                if map.is_empty() {
76                    self.push_line(depth, format!("{}:", encode_key(key)));
77                } else {
78                    self.push_line(depth, format!("{}:", encode_key(key)));
79                    self.encode_object_fields(map, depth + 1)?;
80                }
81            }
82            Value::Array(items) => {
83                self.encode_array(Some(key), items, ArrayContext::Normal { depth })?
84            }
85            primitive => {
86                let rendered =
87                    self.stringify_primitive(primitive, Some(self.options.document_delimiter))?;
88                self.push_line(depth, format!("{}: {}", encode_key(key), rendered));
89            }
90        }
91        Ok(())
92    }
93
94    fn encode_array(
95        &mut self,
96        key: Option<&str>,
97        items: &[Value],
98        context: ArrayContext,
99    ) -> Result<(), ToonifyError> {
100        let delimiter = self.options.document_delimiter;
101        if items.iter().all(is_primitive) {
102            self.emit_inline_array(key, items, delimiter, context)?;
103            return Ok(());
104        }
105
106        if let Some(fields) = detect_tabular(items) {
107            self.emit_tabular_array(key, items, &fields, delimiter, context)?;
108            return Ok(());
109        }
110
111        if is_array_of_primitive_arrays(items) {
112            self.emit_array_of_arrays(key, items, delimiter, context)?;
113            return Ok(());
114        }
115
116        self.emit_general_list(key, items, delimiter, context)
117    }
118
119    fn emit_inline_array(
120        &mut self,
121        key: Option<&str>,
122        items: &[Value],
123        delimiter: Delimiter,
124        context: ArrayContext,
125    ) -> Result<(), ToonifyError> {
126        let header = self.format_header(key, items.len(), delimiter, None);
127        let indent = self.indent(context.header_depth());
128        let prefix = context.header_prefix();
129
130        if items.is_empty() {
131            self.lines.push(format!("{}{}{}", indent, prefix, header));
132        } else {
133            let sep = delimiter.separator().to_string();
134            let values = items
135                .iter()
136                .map(|value| self.stringify_primitive(value, Some(delimiter)))
137                .collect::<Result<Vec<_>, _>>()?;
138            let joined = values.join(&sep);
139            self.lines
140                .push(format!("{}{}{} {}", indent, prefix, header, joined));
141        }
142        Ok(())
143    }
144
145    fn emit_tabular_array(
146        &mut self,
147        key: Option<&str>,
148        items: &[Value],
149        fields: &[String],
150        delimiter: Delimiter,
151        context: ArrayContext,
152    ) -> Result<(), ToonifyError> {
153        let header = self.format_header(key, items.len(), delimiter, Some(fields));
154        let indent = self.indent(context.header_depth());
155        let prefix = context.header_prefix();
156        self.lines.push(format!("{}{}{}", indent, prefix, header));
157
158        let row_indent_depth = context.row_depth();
159        let row_indent = self.indent(row_indent_depth);
160        let sep = delimiter.separator().to_string();
161
162        for item in items {
163            let obj = item.as_object().ok_or_else(|| {
164                ToonifyError::encoding("tabular detection failed due to non-object row")
165            })?;
166            let mut cells = Vec::with_capacity(fields.len());
167            for field in fields {
168                let cell = obj.get(field).expect("field must exist");
169                let rendered = self.stringify_primitive(cell, Some(delimiter))?;
170                cells.push(rendered);
171            }
172            self.lines
173                .push(format!("{}{}", row_indent, cells.join(&sep)));
174        }
175
176        Ok(())
177    }
178
179    fn emit_array_of_arrays(
180        &mut self,
181        key: Option<&str>,
182        items: &[Value],
183        delimiter: Delimiter,
184        context: ArrayContext,
185    ) -> Result<(), ToonifyError> {
186        let header = self.format_header(key, items.len(), delimiter, None);
187        let indent = self.indent(context.header_depth());
188        let prefix = context.header_prefix();
189        self.lines.push(format!("{}{}{}", indent, prefix, header));
190
191        for inner in items {
192            let inner_items = inner
193                .as_array()
194                .ok_or_else(|| ToonifyError::encoding("expected inner array"))?;
195            let inner_header = self.format_header(None, inner_items.len(), delimiter, None);
196            let row_indent = self.indent(context.row_depth());
197            if inner_items.is_empty() {
198                self.lines.push(format!("{}- {}", row_indent, inner_header));
199            } else {
200                let sep = delimiter.separator().to_string();
201                let values = inner_items
202                    .iter()
203                    .map(|value| self.stringify_primitive(value, Some(delimiter)))
204                    .collect::<Result<Vec<_>, _>>()?;
205                let joined = values.join(&sep);
206                self.lines
207                    .push(format!("{}- {} {}", row_indent, inner_header, joined));
208            }
209        }
210
211        Ok(())
212    }
213
214    fn emit_general_list(
215        &mut self,
216        key: Option<&str>,
217        items: &[Value],
218        delimiter: Delimiter,
219        context: ArrayContext,
220    ) -> Result<(), ToonifyError> {
221        let header = self.format_header(key, items.len(), delimiter, None);
222        let indent = self.indent(context.header_depth());
223        let prefix = context.header_prefix();
224        self.lines.push(format!("{}{}{}", indent, prefix, header));
225        let row_indent_depth = context.row_depth();
226
227        for item in items {
228            match item {
229                Value::Object(map) => self.encode_object_list_item(map, row_indent_depth)?,
230                Value::Array(inner) => {
231                    self.encode_array(
232                        None,
233                        inner,
234                        ArrayContext::ListFirstField {
235                            depth: row_indent_depth.saturating_sub(1),
236                        },
237                    )?;
238                }
239                primitive => {
240                    let rendered =
241                        self.stringify_primitive(primitive, Some(self.options.document_delimiter))?;
242                    let indent = self.indent(row_indent_depth);
243                    self.lines.push(format!("{}- {}", indent, rendered));
244                }
245            }
246        }
247
248        Ok(())
249    }
250
251    fn encode_object_list_item(
252        &mut self,
253        map: &Map<String, Value>,
254        depth: usize,
255    ) -> Result<(), ToonifyError> {
256        if map.is_empty() {
257            let indent = self.indent(depth);
258            self.lines.push(format!("{}-", indent));
259            return Ok(());
260        }
261
262        let mut iter = map.iter();
263        if let Some((first_key, first_value)) = iter.next() {
264            let FoldResult { key, value } = self.fold_key(first_key, first_value, map);
265            match value {
266                Value::Object(obj) => {
267                    let indent = self.indent(depth);
268                    self.lines
269                        .push(format!("{}- {}:", indent, encode_key(&key)));
270                    if !obj.is_empty() {
271                        self.encode_object_fields(obj, depth + 2)?;
272                    }
273                }
274                Value::Array(items) => {
275                    self.encode_array(
276                        Some(&key),
277                        items,
278                        ArrayContext::ListFirstField {
279                            depth: depth.saturating_sub(1),
280                        },
281                    )?;
282                }
283                primitive => {
284                    let indent = self.indent(depth);
285                    let rendered =
286                        self.stringify_primitive(primitive, Some(self.options.document_delimiter))?;
287                    self.lines
288                        .push(format!("{}- {}: {}", indent, encode_key(&key), rendered));
289                }
290            }
291
292            for (key, value) in iter {
293                let FoldResult { key, value } = self.fold_key(key, value, map);
294                self.encode_named_value(&key, value, depth + 1)?;
295            }
296        }
297        Ok(())
298    }
299
300    fn stringify_primitive(
301        &self,
302        value: &Value,
303        delimiter: Option<Delimiter>,
304    ) -> Result<String, ToonifyError> {
305        let delimiter = delimiter.unwrap_or(self.options.document_delimiter);
306        match value {
307            Value::Null => Ok("null".into()),
308            Value::Bool(boolean) => Ok(boolean.to_string()),
309            Value::Number(number) => self.canonicalize_number(number),
310            Value::String(text) => Ok(encode_string(text, Some(delimiter))),
311            other => Err(ToonifyError::encoding(format!(
312                "expected primitive value, found {other:?}"
313            ))),
314        }
315    }
316
317    fn canonicalize_number(&self, number: &Number) -> Result<String, ToonifyError> {
318        if let Some(value) = number.as_i64() {
319            return Ok(value.to_string());
320        }
321        if let Some(value) = number.as_u64() {
322            return Ok(value.to_string());
323        }
324
325        let raw = number.to_string();
326        if raw == "-0" {
327            return Ok("0".into());
328        }
329
330        let decimal =
331            BigDecimal::from_str(&raw).map_err(|err| ToonifyError::NumberNormalization {
332                value: raw.clone(),
333                source: Box::new(err),
334            })?;
335
336        let normalized = decimal.normalized();
337        if normalized.is_zero() {
338            Ok("0".into())
339        } else {
340            Ok(normalized.to_string())
341        }
342    }
343
344    fn format_header(
345        &self,
346        key: Option<&str>,
347        len: usize,
348        delimiter: Delimiter,
349        fields: Option<&[String]>,
350    ) -> String {
351        let bracket = format!("[{}{}]", len, delimiter.bracket_suffix());
352        let body = if let Some(fields) = fields {
353            let sep = delimiter.as_char().to_string();
354            let field_list = fields
355                .iter()
356                .map(|field| encode_key(field))
357                .collect::<Vec<_>>()
358                .join(&sep);
359            format!("{bracket}{{{field_list}}}:")
360        } else {
361            format!("{bracket}:")
362        };
363
364        match key {
365            Some(key) => format!("{}{}", encode_key(key), body),
366            None => body,
367        }
368    }
369
370    fn fold_key<'m>(
371        &self,
372        key: &'m str,
373        value: &'m Value,
374        siblings: &'m Map<String, Value>,
375    ) -> FoldResult<'m> {
376        let KeyFoldingMode::Safe { flatten_depth } = self.options.key_folding else {
377            return FoldResult::borrowed(key, value);
378        };
379
380        if !is_identifier_segment(key) {
381            return FoldResult::borrowed(key, value);
382        }
383
384        let max_segments = flatten_depth.unwrap_or(usize::MAX).max(1);
385        let mut segments = vec![key.to_string()];
386        let mut current = value;
387
388        while segments.len() < max_segments {
389            match current {
390                Value::Object(map) if map.len() == 1 => {
391                    let (next_key, next_value) = map.iter().next().unwrap();
392                    if !is_identifier_segment(next_key) {
393                        break;
394                    }
395                    segments.push(next_key.to_string());
396                    current = next_value;
397                }
398                _ => break,
399            }
400        }
401
402        if segments.len() == 1 {
403            return FoldResult::borrowed(key, value);
404        }
405
406        let candidate = segments.join(".");
407        if siblings.contains_key(&candidate) && candidate != key {
408            return FoldResult::borrowed(key, value);
409        }
410
411        FoldResult::owned(candidate, current)
412    }
413
414    fn push_line(&mut self, depth: usize, content: String) {
415        let indent = self.indent(depth);
416        self.lines.push(format!("{indent}{content}"));
417    }
418
419    fn indent(&self, depth: usize) -> String {
420        " ".repeat(depth * self.options.indent)
421    }
422}
423
424struct FoldResult<'a> {
425    key: Cow<'a, str>,
426    value: &'a Value,
427}
428
429impl<'a> FoldResult<'a> {
430    fn borrowed(key: &'a str, value: &'a Value) -> Self {
431        Self {
432            key: Cow::Borrowed(key),
433            value,
434        }
435    }
436
437    fn owned(key: String, value: &'a Value) -> Self {
438        Self {
439            key: Cow::Owned(key),
440            value,
441        }
442    }
443}
444
445#[derive(Clone, Copy)]
446enum ArrayContext {
447    Normal { depth: usize },
448    ListFirstField { depth: usize },
449}
450
451impl ArrayContext {
452    fn header_depth(self) -> usize {
453        match self {
454            ArrayContext::Normal { depth } => depth,
455            ArrayContext::ListFirstField { depth } => depth + 1,
456        }
457    }
458
459    fn row_depth(self) -> usize {
460        self.header_depth() + 1
461    }
462
463    fn header_prefix(self) -> &'static str {
464        match self {
465            ArrayContext::Normal { .. } => "",
466            ArrayContext::ListFirstField { .. } => "- ",
467        }
468    }
469}
470
471fn is_primitive(value: &Value) -> bool {
472    matches!(
473        value,
474        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
475    )
476}
477
478fn detect_tabular(items: &[Value]) -> Option<Vec<String>> {
479    if items.is_empty() {
480        return None;
481    }
482
483    let first = items.get(0)?.as_object()?;
484    if first.is_empty() {
485        return None;
486    }
487
488    let mut fields = Vec::new();
489    for (key, value) in first {
490        if !is_primitive(value) {
491            return None;
492        }
493        fields.push(key.clone());
494    }
495
496    for item in items.iter().skip(1) {
497        let obj = item.as_object()?;
498        if obj.len() != fields.len() {
499            return None;
500        }
501        for field in &fields {
502            let value = obj.get(field)?;
503            if !is_primitive(value) {
504                return None;
505            }
506        }
507    }
508
509    Some(fields)
510}
511
512fn is_array_of_primitive_arrays(items: &[Value]) -> bool {
513    !items.is_empty()
514        && items.iter().all(|value| {
515            value
516                .as_array()
517                .map(|inner| inner.iter().all(is_primitive))
518                .unwrap_or(false)
519        })
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525    use crate::options::{Delimiter, EncoderOptions, KeyFoldingMode};
526    use serde_json::json;
527
528    #[test]
529    fn encodes_object_and_tabular_array() {
530        let value = json!({
531            "users": [
532                { "id": 1, "name": "Ada", "active": true },
533                { "id": 2, "name": "Linus", "active": false }
534            ],
535            "count": 2
536        });
537
538        let output = encode_value(&value, &EncoderOptions::default()).unwrap();
539        assert_eq!(
540            output,
541            "users[2]{id,name,active}:\n  1,Ada,true\n  2,Linus,false\ncount: 2"
542        );
543    }
544
545    #[test]
546    fn folds_keys_when_enabled() {
547        let options = EncoderOptions {
548            indent: 2,
549            document_delimiter: Delimiter::Comma,
550            key_folding: KeyFoldingMode::Safe {
551                flatten_depth: None,
552            },
553        };
554
555        let value = json!({
556            "data": {
557                "meta": {
558                    "payload": {
559                        "id": 1
560                    }
561                }
562            }
563        });
564
565        let output = encode_value(&value, &options).unwrap();
566        assert_eq!(output, "data.meta.payload.id: 1");
567    }
568}