ocpi-tariffs 0.46.1

OCPI tariff calculations
Documentation
use std::{
    collections::{BTreeMap, BTreeSet},
    fmt,
};

use crate::{
    json,
    test::{ExpectValue, Expectation},
    warning::SetDeferred,
};

use super::{
    Caveat, CaveatDeferred, Element, Error, ErrorSet, Group, Id, Path, Set, Verdict, Warning,
};

/// `Verdict` specific extension methods for the `Result` type.
pub trait VerdictTestExt<T, W: Warning> {
    /// Discard all warnings in the `ErrorSet` variant and keep only the warning that caused the error.
    fn unwrap_only_error(self) -> Error<W>;
}

impl<T, W: Warning> VerdictTestExt<T, W> for Verdict<T, W>
where
    T: fmt::Debug,
{
    fn unwrap_only_error(self) -> Error<W> {
        let error = match self {
            Ok(c) => panic!("called `Result::unwrap_only_error` on an `Ok` value: {c:?}"),
            Err(set) => {
                let ErrorSet { error, warnings: _ } = set;
                *error
            }
        };
        error
    }
}

impl<T, W> Caveat<T, W>
where
    W: Warning,
{
    /// Return the value and assert there are no [`Warning`]s.
    ///
    /// # Panics
    ///
    /// Asserts that the warning is empty.
    #[track_caller]
    pub fn unwrap(self) -> T {
        let Self { value, warnings } = self;
        assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
        value
    }
}

impl<T, W> CaveatDeferred<T, W>
where
    W: Warning,
{
    /// Return the value and assert there are no [`Warning`]s.
    ///
    /// # Panics
    ///
    /// Asserts that the warning is empty.
    pub fn unwrap(self) -> T {
        let Self { value, warnings } = self;
        assert!(warnings.is_empty(), "{:#?}", warnings.id_map());
        value
    }
}

impl<W> Group<W>
where
    W: Warning,
{
    /// Convert the Group into String versions of its parts.
    ///
    /// The first tuple field is the [`json::Element`] path as a String.
    /// The second tuple field is a list of [`Warning`] ids.
    fn path_and_ids(&self) -> (&str, Vec<Id>) {
        let Self { element, warnings } = self;
        (
            element.path.as_str(),
            warnings.iter().map(|w| w.id()).collect(),
        )
    }

    /// Convert the Group into String versions of its parts.
    ///
    /// The first tuple field is the [`json::Element`] path as a String.
    /// The second tuple field is a list of [`Warning`] ids.
    fn ids(&self) -> Vec<Id> {
        self.warnings.iter().map(|w| w.id()).collect()
    }
}

/// A Debug representation of a [Warning] at a given location.
pub(crate) struct Debug<'a, W: Warning> {
    /// The line in the source code where this [`Warning`] occurred.
    ///
    /// NOTE: Warning locations are only available in test code.
    location: Location,

    /// The id of the [Warning].
    id: Id,

    /// The warning message.
    message: Message<'a, W>,
}

impl<W: Warning> fmt::Debug for Debug<'_, W> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_map()
            .entry(&"location", &self.location)
            .entry(&"id", &self.id)
            .entry(&"message", &self.message)
            .finish()
    }
}

/// Print a nicely formatted location when asked for the `Debug` representation.
struct Location(&'static std::panic::Location<'static>);

impl fmt::Debug for Location {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Print the [Warning] message when asked for the `Debug` representation.
struct Message<'a, W: Warning>(&'a W);

impl<W: Warning> fmt::Debug for Message<'_, W> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl<W> Set<W>
where
    W: Warning,
{
    /// Return a map of [Element] paths and the [Source] of the [Warning].
    pub(crate) fn path_debug_map(&self) -> BTreeMap<&str, Vec<Debug<'_, W>>> {
        self.0
            .values()
            .map(|group| {
                let warnings = group
                    .warnings
                    .iter()
                    .map(|super::Source { location, warning }| Debug {
                        location: Location(location),
                        id: warning.id(),
                        message: Message(warning),
                    })
                    .collect();
                (group.element.path.as_str(), warnings)
            })
            .collect()
    }

    /// Consume the `Set` and return a map of [`json::Element`] paths to a list of [`Warning`]s.
    ///
    /// This is designed to be used to print out maps of warnings associated with elements.
    pub(crate) fn into_path_as_str_map(self) -> BTreeMap<String, Vec<W>> {
        self.0
            .into_values()
            .map(|Group { element, warnings }| {
                let path = element.path.0;
                let warnings = warnings
                    .into_iter()
                    .map(super::Source::into_warning)
                    .collect();
                (path, warnings)
            })
            .collect()
    }
}

impl<W: Warning> SetDeferred<W> {
    /// Return true if the [`Warning`] set is empty.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Return the set as a list of `warning::Id`s.
    pub fn id_map(&self) -> Vec<Id> {
        self.0.iter().map(|w| w.id()).collect()
    }
}

