ocpi-tariffs 0.49.0

OCPI tariff calculations
Documentation
//! An `ISO 3166-1` country code.
//!
//! Use `CodeSet` to parse a `Code` from JSON.

#[cfg(test)]
pub(crate) mod test;

mod data;

use std::fmt;

#[doc(inline)]
pub use data::Code;

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

const RESERVED_PREFIX: u8 = b'x';
const ALPHA_2_LEN: usize = 2;
const ALPHA_3_LEN: usize = 3;

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
    /// Neither the timezone or country field require char escape codes.
    ContainsEscapeCodes,

    /// The field at the path could not be decoded.
    Decode(json::decode::Warning),

    /// The `country` is not a valid `ISO 3166-1` country code because it's not uppercase.
    PreferUpperCase,

    /// The `country` is not a valid `ISO 3166-1` country code.
    InvalidCode,

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

    /// The `country` is not a valid `ISO 3166-1` country code because it's not 2 or 3 chars in length.
    InvalidLength,

    /// The `country` is not a valid `ISO 3166-1` country code because it's all codes beginning with 'X' are reserved.
    InvalidReserved,
}

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 value contains escape codes but it does not need them"),
            Self::Decode(warning) => fmt::Display::fmt(warning, f),
            Self::PreferUpperCase => f.write_str("The country-code follows the ISO 3166-1 standard which states: the chars should be uppercase."),
            Self::InvalidCode => f.write_str("The country-code is not a valid ISO 3166-1 code."),
            Self::InvalidType { type_found } => {
                write!(f, "The value should be a string but is `{type_found}`")
            }
            Self::InvalidLength => f.write_str("The country-code follows the ISO 3166-1 which states that the code should be 2 or 3 chars in length."),
            Self::InvalidReserved => f.write_str("The country-code follows the ISO 3166-1 standard which states: all codes beginning with 'X' are reserved."),
        }
    }
}

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match self {
            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
            Self::Decode(kind) => kind.id(),
            Self::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
            Self::InvalidCode => warning::Id::from_static("invalid_code"),
            Self::InvalidType { type_found } => {
                warning::Id::from_string(format!("invalid_type({type_found})"))
            }
            Self::InvalidLength => warning::Id::from_static("invalid_length"),
            Self::InvalidReserved => warning::Id::from_static("invalid_reserved"),
        }
    }
}

/// An alpha-2 or alpha-3 `Code`.
///
/// The caller can decide if they want to warn or fail if the wrong variant is parsed.
#[derive(Debug)]
pub(crate) enum CodeSet {
    /// An alpha-2 country code was parsed.
    Alpha2(Code),

    /// An alpha-3 country code was parsed.
    Alpha3(Code),
}

impl From<json::decode::Warning> for Warning {
    fn from(warn_kind: json::decode::Warning) -> Self {
        Self::Decode(warn_kind)
    }
}

impl json::FromJson<'_> for CodeSet {
    type Warning = Warning;

    fn from_json(elem: &json::Element<'_>) -> Verdict<CodeSet, Self::Warning> {
        let mut warnings = warning::Set::new();
        let value = elem.as_value();

        let Some(s) = value.to_raw_str() else {
            return warnings.bail(elem, Warning::invalid_type(elem));
        };

        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);

        let s = match pending_str {
            json::PendingStr::NoEscapes(s) => s,
            json::PendingStr::HasEscapes(_) => {
                return warnings.bail(elem, Warning::ContainsEscapeCodes);
            }
        };

        let bytes = s.as_bytes();

        if let [a, b, c] = bytes {
            let triplet: [u8; ALPHA_3_LEN] = [
                a.to_ascii_uppercase(),
                b.to_ascii_uppercase(),
                c.to_ascii_uppercase(),
            ];

            if triplet != bytes {
                warnings.insert(elem, Warning::PreferUpperCase);
            }

            if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
                warnings.insert(elem, Warning::InvalidReserved);
            }

            let Some(code) = Code::from_alpha_3(triplet) else {
                return warnings.bail(elem, Warning::InvalidCode);
            };

            Ok(CodeSet::Alpha3(code).into_caveat(warnings))
        } else if let [a, b] = bytes {
            let pair: [u8; ALPHA_2_LEN] = [a.to_ascii_uppercase(), b.to_ascii_uppercase()];

            if pair != bytes {
                warnings.insert(elem, Warning::PreferUpperCase);
            }

            if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
                warnings.insert(elem, Warning::InvalidReserved);
            }

            let Some(code) = Code::from_alpha_2(pair) else {
                return warnings.bail(elem, Warning::InvalidCode);
            };

            Ok(CodeSet::Alpha2(code).into_caveat(warnings))
        } else {
            warnings.bail(elem, Warning::InvalidLength)
        }
    }
}

impl fmt::Display for Code {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.into_alpha_2_str())
    }
}

/// Macro to specify a list of valid `ISO 3166-1` `alpha-2` and `alpha-3` country codes strings.
macro_rules! country_codes {
    [$(($name:ident, $alpha2:literal, $alpha3:literal)),*] => {
        /// An `ISO 3166-1` `alpha-2` country code.
        ///
        /// The impl is designed to be converted from `json::RawValue`.
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash,  PartialOrd, Ord)]
        pub enum Code {
            $($name),*
        }

        impl Code {
            /// Try creating a `Code` from two upper ASCII bytes.
            pub(super) const fn from_alpha_2(code: [u8; 2]) -> Option<Self> {
                match &code {
                    $($alpha2 => Some(Self::$name),)*
                    _ => None
                }
            }

            /// Try creating a `Code` from three upper ASCII bytes.
            pub(super) const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
                match &code {
                    $($alpha3 => Some(Self::$name),)*
                    _ => None
                }
            }

            /// Return enum as two byte uppercase `&str`.
            pub fn into_alpha_2_str(self) -> &'static str {
                let bytes = match self {
                    $(Self::$name => $alpha2),*
                };
                std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
            }

            /// Return enum as three byte uppercase `&str`.
            pub fn into_alpha_3_str(self) -> &'static str {
                let bytes = match self {
                    $(Self::$name => $alpha3),*
                };
                std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
            }
        }
    };
}

pub(crate) use country_codes;