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, 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 CdrRequired,
46
47 Deserialize(ParseError),
49
50 File { path: PathBuf, error: io::Error },
52
53 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
55
56 InvalidTimezone(chrono_tz::ParseError),
58
59 Lint(ocpi_tariffs::lint::Error),
61
62 NoTimezone,
64
65 PathNotFile { path: PathBuf },
67
68 PathNoParentDir { path: PathBuf },
70
71 Price(price::Error),
73
74 StdIn(io::Error),
76
77 TariffRequired,
79
80 TotalsDoNotMatch,
82}
83
84impl std::error::Error for Error {}
85
86impl fmt::Display for Error {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::CdrRequired => f.write_str("`--cdr` is required when the process is a TTY"),
90 Self::Deserialize(err) => write!(f, "{err}"),
91 Self::File { path, error } => {
92 write!(f, "File error `{}`: {}", path.display(), error)
93 }
94 Self::Internal(err) => {
95 write!(f, "{err}")
96 }
97 Self::InvalidTimezone(err) => write!(f, "the timezone given is invalid; {err}"),
98 Self::Lint(err) => write!(f, "Lint error {err}"),
99 Self::NoTimezone => write!(f, "A timezone could not be found or inferred."),
100 Self::PathNotFile { path } => {
101 write!(f, "The path given is not a file: `{}`", path.display())
102 }
103 Self::PathNoParentDir { path } => {
104 write!(
105 f,
106 "The path given doesn't have a parent dir: `{}`",
107 path.display()
108 )
109 }
110 Self::Price(err) => write!(f, "{err}"),
111 Self::StdIn(err) => {
112 write!(f, "Stdin error {err}")
113 }
114 Self::TariffRequired => f.write_str("`--tariff` is required when the process is a TTY"),
115 Self::TotalsDoNotMatch => {
116 f.write_str("Calculation does not match all totals in the CDR")
117 }
118 }
119 }
120}
121
122impl From<price::Error> for Error {
123 fn from(err: price::Error) -> Self {
124 Self::Price(err)
125 }
126}
127
128impl From<ocpi_tariffs::lint::Error> for Error {
129 fn from(err: ocpi_tariffs::lint::Error) -> Self {
130 Self::Lint(err)
131 }
132}
133
134impl From<ParseError> for Error {
135 fn from(err: ParseError) -> Self {
136 Self::Deserialize(err)
137 }
138}
139
140impl Error {
141 pub fn file(path: PathBuf, error: io::Error) -> Self {
142 Self::File { path, error }
143 }
144
145 pub fn stdin(err: io::Error) -> Self {
146 Self::StdIn(err)
147 }
148}
149
150#[doc(hidden)]
151pub fn setup_logging() -> Result<(), &'static str> {
152 let stderr = io::stderr();
153 let builder = tracing_subscriber::fmt()
154 .without_time()
155 .with_writer(io::stderr)
156 .with_ansi(stderr.is_terminal());
157
158 let level = match env::var("RUST_LOG") {
159 Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
160 Err(err) => match err {
161 env::VarError::NotPresent => tracing::Level::INFO,
162 env::VarError::NotUnicode(_) => {
163 return Err("RUST_LOG is not unicode");
164 }
165 },
166 };
167
168 builder.with_max_level(level).init();
169
170 Ok(())
171}
172
173#[cfg(test)]
174mod test {
175 use super::Error;
176
177 #[test]
178 const fn error_should_be_send_and_sync() {
179 const fn f<T: Send + Sync>() {}
180
181 f::<Error>();
182 }
183}