ocpi-tariffs 0.45.0

OCPI tariff calculations
Documentation
//! Timestamps are formatted as a string with a max length of 25 chars. Each timestamp follows RFC 3339,
//! with some additional limitations. All timestamps are expected to be in UTC. The absence of the
//! timezone designator implies a UTC timestamp. Fractional seconds may be used.
//!
//! # Examples
//!
//! Example of how timestamps should be formatted in OCPI, other formats/patterns are not allowed:
//!
//! - `"2015-06-29T20:39:09Z"`
//! - `"2015-06-29T20:39:09"`
//! - `"2016-12-29T17:45:09.2Z"`
//! - `"2016-12-29T17:45:09.2"`
//! - `"2018-01-01T01:08:01.123Z"`
//! - `"2018-01-01T01:08:01.123"`

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

#[cfg(test)]
mod test_datetime_from_json;

use std::fmt;

use chrono::{DateTime, NaiveDateTime, TimeZone as _, Utc};

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

/// The warnings that can happen when parsing or linting a `NaiveDate`, `NaiveTime`, or `DateTime`.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Warning {
    /// The datetime does not need to contain escape codes.
    ContainsEscapeCodes,

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

    /// The datetime is not valid.
    ///
    /// Timestamps are formatted as a string with a max length of 25 chars. Each timestamp follows RFC 3339,
    /// with some additional limitations. All timestamps are expected to be in UTC. The absence of the
    /// timezone designator implies a UTC timestamp. Fractional seconds may be used.
    ///
    /// # Examples
    ///
    /// Example of how timestamps should be formatted in OCPI, other formats/patterns are not allowed:
    ///
    /// - `"2015-06-29T20:39:09Z"`
    /// - `"2015-06-29T20:39:09"`
    /// - `"2016-12-29T17:45:09.2Z"`
    /// - `"2016-12-29T17:45:09.2"`
    /// - `"2018-01-01T01:08:01.123Z"`
    /// - `"2018-01-01T01:08:01.123"`
    Invalid(String),

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

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::Invalid(err) => write!(f, "The value is not valid: {err}"),
            Self::InvalidType { type_found } => {
                write!(f, "The value should be a string but found `{type_found}`.")
            }
        }
    }
}

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::Invalid(_) => warning::Id::from_static("invalid"),
            Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
        }
    }
}

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

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

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

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

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

        // First try parsing with a timezone, if that doesn't work try to parse without
        let err = match s.parse::<DateTime<Utc>>() {
            Ok(date) => return Ok(date.into_caveat(warnings)),
            Err(err) => err,
        };

        let Ok(date) = s.parse::<NaiveDateTime>() else {
            return warnings.bail(Warning::Invalid(err.to_string()), elem);
        };

        let datetime = Utc.from_utc_datetime(&date);
        Ok(datetime.into_caveat(warnings))
    }
}

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

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

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

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

        let date = match s.parse::<chrono::NaiveDate>() {
            Ok(v) => v,
            Err(err) => {
                return warnings.bail(Warning::Invalid(err.to_string()), elem);
            }
        };

        Ok(date.into_caveat(warnings))
    }
}

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

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

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

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

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

        let date = match chrono::NaiveTime::parse_from_str(s, "%H:%M") {
            Ok(v) => v,
            Err(err) => {
                return warnings.bail(Warning::Invalid(err.to_string()), elem);
            }
        };

        Ok(date.into_caveat(warnings))
    }
}