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