Skip to main content

mockforge_import/import/
insomnia_import.rs

1//! Insomnia export import functionality
2//!
3//! This module handles parsing Insomnia exports and converting them
4//! to MockForge routes and configurations.
5
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8use std::collections::HashMap;
9
10/// Insomnia export structure
11#[derive(Debug, Deserialize)]
12pub struct InsomniaExport {
13    /// Export format version
14    #[serde(rename = "__export_format")]
15    pub export_format: Option<i32>,
16    /// Export type identifier
17    #[serde(rename = "_type")]
18    pub export_type: Option<String>,
19    /// Array of resources (requests, environments, folders, etc.)
20    pub resources: Vec<InsomniaResource>,
21}
22
23/// Generic Insomnia resource (request, folder, environment, etc.)
24#[derive(Debug, Deserialize)]
25pub struct InsomniaResource {
26    /// Unique resource identifier
27    #[serde(rename = "_id")]
28    pub id: String,
29    /// Resource type (request, folder, environment, etc.)
30    #[serde(rename = "_type")]
31    pub resource_type: String,
32    /// Parent resource ID (for nested resources)
33    pub parent_id: Option<String>,
34    /// Resource name
35    pub name: Option<String>,
36    /// Request URL (for request resources)
37    pub url: Option<String>,
38    /// HTTP method (for request resources)
39    pub method: Option<String>,
40    /// Request headers (for request resources)
41    pub headers: Option<Vec<InsomniaHeader>>,
42    /// Request body (for request resources)
43    pub body: Option<InsomniaBody>,
44    /// Authentication configuration (for request resources)
45    pub authentication: Option<InsomniaAuth>,
46    /// Query/form parameters (for request resources)
47    pub parameters: Option<Vec<InsomniaParameter>>,
48    /// Environment variable data (for environment resources)
49    pub data: Option<Value>,
50    /// Environment name (for environment resources)
51    pub environment: Option<String>,
52}
53
54/// Insomnia header entry
55#[derive(Debug, Deserialize)]
56pub struct InsomniaHeader {
57    /// Header name
58    pub name: String,
59    /// Header value
60    pub value: String,
61    /// Whether this header is disabled
62    pub disabled: Option<bool>,
63}
64
65/// Insomnia request body structure
66#[derive(Debug, Deserialize)]
67pub struct InsomniaBody {
68    /// MIME type of the body
69    pub mime_type: Option<String>,
70    /// Raw body text
71    pub text: Option<String>,
72    /// Form data parameters (for form-data bodies)
73    pub params: Option<Vec<InsomniaParameter>>,
74}
75
76/// Insomnia parameter (query params, form data, etc.)
77#[derive(Debug, Deserialize)]
78pub struct InsomniaParameter {
79    /// Parameter name
80    pub name: String,
81    /// Parameter value
82    pub value: String,
83    /// Whether this parameter is disabled
84    pub disabled: Option<bool>,
85}
86
87/// Insomnia authentication configuration
88#[derive(Debug, Deserialize)]
89pub struct InsomniaAuth {
90    /// Authentication type (bearer, basic, apikey, etc.)
91    #[serde(rename = "type")]
92    pub auth_type: String,
93    /// Whether authentication is disabled
94    pub disabled: Option<bool>,
95    /// Username (for basic auth)
96    pub username: Option<String>,
97    /// Password (for basic auth)
98    pub password: Option<String>,
99    /// Bearer token (for bearer auth)
100    pub token: Option<String>,
101    /// Token prefix (for bearer auth)
102    pub prefix: Option<String>,
103    /// API key name (for apikey auth)
104    pub key: Option<String>,
105    /// API key value (for apikey auth)
106    pub value: Option<String>,
107    /// OAuth2 access token URL
108    #[serde(rename = "accessTokenUrl")]
109    pub access_token_url: Option<String>,
110    /// OAuth2 client ID
111    #[serde(rename = "clientId")]
112    pub client_id: Option<String>,
113    /// OAuth2 grant type
114    #[serde(rename = "grantType")]
115    pub grant_type: Option<String>,
116    /// OAuth2 access token value (stored after successful OAuth flow)
117    #[serde(rename = "accessToken")]
118    pub access_token: Option<String>,
119}
120
121/// MockForge route structure for import
122#[derive(Debug, Serialize)]
123pub struct MockForgeRoute {
124    /// HTTP method
125    pub method: String,
126    /// Request path
127    pub path: String,
128    /// Request headers
129    pub headers: HashMap<String, String>,
130    /// Optional request body
131    pub body: Option<String>,
132    /// Mock response for this route
133    pub response: MockForgeResponse,
134}
135
136/// MockForge response structure
137#[derive(Debug, Serialize)]
138pub struct MockForgeResponse {
139    /// HTTP status code
140    pub status: u16,
141    /// Response headers
142    pub headers: HashMap<String, String>,
143    /// Response body
144    pub body: Value,
145}
146
147/// Result of importing an Insomnia export
148#[derive(Debug)]
149pub struct InsomniaImportResult {
150    /// Converted routes from Insomnia requests
151    pub routes: Vec<MockForgeRoute>,
152    /// Extracted environment variables
153    pub variables: HashMap<String, String>,
154    /// Warnings encountered during import
155    pub warnings: Vec<String>,
156}
157
158/// Import an Insomnia export
159pub 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    // Validate export format
167    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    // Extract environment variables if specified
178    if let Some(env_name) = environment {
179        extract_environment_variables(&export.resources, env_name, &mut variables);
180    } else {
181        // Try to find default environment
182        extract_environment_variables(&export.resources, "Base Environment", &mut variables);
183    }
184
185    // Process all resources to find requests
186    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
206/// Extract variables from specified environment
207fn 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
231/// Convert an Insomnia request to a MockForge route
232fn 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    // Extract path from URL
243    let path = extract_path_from_url(&url)?;
244
245    // Extract headers
246    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    // Add authentication headers
256    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    // Extract body
263    let body = extract_request_body(resource, variables);
264
265    // Generate mock response
266    let response = generate_mock_response(&method);
267
268    Ok(MockForgeRoute {
269        method,
270        path,
271        headers,
272        body,
273        response,
274    })
275}
276
277/// Extract path from URL, handling full URLs and relative paths
278fn 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        // Assume it's a relative path
285        Ok(format!("/{}", url))
286    }
287}
288
289/// Add authentication headers based on Insomnia auth configuration
290fn 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            // OAuth2: use the stored access token if available, otherwise fall back to token field
320            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            // Unsupported auth types are silently skipped
334        }
335    }
336}
337
338/// Extract request body from Insomnia resource
339fn 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
351/// Resolve variables in a string (similar to Postman)
352fn 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
361/// Generate a mock response for the request
362fn 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}