1pub use clap;
2pub use once_cell;
3
4use clap::{CommandFactory, FromArgMatches, Parser};
5use once_cell::sync::OnceCell;
6use serde::Serialize;
7use strum_macros::{AsRefStr, Display, EnumString};
8
9pub type DateTime = chrono::DateTime<chrono::Utc>;
10
11static PROCESS_INFO: OnceCell<ProcessInfo> = OnceCell::new();
12static DEBUG: OnceCell<bool> = OnceCell::new();
13
14#[derive(Debug, Parser)]
15pub struct BaseCli {
16 #[arg(short, long, env = "ATB_CLI_ENV", default_value = "dev")]
18 pub env: Environment,
19
20 #[arg(short, long, env = "ATB_CLI_DEBUG")]
22 pub debug: bool,
23}
24
25pub trait AtbCli: Sized {
26 fn executable_name() -> String {
28 std::env::current_exe()
29 .ok()
30 .and_then(|e| e.file_name().map(|s| s.to_os_string()))
31 .and_then(|w| w.into_string().ok())
32 .unwrap_or_else(|| Self::name())
33 }
34
35 fn name() -> String {
37 "unknown".to_owned()
38 }
39
40 fn version() -> String {
42 "unknown".to_owned()
43 }
44
45 fn authors() -> Vec<String> {
47 vec!["unknown".to_owned()]
48 }
49 fn description() -> String {
51 "unknown".to_owned()
52 }
53
54 fn repository() -> String {
56 "unknown".to_owned()
57 }
58
59 fn impl_version() -> String;
61
62 fn commit() -> String {
64 "unknown".to_owned()
65 }
66
67 fn branch() -> String {
69 "unknown".to_owned()
70 }
71
72 fn platform() -> String {
74 "unknown".to_owned()
75 }
76
77 fn rustc_info() -> String {
79 "unknown".to_owned()
80 }
81
82 fn client_id() -> String {
84 format!("{}/v{}", Self::name(), Self::impl_version())
85 }
86
87 fn globals_from(&self) -> Option<&BaseCli> {
91 None
92 }
93
94 fn parse() -> Self
96 where
97 Self: Parser + Sized,
98 {
99 <Self as AtbCli>::from_iter(std::env::args_os())
100 }
101
102 fn set_globals(base: &BaseCli) {
103 let _ = PROCESS_INFO.set(ProcessInfo {
105 name: Self::name(),
106 version: Self::version(),
107 branch: Self::branch(),
108 commit: Self::commit(),
109 platform: Self::platform(),
110 rustc: Self::rustc_info(),
111 start_time: chrono::Utc::now(),
112 environment: base.env.clone(),
113 });
114
115 let _ = DEBUG.set(base.debug);
116 }
117
118 fn from_iter<I>(iter: I) -> Self
128 where
129 Self: Parser + Sized,
130 I: IntoIterator,
131 I::Item: Into<std::ffi::OsString> + Clone,
132 {
133 let app = <Self as CommandFactory>::command();
134
135 let mut full_version = Self::impl_version();
136 full_version.push('\n');
137
138 let name = Self::executable_name();
139 let authors = ["authors [", &Self::authors().join(","), "]"].concat();
140 let about = Self::description();
141 let app = app
142 .name(name)
143 .author(authors)
144 .about(about)
145 .version(full_version)
146 .propagate_version(true)
147 .args_conflicts_with_subcommands(true)
148 .subcommand_negates_reqs(true);
149
150 let matches = app.try_get_matches_from(iter).unwrap_or_else(|e| e.exit());
151
152 let parsed: Self =
153 <Self as FromArgMatches>::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
154
155 if let Some(base) = parsed.globals_from() {
156 <Self as AtbCli>::set_globals(base);
157 }
158
159 parsed
160 }
161}
162
163pub fn process_info() -> &'static ProcessInfo {
164 PROCESS_INFO
165 .get()
166 .expect("static PROCESS_INFO has not been set.")
167}
168
169pub fn debug() -> bool {
170 *DEBUG.get().expect("static DEBUG has not been set.")
171}
172
173#[derive(Debug, Serialize)]
174#[serde(rename_all = "camelCase")]
175pub struct ProcessInfo {
176 environment: Environment,
177 name: String,
178 version: String,
179 branch: String,
180 commit: String,
181 platform: String,
182 rustc: String,
183 start_time: DateTime,
184}
185
186impl ProcessInfo {
187 pub fn env(&self) -> &Environment {
188 &self.environment
189 }
190}
191
192#[derive(Clone, Debug, PartialEq, Serialize, EnumString, Display, AsRefStr)]
193pub enum Environment {
194 #[strum(serialize = "prod", serialize = "production")]
195 Production,
196 #[strum(serialize = "dev", serialize = "develop")]
197 Develop,
198 #[strum(serialize = "stag", serialize = "staging")]
199 Staging,
200}
201
202impl Environment {
203 pub fn prod(&self) -> bool {
204 matches!(self, Environment::Production)
205 }
206
207 pub fn dev(&self) -> bool {
208 matches!(self, Environment::Develop)
209 }
210
211 pub fn staging(&self) -> bool {
212 matches!(self, Environment::Staging)
213 }
214}