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,
12 Insomnia,
13 Hoppscotch,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct PostmanCollection {
19 pub info: PostmanInfo,
20 pub item: Vec<PostmanItem>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub variable: Option<Vec<PostmanVariable>>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PostmanInfo {
27 pub name: String,
28 pub description: String,
29 pub schema: String,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub version: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PostmanItem {
36 pub name: String,
37 pub request: PostmanRequest,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub response: Option<Vec<Value>>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PostmanRequest {
44 pub method: String,
45 pub header: Vec<PostmanHeader>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub body: Option<PostmanBody>,
48 pub url: PostmanUrl,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub description: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PostmanHeader {
55 pub key: String,
56 pub value: String,
57 #[serde(rename = "type")]
58 pub header_type: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct PostmanBody {
63 pub mode: String,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub raw: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub options: Option<Value>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct PostmanUrl {
72 pub raw: String,
73 pub host: Vec<String>,
74 pub path: Vec<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub query: Option<Vec<PostmanQueryParam>>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct PostmanQueryParam {
81 pub key: String,
82 pub value: String,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct PostmanVariable {
87 pub key: String,
88 pub value: String,
89 #[serde(rename = "type")]
90 pub var_type: String,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct InsomniaCollection {
96 pub _type: String,
97 pub __export_format: u8,
98 pub __export_date: String,
99 pub __export_source: String,
100 pub resources: Vec<InsomniaResource>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct InsomniaResource {
105 pub _id: String,
106 pub _type: String,
107 pub name: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub method: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub url: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub body: Option<Value>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub headers: Option<Vec<InsomniaHeader>>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct InsomniaHeader {
120 pub name: String,
121 pub value: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct HoppscotchCollection {
128 pub name: String,
129 pub requests: Vec<HoppscotchRequest>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub folders: Option<Vec<HoppscotchFolder>>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct HoppscotchRequest {
137 pub name: String,
138 pub method: String,
139 pub endpoint: String,
140 pub headers: Vec<HoppscotchHeader>,
141 pub params: Vec<HoppscotchParam>,
142 pub body: HoppscotchBody,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct HoppscotchHeader {
148 pub key: String,
149 pub value: String,
150 pub active: bool,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct HoppscotchParam {
156 pub key: String,
157 pub value: String,
158 pub active: bool,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct HoppscotchBody {
164 pub content_type: String,
165 pub body: String,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct HoppscotchFolder {
171 pub name: String,
172 pub requests: Vec<HoppscotchRequest>,
173}
174
175pub struct CollectionExporter {
177 base_url: String,
178}
179
180impl CollectionExporter {
181 pub fn new(base_url: String) -> Self {
182 Self { base_url }
183 }
184
185 pub fn to_postman(&self, spec: &crate::openapi::OpenApiSpec) -> PostmanCollection {
187 let mut items = Vec::new();
188
189 for (path, path_item_ref) in &spec.spec.paths.paths {
190 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
192 let operations = vec![
193 ("GET", path_item.get.as_ref()),
194 ("POST", path_item.post.as_ref()),
195 ("PUT", path_item.put.as_ref()),
196 ("DELETE", path_item.delete.as_ref()),
197 ("PATCH", path_item.patch.as_ref()),
198 ("HEAD", path_item.head.as_ref()),
199 ("OPTIONS", path_item.options.as_ref()),
200 ];
201
202 for (method, op_opt) in operations {
203 if let Some(op) = op_opt {
204 let name = op
205 .operation_id
206 .clone()
207 .or_else(|| op.summary.clone())
208 .unwrap_or_else(|| format!("{} {}", method, path));
209
210 let request = PostmanRequest {
211 method: method.to_string(),
212 header: vec![PostmanHeader {
213 key: "Content-Type".to_string(),
214 value: "application/json".to_string(),
215 header_type: "text".to_string(),
216 }],
217 body: if matches!(method, "POST" | "PUT" | "PATCH") {
218 Some(PostmanBody {
219 mode: "raw".to_string(),
220 raw: Some("{}".to_string()),
221 options: Some(serde_json::json!({
222 "raw": {
223 "language": "json"
224 }
225 })),
226 })
227 } else {
228 None
229 },
230 url: PostmanUrl {
231 raw: format!("{}{}", self.base_url, path),
232 host: vec![self.base_url.clone()],
233 path: path
234 .split('/')
235 .filter(|s| !s.is_empty())
236 .map(String::from)
237 .collect(),
238 query: None,
239 },
240 description: op.description.clone(),
241 };
242
243 items.push(PostmanItem {
244 name,
245 request,
246 response: None,
247 });
248 }
249 }
250 }
251 }
252
253 PostmanCollection {
254 info: PostmanInfo {
255 name: spec.spec.info.title.clone(),
256 description: spec.spec.info.description.clone().unwrap_or_default(),
257 schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
258 .to_string(),
259 version: Some(spec.spec.info.version.clone()),
260 },
261 item: items,
262 variable: Some(vec![PostmanVariable {
263 key: "baseUrl".to_string(),
264 value: self.base_url.clone(),
265 var_type: "string".to_string(),
266 }]),
267 }
268 }
269
270 pub fn to_insomnia(&self, spec: &crate::openapi::OpenApiSpec) -> InsomniaCollection {
272 let mut resources = Vec::new();
273
274 resources.push(InsomniaResource {
276 _id: "wrk_1".to_string(),
277 _type: "workspace".to_string(),
278 name: spec.spec.info.title.clone(),
279 method: None,
280 url: None,
281 body: None,
282 headers: None,
283 });
284
285 let mut id_counter = 1;
287 for (path, path_item_ref) in &spec.spec.paths.paths {
288 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
289 let operations = vec![
290 ("GET", path_item.get.as_ref()),
291 ("POST", path_item.post.as_ref()),
292 ("PUT", path_item.put.as_ref()),
293 ("DELETE", path_item.delete.as_ref()),
294 ("PATCH", path_item.patch.as_ref()),
295 ("HEAD", path_item.head.as_ref()),
296 ("OPTIONS", path_item.options.as_ref()),
297 ];
298
299 for (method, op_opt) in operations {
300 if let Some(op) = op_opt {
301 id_counter += 1;
302
303 let name = op
304 .operation_id
305 .clone()
306 .or_else(|| op.summary.clone())
307 .unwrap_or_else(|| format!("{} {}", method, path));
308
309 resources.push(InsomniaResource {
310 _id: format!("req_{}", id_counter),
311 _type: "request".to_string(),
312 name,
313 method: Some(method.to_string()),
314 url: Some(format!("{}{}", self.base_url, path)),
315 body: if matches!(method, "POST" | "PUT" | "PATCH") {
316 Some(serde_json::json!({
317 "mimeType": "application/json",
318 "text": "{}"
319 }))
320 } else {
321 None
322 },
323 headers: Some(vec![InsomniaHeader {
324 name: "Content-Type".to_string(),
325 value: "application/json".to_string(),
326 }]),
327 });
328 }
329 }
330 }
331 }
332
333 InsomniaCollection {
334 _type: "export".to_string(),
335 __export_format: 4,
336 __export_date: chrono::Utc::now().to_rfc3339(),
337 __export_source: "mockforge".to_string(),
338 resources,
339 }
340 }
341
342 pub fn to_hoppscotch(&self, spec: &crate::openapi::OpenApiSpec) -> HoppscotchCollection {
344 let mut requests = Vec::new();
345
346 for (path, path_item_ref) in &spec.spec.paths.paths {
347 if let openapiv3::ReferenceOr::Item(path_item) = path_item_ref {
348 let operations = vec![
349 ("GET", path_item.get.as_ref()),
350 ("POST", path_item.post.as_ref()),
351 ("PUT", path_item.put.as_ref()),
352 ("DELETE", path_item.delete.as_ref()),
353 ("PATCH", path_item.patch.as_ref()),
354 ("HEAD", path_item.head.as_ref()),
355 ("OPTIONS", path_item.options.as_ref()),
356 ];
357
358 for (method, op_opt) in operations {
359 if let Some(op) = op_opt {
360 let name = op
361 .operation_id
362 .clone()
363 .or_else(|| op.summary.clone())
364 .unwrap_or_else(|| format!("{} {}", method, path));
365
366 requests.push(HoppscotchRequest {
367 name,
368 method: method.to_string(),
369 endpoint: format!("{}{}", self.base_url, path),
370 headers: vec![HoppscotchHeader {
371 key: "Content-Type".to_string(),
372 value: "application/json".to_string(),
373 active: true,
374 }],
375 params: vec![],
376 body: HoppscotchBody {
377 content_type: "application/json".to_string(),
378 body: "{}".to_string(),
379 },
380 });
381 }
382 }
383 }
384 }
385
386 HoppscotchCollection {
387 name: spec.spec.info.title.clone(),
388 requests,
389 folders: None,
390 }
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn test_postman_collection_structure() {
400 let collection = PostmanCollection {
401 info: PostmanInfo {
402 name: "Test API".to_string(),
403 description: "Test description".to_string(),
404 schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
405 .to_string(),
406 version: Some("1.0.0".to_string()),
407 },
408 item: vec![],
409 variable: None,
410 };
411
412 assert_eq!(collection.info.name, "Test API");
413 }
414
415 #[test]
416 fn test_insomnia_collection_structure() {
417 let collection = InsomniaCollection {
418 _type: "export".to_string(),
419 __export_format: 4,
420 __export_date: "2024-01-01T00:00:00Z".to_string(),
421 __export_source: "mockforge".to_string(),
422 resources: vec![],
423 };
424
425 assert_eq!(collection._type, "export");
426 assert_eq!(collection.__export_format, 4);
427 }
428
429 #[test]
430 fn test_hoppscotch_collection_structure() {
431 let collection = HoppscotchCollection {
432 name: "Test API".to_string(),
433 requests: vec![],
434 folders: None,
435 };
436
437 assert_eq!(collection.name, "Test API");
438 }
439}