1use crate::error::{BenchError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TargetConfig {
15 pub url: String,
17 pub auth: Option<String>,
19 pub headers: Option<HashMap<String, String>>,
21 pub spec: Option<PathBuf>,
23}
24
25impl TargetConfig {
26 pub fn from_url(url: String) -> Self {
28 Self {
29 url,
30 auth: None,
31 headers: None,
32 spec: None,
33 }
34 }
35
36 pub fn normalize_url(&mut self) {
38 if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
40 if self.url.contains(':') && !self.url.starts_with("http") {
42 self.url = format!("http://{}", self.url);
44 } else {
45 self.url = format!("http://{}", self.url);
47 }
48 }
49 }
50}
51
52#[derive(Debug, Deserialize)]
54struct JsonTargetFile {
55 #[serde(rename = "targets")]
56 targets: Option<Vec<JsonTarget>>,
57}
58
59#[derive(Debug, Deserialize)]
61#[serde(untagged)]
62enum JsonTarget {
63 Simple(String),
65 Object {
67 url: String,
68 auth: Option<String>,
69 headers: Option<HashMap<String, String>>,
70 spec: Option<PathBuf>,
71 },
72}
73
74pub fn parse_targets_file(path: &Path) -> Result<Vec<TargetConfig>> {
80 let content = std::fs::read_to_string(path)
82 .map_err(|e| BenchError::Other(format!("Failed to read targets file: {}", e)))?;
83
84 let is_json = path
86 .extension()
87 .and_then(|ext| ext.to_str())
88 .map(|ext| ext.eq_ignore_ascii_case("json"))
89 .unwrap_or(false)
90 || content.trim_start().starts_with('[')
91 || content.trim_start().starts_with('{');
92
93 if is_json {
94 parse_json_targets(&content)
95 } else {
96 parse_text_targets(&content)
97 }
98}
99
100fn parse_json_targets(content: &str) -> Result<Vec<TargetConfig>> {
102 let json_value: serde_json::Value = serde_json::from_str(content)
104 .map_err(|e| BenchError::Other(format!("Failed to parse JSON: {}", e)))?;
105
106 let targets = match json_value {
107 serde_json::Value::Array(arr) => {
108 arr.into_iter()
110 .map(|item| {
111 if let Ok(target) = serde_json::from_value::<JsonTarget>(item) {
112 Ok(match target {
113 JsonTarget::Simple(url) => TargetConfig::from_url(url),
114 JsonTarget::Object {
115 url,
116 auth,
117 headers,
118 spec,
119 } => TargetConfig {
120 url,
121 auth,
122 headers,
123 spec,
124 },
125 })
126 } else {
127 Err(BenchError::Other("Invalid target format in JSON array".to_string()))
128 }
129 })
130 .collect::<Result<Vec<_>>>()?
131 }
132 serde_json::Value::Object(obj) => {
133 if let Some(targets_val) = obj.get("targets") {
135 if let Some(arr) = targets_val.as_array() {
136 arr.iter()
137 .map(|item| {
138 if let Ok(target) = serde_json::from_value::<JsonTarget>(item.clone()) {
139 Ok(match target {
140 JsonTarget::Simple(url) => TargetConfig::from_url(url),
141 JsonTarget::Object {
142 url,
143 auth,
144 headers,
145 spec,
146 } => TargetConfig {
147 url,
148 auth,
149 headers,
150 spec,
151 },
152 })
153 } else {
154 Err(BenchError::Other("Invalid target format in JSON".to_string()))
155 }
156 })
157 .collect::<Result<Vec<_>>>()?
158 } else {
159 return Err(BenchError::Other("Expected 'targets' to be an array".to_string()));
160 }
161 } else {
162 return Err(BenchError::Other(
163 "JSON object must contain 'targets' array".to_string(),
164 ));
165 }
166 }
167 _ => {
168 return Err(BenchError::Other(
169 "JSON must be an array or object with 'targets' key".to_string(),
170 ));
171 }
172 };
173
174 if targets.is_empty() {
175 return Err(BenchError::Other("No targets found in JSON file".to_string()));
176 }
177
178 let mut normalized_targets = targets;
180 for target in &mut normalized_targets {
181 target.normalize_url();
182 }
183
184 Ok(normalized_targets)
185}
186
187fn parse_text_targets(content: &str) -> Result<Vec<TargetConfig>> {
189 let mut targets = Vec::new();
190
191 for line in content.lines() {
192 let line = line.trim();
193
194 if line.is_empty() || line.starts_with('#') {
196 continue;
197 }
198
199 if line.is_empty() {
201 continue;
202 }
203
204 let mut target = TargetConfig::from_url(line.to_string());
205 target.normalize_url();
206 targets.push(target);
207 }
208
209 if targets.is_empty() {
210 return Err(BenchError::Other("No valid targets found in text file".to_string()));
211 }
212
213 Ok(targets)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use std::io::Write;
220 use tempfile::NamedTempFile;
221
222 #[test]
223 fn test_parse_text_targets() {
224 let content = r#"
225https://api1.example.com
226https://api2.example.com
227192.168.1.100:8080
228api3.example.com
229# This is a comment
230 "#;
231
232 let targets = parse_text_targets(content).unwrap();
233 assert_eq!(targets.len(), 4);
234 assert_eq!(targets[0].url, "https://api1.example.com");
235 assert_eq!(targets[1].url, "https://api2.example.com");
236 assert_eq!(targets[2].url, "http://192.168.1.100:8080");
237 assert_eq!(targets[3].url, "http://api3.example.com");
238 }
239
240 #[test]
241 fn test_parse_json_targets_array() {
242 let content = r#"
243[
244 {"url": "https://api1.example.com", "auth": "Bearer token1"},
245 {"url": "https://api2.example.com"},
246 "https://api3.example.com"
247]
248 "#;
249
250 let targets = parse_json_targets(content).unwrap();
251 assert_eq!(targets.len(), 3);
252 assert_eq!(targets[0].url, "https://api1.example.com");
253 assert_eq!(targets[0].auth, Some("Bearer token1".to_string()));
254 assert_eq!(targets[1].url, "https://api2.example.com");
255 assert_eq!(targets[2].url, "https://api3.example.com");
256 }
257
258 #[test]
259 fn test_parse_json_targets_object() {
260 let content = r#"
261{
262 "targets": [
263 {"url": "https://api1.example.com"},
264 {"url": "https://api2.example.com", "auth": "Bearer token2"}
265 ]
266}
267 "#;
268
269 let targets = parse_json_targets(content).unwrap();
270 assert_eq!(targets.len(), 2);
271 assert_eq!(targets[0].url, "https://api1.example.com");
272 assert_eq!(targets[1].url, "https://api2.example.com");
273 assert_eq!(targets[1].auth, Some("Bearer token2".to_string()));
274 }
275
276 #[test]
277 fn test_normalize_url() {
278 let mut target = TargetConfig::from_url("api.example.com".to_string());
279 target.normalize_url();
280 assert_eq!(target.url, "http://api.example.com");
281
282 let mut target2 = TargetConfig::from_url("192.168.1.1:8080".to_string());
283 target2.normalize_url();
284 assert_eq!(target2.url, "http://192.168.1.1:8080");
285
286 let mut target3 = TargetConfig::from_url("https://api.example.com".to_string());
287 target3.normalize_url();
288 assert_eq!(target3.url, "https://api.example.com");
289 }
290
291 #[test]
292 fn test_parse_targets_file_text() {
293 let mut file = NamedTempFile::new().unwrap();
294 writeln!(file, "https://api1.example.com").unwrap();
295 writeln!(file, "https://api2.example.com").unwrap();
296 writeln!(file, "# comment").unwrap();
297 writeln!(file, "api3.example.com").unwrap();
298
299 let targets = parse_targets_file(file.path()).unwrap();
300 assert_eq!(targets.len(), 3);
301 }
302
303 #[test]
304 fn test_parse_targets_file_json() {
305 let mut file = NamedTempFile::new().unwrap();
306 file.write_all(
307 r#"[
308 {"url": "https://api1.example.com"},
309 {"url": "https://api2.example.com"}
310]"#
311 .as_bytes(),
312 )
313 .unwrap();
314
315 let targets = parse_targets_file(file.path()).unwrap();
316 assert_eq!(targets.len(), 2);
317 }
318
319 #[test]
320 fn test_parse_targets_file_empty() {
321 let file = NamedTempFile::new().unwrap();
322 std::fs::write(file.path(), "").unwrap();
323
324 let result = parse_targets_file(file.path());
325 assert!(result.is_err());
326 }
327
328 #[test]
329 fn test_parse_targets_file_only_comments() {
330 let mut file = NamedTempFile::new().unwrap();
331 writeln!(file, "# comment 1").unwrap();
332 writeln!(file, "# comment 2").unwrap();
333
334 let result = parse_targets_file(file.path());
335 assert!(result.is_err());
336 }
337
338 #[test]
339 fn test_parse_json_targets_with_headers() {
340 let content = r#"
341[
342 {
343 "url": "https://api1.example.com",
344 "auth": "Bearer token1",
345 "headers": {
346 "X-Custom": "value1",
347 "X-Another": "value2"
348 }
349 }
350]
351 "#;
352
353 let targets = parse_json_targets(content).unwrap();
354 assert_eq!(targets.len(), 1);
355 assert_eq!(targets[0].url, "https://api1.example.com");
356 assert_eq!(targets[0].auth, Some("Bearer token1".to_string()));
357 assert_eq!(
358 targets[0].headers.as_ref().unwrap().get("X-Custom"),
359 Some(&"value1".to_string())
360 );
361 assert_eq!(
362 targets[0].headers.as_ref().unwrap().get("X-Another"),
363 Some(&"value2".to_string())
364 );
365 }
366
367 #[test]
368 fn test_parse_json_targets_with_per_target_spec() {
369 let content = r#"
370[
371 {"url": "https://api1.example.com", "spec": "spec_a.json"},
372 {"url": "https://api2.example.com", "spec": "spec_b.json"},
373 {"url": "https://api3.example.com"}
374]
375 "#;
376
377 let targets = parse_json_targets(content).unwrap();
378 assert_eq!(targets.len(), 3);
379 assert_eq!(targets[0].spec, Some(PathBuf::from("spec_a.json")));
380 assert_eq!(targets[1].spec, Some(PathBuf::from("spec_b.json")));
381 assert_eq!(targets[2].spec, None);
382 }
383
384 #[test]
385 fn test_parse_json_targets_with_per_target_spec_mixed() {
386 let content = r#"[
388 {"url": "https://api1.example.com", "spec": "/absolute/path/spec.json"},
389 {"url": "https://api2.example.com"},
390 {"url": "https://api3.example.com", "spec": "relative/spec.yaml"}
391]"#;
392
393 let targets = parse_json_targets(content).unwrap();
394 assert_eq!(targets.len(), 3);
395 assert_eq!(targets[0].spec, Some(PathBuf::from("/absolute/path/spec.json")));
396 assert_eq!(targets[1].spec, None);
397 assert_eq!(targets[2].spec, Some(PathBuf::from("relative/spec.yaml")));
398 }
399
400 #[test]
401 fn test_from_url_has_no_spec() {
402 let target = TargetConfig::from_url("http://example.com".to_string());
403 assert_eq!(target.spec, None);
404 assert_eq!(target.auth, None);
405 assert_eq!(target.headers, None);
406 }
407
408 #[test]
409 fn test_parse_json_targets_invalid_format() {
410 let content = r#"
411{
412 "invalid": "format"
413}
414 "#;
415
416 let result = parse_json_targets(content);
417 assert!(result.is_err());
418 }
419}