ocpi_tariffs_cli/
lib.rs

1#![allow(clippy::print_stderr, reason = "The CLI is allowed to use stderr")]
2#![allow(clippy::print_stdout, reason = "The CLI is allowed to use stdout")]
3#![doc = include_str!("../README.md")]
4
5mod analyze;
6mod guess_version;
7mod lint;
8mod opts;
9mod print;
10mod validate;
11
12use std::{
13    env, fmt,
14    io::{self, IsTerminal as _},
15    path::PathBuf,
16};
17
18use clap::Parser;
19use ocpi_tariffs::{price, ParseError};
20
21#[doc(hidden)]
22#[derive(Parser)]
23#[command(version)]
24pub struct Opts {
25    #[clap(subcommand)]
26    command: opts::Command,
27}
28
29impl Opts {
30    pub fn run(self) -> Result<(), Error> {
31        self.command.run()
32    }
33}
34
35#[derive(Copy, Clone, Debug, clap::ValueEnum)]
36pub enum ObjectKind {
37    Cdr,
38    Tariff,
39}
40
41#[doc(hidden)]
42#[derive(Debug)]
43pub enum Error {
44    /// When the process is a TTY `--cdr` is required.
45    CdrRequired,
46
47    /// A deserialize `Error` occurred.
48    Deserialize(ParseError),
49
50    /// Unable to open a file.
51    File { path: PathBuf, error: io::Error },
52
53    /// An internal error that is a bug.
54    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
55
56    /// A timezone was provided by the user and it failed to parse
57    InvalidTimezone(chrono_tz::ParseError),
58
59    /// An Error happened when calling the `tariff::lint` fn.
60    Lint(ocpi_tariffs::lint::Error),
61
62    /// A timezone could not be found or inferred.
63    NoTimezone,
64
65    /// The `CDR` or tariff path supplied is not a file.
66    PathNotFile { path: PathBuf },
67
68    /// The `CDR` or tariff path supplied doesn't have a parent dir.
69    PathNoParentDir { path: PathBuf },
70
71    /// An Error happened when calling the `cdr::price` fn.
72    Price(price::Error),
73
74    /// An Error happened when performing I/O.
75    StdIn(io::Error),
76
77    /// When the process is a TTY `--tariff` is required.
78    TariffRequired,
79
80    /// The calculated totals deviate from the totals in the CDR.
81    TotalsDoNotMatch,
82}
83
84impl std::error::Error for Error {}
85
86impl fmt::Display for Error {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::CdrRequired => f.write_str("`--cdr` is required when the process is a TTY"),
90            Self::Deserialize(err) => write!(f, "{err}"),
91            Self::File { path, error } => {
92                write!(f, "File error `{}`: {}", path.display(), error)
93            }
94            Self::Internal(err) => {
95                write!(f, "{err}")
96            }
97            Self::InvalidTimezone(err) => write!(f, "the timezone given is invalid; {err}"),
98            Self::Lint(err) => write!(f, "Lint error {err}"),
99            Self::NoTimezone => write!(f, "A timezone could not be found or inferred."),
100            Self::PathNotFile { path } => {
101                write!(f, "The path given is not a file: `{}`", path.display())
102            }
103            Self::PathNoParentDir { path } => {
104                write!(
105                    f,
106                    "The path given doesn't have a parent dir: `{}`",
107                    path.display()
108                )
109            }
110            Self::Price(err) => write!(f, "{err}"),
111            Self::StdIn(err) => {
112                write!(f, "Stdin error {err}")
113            }
114            Self::TariffRequired => f.write_str("`--tariff` is required when the process is a TTY"),
115            Self::TotalsDoNotMatch => {
116                f.write_str("Calculation does not match all totals in the CDR")
117            }
118        }
119    }
120}
121
122impl From<price::Error> for Error {
123    fn from(err: price::Error) -> Self {
124        Self::Price(err)
125    }
126}
127
128impl From<ocpi_tariffs::lint::Error> for Error {
129    fn from(err: ocpi_tariffs::lint::Error) -> Self {
130        Self::Lint(err)
131    }
132}
133
134impl From<ParseError> for Error {
135    fn from(err: ParseError) -> Self {
136        Self::Deserialize(err)
137    }
138}
139
140impl Error {
141    pub fn file(path: PathBuf, error: io::Error) -> Self {
142        Self::File { path, error }
143    }
144
145    pub fn stdin(err: io::Error) -> Self {
146        Self::StdIn(err)
147    }
148}
149
150#[doc(hidden)]
151pub fn setup_logging() -> Result<(), &'static str> {
152    let stderr = io::stderr();
153    let builder = tracing_subscriber::fmt()
154        .without_time()
155        .with_writer(io::stderr)
156        .with_ansi(stderr.is_terminal());
157
158    let level = match env::var("RUST_LOG") {
159        Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
160        Err(err) => match err {
161            env::VarError::NotPresent => tracing::Level::INFO,
162            env::VarError::NotUnicode(_) => {
163                return Err("RUST_LOG is not unicode");
164            }
165        },
166    };
167
168    builder.with_max_level(level).init();
169
170    Ok(())
171}
172
173#[cfg(test)]
174mod test {
175    use super::Error;
176
177    #[test]
178    const fn error_should_be_send_and_sync() {
179        const fn f<T: Send + Sync>() {}
180
181        f::<Error>();
182    }
183}