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
25pub 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#[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
68pub 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 CdrRequired,
86
87 Json(json::ParseError),
89
90 Explain(warning::Error<tariff::Warning>),
92
93 FileIO {
95 path: PathBuf,
96 error: io::Error,
97 },
98
99 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
101
102 InvalidTimezone(chrono_tz::ParseError),
104
105 PathNotFile {
107 path: PathBuf,
108 },
109
110 PathNoParentDir {
112 path: PathBuf,
113 },
114
115 Price(warning::Error<ocpi_tariffs::price::Warning>),
117
118 StdIn(io::Error),
120
121 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 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}