ocpi-tariffs 0.46.1

OCPI tariff calculations
Documentation
//! Case Insensitive String. Only printable ASCII allowed.

#[cfg(test)]
mod test;

use std::{fmt, ops::Deref};

use crate::{
    json,
    warning::{self, IntoCaveat},
    Verdict,
};

/// The warnings that can happen when parsing a case-insensitive string.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
    /// There should be no escape codes in a `CiString`.
    ContainsEscapeCodes,

    /// There should only be printable ASCII bytes in a `CiString`.
    ContainsNonPrintableASCII,

    /// The JSON value given is not a string.
    InvalidType { type_found: json::ValueKind },

    /// The length of the string exceeds the specs constraint.
    InvalidLengthMax { length: usize },

    /// The length of the string is not equal to the specs constraint.
    InvalidLengthExact { length: usize },

    /// The casing of the string is not common practice.
    ///
    /// Note: This is not enforced by the string types in this module, but can be used
    /// by linting code to signal that the casing of a given string is unorthodox.
    PreferUppercase,
}

impl Warning {
    fn invalid_type(elem: &json::Element<'_>) -> Self {
        Self::InvalidType {
            type_found: elem.value().kind(),
        }
    }
}

impl fmt::Display for Warning {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ContainsEscapeCodes => f.write_str("The string contains escape codes."),
            Self::ContainsNonPrintableASCII => {
                f.write_str("The string contains non-printable bytes.")
            }
            Self::InvalidType { type_found } => {
                write!(f, "The value should be a string but is `{type_found}`")
            }
            Self::InvalidLengthMax { length } => {
                write!(
                    f,
                    "The string is longer than the max length `{length}` defined in the spec.",
                )
            }
            Self::InvalidLengthExact { length } => {
                write!(f, "The string should be length `{length}`.")
            }
            Self::PreferUppercase => {
                write!(f, "Upper case is preffered")
            }
        }
    }
}

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match self {
            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
            Self::ContainsNonPrintableASCII => {
                warning::Id::from_static("contains_non_printable_ascii")
            }
            Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
            Self::InvalidLengthMax { .. } => warning::Id::from_static("invalid_length_max"),
            Self::InvalidLengthExact { .. } => warning::Id::from_static("invalid_length_exact"),
            Self::PreferUppercase => warning::Id::from_static("prefer_upper_case"),
        }
    }
}

/// String that can have `[0..=MAX_LEN]` bytes.
///
/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
/// Case insensitivity is not enforced.
///
/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
#[derive(Copy, Clone, Debug)]
pub(crate) struct CiMaxLen<'buf, const MAX_LEN: usize>(&'buf str);

impl<const MAX_LEN: usize> Deref for CiMaxLen<'_, MAX_LEN> {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.0
    }
}

impl<const MAX_LEN: usize> fmt::Display for CiMaxLen<'_, MAX_LEN> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl<'buf, const MAX_LEN: usize> json::FromJson<'buf> for CiMaxLen<'buf, MAX_LEN> {
    type Warning = Warning;

    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
        let (s, mut warnings) = Base::from_json(elem)?.into_parts();

        if s.len() > MAX_LEN {
            warnings.insert(Warning::InvalidLengthMax { length: MAX_LEN }, elem);
        }

        Ok(Self(s.0).into_caveat(warnings))
    }
}

/// String that can have `LEN` bytes exactly.
///
/// Only printable ASCII allowed. Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed.
/// Case insensitivity is not enforced.
///
/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
#[derive(Copy, Clone, Debug)]
pub(crate) struct CiExactLen<'buf, const LEN: usize>(&'buf str);

impl<const LEN: usize> Deref for CiExactLen<'_, LEN> {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.0
    }
}

impl<const LEN: usize> fmt::Display for CiExactLen<'_, LEN> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl<'buf, const LEN: usize> json::FromJson<'buf> for CiExactLen<'buf, LEN> {
    type Warning = Warning;

    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
        let (s, mut warnings) = Base::from_json(elem)?.into_parts();

        if s.len() != LEN {
            warnings.insert(Warning::InvalidLengthExact { length: LEN }, elem);
        }

        Ok(Self(s.0).into_caveat(warnings))
    }
}

/// Case Insensitive String. Only printable ASCII allowed. (Non-printable characters like: Carriage returns, Tabs, Line breaks, etc. are not allowed)
///
/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#11-cistring-type>
/// See: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/types.md#11-cistring-type>
#[derive(Copy, Clone, Debug)]
struct Base<'buf>(&'buf str);

impl Deref for Base<'_> {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.0
    }
}

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

impl<'buf> json::FromJson<'buf> for Base<'buf> {
    type Warning = Warning;

    fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
        let mut warnings = warning::Set::new();
        let Some(id) = elem.to_raw_str() else {
            return warnings.bail(Warning::invalid_type(elem), elem);
        };

        // We don't care about the details of any warnings the escapes in the Id may have.
        // The Id should simply not have any escapes.
        let s = id.has_escapes(elem).ignore_warnings();
        let s = match s {
            json::decode::PendingStr::NoEscapes(s) => {
                if check_printable(s) {
                    warnings.insert(Warning::ContainsNonPrintableASCII, elem);
                }
                s
            }
            json::decode::PendingStr::HasEscapes(escape_str) => {
                warnings.insert(Warning::ContainsEscapeCodes, elem);
                // We decode the escapes to check if any of the escapes result in non-printable ASCII.
                let decoded = escape_str.decode_escapes(elem).ignore_warnings();

                if check_printable(&decoded) {
                    warnings.insert(Warning::ContainsNonPrintableASCII, elem);
                }

                escape_str.into_raw()
            }
        };

        Ok(Self(s).into_caveat(warnings))
    }
}

fn check_printable(s: &str) -> bool {
    s.chars()
        .any(|c| c.is_ascii_whitespace() || c.is_ascii_control())
}