oh_my_toml 0.1.0

Awesome TOML configuration derive macro
Documentation
//! Impls for maps, such as [`HashMap`](std::collections::HashMap)

use nonempty::NonEmpty;
use std::{
    borrow::Cow,
    collections::{BTreeMap, HashMap},
    hash::{BuildHasher, Hash},
};
use toml::de::DeValue;

use miette::{Diagnostic, SourceSpan};
use thiserror::Error;

use crate::{DeserializeErrors, DeserializeTable, DeserializeValue, Error, Key};

/// Trait to easily implement the [`DeserializeTable`] trait for multiple types of "Maps"
trait MapFromTable<'de, K, V>
where
    K: DeserializeValue<'de>,
    V: DeserializeValue<'de>,
{
    /// Output
    type Output: Extend<(K, V)>;

    /// Create the initial hash map
    fn new_map_from_table() -> Self::Output;

    /// Deserialize the table, collecting all errors
    #[allow(clippy::type_complexity, reason = "not complex")]
    fn deserialize_table(
        table: toml::Spanned<toml::de::DeTable<'de>>,
    ) -> Result<
        Self::Output,
        DeserializeErrors<Self::Output, NonEmpty<DeserializeMapError<K::Error, V::Error>>>,
    > {
        let mut map = Self::new_map_from_table();
        let mut errs = vec![];

        let span = table.span();
        for (toml_key, toml_value) in table.into_inner() {
            let key = match K::deserialize_value(
                Key::None,
                toml::Spanned::new(
                    toml_key.span(),
                    DeValue::String(toml_key.clone().into_inner()),
                ),
            ) {
                Ok(k) => Some(k),
                Err(DeserializeErrors { recovered, errors }) => {
                    errs.extend(errors.into_iter().map(CollectionErrorSingle::Key));
                    recovered
                }
            };

            let value = match V::deserialize_value(Key::Field(toml_key), toml_value) {
                Ok(k) => Some(k),
                Err(DeserializeErrors { recovered, errors }) => {
                    errs.extend(errors.into_iter().map(CollectionErrorSingle::Value));
                    recovered
                }
            };

            if let (Some(key_val), Some(value_val)) = (key, value) {
                map.extend([(key_val, value_val)]);
            }
        }

        NonEmpty::from_vec(errs).map_or_else(
            || Ok(map),
            |key_errors| {
                Err(DeserializeErrors::err(
                    DeserializeMapError::CollectionError {
                        description: "table".to_string(),
                        span: span.into(),
                        key_errors,
                    },
                ))
            },
        )
    }
}

/// Error from deserializing a key or a value
#[derive(Diagnostic, Error, Debug, Clone, Eq, PartialEq, Hash)]
pub enum CollectionErrorSingle<K: Error, V: Error> {
    /// Error from deserializing a key
    #[error(transparent)]
    #[diagnostic(transparent)]
    Key(K),
    /// Error from deserializing a value
    #[error(transparent)]
    #[diagnostic(transparent)]
    Value(V),
}

/// Failed to deserialize a map, e.g. [`HashMap`](std::collections::HashMap) or [`BTreeMap`](std::collections::BTreeMap)
#[derive(Diagnostic, Error, Debug, Clone, Eq, PartialEq, Hash)]
#[error("Type mismatch")]
pub enum DeserializeMapError<K: Error, V: Error> {
    /// Encountered errors whilst deserializing the keys and values
    CollectionError {
        /// Details on why deserialization failed
        description: String,
        /// Location of the error in the source file
        #[label("{description}")]
        span: SourceSpan,
        /// A list of all errors encountered whilst deserializing the hashmap
        #[related]
        key_errors: NonEmpty<CollectionErrorSingle<K, V>>,
    },
    /// The provided [`Item`](toml_edit::Item) is not a [`Table`](toml_edit::Table)
    NotTable {
        /// Details on why deserialization failed
        description: String,
        /// Location of the error in the source file
        #[label("{description}")]
        span: SourceSpan,
    },
}

impl<K, V> Error for DeserializeMapError<K, V>
where
    K: Error,
    V: Error,
{
    fn expected_type() -> Cow<'static, str> {
        format!(
            "map of {{ {} => {} }}",
            K::expected_type(),
            V::expected_type()
        )
        .into()
    }
}

/// Implement for maps of key-value pairs
macro_rules! impl_for {
    (
        init = $expr:expr,
        map = $map:ident<$($generic:ident),*> where $($bounds:tt)*
    ) => {
        impl<'de, $($generic),*> DeserializeTable<'de> for $map<$($generic),*>
        where $($bounds)*
        {
            type Error = DeserializeMapError<K::Error, V::Error>;

            fn deserialize_table(
                table: toml::Spanned<toml::de::DeTable<'de>>,
            ) -> Result<Self, DeserializeErrors<Self, NonEmpty<Self::Error>>> {
                <Self as MapFromTable<K, V>>::deserialize_table(table)
            }
        }

        impl<'de, $($generic),*> MapFromTable<'de, K, V> for $map<$($generic),*>
        where $($bounds)*
        {
            type Output = Self;

            fn new_map_from_table() -> Self::Output {
                $expr
            }
        }
    };
}

impl_for! {
    init = Self::new(),
    map = BTreeMap<K, V> where
        K: DeserializeValue<'de> + Ord,
        V: DeserializeValue<'de>
}

impl_for! {
    init = Self::with_hasher(S::default()),
    map = HashMap<K, V, S> where
        K: DeserializeValue<'de> + Hash + Eq,
        V: DeserializeValue<'de>,
        S: BuildHasher + Default
}