ocpi-tariffs 0.43.0

OCPI tariff calculations
Documentation
#[cfg(test)]
mod test_real_world;

#[cfg(test)]
mod test_lints;

use tracing::{debug, instrument};

use crate::{
    country,
    json::{self, FieldsAsExt, FromJson as _},
    required_field, string,
    warning::{self, DeescalateError as _, GatherWarnings as _, IntoCaveat as _},
    Price, Verdict,
};

use super::{v2x, Report, Warning};

/// Lint a v221 tariff.
///
/// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc#131-tariff-object>)
#[instrument(skip_all)]
pub(crate) fn lint<'buf>(
    tariff_elem: &json::Element<'buf>,
    mut warnings: warning::Set<Warning>,
) -> Report {
    let Some(fields) = tariff_elem.as_object_fields() else {
        return Report { warnings };
    };

    let fields = fields.as_raw_map();

    if let Some(elem) = required_field!(tariff_elem, fields, "country_code", warnings) {
        let _code: Option<country::Code> = lint_country_code(elem)
            .gather_warnings_into(&mut warnings)
            .deescalate_error_into(&mut warnings);
    }

    if let Some(elem) = required_field!(tariff_elem, fields, "party_id", warnings) {
        let _drop: Option<()> = lint_party_id(elem)
            .gather_warnings_into(&mut warnings)
            .deescalate_error_into(&mut warnings);
    }

    if let Some(elem) = required_field!(tariff_elem, fields, "currency", warnings) {
        let _drop: Option<()> = v2x::currency::lint(elem)
            .gather_warnings_into(&mut warnings)
            .deescalate_error_into(&mut warnings);
    }

    {
        let start_date_time = fields.get("start_date_time").map(|e| &**e);
        let end_date_time = fields.get("end_date_time").map(|e| &**e);

        let _drop: Option<()> = v2x::datetime::lint_start_end(start_date_time, end_date_time)
            .gather_warnings_into(&mut warnings)
            .deescalate_error_into(&mut warnings);
    }

    {
        let min_price = fields.get("min_price").map(|e| &**e);
        let max_price = fields.get("max_price").map(|e| &**e);

        let _drop: Option<()> = lint_min_max_price(min_price, max_price)
            .gather_warnings_into(&mut warnings)
            .deescalate_error_into(&mut warnings);
    }

    {
        if let Some(elem) = required_field!(tariff_elem, fields, "elements", warnings) {
            let _drop: Option<()> = v2x::elements::lint(elem)
                .gather_warnings_into(&mut warnings)
                .deescalate_error_into(&mut warnings);
        }
    }

    Report { warnings }
}

/// Validate the `country_code` field.
#[instrument(skip_all)]
fn lint_country_code(elem: &json::Element<'_>) -> Verdict<country::Code, Warning> {
    let mut warnings = warning::Set::<Warning>::new();

    let code_set = country::CodeSet::from_json(elem)?.gather_warnings_into(&mut warnings);

    debug!("code_set: {code_set:?}");

    // The `tariff.country` field should be an alpha-2 country code.
    //
    // An alpha-3 code can be converted into an alpha-2, but the caller should be warned.
    let country_code = match code_set {
        country::CodeSet::Alpha2(code) => code,
        country::CodeSet::Alpha3(code) => {
            warnings.insert(Warning::CpoCountryCodeShouldBeAlpha2, elem);
            code
        }
    };

    Ok(country_code.into_caveat(warnings))
}

type PartyId<'buf> = string::CiExactLen<'buf, 3>;

/// Lint the `party_id` field.
fn lint_party_id<'caller: 'buf, 'buf>(elem: &'caller json::Element<'buf>) -> Verdict<(), Warning> {
    let party_id = PartyId::from_json(elem)?;
    let (party_id, mut warnings) = party_id.into_parts();

    if party_id.chars().any(char::is_lowercase) {
        warnings.insert(string::Warning::PreferUppercase, elem);
    }

    Ok(().into_caveat(warnings.into_other()))
}

/// The `min_price` should not be greater than the `max_price`.
#[instrument(skip_all)]
fn lint_min_max_price(
    min_price: Option<&json::Element<'_>>,
    max_price: Option<&json::Element<'_>>,
) -> Verdict<(), Warning> {
    let mut warnings = warning::Set::<Warning>::new();

    if let Some((min_elem, max_elem)) = min_price.zip(max_price) {
        let min = Price::from_json(min_elem)?.gather_warnings_into(&mut warnings);
        let max = Price::from_json(max_elem)?.gather_warnings_into(&mut warnings);

        if min > max {
            warnings.insert(Warning::MinPriceIsGreaterThanMax, min_elem);
        }
    } else if let Some(elem) = min_price {
        let _drop = Price::from_json(elem)?.gather_warnings_into(&mut warnings);
    } else if let Some(elem) = max_price {
        let _drop = Price::from_json(elem)?.gather_warnings_into(&mut warnings);
    }

    Ok(().into_caveat(warnings))
}