Skip to main content

toml_spanner/
ser.rs

1use std::{
2    borrow::Cow,
3    collections::{BTreeMap, BTreeSet, HashMap},
4    fmt::{self, Debug, Display},
5    rc::Rc,
6    sync::Arc,
7};
8
9use crate::{Arena, Array, DateTime, Item, Key, Table, item::Value};
10
11/// extracted out to avoid code bloat
12fn optional_to_required<'a>(
13    optional: Result<Option<Item<'a>>, ToTomlError>,
14) -> Result<Item<'a>, ToTomlError> {
15    match optional {
16        Ok(Some(item)) => Ok(item),
17        Ok(None) => Err(ToTomlError::from("required value was None")),
18        Err(e) => Err(e),
19    }
20}
21
22fn required_to_optional<'a>(
23    required: Result<Item<'a>, ToTomlError>,
24) -> Result<Option<Item<'a>>, ToTomlError> {
25    match required {
26        Ok(item) => Ok(Some(item)),
27        Err(e) => Err(e),
28    }
29}
30
31/// Converts a Rust type into a TOML [`Item`] tree.
32///
33/// `#[derive(Toml)]` with `#[toml(ToToml)]` generates the implementation
34/// (the derive defaults to [`FromToml`](crate::FromToml) without it). See the
35/// [`Toml`](macro@crate::Toml) derive macro for the full set of attributes
36/// and field options like `skip_if` and `style`.
37///
38/// For manual implementations, implement [`to_toml`](Self::to_toml) for
39/// values that are always present, or
40/// [`to_optional_toml`](Self::to_optional_toml) for values that may be
41/// absent (returning `Ok(None)` to omit the field). Default implementations
42/// bridge between the two so only one needs to be provided.
43///
44/// The simplest entry point is [`to_string`](crate::to_string) for default
45/// formatting, or [`Formatting`](crate::Formatting) for format preservation
46/// and customizing formatting defaults.
47///
48/// # Examples
49///
50/// ```
51/// use toml_spanner::Toml;
52///
53/// #[derive(Toml)]
54/// #[toml(ToToml)]
55/// struct Config {
56///     name: String,
57///     port: u16,
58/// }
59///
60/// let config = Config { name: "app".into(), port: 8080 };
61/// let output = toml_spanner::to_string(&config).unwrap();
62/// assert!(output.contains("name = \"app\""));
63/// assert!(output.contains("port = 8080"));
64/// ```
65pub trait ToToml {
66    /// Produces a TOML [`Item`] representing this value.
67    ///
68    /// Override when the value is always present. The default delegates to
69    /// [`to_optional_toml`](Self::to_optional_toml) and returns an error if
70    /// `None` is produced.
71    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
72        optional_to_required(self.to_optional_toml(arena))
73    }
74    /// Produces an optional TOML [`Item`] representing this value.
75    ///
76    /// Override when the value may be absent (e.g. `Option<T>` returning
77    /// `None` to omit the field). The default delegates to
78    /// [`to_toml`](Self::to_toml) and wraps the result in [`Some`].
79    fn to_optional_toml<'a>(&'a self, arena: &'a Arena) -> Result<Option<Item<'a>>, ToTomlError> {
80        required_to_optional(self.to_toml(arena))
81    }
82}
83
84impl<K: ToToml> ToToml for BTreeSet<K> {
85    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
86        let Some(mut array) = Array::try_with_capacity(self.len(), arena) else {
87            return length_of_array_exceeded_maximum();
88        };
89        for item in self {
90            array.push(item.to_toml(arena)?, arena);
91        }
92        Ok(array.into_item())
93    }
94}
95
96impl<K: ToToml, H> ToToml for std::collections::HashSet<K, H> {
97    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
98        let Some(mut array) = Array::try_with_capacity(self.len(), arena) else {
99            return length_of_array_exceeded_maximum();
100        };
101        for item in self {
102            array.push(item.to_toml(arena)?, arena);
103        }
104        Ok(array.into_item())
105    }
106}
107
108impl<const N: usize, T: ToToml> ToToml for [T; N] {
109    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
110        self.as_slice().to_toml(arena)
111    }
112}
113
114macro_rules! impl_to_toml_tuple {
115    ($len:expr, $($idx:tt => $T:ident),+) => {
116        impl<$($T: ToToml),+> ToToml for ($($T,)+) {
117            fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
118                let Some(mut array) = Array::try_with_capacity($len, arena) else {
119                    return length_of_array_exceeded_maximum();
120                };
121                $(
122                    array.push(self.$idx.to_toml(arena)?, arena);
123                )+
124                Ok(array.into_item())
125            }
126        }
127    };
128}
129
130impl_to_toml_tuple!(1, 0 => A);
131impl_to_toml_tuple!(2, 0 => A, 1 => B);
132impl_to_toml_tuple!(3, 0 => A, 1 => B, 2 => C);
133
134impl<T: ToToml> ToToml for Option<T> {
135    fn to_optional_toml<'a>(&'a self, arena: &'a Arena) -> Result<Option<Item<'a>>, ToTomlError> {
136        match self {
137            Some(value) => value.to_optional_toml(arena),
138            None => Ok(None),
139        }
140    }
141}
142
143impl ToToml for str {
144    fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
145        Ok(Item::string(self))
146    }
147}
148
149impl ToToml for String {
150    fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
151        Ok(Item::string(self))
152    }
153}
154
155impl<T: ToToml> ToToml for Box<T> {
156    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
157        <T as ToToml>::to_toml(self, arena)
158    }
159}
160
161impl<T: ToToml> ToToml for [T] {
162    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
163        let Some(mut array) = Array::try_with_capacity(self.len(), arena) else {
164            return length_of_array_exceeded_maximum();
165        };
166        for item in self {
167            array.push(item.to_toml(arena)?, arena);
168        }
169        Ok(array.into_item())
170    }
171}
172
173impl<T: ToToml> ToToml for Vec<T> {
174    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
175        self.as_slice().to_toml(arena)
176    }
177}
178
179impl ToToml for f32 {
180    fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
181        Ok(Item::from(*self as f64))
182    }
183}
184
185impl ToToml for f64 {
186    fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
187        Ok(Item::from(*self))
188    }
189}
190
191impl ToToml for bool {
192    fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
193        Ok(Item::from(*self))
194    }
195}
196
197impl ToToml for DateTime {
198    fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
199        Ok(Item::from(*self))
200    }
201}
202
203impl<T: ToToml + ?Sized> ToToml for &T {
204    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
205        <T as ToToml>::to_toml(self, arena)
206    }
207}
208
209impl<T: ToToml + ?Sized> ToToml for &mut T {
210    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
211        <T as ToToml>::to_toml(self, arena)
212    }
213}
214
215impl<T: ToToml> ToToml for Rc<T> {
216    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
217        <T as ToToml>::to_toml(self, arena)
218    }
219}
220
221impl<T: ToToml> ToToml for Arc<T> {
222    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
223        <T as ToToml>::to_toml(self, arena)
224    }
225}
226
227impl<'b, T: ToToml + Clone> ToToml for Cow<'b, T> {
228    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
229        <T as ToToml>::to_toml(self, arena)
230    }
231}
232
233impl ToToml for char {
234    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
235        let mut buf = [0; 4];
236        Ok(Item::string(arena.alloc_str(self.encode_utf8(&mut buf))))
237    }
238}
239
240impl ToToml for std::path::Path {
241    fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
242        match self.to_str() {
243            Some(s) => Ok(Item::string(s)),
244            None => ToTomlError::msg("path contains invalid UTF-8 characters"),
245        }
246    }
247}
248
249impl ToToml for std::path::PathBuf {
250    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
251        self.as_path().to_toml(arena)
252    }
253}
254
255impl ToToml for Array<'_> {
256    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
257        Ok(self.clone_in(arena).into_item())
258    }
259}
260
261impl ToToml for Table<'_> {
262    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
263        Ok(self.clone_in(arena).into_item())
264    }
265}
266
267impl ToToml for Item<'_> {
268    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
269        Ok(self.clone_in(arena))
270    }
271}
272
273macro_rules! direct_upcast_integers {
274    ($($tt:tt),*) => {
275        $(impl ToToml for $tt {
276            fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
277                Ok(Item::from(*self as i128))
278            }
279        })*
280    };
281}
282
283direct_upcast_integers!(u8, i8, i16, u16, i32, u32, i64, u64, i128);
284
285impl ToToml for u128 {
286    fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
287        if *self > i128::MAX as u128 {
288            return ToTomlError::msg("u128 value exceeds i128::MAX");
289        }
290        Ok(Item::from(*self as i128))
291    }
292}
293
294/// Trait for types that can be converted into flattened TOML table entries.
295///
296/// Used with `#[toml(flatten)]` on struct fields. Built-in implementations
297/// cover `HashMap` and `BTreeMap`.
298///
299/// If your type implements [`ToToml`], use
300/// `#[toml(flatten, with = flatten_any)]` in your derive instead of
301/// implementing this trait. See [`helper::flatten_any`](crate::helper::flatten_any).
302#[diagnostic::on_unimplemented(
303    message = "`{Self}` does not implement `ToFlattened`",
304    note = "if `{Self}` implements `ToToml`, you can use `#[toml(flatten, with = flatten_any)]` instead of a manual `ToFlattened` impl"
305)]
306pub trait ToFlattened {
307    /// Inserts this value's entries directly into an existing table.
308    ///
309    /// Each key-value pair is inserted into `table` rather than wrapping
310    /// them in a nested sub-table.
311    fn to_flattened<'a>(
312        &'a self,
313        arena: &'a Arena,
314        table: &mut Table<'a>,
315    ) -> Result<(), ToTomlError>;
316}
317
318/// Converts a map key to a TOML key string via `ToToml`.
319fn key_to_str<'a>(item: &Item<'a>) -> Option<&'a str> {
320    match item.value() {
321        Value::String(s) => Some(*s),
322        _ => None,
323    }
324}
325
326impl<K: ToToml, V: ToToml> ToFlattened for BTreeMap<K, V> {
327    fn to_flattened<'a>(
328        &'a self,
329        arena: &'a Arena,
330        table: &mut Table<'a>,
331    ) -> Result<(), ToTomlError> {
332        for (k, v) in self {
333            let key_item = k.to_toml(arena)?;
334            let Some(key_str) = key_to_str(&key_item) else {
335                return map_key_did_not_serialize_to_string();
336            };
337            table.insert_unique(Key::new(key_str), v.to_toml(arena)?, arena);
338        }
339        Ok(())
340    }
341}
342
343impl<K: ToToml, V: ToToml, H> ToFlattened for HashMap<K, V, H> {
344    fn to_flattened<'a>(
345        &'a self,
346        arena: &'a Arena,
347        table: &mut Table<'a>,
348    ) -> Result<(), ToTomlError> {
349        for (k, v) in self {
350            let key_item = k.to_toml(arena)?;
351            let Some(key_str) = key_to_str(&key_item) else {
352                return map_key_did_not_serialize_to_string();
353            };
354            table.insert_unique(Key::new(key_str), v.to_toml(arena)?, arena);
355        }
356        Ok(())
357    }
358}
359
360impl ToFlattened for Table<'_> {
361    fn to_flattened<'a>(
362        &'a self,
363        arena: &'a Arena,
364        table: &mut Table<'a>,
365    ) -> Result<(), ToTomlError> {
366        for (key, val) in self {
367            table.insert_unique(*key, val.clone_in(arena), arena);
368        }
369        Ok(())
370    }
371}
372
373impl ToFlattened for Item<'_> {
374    fn to_flattened<'a>(
375        &'a self,
376        arena: &'a Arena,
377        table: &mut Table<'a>,
378    ) -> Result<(), ToTomlError> {
379        let Some(src) = self.as_table() else {
380            return Err(ToTomlError::from("flatten: expected a table"));
381        };
382        src.to_flattened(arena, table)
383    }
384}
385
386impl<K: ToToml, V: ToToml> ToToml for BTreeMap<K, V> {
387    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
388        let Some(mut table) = Table::try_with_capacity(self.len(), arena) else {
389            return length_of_table_exceeded_maximum();
390        };
391        self.to_flattened(arena, &mut table)?;
392        Ok(table.into_item())
393    }
394}
395
396impl<K: ToToml, V: ToToml, H> ToToml for HashMap<K, V, H> {
397    fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
398        let Some(mut table) = Table::try_with_capacity(self.len(), arena) else {
399            return length_of_table_exceeded_maximum();
400        };
401        self.to_flattened(arena, &mut table)?;
402        Ok(table.into_item())
403    }
404}
405
406#[cold]
407fn map_key_did_not_serialize_to_string() -> Result<(), ToTomlError> {
408    Err(ToTomlError::from("map key did not serialize to a string"))
409}
410#[cold]
411fn length_of_array_exceeded_maximum<T>() -> Result<T, ToTomlError> {
412    Err(ToTomlError::from(
413        "length of array exceeded maximum capacity",
414    ))
415}
416
417#[cold]
418fn length_of_table_exceeded_maximum<T>() -> Result<T, ToTomlError> {
419    Err(ToTomlError::from(
420        "length of table exceeded maximum capacity",
421    ))
422}
423
424/// An error produced during [`ToToml`] conversion or TOML emission.
425///
426/// Returned by [`to_string`](crate::to_string),
427/// [`Formatting::format`](crate::Formatting::format), and
428/// [`ToToml::to_toml`].
429pub struct ToTomlError {
430    /// The error message.
431    pub message: Cow<'static, str>,
432}
433
434impl ToTomlError {
435    /// Returns `Err(ToTomlError)` with the given static message.
436    #[cold]
437    pub fn msg<T>(msg: &'static str) -> Result<T, Self> {
438        Err(Self {
439            message: Cow::Borrowed(msg),
440        })
441    }
442}
443
444impl Display for ToTomlError {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        f.write_str(&self.message)
447    }
448}
449
450impl Debug for ToTomlError {
451    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452        f.debug_struct("ToTomlError")
453            .field("message", &self.message)
454            .finish()
455    }
456}
457
458impl std::error::Error for ToTomlError {}
459
460impl From<Cow<'static, str>> for ToTomlError {
461    fn from(message: Cow<'static, str>) -> Self {
462        Self { message }
463    }
464}
465
466impl From<&'static str> for ToTomlError {
467    fn from(message: &'static str) -> Self {
468        Self {
469            message: Cow::Borrowed(message),
470        }
471    }
472}