1use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum CollectionFormat {
11 Postman,
13 Insomnia,
15 Hoppscotch,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PostmanCollection {
22 pub info: PostmanInfo,
24 pub item: Vec<PostmanItem>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub variable: Option<Vec<PostmanVariable>>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct PostmanInfo {
34 pub name: String,
36 pub description: String,
38 pub schema: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub version: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PostmanItem {
48 pub name: String,
50 pub request: PostmanRequest,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub response: Option<Vec<Value>>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct PostmanRequest {
60 pub method: String,
62 pub header: Vec<PostmanHeader>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub body: Option<PostmanBody>,
67 pub url: PostmanUrl,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub description: Option<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct PostmanHeader {
77 pub key: String,
79 pub value: String,
81 #[serde(rename = "type")]
83 pub header_type: String,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct PostmanBody {
89 pub mode: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub raw: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub options: Option<Value>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PostmanUrl {
102 pub raw: String,
104 pub host: Vec<String>,
106 pub path: Vec<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub query: Option<Vec<PostmanQueryParam>>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct PostmanQueryParam {
116 pub key: String,
118 pub value: String,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct PostmanVariable {
125 pub key: String,
127 pub value: String,
129 #[serde(rename = "type")]
131 pub var_type: String,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct InsomniaCollection {
137 pub _type: String,
139 pub __export_format: u8,
141 pub __export_date: String,
143 pub __export_source: String,
145 pub resources: Vec<InsomniaResource>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct InsomniaResource {
152 pub _id: String,
154 pub _type: String,
156 pub name: String,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub method: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub url: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub body: Option<Value>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub headers: Option<Vec<InsomniaHeader>>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct InsomniaHeader {
175 pub name: String,
177 pub value: String,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct HoppscotchCollection {
185 pub name: String,
187 pub requests: Vec<HoppscotchRequest>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub folders: Option<Vec<HoppscotchFolder>>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct HoppscotchRequest {
198 pub name: String,
200 pub method: String,
202 pub endpoint: String,
204 pub headers: Vec<HoppscotchHeader>,
206 pub params: Vec<HoppscotchParam>,
208 pub body: HoppscotchBody,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct HoppscotchHeader {
216 pub key: String,
218 pub value: String,
220 pub active: bool,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct HoppscotchParam {
228 pub key: String,
230 pub value: String,
232 pub active: bool,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct HoppscotchBody {
240 pub content_type: String,
242 pub body: String,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct HoppscotchFolder {
250 pub name: String,
252 pub requests: Vec<HoppscotchRequest>,
254}
255
256pub struct CollectionExporter {
258 base_url: String,
260}
261
262impl CollectionExporter {
263 pub fn new(base_url: String) -> Self {
265 Self { base_url }
266 }
267
268 pub fn to_postman(&self, spec: &crate::openapi::OpenApiSpec) -> PostmanCollection {
270 let mut items = Vec::new();
271
272 for (path, path_item_ref) in &spec.spec.paths.paths {
273 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
275 let operations = vec![
276 ("GET", path_item.get.as_ref()),
277 ("POST", path_item.post.as_ref()),
278 ("PUT", path_item.put.as_ref()),
279 ("DELETE", path_item.delete.as_ref()),
280 ("PATCH", path_item.patch.as_ref()),
281 ("HEAD", path_item.head.as_ref()),
282 ("OPTIONS", path_item.options.as_ref()),
283 ];
284
285 for (method, op_opt) in operations {
286 if let Some(op) = op_opt {
287 let name = op
288 .operation_id
289 .clone()
290 .or_else(|| op.summary.clone())
291 .unwrap_or_else(|| format!("{} {}", method, path));
292
293 let request = PostmanRequest {
294 method: method.to_string(),
295 header: vec![PostmanHeader {
296 key: "Content-Type".to_string(),
297 value: "application/json".to_string(),
298 header_type: "text".to_string(),
299 }],
300 body: if matches!(method, "POST" | "PUT" | "PATCH") {
301 Some(PostmanBody {
302 mode: "raw".to_string(),
303 raw: Some("{}".to_string()),
304 options: Some(serde_json::json!({
305 "raw": {
306 "language": "json"
307 }
308 })),
309 })
310 } else {
311 None
312 },
313 url: PostmanUrl {
314 raw: format!("{}{}", self.base_url, path),
315 host: vec![self.base_url.clone()],
316 path: path
317 .split('/')
318 .filter(|s| !s.is_empty())
319 .map(String::from)
320 .collect(),
321 query: None,
322 },
323 description: op.description.clone(),
324 };
325
326 items.push(PostmanItem {
327 name,
328 request,
329 response: None,
330 });
331 }
332 }
333 }
334 }
335
336 PostmanCollection {
337 info: PostmanInfo {
338 name: spec.spec.info.title.clone(),
339 description: spec.spec.info.description.clone().unwrap_or_default(),
340 schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
341 .to_string(),
342 version: Some(spec.spec.info.version.clone()),
343 },
344 item: items,
345 variable: Some(vec![PostmanVariable {
346 key: "baseUrl".to_string(),
347 value: self.base_url.clone(),
348 var_type: "string".to_string(),
349 }]),
350 }
351 }
352
353 pub fn to_insomnia(&self, spec: &crate::openapi::OpenApiSpec) -> InsomniaCollection {
355 let mut resources = Vec::new();
356
357 resources.push(InsomniaResource {
359 _id: "wrk_1".to_string(),
360 _type: "workspace".to_string(),
361 name: spec.spec.info.title.clone(),
362 method: None,
363 url: None,
364 body: None,
365 headers: None,
366 });
367
368 let mut id_counter = 1;
370 for (path, path_item_ref) in &spec.spec.paths.paths {
371 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
372 let operations = vec![
373 ("GET", path_item.get.as_ref()),
374 ("POST", path_item.post.as_ref()),
375 ("PUT", path_item.put.as_ref()),
376 ("DELETE", path_item.delete.as_ref()),
377 ("PATCH", path_item.patch.as_ref()),
378 ("HEAD", path_item.head.as_ref()),
379 ("OPTIONS", path_item.options.as_ref()),
380 ];
381
382 for (method, op_opt) in operations {
383 if let Some(op) = op_opt {
384 id_counter += 1;
385
386 let name = op
387 .operation_id
388 .clone()
389 .or_else(|| op.summary.clone())
390 .unwrap_or_else(|| format!("{} {}", method, path));
391
392 resources.push(InsomniaResource {
393 _id: format!("req_{}", id_counter),
394 _type: "request".to_string(),
395 name,
396 method: Some(method.to_string()),
397 url: Some(format!("{}{}", self.base_url, path)),
398 body: if matches!(method, "POST" | "PUT" | "PATCH") {
399 Some(serde_json::json!({
400 "mimeType": "application/json",
401 "text": "{}"
402 }))
403 } else {
404 None
405 },
406 headers: Some(vec![InsomniaHeader {
407 name: "Content-Type".to_string(),
408 value: "application/json".to_string(),
409 }]),
410 });
411 }
412 }
413 }
414 }
415
416 InsomniaCollection {
417 _type: "export".to_string(),
418 __export_format: 4,
419 __export_date: chrono::Utc::now().to_rfc3339(),
420 __export_source: "mockforge".to_string(),
421 resources,
422 }
423 }
424
425 pub fn to_hoppscotch(&self, spec: &crate::openapi::OpenApiSpec) -> HoppscotchCollection {
427 let mut requests = Vec::new();
428
429 for (path, path_item_ref) in &spec.spec.paths.paths {
430 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
431 let operations = vec![
432 ("GET", path_item.get.as_ref()),
433 ("POST", path_item.post.as_ref()),
434 ("PUT", path_item.put.as_ref()),
435 ("DELETE", path_item.delete.as_ref()),
436 ("PATCH", path_item.patch.as_ref()),
437 ("HEAD", path_item.head.as_ref()),
438 ("OPTIONS", path_item.options.as_ref()),
439 ];
440
441 for (method, op_opt) in operations {
442 if let Some(op) = op_opt {
443 let name = op
444 .operation_id
445 .clone()
446 .or_else(|| op.summary.clone())
447 .unwrap_or_else(|| format!("{} {}", method, path));
448
449 requests.push(HoppscotchRequest {
450 name,
451 method: method.to_string(),
452 endpoint: format!("{}{}", self.base_url, path),
453 headers: vec![HoppscotchHeader {
454 key: "Content-Type".to_string(),
455 value: "application/json".to_string(),
456 active: true,
457 }],
458 params: vec![],
459 body: HoppscotchBody {
460 content_type: "application/json".to_string(),
461 body: "{}".to_string(),
462 },
463 });
464 }
465 }
466 }
467 }
468
469 HoppscotchCollection {
470 name: spec.spec.info.title.clone(),
471 requests,
472 folders: None,
473 }
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_postman_collection_structure() {
483 let collection = PostmanCollection {
484 info: PostmanInfo {
485 name: "Test API".to_string(),
486 description: "Test description".to_string(),
487 schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
488 .to_string(),
489 version: Some("1.0.0".to_string()),
490 },
491 item: vec![],
492 variable: None,
493 };
494
495 assert_eq!(collection.info.name, "Test API");
496 }
497
498 #[test]
499 fn test_insomnia_collection_structure() {
500 let collection = InsomniaCollection {
501 _type: "export".to_string(),
502 __export_format: 4,
503 __export_date: "2024-01-01T00:00:00Z".to_string(),
504 __export_source: "mockforge".to_string(),
505 resources: vec![],
506 };
507
508 assert_eq!(collection._type, "export");
509 assert_eq!(collection.__export_format, 4);
510 }
511
512 #[test]
513 fn test_hoppscotch_collection_structure() {
514 let collection = HoppscotchCollection {
515 name: "Test API".to_string(),
516 requests: vec![],
517 folders: None,
518 };
519
520 assert_eq!(collection.name, "Test API");
521 }
522}