1use regex::Regex;
7use serde_json::json;
8use std::collections::HashMap;
9
10#[derive(Debug)]
12pub struct ParsedCurlCommand {
13 pub method: String,
15 pub url: String,
17 pub headers: HashMap<String, String>,
19 pub body: Option<String>,
21}
22
23#[derive(Debug, serde::Serialize)]
25pub struct MockForgeRoute {
26 pub method: String,
28 pub path: String,
30 pub headers: HashMap<String, String>,
32 pub body: Option<String>,
34 pub response: MockForgeResponse,
36}
37
38#[derive(Debug, serde::Serialize)]
40pub struct MockForgeResponse {
41 pub status: u16,
43 pub headers: HashMap<String, String>,
45 pub body: serde_json::Value,
47}
48
49pub struct CurlImportResult {
51 pub routes: Vec<MockForgeRoute>,
53 pub warnings: Vec<String>,
55}
56
57pub fn import_curl_commands(
59 content: &str,
60 base_url: Option<&str>,
61) -> Result<CurlImportResult, String> {
62 let mut routes = Vec::new();
63 let mut warnings = Vec::new();
64
65 let commands = split_curl_commands(content);
67
68 for (i, command) in commands.into_iter().enumerate() {
69 let trimmed = command.trim();
70 if trimmed.is_empty() || trimmed.starts_with('#') {
71 continue; }
73
74 match parse_curl_command(trimmed) {
75 Ok(parsed) => match convert_curl_to_route(parsed, base_url) {
76 Ok(route) => routes.push(route),
77 Err(e) => warnings.push(format!("Failed to convert curl command {}: {}", i + 1, e)),
78 },
79 Err(e) => {
80 warnings.push(format!("Failed to parse curl command {}: {}", i + 1, e));
81 }
82 }
83 }
84
85 Ok(CurlImportResult { routes, warnings })
86}
87
88fn split_curl_commands(content: &str) -> Vec<String> {
90 let mut commands = Vec::new();
91 let mut current_command = String::new();
92 let mut in_quotes = false;
93 let mut quote_char = '\0';
94 let mut escaped = false;
95
96 for ch in content.chars() {
97 match ch {
98 '"' | '\'' if !escaped => {
99 if !in_quotes {
100 in_quotes = true;
101 quote_char = ch;
102 } else if ch == quote_char {
103 in_quotes = false;
104 quote_char = '\0';
105 }
106 }
107 '\\' if !escaped => {
108 escaped = true;
109 }
110 '\n' if !in_quotes && !escaped => {
111 let cmd = current_command.trim().to_string();
112 if !cmd.is_empty() {
113 commands.push(cmd);
114 }
115 current_command.clear();
116 }
117 _ => {
118 escaped = false;
119 current_command.push(ch);
120 }
121 }
122 }
123
124 let cmd = current_command.trim().to_string();
126 if !cmd.is_empty() {
127 commands.push(cmd);
128 }
129
130 commands
131}
132
133fn parse_curl_command(command: &str) -> Result<ParsedCurlCommand, String> {
135 let mut method = "GET".to_string();
136 let mut url = String::new();
137 let mut headers = HashMap::new();
138 let mut body = None;
139
140 let url_regex = Regex::new(r#"(?:^|\s)((?:https?://|http://|www\.)[^\s"']+)"#)
145 .map_err(|e| format!("Regex error: {}", e))?;
146
147 if let Some(captures) = url_regex.captures(command) {
148 if let Some(url_match) = captures.get(1) {
149 url = url_match.as_str().to_string();
150 }
151 }
152
153 if url.is_empty() {
154 return Err("No URL found in curl command".to_string());
155 }
156
157 let method_regex = Regex::new(r#"-X\s+(\w+)"#).map_err(|e| format!("Regex error: {}", e))?;
159
160 if let Some(captures) = method_regex.captures(command) {
161 if let Some(method_match) = captures.get(1) {
162 method = method_match.as_str().to_uppercase();
163 }
164 }
165
166 let header_regex = Regex::new(r#"-H\s+(?:["']([^"']+)["']|([^\s]+(?:\s+[^\s-]+)*))"#)
168 .map_err(|e| format!("Regex error: {}", e))?;
169
170 for captures in header_regex.captures_iter(command) {
171 let header_str = if let Some(quoted_match) = captures.get(1) {
172 quoted_match.as_str()
173 } else if let Some(unquoted_match) = captures.get(2) {
174 unquoted_match.as_str()
175 } else {
176 continue;
177 };
178
179 if let Some(colon_pos) = header_str.find(':') {
180 let key = header_str[..colon_pos].trim();
181 let value = header_str[colon_pos + 1..].trim();
182 headers.insert(key.to_string(), value.to_string());
183 }
184 }
185
186 let body_regex =
189 Regex::new(r#"(?:-d|--data)\s+(?:'([^']*)'|"([^"]*)"|([^\s]+(?:\s+[^\s-]+)*))"#)
190 .map_err(|e| format!("Regex error: {}", e))?;
191
192 if let Some(captures) = body_regex.captures(command) {
193 let body_str = if let Some(single_quoted_match) = captures.get(1) {
194 single_quoted_match.as_str()
195 } else if let Some(double_quoted_match) = captures.get(2) {
196 double_quoted_match.as_str()
197 } else if let Some(unquoted_match) = captures.get(3) {
198 unquoted_match.as_str()
199 } else {
200 ""
201 };
202
203 if !body_str.is_empty() {
204 body = Some(body_str.to_string());
205 }
206 }
207
208 Ok(ParsedCurlCommand {
209 method,
210 url,
211 headers,
212 body,
213 })
214}
215
216fn convert_curl_to_route(
218 parsed: ParsedCurlCommand,
219 base_url: Option<&str>,
220) -> Result<MockForgeRoute, String> {
221 let path = extract_path_from_url(&parsed.url, base_url)?;
223
224 let response = generate_mock_response(&parsed.method);
226
227 Ok(MockForgeRoute {
228 method: parsed.method,
229 path,
230 headers: parsed.headers,
231 body: parsed.body,
232 response,
233 })
234}
235
236fn extract_path_from_url(url: &str, base_url: Option<&str>) -> Result<String, String> {
238 if let Some(base) = base_url {
240 if url.starts_with(base) {
241 let path = url.trim_start_matches(base).trim_start_matches('/');
242 return Ok(if path.is_empty() {
243 "/".to_string()
244 } else {
245 format!("/{}", path)
246 });
247 }
248 }
249
250 if let Ok(parsed_url) = url::Url::parse(url) {
252 let path = parsed_url.path();
253 if path.is_empty() || path == "/" {
254 Ok("/".to_string())
255 } else {
256 Ok(path.to_string())
257 }
258 } else {
259 Err(format!("Invalid URL: {}", url))
260 }
261}
262
263fn generate_mock_response(method: &str) -> MockForgeResponse {
265 let mut headers = HashMap::new();
266 headers.insert("Content-Type".to_string(), "application/json".to_string());
267
268 let body = match method {
269 "GET" => json!({"message": "Mock GET response", "method": "GET"}),
270 "POST" => json!({"message": "Mock POST response", "method": "POST", "created": true}),
271 "PUT" => json!({"message": "Mock PUT response", "method": "PUT", "updated": true}),
272 "DELETE" => json!({"message": "Mock DELETE response", "method": "DELETE", "deleted": true}),
273 "PATCH" => json!({"message": "Mock PATCH response", "method": "PATCH", "patched": true}),
274 _ => json!({"message": "Mock response", "method": method}),
275 };
276
277 MockForgeResponse {
278 status: 200,
279 headers,
280 body,
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_parse_simple_curl() {
290 let command = "curl https://api.example.com/users";
291 let parsed = parse_curl_command(command).unwrap();
292
293 assert_eq!(parsed.method, "GET");
294 assert_eq!(parsed.url, "https://api.example.com/users");
295 assert!(parsed.headers.is_empty());
296 assert!(parsed.body.is_none());
297 }
298
299 #[test]
300 fn test_parse_curl_with_method() {
301 let command = "curl -X POST https://api.example.com/users";
302 let parsed = parse_curl_command(command).unwrap();
303
304 assert_eq!(parsed.method, "POST");
305 assert_eq!(parsed.url, "https://api.example.com/users");
306 }
307
308 #[test]
309 fn test_parse_curl_with_headers() {
310 let command = "curl -H 'Authorization: Bearer token' -H 'Content-Type: application/json' https://api.example.com/users";
311 let parsed = parse_curl_command(command).unwrap();
312
313 assert_eq!(parsed.headers.get("Authorization"), Some(&"Bearer token".to_string()));
314 assert_eq!(parsed.headers.get("Content-Type"), Some(&"application/json".to_string()));
315 }
316
317 #[test]
318 fn test_parse_curl_with_body() {
319 let command = "curl -X POST -d '{\"name\":\"John\"}' https://api.example.com/users";
320 let parsed = parse_curl_command(command).unwrap();
321
322 assert_eq!(parsed.method, "POST");
323 assert_eq!(parsed.body, Some("{\"name\":\"John\"}".to_string()));
324 }
325
326 #[test]
327 fn test_split_curl_commands() {
328 let content = r#"curl https://api.example.com/users
329curl -X POST https://api.example.com/users -d '{"name":"John"}'
330# This is a comment
331curl -H 'Auth: token' https://api.example.com/data"#;
332
333 let commands = split_curl_commands(content);
334 assert_eq!(commands.len(), 4);
337 assert!(commands[0].contains("users"));
338 assert!(commands[1].contains("POST"));
339 assert!(commands[2].contains("comment"));
340 assert!(commands[3].contains("data"));
341 }
342
343 #[test]
344 fn test_import_curl_commands() {
345 let content = "curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -d '{\"name\":\"John\"}'";
346
347 let result = import_curl_commands(content, Some("https://api.example.com")).unwrap();
348 assert_eq!(result.routes.len(), 1);
349
350 let route = &result.routes[0];
351 assert_eq!(route.method, "POST");
352 assert_eq!(route.path, "/users");
353 assert_eq!(route.headers.get("Content-Type"), Some(&"application/json".to_string()));
354 assert_eq!(route.body, Some("{name:John}".to_string()));
356 }
357}