toml_pretty/
lib.rs

1use std::fmt::Write;
2
3use ordered_hash_map::OrderedHashMap;
4use serde::Serialize;
5use serde_json::Value;
6use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, Error>;
9
10#[derive(Debug, Error)]
11pub enum Error {
12  #[error("Failed to de/serialize value to json")]
13  JsonSerialization(#[from] serde_json::Error),
14  #[error("Failed to format args")]
15  Format(#[from] std::fmt::Error),
16  #[error("Came across triple nested array. Not supported.")]
17  TripleNestedArray,
18  #[error("Came across Value::Object after flatten_map. This shouldn't happen")]
19  ObjectReached,
20}
21
22#[derive(Clone, Copy)]
23pub struct Options<'a> {
24  pub tab: &'a str,
25  pub skip_empty_string: bool,
26  pub skip_empty_object: bool,
27  pub inline_array: bool,
28  pub max_inline_array_length: usize,
29}
30
31impl<'a> Default for Options<'a> {
32  fn default() -> Self {
33    Self {
34      tab: "\t",
35      skip_empty_string: false,
36      skip_empty_object: false,
37      inline_array: false,
38      max_inline_array_length: 50,
39    }
40  }
41}
42
43impl<'a> Options<'a> {
44  /// Specify the symbol to use for tab. Default is '\t'
45  pub fn tab(mut self, tab: &'a str) -> Self {
46    self.tab = tab;
47    self
48  }
49
50  /// Specify whether to skip serializing string fields containing empty strings
51  pub fn skip_empty_string(mut self, skip_empty_string: bool) -> Self {
52    self.skip_empty_string = skip_empty_string;
53    self
54  }
55
56  /// Specify whether to skip serializing object fields containing empty objects
57  pub fn skip_empty_object(mut self, skip_empty_object: bool) -> Self {
58    self.skip_empty_object = skip_empty_object;
59    self
60  }
61
62  /// Specify whether to serialize arrays inline, rather than on multiple lines.
63  pub fn inline_array(mut self, inline_array: bool) -> Self {
64    self.inline_array = inline_array;
65    self
66  }
67
68  pub fn max_inline_array_length(mut self, max_inline_array_length: usize) -> Self {
69    self.max_inline_array_length = max_inline_array_length;
70    self
71  }
72}
73
74pub fn to_string<T: Serialize>(value: &T, options: Options<'_>) -> Result<String> {
75  let Options {
76    tab,
77    skip_empty_string,
78    skip_empty_object,
79    inline_array,
80    max_inline_array_length,
81  } = options;
82  let map = serde_json::from_str(&serde_json::to_string(value).map_err(Error::JsonSerialization)?)
83    .map_err(Error::JsonSerialization)?;
84  let mut res = String::new();
85  for (i, (key, val)) in flatten_map(map, skip_empty_object).into_iter().enumerate() {
86    match &val {
87      Value::Null => {}
88
89      Value::Bool(_) | Value::Number(_) => {
90        if i != 0 {
91          res.push('\n');
92        }
93        res
94          .write_fmt(format_args!("{key} = {val}"))
95          .map_err(Error::Format)?;
96      }
97
98      Value::String(val) => {
99        if skip_empty_string && val.is_empty() {
100          continue;
101        }
102        if i != 0 {
103          res.push('\n');
104        }
105        if val.contains('\n') {
106          res
107            .write_fmt(format_args!("{key} = \"\"\"\n{val}\"\"\""))
108            .map_err(Error::Format)?;
109        } else {
110          res
111            .write_fmt(format_args!("{key} = \"{}\"", val.replace('"', "\\\"")))
112            .map_err(Error::Format)?;
113        }
114      }
115
116      Value::Array(vals) => {
117        if vals.is_empty() {
118          if i != 0 {
119            res.push('\n');
120          }
121          res
122            .write_fmt(format_args!("{key} = []"))
123            .map_err(Error::Format)?;
124          continue;
125        }
126        let mut strs = Vec::<String>::with_capacity(vals.capacity());
127        for val in vals {
128          match val {
129            Value::Null => {}
130            Value::Bool(_) | Value::Number(_) => strs.push(val.to_string()),
131            Value::String(string) => {
132              if skip_empty_string && string.is_empty() {
133                continue;
134              }
135              strs.push(format!("\"{}\"", string.replace('"', "\\\"")))
136            }
137            Value::Object(map) => strs.push(to_array_object_string(&map, options)?),
138            Value::Array(vals) => {
139              let mut out = Vec::new();
140              for val in vals {
141                match val {
142                  Value::Null => {}
143                  Value::Bool(_) | Value::Number(_) => out.push(val.to_string()),
144                  Value::String(string) => out.push(format!("\"{}\"", string.replace('"', "\\\""))),
145                  Value::Object(map) => out.push(to_array_object_string(&map, options)?),
146                  Value::Array(_) => return Err(Error::TripleNestedArray),
147                }
148              }
149              strs.push(format!("[{}]", out.join(", ")));
150            }
151          }
152        }
153        let total_length = strs.iter().fold(0, |total, curr| total + curr.len());
154        let inline_array = inline_array || total_length <= max_inline_array_length;
155        let join = if inline_array {
156          String::from(", ")
157        } else {
158          format!(",\n{tab}")
159        };
160        let val = strs.join(&join);
161        if i != 0 {
162          res.push('\n');
163        }
164        if inline_array {
165          res
166            .write_fmt(format_args!("{key} = [{val}]"))
167            .map_err(Error::Format)?;
168        } else {
169          res
170            .write_fmt(format_args!("{key} = [\n{tab}{val}\n]"))
171            .map_err(Error::Format)?;
172        }
173      }
174
175      // Special Object case for including empty objects
176      Value::Object(obj) if !skip_empty_object && obj.is_empty() => {
177        if i != 0 {
178          res.push('\n');
179        }
180        // Write empty object eg 'database = {}'
181        res
182          .write_fmt(format_args!("{key} = {{}}"))
183          .map_err(Error::Format)?;
184      }
185
186      // All other object cases should be removed by flatten_map
187      Value::Object(_) => return Err(Error::ObjectReached),
188    }
189  }
190  Ok(res)
191}
192
193fn flatten_map(
194  map: OrderedHashMap<String, Value>,
195  skip_empty_object: bool,
196) -> OrderedHashMap<String, Value> {
197  let mut target = OrderedHashMap::new();
198  flatten_map_rec(&mut target, None, map, skip_empty_object);
199  target
200}
201
202fn flatten_map_rec(
203  target: &mut OrderedHashMap<String, Value>,
204  parent_field: Option<String>,
205  source: OrderedHashMap<String, Value>,
206  skip_empty_object: bool,
207) {
208  if !skip_empty_object && source.is_empty() {
209    if let Some(parent_field) = &parent_field {
210      target.insert(
211        parent_field.to_string(),
212        Value::Object(serde_json::Map::new()),
213      );
214      return;
215    }
216  }
217  for (field, val) in source {
218    let parent_field = if let Some(parent_field) = &parent_field {
219      let mut parent_field = parent_field.clone();
220      parent_field.push('.');
221      parent_field.push_str(&field);
222      parent_field
223    } else {
224      field
225    };
226    if let Value::Object(source) = val {
227      flatten_map_rec(
228        target,
229        Some(parent_field),
230        source.into_iter().collect(),
231        skip_empty_object,
232      )
233    } else {
234      target.insert(parent_field, val);
235    }
236  }
237}
238
239/// Serializes to:
240///
241/// ```toml
242/// { name = "asdf", pattern = """
243/// asdf
244/// bbbb""", last = true }
245/// ```
246fn to_array_object_string(
247  map: &serde_json::Map<String, Value>,
248  options: Options<'_>,
249) -> Result<String> {
250  let mut res = String::new();
251  for token in to_string(&map, options.inline_array(true))?
252    .split(" = ")
253    .map(str::trim)
254  {
255    // Split into val / next key, if exists
256    match token.rsplit_once('\n') {
257      Some((val, next_key)) => {
258        res.push_str(" = ");
259        res.push_str(val.trim());
260        res.push_str(", ");
261        res.push_str(next_key.trim());
262      }
263      None => {
264        if res.is_empty() {
265          // first key
266          res.push_str(token);
267        } else {
268          // last val
269          res.push_str(" = ");
270          res.push_str(token);
271        }
272      }
273    }
274  }
275  Ok(format!("{{ {res} }}"))
276}