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
5mod analyze;
6mod guess_version;
7mod lint;
8mod opts;
9mod print;
10mod validate;
11
12use std::{
13 env, fmt,
14 io::{self, IsTerminal as _},
15 path::PathBuf,
16};
17
18use clap::Parser;
19use ocpi_tariffs::{price, warning, ParseError};
20
21#[doc(hidden)]
22#[derive(Parser)]
23#[command(version)]
24pub struct Opts {
25 #[clap(subcommand)]
26 command: opts::Command,
27}
28
29impl Opts {
30 pub fn run(self) -> Result<(), Error> {
31 self.command.run()
32 }
33}
34
35#[derive(Copy, Clone, Debug, clap::ValueEnum)]
36pub enum ObjectKind {
37 Cdr,
38 Tariff,
39}
40
41#[doc(hidden)]
42#[derive(Debug)]
43pub enum Error {
44 Handled,
45
46 CdrRequired,
48
49 Deserialize(ParseError),
51
52 File {
54 path: PathBuf,
55 error: io::Error,
56 },
57
58 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
60
61 InvalidTimezone(chrono_tz::ParseError),
63
64 PathNotFile {
66 path: PathBuf,
67 },
68
69 PathNoParentDir {
71 path: PathBuf,
72 },
73
74 Price(warning::Error<price::Warning>),
76
77 StdIn(io::Error),
79
80 TariffRequired,
82
83 TotalsDoNotMatch,
85}
86
87impl std::error::Error for Error {}
88
89impl fmt::Display for Error {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 Self::Handled => Ok(()),
93 Self::CdrRequired => f.write_str("`--cdr` is required when the process is a TTY"),
94 Self::Deserialize(err) => write!(f, "{err}"),
95 Self::File { path, error } => {
96 write!(f, "File error `{}`: {}", path.display(), error)
97 }
98 Self::Internal(err) => {
99 write!(f, "{err}")
100 }
101 Self::InvalidTimezone(err) => write!(f, "the timezone given is invalid; {err}"),
102 Self::PathNotFile { path } => {
103 write!(f, "The path given is not a file: `{}`", path.display())
104 }
105 Self::PathNoParentDir { path } => {
106 write!(
107 f,
108 "The path given doesn't have a parent dir: `{}`",
109 path.display()
110 )
111 }
112 Self::Price(err) => write!(f, "{err}"),
113 Self::StdIn(err) => {
114 write!(f, "Stdin error {err}")
115 }
116 Self::TariffRequired => f.write_str("`--tariff` is required when the process is a TTY"),
117 Self::TotalsDoNotMatch => {
118 f.write_str("Calculation does not match all totals in the CDR")
119 }
120 }
121 }
122}
123
124impl From<warning::Error<price::Warning>> for Error {
125 fn from(value: warning::Error<price::Warning>) -> Self {
126 Self::Price(value)
127 }
128}
129
130impl From<ParseError> for Error {
131 fn from(err: ParseError) -> Self {
132 Self::Deserialize(err)
133 }
134}
135
136impl Error {
137 pub fn file(path: PathBuf, error: io::Error) -> Self {
138 Self::File { path, error }
139 }
140
141 pub fn stdin(err: io::Error) -> Self {
142 Self::StdIn(err)
143 }
144}
145
146#[doc(hidden)]
147pub fn setup_logging() -> Result<(), &'static str> {
148 let stderr = io::stderr();
149 let builder = tracing_subscriber::fmt()
150 .without_time()
151 .with_writer(io::stderr)
152 .with_ansi(stderr.is_terminal());
153
154 let level = match env::var("RUST_LOG") {
155 Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
156 Err(err) => match err {
157 env::VarError::NotPresent => tracing::Level::INFO,
158 env::VarError::NotUnicode(_) => {
159 return Err("RUST_LOG is not unicode");
160 }
161 },
162 };
163
164 builder.with_max_level(level).init();
165
166 Ok(())
167}
168
169#[cfg(test)]
170mod test {
171 use super::Error;
172
173 #[test]
174 const fn error_should_be_send_and_sync() {
175 const fn f<T: Send + Sync>() {}
176
177 f::<Error>();
178 }
179}