adaptive_pipeline_bootstrap/
config.rs1use std::path::PathBuf;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum LogLevel {
47 Error,
49 Warn,
51 #[default]
53 Info,
54 Debug,
56 Trace,
58}
59
60impl LogLevel {
61 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#[derive(Debug, Clone)]
77pub struct AppConfig {
78 app_name: String,
80
81 log_level: LogLevel,
83
84 input_path: Option<PathBuf>,
86
87 output_path: Option<PathBuf>,
89
90 worker_threads: Option<usize>,
92
93 verbose: bool,
95
96 dry_run: bool,
98}
99
100impl AppConfig {
101 pub fn builder() -> AppConfigBuilder {
103 AppConfigBuilder::default()
104 }
105
106 pub fn app_name(&self) -> &str {
108 &self.app_name
109 }
110
111 pub fn log_level(&self) -> LogLevel {
113 self.log_level
114 }
115
116 pub fn input_path(&self) -> Option<&PathBuf> {
118 self.input_path.as_ref()
119 }
120
121 pub fn output_path(&self) -> Option<&PathBuf> {
123 self.output_path.as_ref()
124 }
125
126 pub fn worker_threads(&self) -> Option<usize> {
128 self.worker_threads
129 }
130
131 pub fn is_verbose(&self) -> bool {
133 self.verbose
134 }
135
136 pub fn is_dry_run(&self) -> bool {
138 self.dry_run
139 }
140}
141
142#[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 pub fn app_name(mut self, name: impl Into<String>) -> Self {
157 self.app_name = Some(name.into());
158 self
159 }
160
161 pub fn log_level(mut self, level: LogLevel) -> Self {
163 self.log_level = Some(level);
164 self
165 }
166
167 pub fn input_path(mut self, path: impl Into<PathBuf>) -> Self {
169 self.input_path = Some(path.into());
170 self
171 }
172
173 pub fn output_path(mut self, path: impl Into<PathBuf>) -> Self {
175 self.output_path = Some(path.into());
176 self
177 }
178
179 pub fn worker_threads(mut self, count: usize) -> Self {
181 self.worker_threads = Some(count);
182 self
183 }
184
185 pub fn verbose(mut self, enabled: bool) -> Self {
187 self.verbose = enabled;
188 self
189 }
190
191 pub fn dry_run(mut self, enabled: bool) -> Self {
193 self.dry_run = enabled;
194 self
195 }
196
197 #[allow(clippy::expect_used)] 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 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); 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}