1use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8use std::collections::HashMap;
9
10#[derive(Debug, Deserialize)]
12pub struct InsomniaExport {
13 #[serde(rename = "__export_format")]
15 pub export_format: Option<i32>,
16 #[serde(rename = "_type")]
18 pub export_type: Option<String>,
19 pub resources: Vec<InsomniaResource>,
21}
22
23#[derive(Debug, Deserialize)]
25pub struct InsomniaResource {
26 #[serde(rename = "_id")]
28 pub id: String,
29 #[serde(rename = "_type")]
31 pub resource_type: String,
32 pub parent_id: Option<String>,
34 pub name: Option<String>,
36 pub url: Option<String>,
38 pub method: Option<String>,
40 pub headers: Option<Vec<InsomniaHeader>>,
42 pub body: Option<InsomniaBody>,
44 pub authentication: Option<InsomniaAuth>,
46 pub parameters: Option<Vec<InsomniaParameter>>,
48 pub data: Option<Value>,
50 pub environment: Option<String>,
52}
53
54#[derive(Debug, Deserialize)]
56pub struct InsomniaHeader {
57 pub name: String,
59 pub value: String,
61 pub disabled: Option<bool>,
63}
64
65#[derive(Debug, Deserialize)]
67pub struct InsomniaBody {
68 pub mime_type: Option<String>,
70 pub text: Option<String>,
72 pub params: Option<Vec<InsomniaParameter>>,
74}
75
76#[derive(Debug, Deserialize)]
78pub struct InsomniaParameter {
79 pub name: String,
81 pub value: String,
83 pub disabled: Option<bool>,
85}
86
87#[derive(Debug, Deserialize)]
89pub struct InsomniaAuth {
90 #[serde(rename = "type")]
92 pub auth_type: String,
93 pub disabled: Option<bool>,
95 pub username: Option<String>,
97 pub password: Option<String>,
99 pub token: Option<String>,
101 pub prefix: Option<String>,
103 pub key: Option<String>,
105 pub value: Option<String>,
107 #[serde(rename = "accessTokenUrl")]
109 pub access_token_url: Option<String>,
110 #[serde(rename = "clientId")]
112 pub client_id: Option<String>,
113 #[serde(rename = "grantType")]
115 pub grant_type: Option<String>,
116 #[serde(rename = "accessToken")]
118 pub access_token: Option<String>,
119}
120
121#[derive(Debug, Serialize)]
123pub struct MockForgeRoute {
124 pub method: String,
126 pub path: String,
128 pub headers: HashMap<String, String>,
130 pub body: Option<String>,
132 pub response: MockForgeResponse,
134}
135
136#[derive(Debug, Serialize)]
138pub struct MockForgeResponse {
139 pub status: u16,
141 pub headers: HashMap<String, String>,
143 pub body: Value,
145}
146
147#[derive(Debug)]
149pub struct InsomniaImportResult {
150 pub routes: Vec<MockForgeRoute>,
152 pub variables: HashMap<String, String>,
154 pub warnings: Vec<String>,
156}
157
158pub fn import_insomnia_export(
160 content: &str,
161 environment: Option<&str>,
162) -> Result<InsomniaImportResult, String> {
163 let export: InsomniaExport = serde_json::from_str(content)
164 .map_err(|e| format!("Failed to parse Insomnia export: {}", e))?;
165
166 if let Some(format) = export.export_format {
168 if format < 3 {
169 return Err("Insomnia export format version 3 or higher is required".to_string());
170 }
171 }
172
173 let mut routes = Vec::new();
174 let mut variables = HashMap::new();
175 let mut warnings = Vec::new();
176
177 if let Some(env_name) = environment {
179 extract_environment_variables(&export.resources, env_name, &mut variables);
180 } else {
181 extract_environment_variables(&export.resources, "Base Environment", &mut variables);
183 }
184
185 for resource in &export.resources {
187 if resource.resource_type == "request" {
188 match convert_insomnia_request_to_route(resource, &variables) {
189 Ok(route) => routes.push(route),
190 Err(e) => warnings.push(format!(
191 "Failed to convert request '{}': {}",
192 resource.name.as_deref().unwrap_or("unnamed"),
193 e
194 )),
195 }
196 }
197 }
198
199 Ok(InsomniaImportResult {
200 routes,
201 variables,
202 warnings,
203 })
204}
205
206fn extract_environment_variables(
208 resources: &[InsomniaResource],
209 env_name: &str,
210 variables: &mut HashMap<String, String>,
211) {
212 for resource in resources {
213 if resource.resource_type == "environment" && resource.name.as_deref() == Some(env_name) {
214 if let Some(data) = &resource.data {
215 if let Some(obj) = data.as_object() {
216 for (key, value) in obj {
217 if let Some(str_value) = value.as_str() {
218 variables.insert(key.clone(), str_value.to_string());
219 } else if let Some(num_value) = value.as_f64() {
220 variables.insert(key.clone(), num_value.to_string());
221 } else if let Some(bool_value) = value.as_bool() {
222 variables.insert(key.clone(), bool_value.to_string());
223 }
224 }
225 }
226 }
227 }
228 }
229}
230
231fn convert_insomnia_request_to_route(
233 resource: &InsomniaResource,
234 variables: &HashMap<String, String>,
235) -> Result<MockForgeRoute, String> {
236 let method = resource.method.as_deref().ok_or("Request missing method")?.to_uppercase();
237
238 let raw_url = resource.url.as_deref().ok_or("Request missing URL")?;
239
240 let url = resolve_variables(raw_url, variables);
241
242 let path = extract_path_from_url(&url)?;
244
245 let mut headers = HashMap::new();
247 if let Some(resource_headers) = &resource.headers {
248 for header in resource_headers {
249 if !header.disabled.unwrap_or(false) && !header.name.is_empty() {
250 headers.insert(header.name.clone(), resolve_variables(&header.value, variables));
251 }
252 }
253 }
254
255 if let Some(auth) = &resource.authentication {
257 if !auth.disabled.unwrap_or(false) {
258 add_auth_headers(auth, &mut headers, variables);
259 }
260 }
261
262 let body = extract_request_body(resource, variables);
264
265 let response = generate_mock_response(&method);
267
268 Ok(MockForgeRoute {
269 method,
270 path,
271 headers,
272 body,
273 response,
274 })
275}
276
277fn extract_path_from_url(url: &str) -> Result<String, String> {
279 if let Ok(parsed_url) = url::Url::parse(url) {
280 Ok(parsed_url.path().to_string())
281 } else if url.starts_with('/') {
282 Ok(url.to_string())
283 } else {
284 Ok(format!("/{}", url))
286 }
287}
288
289fn add_auth_headers(
291 auth: &InsomniaAuth,
292 headers: &mut HashMap<String, String>,
293 variables: &HashMap<String, String>,
294) {
295 match auth.auth_type.as_str() {
296 "bearer" => {
297 if let Some(token) = &auth.token {
298 let resolved_token = resolve_variables(token, variables);
299 headers.insert("Authorization".to_string(), format!("Bearer {}", resolved_token));
300 }
301 }
302 "basic" => {
303 if let (Some(username), Some(password)) = (&auth.username, &auth.password) {
304 let user = resolve_variables(username, variables);
305 let pass = resolve_variables(password, variables);
306 use base64::{engine::general_purpose, Engine as _};
307 let credentials = general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
308 headers.insert("Authorization".to_string(), format!("Basic {}", credentials));
309 }
310 }
311 "apikey" => {
312 if let (Some(key), Some(value)) = (&auth.key, &auth.value) {
313 let resolved_key = resolve_variables(key, variables);
314 let resolved_value = resolve_variables(value, variables);
315 headers.insert(resolved_key, resolved_value);
316 }
317 }
318 "oauth2" => {
319 if let Some(access_token) = &auth.access_token {
321 let resolved = resolve_variables(access_token, variables);
322 if !resolved.is_empty() {
323 headers.insert("Authorization".to_string(), format!("Bearer {}", resolved));
324 }
325 } else if let Some(token) = &auth.token {
326 let resolved = resolve_variables(token, variables);
327 if !resolved.is_empty() {
328 headers.insert("Authorization".to_string(), format!("Bearer {}", resolved));
329 }
330 }
331 }
332 _ => {
333 }
335 }
336}
337
338fn extract_request_body(
340 resource: &InsomniaResource,
341 variables: &HashMap<String, String>,
342) -> Option<String> {
343 if let Some(body) = &resource.body {
344 if let Some(text) = &body.text {
345 return Some(resolve_variables(text, variables));
346 }
347 }
348 None
349}
350
351fn resolve_variables(input: &str, variables: &HashMap<String, String>) -> String {
353 let mut result = input.to_string();
354 for (key, value) in variables {
355 let pattern = format!("{{{{{}}}}}", key);
356 result = result.replace(&pattern, value);
357 }
358 result
359}
360
361fn generate_mock_response(method: &str) -> MockForgeResponse {
363 let mut headers = HashMap::new();
364 headers.insert("Content-Type".to_string(), "application/json".to_string());
365
366 let body = match method {
367 "GET" => json!({"message": "Mock GET response", "method": "GET"}),
368 "POST" => json!({"message": "Mock POST response", "method": "POST", "created": true}),
369 "PUT" => json!({"message": "Mock PUT response", "method": "PUT", "updated": true}),
370 "DELETE" => json!({"message": "Mock DELETE response", "method": "DELETE", "deleted": true}),
371 "PATCH" => json!({"message": "Mock PATCH response", "method": "PATCH", "patched": true}),
372 _ => json!({"message": "Mock response", "method": method}),
373 };
374
375 MockForgeResponse {
376 status: 200,
377 headers,
378 body,
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn test_parse_insomnia_export() {
388 let export_json = r#"{
389 "__export_format": 4,
390 "_type": "export",
391 "resources": [
392 {
393 "_id": "req_1",
394 "_type": "request",
395 "name": "Get Users",
396 "method": "GET",
397 "url": "{{baseUrl}}/users",
398 "headers": [
399 {"name": "Authorization", "value": "Bearer {{token}}"}
400 ],
401 "authentication": {
402 "type": "bearer",
403 "token": "{{token}}"
404 }
405 },
406 {
407 "_id": "env_1",
408 "_type": "environment",
409 "name": "Base Environment",
410 "data": {
411 "baseUrl": "https://api.example.com",
412 "token": "test-token"
413 }
414 }
415 ]
416 }"#;
417
418 let result = import_insomnia_export(export_json, Some("Base Environment")).unwrap();
419
420 assert_eq!(result.routes.len(), 1);
421 assert_eq!(result.routes[0].method, "GET");
422 assert_eq!(result.routes[0].path, "/users");
423 assert!(result.routes[0].headers.contains_key("Authorization"));
424 assert!(result.variables.contains_key("baseUrl"));
425 }
426
427 #[test]
428 fn test_insomnia_format_validation() {
429 let old_format = r#"{
430 "__export_format": 2,
431 "resources": []
432 }"#;
433
434 let result = import_insomnia_export(old_format, None);
435 assert!(result.is_err());
436 assert!(result.unwrap_err().contains("version 3 or higher"));
437 }
438}