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 pub quiet: bool,
38}
39
40impl Config {
41 #[must_use]
42 pub fn from_args(args: &CliArgs) -> Self {
43 let file_config = load_config_file().unwrap_or_default();
44
45 let merge_bool = |cli: bool, file: Option<bool>| cli || file.unwrap_or(false);
56 let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
57 if cli == default {
59 file.unwrap_or(default)
60 } else {
61 cli
62 }
63 };
64
65 Self {
66 no_download: merge_bool(args.no_download, file_config.no_download),
67 no_upload: merge_bool(args.no_upload, file_config.no_upload),
68 single: merge_bool(args.single, file_config.single),
69 bytes: merge_bool(args.bytes, file_config.bytes),
70 simple: merge_bool(args.simple, file_config.simple),
71 csv: merge_bool(args.csv, file_config.csv),
72 csv_delimiter: if args.csv_delimiter == ',' {
73 file_config.csv_delimiter.unwrap_or(',')
74 } else {
75 args.csv_delimiter
76 },
77 csv_header: merge_bool(args.csv_header, file_config.csv_header),
78 json: merge_bool(args.json, file_config.json),
79 list: args.list,
80 server_ids: args.server.clone(),
81 exclude_ids: args.exclude.clone(),
82 source: args.source.clone(),
83 timeout: merge_u64(args.timeout, file_config.timeout, 10),
84 quiet: merge_bool(args.quiet, None),
85 }
86 }
87}
88
89fn get_config_path() -> Option<PathBuf> {
90 ProjectDirs::from("dev", "vibe", "netspeed-cli").map(|proj_dirs| {
91 let config_dir = proj_dirs.config_dir();
92 fs::create_dir_all(config_dir).ok();
93 config_dir.join("config.toml")
94 })
95}
96
97fn load_config_file() -> Option<ConfigFile> {
98 let path = get_config_path()?;
99 if !path.exists() {
100 return None;
101 }
102
103 let content = fs::read_to_string(path).ok()?;
104 let mut config: ConfigFile = toml::from_str(&content).ok()?;
105
106 if let Some(timeout) = config.timeout {
108 if timeout == 0 || timeout > 300 {
109 config.timeout = None;
111 }
112 }
113
114 Some(config)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use clap::Parser;
121
122 #[test]
123 fn test_config_from_args_defaults() {
124 let args = CliArgs::parse_from(["netspeed-cli"]);
125 let config = Config::from_args(&args);
126
127 assert!(!config.no_download);
128 assert!(!config.no_upload);
129 assert!(!config.single);
130 assert!(!config.bytes);
131 assert!(!config.simple);
132 assert!(!config.csv);
133 assert!(!config.json);
134 assert!(!config.list);
135 assert!(!config.quiet);
136 assert_eq!(config.timeout, 10);
137 assert_eq!(config.csv_delimiter, ',');
138 assert!(!config.csv_header);
139 assert!(config.server_ids.is_empty());
140 assert!(config.exclude_ids.is_empty());
141 }
142
143 #[test]
144 fn test_config_from_args_no_download() {
145 let args = CliArgs::parse_from(["netspeed-cli", "--no-download"]);
146 let config = Config::from_args(&args);
147 assert!(config.no_download);
148 assert!(!config.no_upload);
149 }
150
151 #[test]
152 fn test_config_file_deserialization() {
153 let toml_content = r#"
154 no_download = true
155 no_upload = false
156 single = true
157 bytes = true
158 simple = false
159 csv = false
160 csv_delimiter = ';'
161 csv_header = true
162 json = true
163 timeout = 30
164 "#;
165
166 let config: ConfigFile = toml::from_str(toml_content).unwrap();
167 assert_eq!(config.no_download, Some(true));
168 assert_eq!(config.no_upload, Some(false));
169 assert_eq!(config.single, Some(true));
170 assert_eq!(config.bytes, Some(true));
171 assert_eq!(config.simple, Some(false));
172 assert_eq!(config.csv, Some(false));
173 assert_eq!(config.csv_delimiter, Some(';'));
174 assert_eq!(config.csv_header, Some(true));
175 assert_eq!(config.json, Some(true));
176 assert_eq!(config.timeout, Some(30));
177 }
178
179 #[test]
180 fn test_config_file_partial() {
181 let toml_content = r#"
182 no_download = true
183 timeout = 20
184 "#;
185
186 let config: ConfigFile = toml::from_str(toml_content).unwrap();
187 assert_eq!(config.no_download, Some(true));
188 assert!(config.no_upload.is_none());
189 assert!(config.single.is_none());
190 assert_eq!(config.timeout, Some(20));
191 assert!(config.csv_delimiter.is_none());
192 }
193
194 #[test]
195 fn test_config_from_args_overrides_file() {
196 let args = CliArgs::parse_from(["netspeed-cli", "--no-download"]);
198 let config = Config::from_args(&args);
199 assert!(config.no_download);
200 }
201
202 #[test]
203 fn test_config_merge_bool_file_true_cli_false() {
204 let toml_content = r#"
209 no_download = true
210 "#;
211 let file_config: ConfigFile = toml::from_str(toml_content).unwrap();
212
213 let args = CliArgs::parse_from(["netspeed-cli"]);
215 let file_config_loaded = Some(file_config);
216
217 let cli_val = args.no_download; let file_val = file_config_loaded.and_then(|c| c.no_download); let merged = cli_val || file_val.unwrap_or(false);
221 assert!(merged);
224 }
225}