elif_openapi/
export.rs

1/*!
2Export functionality for OpenAPI specifications.
3
4This module provides functionality to export OpenAPI specifications to various
5formats including Postman collections and Insomnia workspaces.
6*/
7
8use crate::{error::OpenApiResult, specification::OpenApiSpec};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// Export service for OpenAPI specifications
14pub struct OpenApiExporter;
15
16impl OpenApiExporter {
17    /// Export to Postman collection format
18    pub fn export_postman(spec: &OpenApiSpec) -> OpenApiResult<PostmanCollection> {
19        let mut collection = PostmanCollection::new(&spec.info.title, &spec.info.description);
20
21        // Set collection info
22        collection.info.version = spec.info.version.clone();
23
24        // Process paths and create requests
25        for (path, path_item) in &spec.paths {
26            let operations = vec![
27                ("GET", &path_item.get),
28                ("POST", &path_item.post),
29                ("PUT", &path_item.put),
30                ("DELETE", &path_item.delete),
31                ("PATCH", &path_item.patch),
32                ("OPTIONS", &path_item.options),
33                ("HEAD", &path_item.head),
34                ("TRACE", &path_item.trace),
35            ];
36
37            for (method, operation) in operations {
38                if let Some(op) = operation {
39                    let request = Self::create_postman_request(spec, method, path, op)?;
40                    collection.add_item(PostmanItem::Request(request));
41                }
42            }
43        }
44
45        Ok(collection)
46    }
47
48    /// Create Postman request from OpenAPI operation
49    fn create_postman_request(
50        spec: &OpenApiSpec,
51        method: &str,
52        path: &str,
53        operation: &crate::specification::Operation,
54    ) -> OpenApiResult<PostmanRequest> {
55        let mut request = PostmanRequest {
56            name: operation
57                .summary
58                .clone()
59                .unwrap_or_else(|| format!("{} {}", method, path)),
60            method: method.to_string(),
61            header: Vec::new(),
62            url: PostmanUrl::new(path),
63            body: None,
64        };
65
66        // Add base URL from servers
67        if let Some(server) = spec.servers.first() {
68            request.url.host = Some(vec![server.url.clone()]);
69        }
70
71        // Process parameters
72        for param in &operation.parameters {
73            match param.location.as_str() {
74                "header" => {
75                    request.header.push(PostmanHeader {
76                        key: param.name.clone(),
77                        value: param
78                            .example
79                            .as_ref()
80                            .and_then(|v| v.as_str())
81                            .unwrap_or("{{value}}")
82                            .to_string(),
83                        description: param.description.clone(),
84                    });
85                }
86                "query" => {
87                    request.url.query.push(PostmanQuery {
88                        key: param.name.clone(),
89                        value: param
90                            .example
91                            .as_ref()
92                            .and_then(|v| v.as_str())
93                            .unwrap_or("{{value}}")
94                            .to_string(),
95                        description: param.description.clone(),
96                    });
97                }
98                "path" => {
99                    request.url.variable.push(PostmanVariable {
100                        key: param.name.clone(),
101                        value: param
102                            .example
103                            .as_ref()
104                            .and_then(|v| v.as_str())
105                            .unwrap_or("{{value}}")
106                            .to_string(),
107                        description: param.description.clone(),
108                    });
109                }
110                _ => {}
111            }
112        }
113
114        // Process request body
115        if let Some(request_body) = &operation.request_body {
116            if let Some(json_content) = request_body.content.get("application/json") {
117                if let Some(schema) = &json_content.schema {
118                    let example = Self::generate_request_body_example(schema)?;
119                    request.body = Some(PostmanBody {
120                        mode: "raw".to_string(),
121                        raw: serde_json::to_string_pretty(&example)?,
122                        options: Some(PostmanBodyOptions {
123                            raw: PostmanRawOptions {
124                                language: "json".to_string(),
125                            },
126                        }),
127                    });
128                }
129            }
130        }
131
132        Ok(request)
133    }
134
135    /// Generate example request body from schema
136    fn generate_request_body_example(
137        schema: &crate::specification::Schema,
138    ) -> OpenApiResult<Value> {
139        crate::utils::OpenApiUtils::generate_example_from_schema(schema)
140    }
141
142    /// Export to Insomnia workspace format
143    pub fn export_insomnia(spec: &OpenApiSpec) -> OpenApiResult<InsomniaWorkspace> {
144        let mut workspace = InsomniaWorkspace::new(&spec.info.title);
145        let workspace_id = workspace.workspace.id.clone();
146
147        // Create base environment
148        let base_url = spec
149            .servers
150            .first()
151            .map(|s| s.url.clone())
152            .unwrap_or_else(|| "http://localhost:3000".to_string());
153
154        workspace.add_environment("Base Environment", &base_url);
155
156        // Process paths and create requests
157        for (path, path_item) in &spec.paths {
158            let operations = vec![
159                ("GET", &path_item.get),
160                ("POST", &path_item.post),
161                ("PUT", &path_item.put),
162                ("DELETE", &path_item.delete),
163                ("PATCH", &path_item.patch),
164                ("OPTIONS", &path_item.options),
165                ("HEAD", &path_item.head),
166                ("TRACE", &path_item.trace),
167            ];
168
169            for (method, operation) in operations {
170                if let Some(op) = operation {
171                    let request = Self::create_insomnia_request(method, path, op, &workspace_id)?;
172                    workspace.add_resource(InsomniaResource::Request(request));
173                }
174            }
175        }
176
177        Ok(workspace)
178    }
179
180    /// Create Insomnia request from OpenAPI operation
181    fn create_insomnia_request(
182        method: &str,
183        path: &str,
184        operation: &crate::specification::Operation,
185        parent_id: &str,
186    ) -> OpenApiResult<InsomniaRequest> {
187        let mut request = InsomniaRequest::new(
188            &operation
189                .summary
190                .clone()
191                .unwrap_or_else(|| format!("{} {}", method, path)),
192            method,
193            "{{ _.base_url }}", // Use environment variable
194            parent_id,
195        );
196
197        // Build full URL with path
198        request.url = format!("{{{{ _.base_url }}}}{}", path);
199
200        // Process parameters
201        for param in &operation.parameters {
202            match param.location.as_str() {
203                "header" => {
204                    request.headers.push(InsomniaHeader {
205                        name: param.name.clone(),
206                        value: param
207                            .example
208                            .as_ref()
209                            .and_then(|v| v.as_str())
210                            .unwrap_or("")
211                            .to_string(),
212                    });
213                }
214                "query" => {
215                    request.parameters.push(InsomniaParameter {
216                        name: param.name.clone(),
217                        value: param
218                            .example
219                            .as_ref()
220                            .and_then(|v| v.as_str())
221                            .unwrap_or("")
222                            .to_string(),
223                    });
224                }
225                _ => {}
226            }
227        }
228
229        // Process request body
230        if let Some(request_body) = &operation.request_body {
231            if let Some(json_content) = request_body.content.get("application/json") {
232                if let Some(schema) = &json_content.schema {
233                    let example = Self::generate_request_body_example(schema)?;
234                    request.body = InsomniaBody {
235                        mime_type: "application/json".to_string(),
236                        text: serde_json::to_string_pretty(&example)?,
237                    };
238                }
239            }
240        }
241
242        Ok(request)
243    }
244}
245
246// Postman collection format structures
247
248#[derive(Debug, Serialize, Deserialize)]
249pub struct PostmanCollection {
250    pub info: PostmanInfo,
251    pub item: Vec<PostmanItem>,
252}
253
254#[derive(Debug, Serialize, Deserialize)]
255pub struct PostmanInfo {
256    pub name: String,
257    pub description: Option<String>,
258    pub version: String,
259    pub schema: String,
260}
261
262#[derive(Debug, Serialize, Deserialize)]
263#[serde(untagged)]
264pub enum PostmanItem {
265    Request(PostmanRequest),
266    Folder(PostmanFolder),
267}
268
269#[derive(Debug, Serialize, Deserialize)]
270pub struct PostmanFolder {
271    pub name: String,
272    pub item: Vec<PostmanItem>,
273}
274
275#[derive(Debug, Serialize, Deserialize)]
276pub struct PostmanRequest {
277    pub name: String,
278    pub method: String,
279    pub header: Vec<PostmanHeader>,
280    pub url: PostmanUrl,
281    pub body: Option<PostmanBody>,
282}
283
284#[derive(Debug, Serialize, Deserialize)]
285pub struct PostmanHeader {
286    pub key: String,
287    pub value: String,
288    pub description: Option<String>,
289}
290
291#[derive(Debug, Serialize, Deserialize)]
292pub struct PostmanUrl {
293    pub raw: Option<String>,
294    pub host: Option<Vec<String>>,
295    pub path: Vec<String>,
296    pub query: Vec<PostmanQuery>,
297    pub variable: Vec<PostmanVariable>,
298}
299
300#[derive(Debug, Serialize, Deserialize)]
301pub struct PostmanQuery {
302    pub key: String,
303    pub value: String,
304    pub description: Option<String>,
305}
306
307#[derive(Debug, Serialize, Deserialize)]
308pub struct PostmanVariable {
309    pub key: String,
310    pub value: String,
311    pub description: Option<String>,
312}
313
314#[derive(Debug, Serialize, Deserialize)]
315pub struct PostmanBody {
316    pub mode: String,
317    pub raw: String,
318    pub options: Option<PostmanBodyOptions>,
319}
320
321#[derive(Debug, Serialize, Deserialize)]
322pub struct PostmanBodyOptions {
323    pub raw: PostmanRawOptions,
324}
325
326#[derive(Debug, Serialize, Deserialize)]
327pub struct PostmanRawOptions {
328    pub language: String,
329}
330
331impl PostmanCollection {
332    pub fn new(name: &str, description: &Option<String>) -> Self {
333        Self {
334            info: PostmanInfo {
335                name: name.to_string(),
336                description: description.clone(),
337                version: "1.0.0".to_string(),
338                schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
339                    .to_string(),
340            },
341            item: Vec::new(),
342        }
343    }
344
345    pub fn add_item(&mut self, item: PostmanItem) {
346        self.item.push(item);
347    }
348}
349
350impl PostmanUrl {
351    pub fn new(path: &str) -> Self {
352        let path_parts: Vec<String> = path
353            .split('/')
354            .filter(|p| !p.is_empty())
355            .map(|p| p.to_string())
356            .collect();
357
358        Self {
359            raw: None,
360            host: None,
361            path: path_parts,
362            query: Vec::new(),
363            variable: Vec::new(),
364        }
365    }
366}
367
368// Insomnia workspace format structures
369
370#[derive(Debug, Serialize, Deserialize)]
371pub struct InsomniaWorkspace {
372    #[serde(rename = "_type")]
373    pub workspace_type: String,
374    pub workspace: InsomniaWorkspaceInfo,
375    pub resources: Vec<InsomniaResource>,
376}
377
378#[derive(Debug, Serialize, Deserialize)]
379pub struct InsomniaWorkspaceInfo {
380    #[serde(rename = "_id")]
381    pub id: String,
382    pub name: String,
383    pub description: String,
384}
385
386#[derive(Debug, Serialize, Deserialize)]
387#[serde(tag = "_type")]
388pub enum InsomniaResource {
389    #[serde(rename = "request")]
390    Request(InsomniaRequest),
391    #[serde(rename = "environment")]
392    Environment(InsomniaEnvironment),
393}
394
395#[derive(Debug, Serialize, Deserialize)]
396pub struct InsomniaRequest {
397    #[serde(rename = "_id")]
398    pub id: String,
399    pub name: String,
400    pub method: String,
401    pub url: String,
402    pub headers: Vec<InsomniaHeader>,
403    pub parameters: Vec<InsomniaParameter>,
404    pub body: InsomniaBody,
405    #[serde(rename = "parentId")]
406    pub parent_id: String,
407}
408
409#[derive(Debug, Serialize, Deserialize)]
410pub struct InsomniaHeader {
411    pub name: String,
412    pub value: String,
413}
414
415#[derive(Debug, Serialize, Deserialize)]
416pub struct InsomniaParameter {
417    pub name: String,
418    pub value: String,
419}
420
421#[derive(Debug, Serialize, Deserialize)]
422pub struct InsomniaBody {
423    #[serde(rename = "mimeType")]
424    pub mime_type: String,
425    pub text: String,
426}
427
428#[derive(Debug, Serialize, Deserialize)]
429pub struct InsomniaEnvironment {
430    #[serde(rename = "_id")]
431    pub id: String,
432    pub name: String,
433    pub data: HashMap<String, String>,
434    #[serde(rename = "parentId")]
435    pub parent_id: String,
436}
437
438impl InsomniaWorkspace {
439    pub fn new(name: &str) -> Self {
440        let workspace_id = format!("wrk_{}", uuid::Uuid::new_v4().simple());
441
442        Self {
443            workspace_type: "export".to_string(),
444            workspace: InsomniaWorkspaceInfo {
445                id: workspace_id,
446                name: name.to_string(),
447                description: "Exported from OpenAPI specification".to_string(),
448            },
449            resources: Vec::new(),
450        }
451    }
452
453    pub fn add_resource(&mut self, resource: InsomniaResource) {
454        self.resources.push(resource);
455    }
456
457    pub fn add_environment(&mut self, name: &str, base_url: &str) {
458        let mut data = HashMap::new();
459        data.insert("base_url".to_string(), base_url.to_string());
460
461        let environment = InsomniaEnvironment {
462            id: format!("env_{}", uuid::Uuid::new_v4().simple()),
463            name: name.to_string(),
464            data,
465            parent_id: self.workspace.id.clone(),
466        };
467
468        self.resources
469            .push(InsomniaResource::Environment(environment));
470    }
471}
472
473impl InsomniaRequest {
474    pub fn new(name: &str, method: &str, url: &str, parent_id: &str) -> Self {
475        Self {
476            id: format!("req_{}", uuid::Uuid::new_v4().simple()),
477            name: name.to_string(),
478            method: method.to_string(),
479            url: url.to_string(),
480            headers: Vec::new(),
481            parameters: Vec::new(),
482            body: InsomniaBody {
483                mime_type: "application/json".to_string(),
484                text: "".to_string(),
485            },
486            parent_id: parent_id.to_string(),
487        }
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use crate::specification::OpenApiSpec;
495
496    #[test]
497    fn test_postman_collection_creation() {
498        let collection = PostmanCollection::new("Test API", &Some("Test description".to_string()));
499        assert_eq!(collection.info.name, "Test API");
500        assert_eq!(
501            collection.info.description,
502            Some("Test description".to_string())
503        );
504        assert!(collection.item.is_empty());
505    }
506
507    #[test]
508    fn test_insomnia_workspace_creation() {
509        let workspace = InsomniaWorkspace::new("Test API");
510        assert_eq!(workspace.workspace.name, "Test API");
511        assert!(workspace.resources.is_empty());
512    }
513
514    #[test]
515    fn test_postman_export() {
516        let spec = OpenApiSpec::new("Test API", "1.0.0");
517        let collection = OpenApiExporter::export_postman(&spec).unwrap();
518        assert_eq!(collection.info.name, "Test API");
519        // Empty spec should have no items
520        assert_eq!(collection.item.len(), 0);
521    }
522}