#![allow(clippy::print_stderr, reason = "The CLI is allowed to use stderr")]
#![allow(clippy::print_stdout, reason = "The CLI is allowed to use stdout")]
#![cfg_attr(doc, doc = include_str!("../README.md"))]
#[cfg(test)]
mod test;
mod guess_version;
mod lint;
mod opts;
mod price;
mod print;
use std::{
env, fmt, fs,
io::{self, IsTerminal as _, Read as _},
path::{Path, PathBuf},
};
use clap::Parser;
use ocpi_tariffs::{warning, ParseError};
use tracing::{debug, instrument};
pub const DEFAULT_STDIO_BUF_SIZE: usize = 1024;
#[doc(hidden)]
#[derive(Parser)]
#[command(version)]
pub struct Opts {
#[clap(subcommand)]
command: opts::Command,
}
impl Opts {
pub fn run(self) -> Result<(), Error> {
self.command.run()
}
}
#[derive(Copy, Clone, clap::ValueEnum)]
pub enum ObjectKind {
Cdr,
Tariff,
}
impl fmt::Debug for ObjectKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cdr => write!(f, "CDR"),
Self::Tariff => write!(f, "Tariff"),
}
}
}
#[instrument]
pub fn load_object_file(path: &Path, object_kind: ObjectKind) -> Result<String, Error> {
debug!("Loading {object_kind} from file");
let mut content = fs::read_to_string(path).map_err(Error::then_file_io(path))?;
json_strip_comments::strip(&mut content).map_err(Error::then_file_io(path))?;
debug!(bytes_read = content.len(), "{object_kind} read from file");
Ok(content)
}
pub fn load_object_from_stdin(object_kind: ObjectKind) -> Result<String, Error> {
debug!("Loading {object_kind} from stdin");
let mut stdin = io::stdin().lock();
let mut content = String::with_capacity(DEFAULT_STDIO_BUF_SIZE);
let bytes_read = stdin.read_to_string(&mut content).map_err(Error::stdin)?;
debug!(bytes_read, "{object_kind} read from stdin");
json_strip_comments::strip(&mut content).map_err(Error::stdin)?;
Ok(content)
}
#[doc(hidden)]
#[derive(Debug)]
pub enum Error {
Handled,
CdrRequired,
Deserialize(ParseError),
FileIO {
path: PathBuf,
error: io::Error,
},
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
InvalidTimezone(chrono_tz::ParseError),
PathNotFile {
path: PathBuf,
},
PathNoParentDir {
path: PathBuf,
},
Price(warning::Error<ocpi_tariffs::price::Warning>),
StdIn(io::Error),
TariffRequired,
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Handled => Ok(()),
Self::CdrRequired => f.write_str("`--cdr` is required when the process is a TTY"),
Self::Deserialize(err) => write!(f, "{err}"),
Self::FileIO { path, error } => {
write!(f, "File error `{}`: {}", path.display(), error)
}
Self::Internal(err) => {
write!(f, "{err}")
}
Self::InvalidTimezone(err) => write!(f, "the timezone given is invalid; {err}"),
Self::PathNotFile { path } => {
write!(f, "The path given is not a file: `{}`", path.display())
}
Self::PathNoParentDir { path } => {
write!(
f,
"The path given doesn't have a parent dir: `{}`",
path.display()
)
}
Self::Price(err) => write!(f, "{err}"),
Self::StdIn(err) => {
write!(f, "Stdin error {err}")
}
Self::TariffRequired => f.write_str("`--tariff` is required when the process is a TTY"),
}
}
}
impl From<warning::Error<ocpi_tariffs::price::Warning>> for Error {
fn from(value: warning::Error<ocpi_tariffs::price::Warning>) -> Self {
Self::Price(value)
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Self {
Self::Deserialize(err)
}
}
impl Error {
pub fn then_file_io(path: &Path) -> impl FnOnce(io::Error) -> Error + use<'_> {
|error| Self::FileIO {
path: path.to_path_buf(),
error,
}
}
pub fn stdin(err: io::Error) -> Self {
Self::StdIn(err)
}
}
#[doc(hidden)]
pub fn setup_logging() -> Result<(), &'static str> {
let stderr = io::stderr();
let builder = tracing_subscriber::fmt()
.without_time()
.with_writer(io::stderr)
.with_ansi(stderr.is_terminal());
let level = match env::var("RUST_LOG") {
Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
Err(err) => match err {
env::VarError::NotPresent => tracing::Level::INFO,
env::VarError::NotUnicode(_) => {
return Err("RUST_LOG is not unicode");
}
},
};
builder.with_max_level(level).init();
Ok(())
}