ocpi-tariffs-cli 0.46.1

CLI application for OCPI tariff calculation
Documentation
use std::{
    fmt,
    ops::Deref,
    path::{Path, PathBuf},
};

use clap::{Args, Subcommand, ValueEnum};
use console::style;

use crate::{guess_version, lint, price, Error};

#[derive(Subcommand)]
pub enum Command {
    /// Guess the version of the given CDR or Tariff.
    GuessVersion(guess_version::Command),

    /// Lint the given tariff and display a list of warnings.
    Lint(lint::Command),

    /// Price a given charge detail record (CDR) against either a provided tariff structure or a
    /// tariff that is contained in the CDR itself.
    ///
    /// This command will show you a breakdown of all the calculated costs.
    Price(price::Command),
}

impl Command {
    pub fn run(self) -> Result<(), Error> {
        match self {
            Self::GuessVersion(command) => command.run(),
            Self::Lint(command) => command.run(),
            Self::Price(command) => command.run(),
        }
    }
}

#[derive(Args)]
pub struct TariffArgs {
    /// A path to the charge detail record in JSON format.
    ///
    /// If no path is provided the CDR is read from standard in.
    #[arg(short = 'c', long)]
    pub cdr: Option<PathBuf>,

    /// A path to the tariff structure in JSON format.
    ///
    /// If no path is provided, then the tariff is inferred to be contained inside the
    /// provided CDR. If the CDR contains multiple tariff structures, the first valid tariff
    /// will be used.
    #[arg(short = 't', long)]
    pub tariff: Option<PathBuf>,

    /// Timezone for evaluating any local times contained in the tariff structure.
    #[arg(short = 'z', long, alias = "tz")]
    pub timezone: Option<String>,

    /// The OCPI version that should be used for the input structures.
    ///
    /// If the input consists of version 2.1.1 structures they will be converted to 2.2.1
    /// structures. The actual calculation and output will always be according to OCPI 2.2.1.
    ///
    /// Use `detect` to let to tool try to find the matching version.
    #[arg(short = 'o', long, value_enum)]
    pub ocpi_version: Version,
}

#[derive(Clone, Debug, ValueEnum)]
pub(crate) enum Version {
    V221,
    V211,
}

impl From<Version> for ocpi_tariffs::Version {
    fn from(value: Version) -> Self {
        match value {
            Version::V221 => ocpi_tariffs::Version::V221,
            Version::V211 => ocpi_tariffs::Version::V211,
        }
    }
}

#[derive(Clone, Debug)]
pub(crate) struct FilePath {
    path: PathBuf,
    file_name: String,
    dir_name: String,
}

impl FilePath {
    /// Create a new `FilePath` from a `PathBuf` and assert that the path has a valid filename.
    pub fn from_path(path: &Path) -> Result<Self, Error> {
        let path = path.canonicalize().map_err(Error::then_file_io(path))?;
        let mut parts = path.iter().rev();

        let Some(file_name) = parts.next() else {
            return Err(Error::PathNotFile { path });
        };

        let Some(dir_name) = parts.next() else {
            return Err(Error::PathNoParentDir { path });
        };

        let file_name = file_name.to_string_lossy().to_string();
        let dir_name = dir_name.to_string_lossy().to_string();

        Ok(Self {
            path,
            file_name,
            dir_name,
        })
    }

    /// Return the filename.
    pub fn file_name(&self) -> &str {
        &self.file_name
    }

    /// Return the directory and filename parts of the path.
    pub fn dir_and_name(&self) -> String {
        format!("{}/{}", self.dir_name, self.file_name)
    }
}

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

impl AsRef<Path> for FilePath {
    fn as_ref(&self) -> &Path {
        &self.path
    }
}

impl Deref for FilePath {
    type Target = PathBuf;

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

/// Create a job description for display on the CLI
pub fn job_description(cdr: Option<&FilePath>, tariff: Option<&FilePath>) -> String {
    let cdr_name = cdr
        .as_ref()
        .map(|c| c.file_name())
        .unwrap_or_else(|| "<stdin>");

    let tariff_name = tariff
        .as_ref()
        .map(|c| c.file_name())
        .unwrap_or_else(|| "<CDR-tariff>");

    format!(
        "`{}` with tarifff `{}`",
        style(cdr_name).blue(),
        style(tariff_name).blue(),
    )
}