adaptive_pipeline_bootstrap/
config.rs

1// /////////////////////////////////////////////////////////////////////////////
2// Adaptive Pipeline
3// Copyright (c) 2025 Michael Gardner, A Bit of Help, Inc.
4// SPDX-License-Identifier: BSD-3-Clause
5// See LICENSE file in the project root.
6// /////////////////////////////////////////////////////////////////////////////
7
8//! # Application Configuration
9//!
10//! Bootstrap-phase configuration structure.
11//!
12//! ## Design Philosophy
13//!
14//! `AppConfig` holds **validated** configuration after:
15//! 1. Command-line argument parsing
16//! 2. Security validation
17//! 3. Environment variable resolution
18//! 4. Default value application
19//!
20//! ## Immutability
21//!
22//! All configuration is **immutable** after creation. This ensures:
23//! - Thread safety (no synchronization needed)
24//! - Predictable behavior
25//! - Safe sharing across async tasks
26//!
27//! ## Usage
28//!
29//! ```rust
30//! use adaptive_pipeline_bootstrap::config::{AppConfig, LogLevel};
31//! use std::path::PathBuf;
32//!
33//! let config = AppConfig::builder()
34//!     .app_name("my-app")
35//!     .log_level(LogLevel::Info)
36//!     .input_path(PathBuf::from("/path/to/input"))
37//!     .build();
38//!
39//! println!("Running: {}", config.app_name());
40//! ```
41
42use std::path::PathBuf;
43
44/// Log level configuration
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum LogLevel {
47    /// Error messages only
48    Error,
49    /// Warnings and errors
50    Warn,
51    /// Info, warnings, and errors (default)
52    #[default]
53    Info,
54    /// All messages including debug
55    Debug,
56    /// All messages including trace
57    Trace,
58}
59
60impl LogLevel {
61    /// Convert to tracing Level
62    pub fn to_tracing_level(&self) -> tracing::Level {
63        match self {
64            LogLevel::Error => tracing::Level::ERROR,
65            LogLevel::Warn => tracing::Level::WARN,
66            LogLevel::Info => tracing::Level::INFO,
67            LogLevel::Debug => tracing::Level::DEBUG,
68            LogLevel::Trace => tracing::Level::TRACE,
69        }
70    }
71}
72
73/// Application configuration
74///
75/// Immutable configuration structure holding all bootstrap-phase settings.
76#[derive(Debug, Clone)]
77pub struct AppConfig {
78    /// Application name
79    app_name: String,
80
81    /// Log level
82    log_level: LogLevel,
83
84    /// Input file or directory path
85    input_path: Option<PathBuf>,
86
87    /// Output file or directory path
88    output_path: Option<PathBuf>,
89
90    /// Number of worker threads (None = automatic)
91    worker_threads: Option<usize>,
92
93    /// Enable verbose output
94    verbose: bool,
95
96    /// Dry run mode (no actual changes)
97    dry_run: bool,
98}
99
100impl AppConfig {
101    /// Create a new configuration builder
102    pub fn builder() -> AppConfigBuilder {
103        AppConfigBuilder::default()
104    }
105
106    /// Get application name
107    pub fn app_name(&self) -> &str {
108        &self.app_name
109    }
110
111    /// Get log level
112    pub fn log_level(&self) -> LogLevel {
113        self.log_level
114    }
115
116    /// Get input path
117    pub fn input_path(&self) -> Option<&PathBuf> {
118        self.input_path.as_ref()
119    }
120
121    /// Get output path
122    pub fn output_path(&self) -> Option<&PathBuf> {
123        self.output_path.as_ref()
124    }
125
126    /// Get worker thread count
127    pub fn worker_threads(&self) -> Option<usize> {
128        self.worker_threads
129    }
130
131    /// Check if verbose mode is enabled
132    pub fn is_verbose(&self) -> bool {
133        self.verbose
134    }
135
136    /// Check if dry run mode is enabled
137    pub fn is_dry_run(&self) -> bool {
138        self.dry_run
139    }
140}
141
142/// Builder for AppConfig
143#[derive(Debug, Default)]
144pub struct AppConfigBuilder {
145    app_name: Option<String>,
146    log_level: Option<LogLevel>,
147    input_path: Option<PathBuf>,
148    output_path: Option<PathBuf>,
149    worker_threads: Option<usize>,
150    verbose: bool,
151    dry_run: bool,
152}
153
154impl AppConfigBuilder {
155    /// Set application name
156    pub fn app_name(mut self, name: impl Into<String>) -> Self {
157        self.app_name = Some(name.into());
158        self
159    }
160
161    /// Set log level
162    pub fn log_level(mut self, level: LogLevel) -> Self {
163        self.log_level = Some(level);
164        self
165    }
166
167    /// Set input path
168    pub fn input_path(mut self, path: impl Into<PathBuf>) -> Self {
169        self.input_path = Some(path.into());
170        self
171    }
172
173    /// Set output path
174    pub fn output_path(mut self, path: impl Into<PathBuf>) -> Self {
175        self.output_path = Some(path.into());
176        self
177    }
178
179    /// Set worker thread count
180    pub fn worker_threads(mut self, count: usize) -> Self {
181        self.worker_threads = Some(count);
182        self
183    }
184
185    /// Enable verbose mode
186    pub fn verbose(mut self, enabled: bool) -> Self {
187        self.verbose = enabled;
188        self
189    }
190
191    /// Enable dry run mode
192    pub fn dry_run(mut self, enabled: bool) -> Self {
193        self.dry_run = enabled;
194        self
195    }
196
197    /// Build the configuration
198    ///
199    /// # Panics
200    ///
201    /// Panics if app_name was not set
202    ///
203    /// # Note
204    ///
205    /// This method is intended for test code and examples where panicking on
206    /// misconfiguration is acceptable. Production code should use `try_build()`
207    /// instead for proper error handling.
208    #[allow(clippy::expect_used)] // Builder pattern convention: panics are documented
209    pub fn build(self) -> AppConfig {
210        AppConfig {
211            app_name: self.app_name.expect("app_name is required"),
212            log_level: self.log_level.unwrap_or_default(),
213            input_path: self.input_path,
214            output_path: self.output_path,
215            worker_threads: self.worker_threads,
216            verbose: self.verbose,
217            dry_run: self.dry_run,
218        }
219    }
220
221    /// Try to build the configuration
222    ///
223    /// Returns Err if required fields are missing
224    pub fn try_build(self) -> Result<AppConfig, String> {
225        Ok(AppConfig {
226            app_name: self.app_name.ok_or("app_name is required")?,
227            log_level: self.log_level.unwrap_or_default(),
228            input_path: self.input_path,
229            output_path: self.output_path,
230            worker_threads: self.worker_threads,
231            verbose: self.verbose,
232            dry_run: self.dry_run,
233        })
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    mod builder {
242        use super::*;
243
244        #[test]
245        fn builds_minimal_config() {
246            let config = AppConfig::builder().app_name("test-app").build();
247
248            assert_eq!(config.app_name(), "test-app");
249            assert_eq!(config.log_level(), LogLevel::Info); // default
250            assert!(config.input_path().is_none());
251            assert!(config.output_path().is_none());
252            assert!(config.worker_threads().is_none());
253            assert!(!config.is_verbose());
254            assert!(!config.is_dry_run());
255        }
256
257        #[test]
258        fn builds_full_config() {
259            let config = AppConfig::builder()
260                .app_name("full-app")
261                .log_level(LogLevel::Debug)
262                .input_path("/input")
263                .output_path("/output")
264                .worker_threads(8)
265                .verbose(true)
266                .dry_run(true)
267                .build();
268
269            assert_eq!(config.app_name(), "full-app");
270            assert_eq!(config.log_level(), LogLevel::Debug);
271            assert_eq!(config.input_path(), Some(&PathBuf::from("/input")));
272            assert_eq!(config.output_path(), Some(&PathBuf::from("/output")));
273            assert_eq!(config.worker_threads(), Some(8));
274            assert!(config.is_verbose());
275            assert!(config.is_dry_run());
276        }
277
278        #[test]
279        #[should_panic(expected = "app_name is required")]
280        fn panics_on_missing_app_name() {
281            AppConfig::builder().build();
282        }
283
284        #[test]
285        fn try_build_succeeds_with_required_fields() {
286            let result = AppConfig::builder().app_name("test").try_build();
287
288            assert!(result.is_ok());
289        }
290
291        #[test]
292        fn try_build_fails_on_missing_required_fields() {
293            let result = AppConfig::builder().try_build();
294
295            assert!(result.is_err());
296            assert_eq!(result.unwrap_err(), "app_name is required");
297        }
298    }
299
300    mod log_level {
301        use super::*;
302
303        #[test]
304        fn defaults_to_info() {
305            assert_eq!(LogLevel::default(), LogLevel::Info);
306        }
307
308        #[test]
309        fn converts_to_tracing_levels() {
310            assert_eq!(LogLevel::Error.to_tracing_level(), tracing::Level::ERROR);
311            assert_eq!(LogLevel::Warn.to_tracing_level(), tracing::Level::WARN);
312            assert_eq!(LogLevel::Info.to_tracing_level(), tracing::Level::INFO);
313            assert_eq!(LogLevel::Debug.to_tracing_level(), tracing::Level::DEBUG);
314            assert_eq!(LogLevel::Trace.to_tracing_level(), tracing::Level::TRACE);
315        }
316    }
317
318    mod app_config {
319        use super::*;
320
321        #[test]
322        fn clones_correctly() {
323            let config1 = AppConfig::builder()
324                .app_name("clone-test")
325                .log_level(LogLevel::Debug)
326                .build();
327
328            let config2 = config1.clone();
329
330            assert_eq!(config1.app_name(), config2.app_name());
331            assert_eq!(config1.log_level(), config2.log_level());
332        }
333    }
334}