1use crate::{error::OpenApiResult, specification::OpenApiSpec};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13pub struct OpenApiExporter;
15
16impl OpenApiExporter {
17 pub fn export_postman(spec: &OpenApiSpec) -> OpenApiResult<PostmanCollection> {
19 let mut collection = PostmanCollection::new(&spec.info.title, &spec.info.description);
20
21 collection.info.version = spec.info.version.clone();
23
24 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 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 if let Some(server) = spec.servers.first() {
68 request.url.host = Some(vec![server.url.clone()]);
69 }
70
71 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 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 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 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 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 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 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 }}", parent_id,
195 );
196
197 request.url = format!("{{{{ _.base_url }}}}{}", path);
199
200 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 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#[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#[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 assert_eq!(collection.item.len(), 0);
521 }
522}