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 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 == 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 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 let toml_content = r#"
187 no_download = true
188 "#;
189 let file_config: ConfigFile = toml::from_str(toml_content).unwrap();
190
191 let args = CliArgs::parse_from(["netspeed-cli"]);
193 let file_config_loaded = Some(file_config);
194
195 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);
199 assert!(merged);
202 }
203}