#[derive(Debug)]
pub struct ErrorSourceContext<'buf, W: Warning> {
    /// The element as source JSON and surrounding context.
    pub context: &'buf str,

    /// The elements path.
    pub element_path: Path,

    /// The position of the element in the JSON.
    pub element_position: json::LineCol,

    /// The `Warning` that caused the failure.
    pub error: W,
}

impl<W: Warning> fmt::Display for ErrorSourceContext<'_, W> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "The element at `{}` path `{}: {}` has an error: {}",
            self.element_position, self.element_path, self.context, self.error
        )
    }
}

pub struct IncorrectSource(());

impl fmt::Debug for IncorrectSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}

impl fmt::Display for IncorrectSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("The JSON given is not the JSON that generated these warnings")
            .field(&self.0)
            .finish()
    }
}

impl std::error::Error for IncorrectSource {}

impl<W: Warning> Error<W> {
    /// Convert the [`Error`] into a source context ready for printing.
    pub(crate) fn into_context(
        self,
        json: &str,
    ) -> Result<ErrorSourceContext<'_, W>, IncorrectSource> {
        let Self { warning, element } = self;
        let Element { id: _, span, path } = element;

        // Slice up to the start of the span to calculate line and col numbers.
        let Some(lead_in) = json.get(..span.start) else {
            return Err(IncorrectSource(()));
        };
        // We can start the newline check from this byte index the next time.
        let element_position = json::line_col(lead_in);
        let Some(context) = json.get(span.start..span.end) else {
            return Err(IncorrectSource(()));
        };

        Ok(ErrorSourceContext {
            context,
            element_path: path,
            element_position,
            error: warning,
        })
    }
}

/// Assert that the warnings given are expected.
///
/// # Panics
///
/// If the expected warnings don't match the actual the function panics with a print out of the
/// warnings and the expectations if any warnings were unexpected.
#[track_caller]
pub(crate) fn assert_warnings<W>(
    expect_file_name: &str,
    warnings: &Set<W>,
    expected: Expectation<BTreeMap<String, Vec<String>>>,
) where
    W: Warning,
{
    let Expectation::Present(ExpectValue::Some(mut expected)) = expected else {
        assert!(
            warnings.is_empty(),
            "There is no `warnings` field in the `{expect_file_name}` file but the tariff has warnings;\n{:#?}",
            warnings.path_id_map()
        );
        return;
    };

    {
        // Assert that the `expect` file doesn't have extraneous entries.
        let warnings_grouped = warnings
            .iter()
            .map(|Group { element, warnings }| (element.path.as_str(), warnings))
            .collect::<BTreeMap<_, _>>();

        let mut elems_in_expect_without_warning = vec![];

        for elem_path in expected.keys() {
            if !warnings_grouped.contains_key(elem_path.as_str()) {
                elems_in_expect_without_warning.push(elem_path);
            }
        }

        assert!(elems_in_expect_without_warning.is_empty(),
            "The expect file `{expect_file_name}` has entries for elements that have no warnings:\n\
            {elems_in_expect_without_warning:#?}"
        );
    }

    // The elements that have warnings but have no entry for the elements path in the `expect` file.
    let mut elems_missing_from_expect = vec![];
    // The element that have warnings and an entry in the `expect` file, but the list of expected warnings
    // is not equal to the list of actual warnings.
    let mut unequal_warnings = vec![];

    for group in warnings {
        let Some(warnings_expected) = expected.remove(group.element.path.as_str()) else {
            elems_missing_from_expect.push(group);
            continue;
        };

        // Make two sets of actual and expected warnings.
        let warnings_expected = warnings_expected
            .into_iter()
            .map(Id::from_string)
            .collect::<BTreeSet<_>>();
        let warnings = group.ids().into_iter().collect::<BTreeSet<_>>();

        if warnings_expected != warnings {
            unequal_warnings.push(group);
        }
    }

    if !elems_missing_from_expect.is_empty() || !unequal_warnings.is_empty() {
        let missing = elems_missing_from_expect
            .into_iter()
            .map(Group::path_and_ids)
            .collect::<BTreeMap<_, _>>();

        let unequal = unequal_warnings
            .into_iter()
            .map(Group::path_and_ids)
            .collect::<BTreeMap<_, _>>();

        match (!missing.is_empty(), !unequal.is_empty()) {
            (true, true) => panic!(
                "Elements with warnings but are not defined in the `{expect_file_name}` file:\n{missing:#?}\n\
                Elements that are in the `{expect_file_name}` file but the warnings list is not correct.\n\
                The warnings reported are: \n{unequal:#?}"                  
            ),
            (true, false) => panic!("Elements with warnings but are not defined in the `{expect_file_name}` file:\n{missing:#?}"),
            (false, true) => panic!(
                "Elements that are in the `{expect_file_name}` file but the warnings list is not correct.\n\
                The warnings reported are: \n{unequal:#?}"
            ),
            (false, false) => (),
        }
    }
}