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
5#[cfg(test)]
6mod test;
7
8mod guess_version;
9mod lint;
10mod opts;
11mod price;
12mod print;
13
14use std::{
15    env, fmt, fs,
16    io::{self, IsTerminal as _, Read as _},
17    path::{Path, PathBuf},
18};
19
20use clap::Parser;
21use ocpi_tariffs::{warning, ParseError};
22use tracing::{debug, instrument};
23
24/// When reading a CDR or tariff from `stdin` create a String with this default capacity to avoid
25/// needless allocations.
26pub const DEFAULT_STDIO_BUF_SIZE: usize = 1024;
27
28#[doc(hidden)]
29#[derive(Parser)]
30#[command(version)]
31pub struct Opts {
32    #[clap(subcommand)]
33    command: opts::Command,
34}
35
36impl Opts {
37    pub fn run(self) -> Result<(), Error> {
38        self.command.run()
39    }
40}
41
42#[derive(Copy, Clone, clap::ValueEnum)]
43pub enum ObjectKind {
44    Cdr,
45    Tariff,
46}
47
48impl fmt::Debug for ObjectKind {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::Cdr => write!(f, "CDR"),
52            Self::Tariff => write!(f, "Tariff"),
53        }
54    }
55}
56
57/// Load a CDR or tariff from file.
58#[instrument]
59pub fn load_object_file(path: &Path, object_kind: ObjectKind) -> Result<String, Error> {
60    debug!("Loading {object_kind} from file");
61    let mut content = fs::read_to_string(path).map_err(Error::then_file_io(path))?;
62    json_strip_comments::strip(&mut content).map_err(Error::then_file_io(path))?;
63    debug!(bytes_read = content.len(), "{object_kind} read from file");
64    Ok(content)
65}
66
67/// Load a CDR or tariff from `stdin`.
68pub fn load_object_from_stdin(object_kind: ObjectKind) -> Result<String, Error> {
69    debug!("Loading {object_kind} from stdin");
70    let mut stdin = io::stdin().lock();
71    let mut content = String::with_capacity(DEFAULT_STDIO_BUF_SIZE);
72    let bytes_read = stdin.read_to_string(&mut content).map_err(Error::stdin)?;
73    debug!(bytes_read, "{object_kind} read from stdin");
74    json_strip_comments::strip(&mut content).map_err(Error::stdin)?;
75    Ok(content)
76}
77
78#[doc(hidden)]
79#[derive(Debug)]
80pub enum Error {
81    Handled,
82
83    /// When the process is a `tty` `--cdr` is required.
84    CdrRequired,
85
86    /// A deserialize `Error` occurred.
87    Deserialize(ParseError),
88
89    /// An `io::Error` occurred when opening or reading a file.
90    FileIO {
91        path: PathBuf,
92        error: io::Error,
93    },
94
95    /// An internal error that is a bug.
96    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
97
98    /// A timezone was provided by the user and it failed to parse
99    InvalidTimezone(chrono_tz::ParseError),
100
101    /// The `CDR` or tariff path supplied is not a file.
102    PathNotFile {
103        path: PathBuf,
104    },
105
106    /// The `CDR` or tariff path supplied doesn't have a parent directory.
107    PathNoParentDir {
108        path: PathBuf,
109    },
110
111    /// An Error happened when calling the `cdr::price` fn.
112    Price(warning::Error<ocpi_tariffs::price::Warning>),
113
114    /// An Error happened when performing I/O.
115    StdIn(io::Error),
116
117    /// When the process is a `tty` `--tariff` is required.
118    TariffRequired,
119}
120
121impl std::error::Error for Error {}
122
123impl fmt::Display for Error {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match self {
126            Self::Handled => Ok(()),
127            Self::CdrRequired => f.write_str("`--cdr` is required when the process is a TTY"),
128            Self::Deserialize(err) => write!(f, "{err}"),
129            Self::FileIO { path, error } => {
130                write!(f, "File error `{}`: {}", path.display(), error)
131            }
132            Self::Internal(err) => {
133                write!(f, "{err}")
134            }
135            Self::InvalidTimezone(err) => write!(f, "the timezone given is invalid; {err}"),
136            Self::PathNotFile { path } => {
137                write!(f, "The path given is not a file: `{}`", path.display())
138            }
139            Self::PathNoParentDir { path } => {
140                write!(
141                    f,
142                    "The path given doesn't have a parent dir: `{}`",
143                    path.display()
144                )
145            }
146            Self::Price(err) => write!(f, "{err}"),
147            Self::StdIn(err) => {
148                write!(f, "Stdin error {err}")
149            }
150            Self::TariffRequired => f.write_str("`--tariff` is required when the process is a TTY"),
151        }
152    }
153}
154
155impl From<warning::Error<ocpi_tariffs::price::Warning>> for Error {
156    fn from(value: warning::Error<ocpi_tariffs::price::Warning>) -> Self {
157        Self::Price(value)
158    }
159}
160
161impl From<ParseError> for Error {
162    fn from(err: ParseError) -> Self {
163        Self::Deserialize(err)
164    }
165}
166
167impl Error {
168    /// Return a fn that can be used in `Result::map_err` to create a `FileIO`.
169    pub fn then_file_io(path: &Path) -> impl FnOnce(io::Error) -> Error + use<'_> {
170        |error| Self::FileIO {
171            path: path.to_path_buf(),
172            error,
173        }
174    }
175
176    pub fn stdin(err: io::Error) -> Self {
177        Self::StdIn(err)
178    }
179}
180
181#[doc(hidden)]
182pub fn setup_logging() -> Result<(), &'static str> {
183    let stderr = io::stderr();
184    let builder = tracing_subscriber::fmt()
185        .without_time()
186        .with_writer(io::stderr)
187        .with_ansi(stderr.is_terminal());
188
189    let level = match env::var("RUST_LOG") {
190        Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
191        Err(err) => match err {
192            env::VarError::NotPresent => tracing::Level::INFO,
193            env::VarError::NotUnicode(_) => {
194                return Err("RUST_LOG is not unicode");
195            }
196        },
197    };
198
199    builder.with_max_level(level).init();
200
201    Ok(())
202}