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