Skip to main content

netspeed_cli/
config.rs

1use crate::cli::CliArgs;
2use directories::ProjectDirs;
3use serde::Deserialize;
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Debug, Default, Deserialize)]
8pub struct ConfigFile {
9    pub no_download: Option<bool>,
10    pub no_upload: Option<bool>,
11    pub single: Option<bool>,
12    pub bytes: Option<bool>,
13    pub simple: Option<bool>,
14    pub csv: Option<bool>,
15    pub csv_delimiter: Option<char>,
16    pub csv_header: Option<bool>,
17    pub json: Option<bool>,
18    pub timeout: Option<u64>,
19}
20
21#[allow(clippy::struct_excessive_bools)]
22pub struct Config {
23    pub no_download: bool,
24    pub no_upload: bool,
25    pub single: bool,
26    pub bytes: bool,
27    pub simple: bool,
28    pub csv: bool,
29    pub csv_delimiter: char,
30    pub csv_header: bool,
31    pub json: bool,
32    pub list: bool,
33    pub server_ids: Vec<String>,
34    pub exclude_ids: Vec<String>,
35    pub source: Option<String>,
36    pub timeout: u64,
37}
38
39impl Config {
40    #[must_use]
41    pub fn from_args(args: &CliArgs) -> Self {
42        let file_config = load_config_file().unwrap_or_default();
43
44        // Helper to prefer CLI arg over file config over default
45        let merge_bool = |cli: bool, file: Option<bool>| cli || file.unwrap_or(false);
46        let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
47            // If CLI is at default value, check file; otherwise use CLI
48            if cli == default {
49                file.unwrap_or(default)
50            } else {
51                cli
52            }
53        };
54
55        Self {
56            no_download: merge_bool(args.no_download, file_config.no_download),
57            no_upload: merge_bool(args.no_upload, file_config.no_upload),
58            single: merge_bool(args.single, file_config.single),
59            bytes: merge_bool(args.bytes, file_config.bytes),
60            simple: merge_bool(args.simple, file_config.simple),
61            csv: merge_bool(args.csv, file_config.csv),
62            csv_delimiter: if args.csv_delimiter == ',' {
63                file_config.csv_delimiter.unwrap_or(',')
64            } else {
65                args.csv_delimiter
66            },
67            csv_header: merge_bool(args.csv_header, file_config.csv_header),
68            json: merge_bool(args.json, file_config.json),
69            list: args.list,
70            server_ids: args.server.clone(),
71            exclude_ids: args.exclude.clone(),
72            source: args.source.clone(),
73            timeout: merge_u64(args.timeout, file_config.timeout, 10),
74        }
75    }
76}
77
78fn get_config_path() -> Option<PathBuf> {
79    ProjectDirs::from("dev", "vibe", "netspeed-cli").map(|proj_dirs| {
80        let config_dir = proj_dirs.config_dir();
81        fs::create_dir_all(config_dir).ok();
82        config_dir.join("config.toml")
83    })
84}
85
86fn load_config_file() -> Option<ConfigFile> {
87    let path = get_config_path()?;
88    if !path.exists() {
89        return None;
90    }
91
92    let content = fs::read_to_string(path).ok()?;
93    toml::from_str(&content).ok()
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use clap::Parser;
100
101    #[test]
102    fn test_config_from_args_defaults() {
103        let args = CliArgs::parse_from(["netspeed-cli"]);
104        let config = Config::from_args(&args);
105
106        assert!(!config.no_download);
107        assert!(!config.no_upload);
108        assert!(!config.single);
109        assert!(!config.bytes);
110        assert!(!config.simple);
111        assert!(!config.csv);
112        assert!(!config.json);
113        assert!(!config.list);
114        assert_eq!(config.timeout, 10);
115        assert_eq!(config.csv_delimiter, ',');
116        assert!(!config.csv_header);
117        assert!(config.server_ids.is_empty());
118        assert!(config.exclude_ids.is_empty());
119    }
120
121    #[test]
122    fn test_config_from_args_no_download() {
123        let args = CliArgs::parse_from(["netspeed-cli", "--no-download"]);
124        let config = Config::from_args(&args);
125        assert!(config.no_download);
126        assert!(!config.no_upload);
127    }
128
129    #[test]
130    fn test_config_file_deserialization() {
131        let toml_content = r#"
132            no_download = true
133            no_upload = false
134            single = true
135            bytes = true
136            simple = false
137            csv = false
138            csv_delimiter = ';'
139            csv_header = true
140            json = true
141            timeout = 30
142        "#;
143
144        let config: ConfigFile = toml::from_str(toml_content).unwrap();
145        assert_eq!(config.no_download, Some(true));
146        assert_eq!(config.no_upload, Some(false));
147        assert_eq!(config.single, Some(true));
148        assert_eq!(config.bytes, Some(true));
149        assert_eq!(config.simple, Some(false));
150        assert_eq!(config.csv, Some(false));
151        assert_eq!(config.csv_delimiter, Some(';'));
152        assert_eq!(config.csv_header, Some(true));
153        assert_eq!(config.json, Some(true));
154        assert_eq!(config.timeout, Some(30));
155    }
156
157    #[test]
158    fn test_config_file_partial() {
159        let toml_content = r#"
160            no_download = true
161            timeout = 20
162        "#;
163
164        let config: ConfigFile = toml::from_str(toml_content).unwrap();
165        assert_eq!(config.no_download, Some(true));
166        assert!(config.no_upload.is_none());
167        assert!(config.single.is_none());
168        assert_eq!(config.timeout, Some(20));
169        assert!(config.csv_delimiter.is_none());
170    }
171
172    #[test]
173    fn test_config_from_args_overrides_file() {
174        // Test that CLI flags override file config when explicitly set
175        let args = CliArgs::parse_from(["netspeed-cli", "--no-download"]);
176        let config = Config::from_args(&args);
177        assert!(config.no_download);
178    }
179
180    #[test]
181    fn test_config_merge_bool_file_true_cli_false() {
182        // When CLI flag is false (default) and file config is true, result should be false
183        // because merge_bool = cli || file.unwrap_or(false)
184        // Actually merge_bool returns true only if CLI is true OR file is Some(true)
185        // Let's verify the actual behavior
186        let toml_content = r#"
187            no_download = true
188        "#;
189        let file_config: ConfigFile = toml::from_str(toml_content).unwrap();
190
191        // CLI args with no_download=false (default)
192        let args = CliArgs::parse_from(["netspeed-cli"]);
193        let file_config_loaded = Some(file_config);
194
195        // Manual merge check
196        let cli_val = args.no_download; // false
197        let file_val = file_config_loaded.and_then(|c| c.no_download); // Some(true)
198        let merged = cli_val || file_val.unwrap_or(false);
199        // Since CLI is false and file is Some(true), result depends on merge logic
200        // The current merge is: cli || file.unwrap_or(false) = false || true = true
201        assert!(merged);
202    }
203}