mockforge_bench/
target_parser.rs1use crate::error::{BenchError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
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}
22
23impl TargetConfig {
24 pub fn from_url(url: String) -> Self {
26 Self {
27 url,
28 auth: None,
29 headers: None,
30 }
31 }
32
33 pub fn normalize_url(&mut self) {
35 if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
37 if self.url.contains(':') && !self.url.starts_with("http") {
39 self.url = format!("http://{}", self.url);
41 } else {
42 self.url = format!("http://{}", self.url);
44 }
45 }
46 }
47}
48
49#[derive(Debug, Deserialize)]
51struct JsonTargetFile {
52 #[serde(rename = "targets")]
53 targets: Option<Vec<JsonTarget>>,
54}
55
56#[derive(Debug, Deserialize)]
58#[serde(untagged)]
59enum JsonTarget {
60 Simple(String),
62 Object {
64 url: String,
65 auth: Option<String>,
66 headers: Option<HashMap<String, String>>,
67 },
68}
69
70pub fn parse_targets_file(path: &Path) -> Result<Vec<TargetConfig>> {
76 let content = std::fs::read_to_string(path)
78 .map_err(|e| BenchError::Other(format!("Failed to read targets file: {}", e)))?;
79
80 let is_json = path
82 .extension()
83 .and_then(|ext| ext.to_str())
84 .map(|ext| ext.eq_ignore_ascii_case("json"))
85 .unwrap_or(false)
86 || content.trim_start().starts_with('[')
87 || content.trim_start().starts_with('{');
88
89 if is_json {
90 parse_json_targets(&content)
91 } else {
92 parse_text_targets(&content)
93 }
94}
95
96fn parse_json_targets(content: &str) -> Result<Vec<TargetConfig>> {
98 let json_value: serde_json::Value = serde_json::from_str(content)
100 .map_err(|e| BenchError::Other(format!("Failed to parse JSON: {}", e)))?;
101
102 let targets = match json_value {
103 serde_json::Value::Array(arr) => {
104 arr.into_iter()
106 .map(|item| {
107 if let Ok(target) = serde_json::from_value::<JsonTarget>(item) {
108 Ok(match target {
109 JsonTarget::Simple(url) => TargetConfig::from_url(url),
110 JsonTarget::Object { url, auth, headers } => {
111 TargetConfig { url, auth, headers }
112 }
113 })
114 } else {
115 Err(BenchError::Other("Invalid target format in JSON array".to_string()))
116 }
117 })
118 .collect::<Result<Vec<_>>>()?
119 }
120 serde_json::Value::Object(obj) => {
121 if let Some(targets_val) = obj.get("targets") {
123 if let Some(arr) = targets_val.as_array() {
124 arr.into_iter()
125 .map(|item| {
126 if let Ok(target) = serde_json::from_value::<JsonTarget>(item.clone()) {
127 Ok(match target {
128 JsonTarget::Simple(url) => TargetConfig::from_url(url),
129 JsonTarget::Object { url, auth, headers } => {
130 TargetConfig { url, auth, headers }
131 }
132 })
133 } else {
134 Err(BenchError::Other("Invalid target format in JSON".to_string()))
135 }
136 })
137 .collect::<Result<Vec<_>>>()?
138 } else {
139 return Err(BenchError::Other("Expected 'targets' to be an array".to_string()));
140 }
141 } else {
142 return Err(BenchError::Other(
143 "JSON object must contain 'targets' array".to_string(),
144 ));
145 }
146 }
147 _ => {
148 return Err(BenchError::Other(
149 "JSON must be an array or object with 'targets' key".to_string(),
150 ));
151 }
152 };
153
154 if targets.is_empty() {
155 return Err(BenchError::Other("No targets found in JSON file".to_string()));
156 }
157
158 let mut normalized_targets = targets;
160 for target in &mut normalized_targets {
161 target.normalize_url();
162 }
163
164 Ok(normalized_targets)
165}
166
167fn parse_text_targets(content: &str) -> Result<Vec<TargetConfig>> {
169 let mut targets = Vec::new();
170
171 for line in content.lines() {
172 let line = line.trim();
173
174 if line.is_empty() || line.starts_with('#') {
176 continue;
177 }
178
179 if line.is_empty() {
181 continue;
182 }
183
184 let mut target = TargetConfig::from_url(line.to_string());
185 target.normalize_url();
186 targets.push(target);
187 }
188
189 if targets.is_empty() {
190 return Err(BenchError::Other("No valid targets found in text file".to_string()));
191 }
192
193 Ok(targets)
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use std::io::Write;
200 use tempfile::NamedTempFile;
201
202 #[test]
203 fn test_parse_text_targets() {
204 let content = r#"
205https://api1.example.com
206https://api2.example.com
207192.168.1.100:8080
208api3.example.com
209# This is a comment
210 "#;
211
212 let targets = parse_text_targets(content).unwrap();
213 assert_eq!(targets.len(), 4);
214 assert_eq!(targets[0].url, "https://api1.example.com");
215 assert_eq!(targets[1].url, "https://api2.example.com");
216 assert_eq!(targets[2].url, "http://192.168.1.100:8080");
217 assert_eq!(targets[3].url, "http://api3.example.com");
218 }
219
220 #[test]
221 fn test_parse_json_targets_array() {
222 let content = r#"
223[
224 {"url": "https://api1.example.com", "auth": "Bearer token1"},
225 {"url": "https://api2.example.com"},
226 "https://api3.example.com"
227]
228 "#;
229
230 let targets = parse_json_targets(content).unwrap();
231 assert_eq!(targets.len(), 3);
232 assert_eq!(targets[0].url, "https://api1.example.com");
233 assert_eq!(targets[0].auth, Some("Bearer token1".to_string()));
234 assert_eq!(targets[1].url, "https://api2.example.com");
235 assert_eq!(targets[2].url, "https://api3.example.com");
236 }
237
238 #[test]
239 fn test_parse_json_targets_object() {
240 let content = r#"
241{
242 "targets": [
243 {"url": "https://api1.example.com"},
244 {"url": "https://api2.example.com", "auth": "Bearer token2"}
245 ]
246}
247 "#;
248
249 let targets = parse_json_targets(content).unwrap();
250 assert_eq!(targets.len(), 2);
251 assert_eq!(targets[0].url, "https://api1.example.com");
252 assert_eq!(targets[1].url, "https://api2.example.com");
253 assert_eq!(targets[1].auth, Some("Bearer token2".to_string()));
254 }
255
256 #[test]
257 fn test_normalize_url() {
258 let mut target = TargetConfig::from_url("api.example.com".to_string());
259 target.normalize_url();
260 assert_eq!(target.url, "http://api.example.com");
261
262 let mut target2 = TargetConfig::from_url("192.168.1.1:8080".to_string());
263 target2.normalize_url();
264 assert_eq!(target2.url, "http://192.168.1.1:8080");
265
266 let mut target3 = TargetConfig::from_url("https://api.example.com".to_string());
267 target3.normalize_url();
268 assert_eq!(target3.url, "https://api.example.com");
269 }
270
271 #[test]
272 fn test_parse_targets_file_text() {
273 let mut file = NamedTempFile::new().unwrap();
274 writeln!(file, "https://api1.example.com").unwrap();
275 writeln!(file, "https://api2.example.com").unwrap();
276 writeln!(file, "# comment").unwrap();
277 writeln!(file, "api3.example.com").unwrap();
278
279 let targets = parse_targets_file(file.path()).unwrap();
280 assert_eq!(targets.len(), 3);
281 }
282
283 #[test]
284 fn test_parse_targets_file_json() {
285 let mut file = NamedTempFile::new().unwrap();
286 file.write_all(
287 r#"[
288 {"url": "https://api1.example.com"},
289 {"url": "https://api2.example.com"}
290]"#
291 .as_bytes(),
292 )
293 .unwrap();
294
295 let targets = parse_targets_file(file.path()).unwrap();
296 assert_eq!(targets.len(), 2);
297 }
298
299 #[test]
300 fn test_parse_targets_file_empty() {
301 let file = NamedTempFile::new().unwrap();
302 std::fs::write(file.path(), "").unwrap();
303
304 let result = parse_targets_file(file.path());
305 assert!(result.is_err());
306 }
307
308 #[test]
309 fn test_parse_targets_file_only_comments() {
310 let mut file = NamedTempFile::new().unwrap();
311 writeln!(file, "# comment 1").unwrap();
312 writeln!(file, "# comment 2").unwrap();
313
314 let result = parse_targets_file(file.path());
315 assert!(result.is_err());
316 }
317
318 #[test]
319 fn test_parse_json_targets_with_headers() {
320 let content = r#"
321[
322 {
323 "url": "https://api1.example.com",
324 "auth": "Bearer token1",
325 "headers": {
326 "X-Custom": "value1",
327 "X-Another": "value2"
328 }
329 }
330]
331 "#;
332
333 let targets = parse_json_targets(content).unwrap();
334 assert_eq!(targets.len(), 1);
335 assert_eq!(targets[0].url, "https://api1.example.com");
336 assert_eq!(targets[0].auth, Some("Bearer token1".to_string()));
337 assert_eq!(
338 targets[0].headers.as_ref().unwrap().get("X-Custom"),
339 Some(&"value1".to_string())
340 );
341 assert_eq!(
342 targets[0].headers.as_ref().unwrap().get("X-Another"),
343 Some(&"value2".to_string())
344 );
345 }
346
347 #[test]
348 fn test_parse_json_targets_invalid_format() {
349 let content = r#"
350{
351 "invalid": "format"
352}
353 "#;
354
355 let result = parse_json_targets(content);
356 assert!(result.is_err());
357 }
358}