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}