oh_my_toml 0.1.0

Awesome TOML configuration derive macro
Documentation
//! Errors

use miette::Diagnostic;
use nonempty::NonEmpty;
use std::borrow::Cow;
use std::fmt::Display;

/// Represents a single error encountered during deserialization
pub trait Error: Diagnostic {
    /// Human-friendly name of the type that we were expecting
    ///
    /// This type will be used when reporting errors.
    ///
    /// For instance, if we try to deserialize `[String; 4]` but the TOML
    /// value provided is invalid, we might get an error like this:
    ///
    /// ```text
    ///  × Type mismatch
    ///   ╭─[file:1:8]
    /// 1 │ item = 4
    ///   ·        ┬
    ///   ·        ╰─┤ expected:
    ///   ·          │     array of 4 string
    ///   ·          │ but found:
    ///   ·          │     int
    ///   ╰────
    /// ```
    ///
    /// In general, it is sufficient to have an implementation that just returns
    /// a static string:
    ///
    /// ```ignore
    /// impl oh_my_toml::Error for DeserializeIpAddrError {
    ///     fn expected_type() -> Cow<'static, str> {
    ///         "IP Address".into()
    ///     }
    /// }
    /// ```
    ///
    /// However, when dealing with sequences of values (such as a [`HashMap`](std::collections::HashMap)
    /// or a [`Vec`]) -- placing a `: oh_my_toml::Error` bound on the generic type will allow you to
    /// describe it in more detail.
    ///
    /// For instance, the implementation of [`DeserializeNonEmptyVecError<T>`](crate::error::DeserializeNonEmptyVecError) calls the same
    /// method on the type it contains:
    ///
    /// ```ignore
    /// impl<E: oh_my_toml::Error> oh_my_toml::Error for NonEmptyVecError<E> {
    ///     fn expected_type() -> Cow<'static, str> {
    ///         format!("non-empty list of {}", E::expected_type()).into()
    ///     }
    /// }
    /// ```
    ///
    /// As you can see, this is recursive - It will describe your type in *absolute* detail.
    ///
    /// One more example of the implementation for `[T; N]`:
    ///
    /// ```ignore
    /// impl<E: Error, const N: usize> Error for DeserializeArrayError<E, N> {
    ///     fn expected_type() -> Cow<'static, str> {
    ///         format!("array of {N} {}", E::expected_type()).into()
    ///     }
    /// }
    /// ```
    ///
    /// This implementation describes the array items but also the length of them.
    fn expected_type() -> Cow<'static, str>;

    /// Returns details about the error message, an example output might be:
    ///
    /// ```txt
    /// expected:
    ///     list of string
    /// but found:
    ///     list of int
    /// ```
    ///
    /// # Arguments
    ///
    /// - `type_name`: This is the type name of the TOML data that we actually got
    #[must_use]
    fn details(type_name: impl Display) -> String {
        format!(
            "expected:\n    {}\nbut found:\n    {type_name}",
            Self::expected_type()
        )
    }
}

/// *Potentially recoverable* errors that occured while deserializing the TOML
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct DeserializeErrors<T, E> {
    /// The value we were (possibly) able to recover, even despite the errors
    pub recovered: Option<T>,
    /// Errors obtained while deserializing
    pub errors: E,
}

impl<T, E: Into<miette::Report>> DeserializeErrors<T, NonEmpty<E>> {
    /// Render the errors into a `String`
    pub fn render_errors(self) -> DeserializeErrors<T, NonEmpty<String>> {
        self.map_errors(|err| {
            let report: miette::Report = err.into();
            format!("{report:?}")
        })
    }
}

impl<T, E> DeserializeErrors<T, NonEmpty<E>> {
    /// Possibly recovered a value with a single error
    pub fn new(recovered: Option<T>, err: E) -> Self {
        Self {
            recovered,
            errors: NonEmpty::new(err),
        }
    }

    /// Provided a function `T -> U`, maps a `DeserializeErrors<T, E>` to a `DeserializeErrors<U, E>`
    pub fn map<U, F: Fn(T) -> U>(self, f: F) -> DeserializeErrors<U, NonEmpty<E>> {
        DeserializeErrors {
            recovered: self.recovered.map(f),
            errors: self.errors,
        }
    }

    /// Provided a function `E -> U`, maps a `DeserializeErrors<T, E>` to a `DeserializeErrors<T, U>`
    pub fn map_errors<U, F: Fn(E) -> U>(self, f: F) -> DeserializeErrors<T, NonEmpty<U>> {
        DeserializeErrors {
            recovered: self.recovered,
            errors: self.errors.map(f),
        }
    }

    /// Recovered a value with a single error
    pub fn recovered(recovered: T, err: E) -> Self {
        Self {
            recovered: Some(recovered),
            errors: NonEmpty::new(err),
        }
    }

    /// Failed to recover a value with a single error
    pub fn err(err: E) -> Self {
        Self {
            recovered: None,
            errors: NonEmpty::new(err),
        }
    }
}