Skip to main content

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, warning, 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    Handled,
45
46    /// When the process is a `tty` `--cdr` is required.
47    CdrRequired,
48
49    /// A deserialize `Error` occurred.
50    Deserialize(ParseError),
51
52    /// Unable to open a file.
53    File {
54        path: PathBuf,
55        error: io::Error,
56    },
57
58    /// An internal error that is a bug.
59    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
60
61    /// A timezone was provided by the user and it failed to parse
62    InvalidTimezone(chrono_tz::ParseError),
63
64    /// The `CDR` or tariff path supplied is not a file.
65    PathNotFile {
66        path: PathBuf,
67    },
68
69    /// The `CDR` or tariff path supplied doesn't have a parent directory.
70    PathNoParentDir {
71        path: PathBuf,
72    },
73
74    /// An Error happened when calling the `cdr::price` fn.
75    Price(warning::Error<price::Warning>),
76
77    /// An Error happened when performing I/O.
78    StdIn(io::Error),
79
80    /// When the process is a `tty` `--tariff` is required.
81    TariffRequired,
82
83    /// The calculated totals deviate from the totals in the CDR.
84    TotalsDoNotMatch,
85}
86
87impl std::error::Error for Error {}
88
89impl fmt::Display for Error {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            Self::Handled => Ok(()),
93            Self::CdrRequired => f.write_str("`--cdr` is required when the process is a TTY"),
94            Self::Deserialize(err) => write!(f, "{err}"),
95            Self::File { path, error } => {
96                write!(f, "File error `{}`: {}", path.display(), error)
97            }
98            Self::Internal(err) => {
99                write!(f, "{err}")
100            }
101            Self::InvalidTimezone(err) => write!(f, "the timezone given is invalid; {err}"),
102            Self::PathNotFile { path } => {
103                write!(f, "The path given is not a file: `{}`", path.display())
104            }
105            Self::PathNoParentDir { path } => {
106                write!(
107                    f,
108                    "The path given doesn't have a parent dir: `{}`",
109                    path.display()
110                )
111            }
112            Self::Price(err) => write!(f, "{err}"),
113            Self::StdIn(err) => {
114                write!(f, "Stdin error {err}")
115            }
116            Self::TariffRequired => f.write_str("`--tariff` is required when the process is a TTY"),
117            Self::TotalsDoNotMatch => {
118                f.write_str("Calculation does not match all totals in the CDR")
119            }
120        }
121    }
122}
123
124impl From<warning::Error<price::Warning>> for Error {
125    fn from(value: warning::Error<price::Warning>) -> Self {
126        Self::Price(value)
127    }
128}
129
130impl From<ParseError> for Error {
131    fn from(err: ParseError) -> Self {
132        Self::Deserialize(err)
133    }
134}
135
136impl Error {
137    pub fn file(path: PathBuf, error: io::Error) -> Self {
138        Self::File { path, error }
139    }
140
141    pub fn stdin(err: io::Error) -> Self {
142        Self::StdIn(err)
143    }
144}
145
146#[doc(hidden)]
147pub fn setup_logging() -> Result<(), &'static str> {
148    let stderr = io::stderr();
149    let builder = tracing_subscriber::fmt()
150        .without_time()
151        .with_writer(io::stderr)
152        .with_ansi(stderr.is_terminal());
153
154    let level = match env::var("RUST_LOG") {
155        Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
156        Err(err) => match err {
157            env::VarError::NotPresent => tracing::Level::INFO,
158            env::VarError::NotUnicode(_) => {
159                return Err("RUST_LOG is not unicode");
160            }
161        },
162    };
163
164    builder.with_max_level(level).init();
165
166    Ok(())
167}
168
169#[cfg(test)]
170mod test {
171    use super::Error;
172
173    #[test]
174    const fn error_should_be_send_and_sync() {
175        const fn f<T: Send + Sync>() {}
176
177        f::<Error>();
178    }
179}