1use crate::error::{Error, Result};
10use crate::models::TestOutcome;
11use serde::Deserialize;
12use std::path::{Path, PathBuf};
13
14const DEFAULT_CONFIG_DIR: &str = ".develocity";
16const DEFAULT_CONFIG_FILE: &str = "config.toml";
17
18#[derive(Debug, Default, Deserialize)]
20#[serde(default)]
21pub struct ConfigFile {
22 pub server: Option<String>,
24 pub token: Option<String>,
26 pub output_format: Option<String>,
28 pub verbose: Option<bool>,
30 pub timeout: Option<u64>,
32}
33
34impl ConfigFile {
35 pub fn load_default() -> Result<Self> {
37 let path = Self::default_config_path();
38 if path.exists() {
39 Self::load(&path)
40 } else {
41 Ok(Self::default())
42 }
43 }
44
45 pub fn load(path: &Path) -> Result<Self> {
47 let content = std::fs::read_to_string(path).map_err(|e| Error::ConfigRead {
48 path: path.display().to_string(),
49 source: e,
50 })?;
51
52 toml::from_str(&content).map_err(|e| Error::ConfigParse {
53 path: path.display().to_string(),
54 source: e,
55 })
56 }
57
58 pub fn default_config_path() -> PathBuf {
60 dirs::home_dir()
61 .unwrap_or_else(|| PathBuf::from("."))
62 .join(DEFAULT_CONFIG_DIR)
63 .join(DEFAULT_CONFIG_FILE)
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct Config {
70 pub server: String,
72 pub token: String,
74 pub output_format: OutputFormat,
76 pub include: IncludeOptions,
78 pub verbose: bool,
80 pub timeout: u64,
82 pub test_outcomes: Vec<TestOutcome>,
84}
85
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
88pub enum OutputFormat {
89 Json,
91 #[default]
93 Human,
94}
95
96impl std::str::FromStr for OutputFormat {
97 type Err = String;
98
99 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
100 match s.to_lowercase().as_str() {
101 "json" => Ok(OutputFormat::Json),
102 "human" => Ok(OutputFormat::Human),
103 other => Err(format!(
104 "Invalid output format '{}'. Valid options: json, human",
105 other
106 )),
107 }
108 }
109}
110
111impl std::fmt::Display for OutputFormat {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 OutputFormat::Json => write!(f, "json"),
115 OutputFormat::Human => write!(f, "human"),
116 }
117 }
118}
119
120#[derive(Debug, Clone, Default)]
122pub struct IncludeOptions {
123 pub result: bool,
125 pub deprecations: bool,
127 pub failures: bool,
129 pub tests: bool,
131 pub task_execution: bool,
133 pub network_activity: bool,
135 pub dependencies: bool,
137}
138
139impl IncludeOptions {
140 pub fn all() -> Self {
142 Self {
143 result: true,
144 deprecations: true,
145 failures: true,
146 tests: true,
147 task_execution: true,
148 network_activity: true,
149 dependencies: true,
150 }
151 }
152
153 pub fn parse(s: &str) -> std::result::Result<Self, String> {
155 let s = s.trim().to_lowercase();
156
157 if s == "all" {
158 return Ok(Self::all());
159 }
160
161 let mut opts = Self::default();
162
163 for part in s.split(',') {
164 match part.trim() {
165 "result" => opts.result = true,
166 "deprecations" => opts.deprecations = true,
167 "failures" => opts.failures = true,
168 "tests" => opts.tests = true,
169 "task-execution" => opts.task_execution = true,
170 "network-activity" => opts.network_activity = true,
171 "dependencies" => opts.dependencies = true,
172 "all" => return Ok(Self::all()),
173 other if !other.is_empty() => {
174 return Err(format!(
175 "Invalid include option '{}'. Valid options: result, deprecations, failures, tests, task-execution, network-activity, dependencies, all",
176 other
177 ));
178 }
179 _ => {}
180 }
181 }
182
183 if !opts.result
185 && !opts.deprecations
186 && !opts.failures
187 && !opts.tests
188 && !opts.task_execution
189 && !opts.network_activity
190 && !opts.dependencies
191 {
192 return Ok(Self::all());
193 }
194
195 Ok(opts)
196 }
197
198 pub fn any(&self) -> bool {
200 self.result
201 || self.deprecations
202 || self.failures
203 || self.tests
204 || self.task_execution
205 || self.network_activity
206 || self.dependencies
207 }
208}
209
210#[derive(Debug, Default)]
212pub struct ConfigBuilder {
213 server: Option<String>,
214 token: Option<String>,
215 output_format: Option<OutputFormat>,
216 include: Option<IncludeOptions>,
217 verbose: Option<bool>,
218 timeout: Option<u64>,
219 config_file_path: Option<PathBuf>,
220 test_outcomes: Vec<TestOutcome>,
221}
222
223impl ConfigBuilder {
224 pub fn new() -> Self {
226 Self::default()
227 }
228
229 pub fn server(mut self, server: Option<String>) -> Self {
231 if server.is_some() {
232 self.server = server;
233 }
234 self
235 }
236
237 pub fn token(mut self, token: Option<String>) -> Self {
239 if token.is_some() {
240 self.token = token;
241 }
242 self
243 }
244
245 pub fn output_format(mut self, format: Option<OutputFormat>) -> Self {
247 if format.is_some() {
248 self.output_format = format;
249 }
250 self
251 }
252
253 pub fn include(mut self, include: Option<IncludeOptions>) -> Self {
255 if include.is_some() {
256 self.include = include;
257 }
258 self
259 }
260
261 pub fn verbose(mut self, verbose: bool) -> Self {
263 if verbose {
264 self.verbose = Some(true);
265 }
266 self
267 }
268
269 pub fn timeout(mut self, timeout: Option<u64>) -> Self {
271 if timeout.is_some() {
272 self.timeout = timeout;
273 }
274 self
275 }
276
277 pub fn config_file(mut self, path: Option<PathBuf>) -> Self {
279 self.config_file_path = path;
280 self
281 }
282
283 pub fn test_outcomes(mut self, outcomes: Vec<TestOutcome>) -> Self {
285 self.test_outcomes = outcomes;
286 self
287 }
288
289 pub fn build(self) -> Result<Config> {
291 let config_file = match &self.config_file_path {
293 Some(path) => ConfigFile::load(path)?,
294 None => ConfigFile::load_default()?,
295 };
296
297 let server = self
299 .server
300 .or_else(|| std::env::var("DEVELOCITY_SERVER").ok())
301 .or(config_file.server)
302 .ok_or(Error::MissingServer)?;
303
304 let token = self
306 .token
307 .or_else(|| std::env::var("DEVELOCITY_API_KEY").ok())
308 .or(config_file.token)
309 .ok_or(Error::MissingToken)?;
310
311 let output_format = self.output_format.unwrap_or_else(|| {
313 config_file
314 .output_format
315 .and_then(|s| s.parse().ok())
316 .unwrap_or_default()
317 });
318
319 let include = self.include.unwrap_or_else(IncludeOptions::all);
321
322 let verbose = self
324 .verbose
325 .unwrap_or_else(|| config_file.verbose.unwrap_or(false));
326
327 let timeout = self
329 .timeout
330 .unwrap_or_else(|| config_file.timeout.unwrap_or(30));
331
332 Ok(Config {
333 server,
334 token,
335 output_format,
336 include,
337 verbose,
338 timeout,
339 test_outcomes: self.test_outcomes,
340 })
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_include_options_parse_all() {
350 let opts = IncludeOptions::parse("all").unwrap();
351 assert!(opts.result);
352 assert!(opts.deprecations);
353 assert!(opts.failures);
354 assert!(opts.tests);
355 assert!(opts.task_execution);
356 assert!(opts.network_activity);
357 assert!(opts.dependencies);
358 }
359
360 #[test]
361 fn test_include_options_parse_single() {
362 let opts = IncludeOptions::parse("result").unwrap();
363 assert!(opts.result);
364 assert!(!opts.deprecations);
365 assert!(!opts.failures);
366 assert!(!opts.tests);
367 assert!(!opts.task_execution);
368 }
369
370 #[test]
371 fn test_include_options_parse_multiple() {
372 let opts = IncludeOptions::parse("result,failures").unwrap();
373 assert!(opts.result);
374 assert!(!opts.deprecations);
375 assert!(opts.failures);
376 assert!(!opts.tests);
377 assert!(!opts.task_execution);
378 }
379
380 #[test]
381 fn test_include_options_parse_tests() {
382 let opts = IncludeOptions::parse("tests").unwrap();
383 assert!(!opts.result);
384 assert!(!opts.deprecations);
385 assert!(!opts.failures);
386 assert!(opts.tests);
387 assert!(!opts.task_execution);
388 }
389
390 #[test]
391 fn test_include_options_parse_multiple_with_tests() {
392 let opts = IncludeOptions::parse("result,tests").unwrap();
393 assert!(opts.result);
394 assert!(!opts.deprecations);
395 assert!(!opts.failures);
396 assert!(opts.tests);
397 assert!(!opts.task_execution);
398 }
399
400 #[test]
401 fn test_include_options_parse_task_execution() {
402 let opts = IncludeOptions::parse("task-execution").unwrap();
403 assert!(!opts.result);
404 assert!(!opts.deprecations);
405 assert!(!opts.failures);
406 assert!(!opts.tests);
407 assert!(opts.task_execution);
408 }
409
410 #[test]
411 fn test_include_options_parse_network_activity() {
412 let opts = IncludeOptions::parse("network-activity").unwrap();
413 assert!(!opts.result);
414 assert!(!opts.deprecations);
415 assert!(!opts.failures);
416 assert!(!opts.tests);
417 assert!(!opts.task_execution);
418 assert!(opts.network_activity);
419 }
420
421 #[test]
422 fn test_include_options_parse_dependencies() {
423 let opts = IncludeOptions::parse("dependencies").unwrap();
424 assert!(!opts.result);
425 assert!(!opts.deprecations);
426 assert!(!opts.failures);
427 assert!(!opts.tests);
428 assert!(!opts.task_execution);
429 assert!(!opts.network_activity);
430 assert!(opts.dependencies);
431 }
432
433 #[test]
434 fn test_include_options_parse_empty() {
435 let opts = IncludeOptions::parse("").unwrap();
436 assert!(opts.result);
438 assert!(opts.deprecations);
439 assert!(opts.failures);
440 assert!(opts.tests);
441 assert!(opts.task_execution);
442 assert!(opts.network_activity);
443 assert!(opts.dependencies);
444 }
445
446 #[test]
447 fn test_include_options_parse_invalid() {
448 let result = IncludeOptions::parse("invalid");
449 assert!(result.is_err());
450 }
451
452 #[test]
453 fn test_output_format_parse() {
454 assert_eq!("json".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
455 assert_eq!("JSON".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
456 assert_eq!(
457 "human".parse::<OutputFormat>().unwrap(),
458 OutputFormat::Human
459 );
460 assert!("invalid".parse::<OutputFormat>().is_err());
461 }
462}