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