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