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
24pub 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#[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
67pub 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 CdrRequired,
85
86 Deserialize(ParseError),
88
89 FileIO {
91 path: PathBuf,
92 error: io::Error,
93 },
94
95 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
97
98 InvalidTimezone(chrono_tz::ParseError),
100
101 PathNotFile {
103 path: PathBuf,
104 },
105
106 PathNoParentDir {
108 path: PathBuf,
109 },
110
111 Price(warning::Error<ocpi_tariffs::price::Warning>),
113
114 StdIn(io::Error),
116
117 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 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}