Skip to main content

atb_cli_utils/
lib.rs

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    /// Executing Environment
17    #[arg(short, long, env = "ATB_CLI_ENV", default_value = "dev")]
18    pub env: Environment,
19
20    /// Activate debug mode
21    #[arg(short, long, env = "ATB_CLI_DEBUG")]
22    pub debug: bool,
23}
24
25pub trait AtbCli: Sized {
26    /// Executable file name.
27    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    /// Application name.
36    fn name() -> String {
37        "unknown".to_owned()
38    }
39
40    /// Application version.
41    fn version() -> String {
42        "unknown".to_owned()
43    }
44
45    /// Application authors.
46    fn authors() -> Vec<String> {
47        vec!["unknown".to_owned()]
48    }
49    /// Application description.
50    fn description() -> String {
51        "unknown".to_owned()
52    }
53
54    /// Application repository.
55    fn repository() -> String {
56        "unknown".to_owned()
57    }
58
59    /// Returns implementation details.  
60    fn impl_version() -> String;
61
62    /// Returns git commit hash.
63    fn commit() -> String {
64        "unknown".to_owned()
65    }
66
67    /// Returns git branch.
68    fn branch() -> String {
69        "unknown".to_owned()
70    }
71
72    /// Returns OS platform.
73    fn platform() -> String {
74        "unknown".to_owned()
75    }
76
77    /// Returns rustc version.
78    fn rustc_info() -> String {
79        "unknown".to_owned()
80    }
81
82    /// Returns the client ID: `{name}/v{version}`
83    fn client_id() -> String {
84        format!("{}/v{}", Self::name(), Self::impl_version())
85    }
86
87    /// Optional hook to expose the embedded `BaseCli` from the parsed value.
88    /// If implemented, `parse()`/`from_iter()` will automatically call `set_globals`
89    /// using the returned `BaseCli`. Default is `None` for backward compatibility.
90    fn globals_from(&self) -> Option<&BaseCli> {
91        None
92    }
93
94    /// Helper function used to parse the command line arguments
95    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        // Idempotent: ignore if already initialized
104        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    /// Helper function used to parse the command line arguments. This is the equivalent of
119    /// [`clap::Parser::parse_from`].
120    ///
121    /// To allow running the command without subcommand, it also sets a few more settings:
122    /// [`clap::Command::propagate_version`], [`clap::Command::args_conflicts_with_subcommands`],
123    /// [`clap::Command::subcommand_negates_reqs`].
124    ///
125    /// Creates `Self` from any iterator over arguments.
126    /// Print the error message and quit the program in case of failure.
127    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}