ocpi-tariffs 0.46.1

OCPI tariff calculations
Documentation
#![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
#![allow(clippy::panic, reason = "tests are allowed panic")]
#![allow(
    clippy::arithmetic_side_effects,
    reason = "tests are allowed have arithmetic_side_effects"
)]

use std::fmt;

use crate::test::Expectation;

use super::{
    match_path_node, parser::Span, ElemId, Element, Field, FieldsAsExt as _, LineCol, PathNode,
    PathNodeRef, UnexpectedFields, Value,
};

impl<'buf> Element<'buf> {
    /// Consume the `Element` and return only the `Value`.
    pub(crate) fn into_value(self) -> Value<'buf> {
        self.value
    }

    /// Consume the `Element` and return the `Path`, `Span` and `Value` as a tuple.
    pub(crate) fn into_parts(self) -> (ElemId, PathNodeRef<'buf>, Span, Value<'buf>) {
        let Self {
            id,
            path_node: path,
            span,
            value,
        } = self;
        (id, path, span, value)
    }

    pub(crate) fn find_field(&self, key: &str) -> Option<&Field<'buf>> {
        self.as_object_fields()
            .and_then(|fields| fields.find_field(key))
    }
}

impl Value<'_> {
    /// Return true if the `Value` is an `Array`.
    pub(crate) fn is_array(&self) -> bool {
        matches!(self, Value::Array(_))
    }

    /// Return true if the `Value` is an `Object`.
    pub(crate) fn is_object(&self) -> bool {
        matches!(self, Value::Object(_))
    }
}

impl<'buf> Field<'buf> {
    pub fn id(&self) -> ElemId {
        self.0.id()
    }

    pub fn into_parts(self) -> (ElemId, PathNodeRef<'buf>, Span, Value<'buf>) {
        self.0.into_parts()
    }
}

impl<'buf> UnexpectedFields<'buf> {
    /// The tests need to assert against the contents.
    pub(super) fn into_inner(self) -> Vec<PathNodeRef<'buf>> {
        self.0
    }

    /// Filter off the fields that match the glob.
    fn filter_matches(&mut self, glob: &PathGlob) {
        self.0.retain(|path| !glob.matches(path));
    }
}

/// A `Display` object that writes out the JSON source and highlights the given line.
pub struct LineHighlighter {
    json: String,
    pos: LineCol,
}

impl LineHighlighter {
    pub fn from_value(json: &serde_json::Value, pos: LineCol) -> Self {
        let json = serde_json::to_string_pretty(json).unwrap();
        Self { json, pos }
    }
}

impl fmt::Display for LineHighlighter {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let Ok(target_line) = usize::try_from(self.pos.line) else {
            return Ok(());
        };

        for (index, line) in self.json.lines().enumerate() {
            let line_no = index + 1;
            if index == target_line {
                writeln!(f, "{line_no:3}| {line}")?;
            } else {
                writeln!(f, "   | {line}")?;
            }
        }

        Ok(())
    }
}

/// A string based `Path` that can contain glob `*` patterns in place of a literal path element.
/// The glob means that the path section can be any valid section.
#[derive(Debug)]
pub(crate) struct PathGlob(String);

impl PathGlob {
    /// Return true if this `PathGlob` matches the given `PathNode`.
    pub(crate) fn matches(&self, path: &PathNode<'_>) -> bool {
        const WILDCARD: &str = "*";

        match_path_node(path, &self.0, |s| {
            // If the `PathGlob` segment is a glob, then continue to the next segment.
            s == WILDCARD
        })
    }
}

/// The tests need to assert against literal `ElemId`s.
impl From<usize> for ElemId {
    fn from(value: usize) -> Self {
        Self(value)
    }
}

impl<'a> From<&'a str> for PathGlob {
    fn from(s: &'a str) -> Self {
        Self(s.into())
    }
}

impl From<String> for PathGlob {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl<'de> serde::Deserialize<'de> for PathGlob {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: ::serde::Deserializer<'de>,
    {
        let s = <String as ::serde::Deserialize>::deserialize(deserializer)?;
        Ok(Self(s))
    }
}

/// The `unexpected_fields` should be empty. If not then panic with a truncated list of the fields.
#[track_caller]
pub(crate) fn expect_no_unexpected_fields(
    expect_file_name: &str,
    unexpected_fields: &UnexpectedFields<'_>,
) {
    if !unexpected_fields.is_empty() {
        const MAX_FIELD_DISPLAY: usize = 20;

        if unexpected_fields.len() > MAX_FIELD_DISPLAY {
            let truncated_fields = unexpected_fields
                .iter()
                .take(MAX_FIELD_DISPLAY)
                .map(|path| path.to_string())
                .collect::<Vec<_>>();

            panic!(
                "The expect file `{expect_file_name}` didn't expect `{}` unexpected fields;\n\
                    displaying the first ({}):\n{}\n... and {} more",
                unexpected_fields.len(),
                truncated_fields.len(),
                truncated_fields.join(",\n"),
                unexpected_fields.len() - truncated_fields.len(),
            )
        } else {
            panic!(
                "The expect file `{expect_file_name}` didn't expect `{}` unexpected fields:\n{}",
                unexpected_fields.len(),
                unexpected_fields.to_strings().join(",\n")
            )
        };
    }
}

/// Compare the `unexpected_fields` to the expected fields globs.
///
/// Panic if there are any fields not expected.
#[track_caller]
pub(crate) fn expect_unexpected_fields(
    expect_file_name: &str,
    unexpected_fields: &mut UnexpectedFields<'_>,
    expected: Expectation<Vec<PathGlob>>,
) {
    if let Expectation::Present(expectation) = expected {
        let unexpected_fields_expect = expectation.expect_value();

        // Remove any fields that match the expected glob.
        // The remaining fields are truly unexpected.
        for expect_glob in unexpected_fields_expect {
            unexpected_fields.filter_matches(&expect_glob);
        }

        expect_no_unexpected_fields(expect_file_name, unexpected_fields);
    } else {
        expect_no_unexpected_fields(expect_file_name, unexpected_fields);
    }
}