Skip to main content

hyperi_rustlib/cli/
args.rs

1// Project:   hyperi-rustlib
2// File:      src/cli/args.rs
3// Purpose:   Standard CLI arguments for DFE services
4// Language:  Rust
5//
6// License:   FSL-1.1-ALv2
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! Common CLI arguments shared across all DFE services.
10//!
11//! Use `#[command(flatten)]` to embed these in your application's Clap parser:
12//!
13//! ```rust,ignore
14//! use clap::Parser;
15//! use hyperi_rustlib::cli::CommonArgs;
16//!
17//! #[derive(Parser)]
18//! struct App {
19//!     #[command(flatten)]
20//!     common: CommonArgs,
21//! }
22//! ```
23
24/// Standard CLI arguments for DFE services.
25///
26/// Provides the 80% of flags that every service needs:
27/// config path, log level/format, metrics address, verbose/quiet modes.
28///
29/// Embed in your Clap parser with `#[command(flatten)]`.
30#[derive(Debug, Clone, clap::Args)]
31pub struct CommonArgs {
32    /// Path to configuration file.
33    #[arg(short = 'c', long = "config")]
34    pub config: Option<String>,
35
36    /// Log level (trace, debug, info, warn, error).
37    #[arg(
38        short = 'l',
39        long = "log-level",
40        env = "LOG_LEVEL",
41        default_value = "info"
42    )]
43    pub log_level: String,
44
45    /// Log output format (json, text, auto).
46    #[arg(long = "log-format", env = "LOG_FORMAT", default_value = "auto")]
47    pub log_format: String,
48
49    /// Metrics server bind address.
50    #[arg(
51        long = "metrics-addr",
52        env = "METRICS_ADDR",
53        default_value = "0.0.0.0:9090"
54    )]
55    pub metrics_addr: String,
56
57    /// Enable verbose output (sets log level to debug).
58    #[arg(short = 'v', long, conflicts_with = "quiet")]
59    pub verbose: bool,
60
61    /// Suppress all output except errors.
62    #[arg(short = 'q', long, conflicts_with = "verbose")]
63    pub quiet: bool,
64}
65
66impl CommonArgs {
67    /// Resolve the effective log level, accounting for --verbose and --quiet flags.
68    #[must_use]
69    pub fn effective_log_level(&self) -> &str {
70        if self.verbose {
71            "debug"
72        } else if self.quiet {
73            "error"
74        } else {
75            &self.log_level
76        }
77    }
78
79    /// Convert to `LoggerOptions` for use with `logger::setup()`.
80    ///
81    /// Parses the log level and format strings into their typed equivalents.
82    ///
83    /// # Errors
84    ///
85    /// Returns `CliError::InvalidArgument` if the log level or format is invalid.
86    #[cfg(feature = "logger")]
87    pub fn to_logger_options(&self) -> Result<crate::logger::LoggerOptions, super::CliError> {
88        use std::str::FromStr;
89
90        let level: tracing::Level =
91            self.effective_log_level()
92                .to_uppercase()
93                .parse()
94                .map_err(|_| {
95                    super::CliError::InvalidArgument(format!(
96                        "invalid log level: {}",
97                        self.effective_log_level()
98                    ))
99                })?;
100
101        let format = crate::logger::LogFormat::from_str(&self.log_format)
102            .map_err(|e| super::CliError::InvalidArgument(format!("invalid log format: {e}")))?;
103
104        Ok(crate::logger::LoggerOptions {
105            level,
106            format,
107            ..Default::default()
108        })
109    }
110
111    /// Convert to `ConfigOptions` for use with `config::setup()`.
112    #[cfg(feature = "config")]
113    #[must_use]
114    pub fn to_config_options(&self, env_prefix: &str) -> crate::config::ConfigOptions {
115        let mut opts = crate::config::ConfigOptions {
116            env_prefix: env_prefix.to_string(),
117            ..Default::default()
118        };
119        if let Some(ref path) = self.config {
120            opts.config_paths.push(path.into());
121        }
122        opts
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_effective_log_level_default() {
132        let args = CommonArgs {
133            config: None,
134            log_level: "info".to_string(),
135            log_format: "auto".to_string(),
136            metrics_addr: "0.0.0.0:9090".to_string(),
137            verbose: false,
138            quiet: false,
139        };
140        assert_eq!(args.effective_log_level(), "info");
141    }
142
143    #[test]
144    fn test_effective_log_level_verbose() {
145        let args = CommonArgs {
146            config: None,
147            log_level: "info".to_string(),
148            log_format: "auto".to_string(),
149            metrics_addr: "0.0.0.0:9090".to_string(),
150            verbose: true,
151            quiet: false,
152        };
153        assert_eq!(args.effective_log_level(), "debug");
154    }
155
156    #[test]
157    fn test_effective_log_level_quiet() {
158        let args = CommonArgs {
159            config: None,
160            log_level: "info".to_string(),
161            log_format: "auto".to_string(),
162            metrics_addr: "0.0.0.0:9090".to_string(),
163            verbose: false,
164            quiet: true,
165        };
166        assert_eq!(args.effective_log_level(), "error");
167    }
168
169    #[test]
170    fn test_effective_log_level_custom() {
171        let args = CommonArgs {
172            config: None,
173            log_level: "warn".to_string(),
174            log_format: "auto".to_string(),
175            metrics_addr: "0.0.0.0:9090".to_string(),
176            verbose: false,
177            quiet: false,
178        };
179        assert_eq!(args.effective_log_level(), "warn");
180    }
181}