1use crate::{
2 error::SerializeError,
3 model::{self, CommentKind, HttpRestFile, ResponseHandler, WithDefault},
4};
5
6pub struct Serializer {}
7
8impl Serializer {
9 pub fn serialize_to_file(file_model: &HttpRestFile) -> Result<(), SerializeError> {
11 let mut path = (*file_model.path).clone();
12 if let Some(ext) = file_model.extension.as_ref() {
13 path = file_model.path.with_extension(ext.to_string());
14 }
15 let content = Serializer::serialize_requests(
16 &file_model.requests.iter().collect::<Vec<&model::Request>>()[..],
17 );
18
19 match std::fs::write(path, content) {
20 Ok(_) => Ok(()),
21 Err(io_err) => Err(SerializeError::IoError(io_err.to_string())),
22 }
23 }
24
25 pub fn serialize_requests(requests: &[&model::Request]) -> String {
27 let mut result = String::new();
28 let num_requests = requests.len();
29 for (index, request) in requests.iter().enumerate() {
30 if index > 0
32 && !request.comments.first().map_or(false, |comment| {
33 comment.kind == CommentKind::RequestSeparator
34 })
35 {
36 result.push_str(crate::parser::REQUEST_SEPARATOR);
37 }
38 result.push_str(&Serializer::serialize_request(request));
39
40 if num_requests > 1 && index != num_requests - 1 {
42 result.push('\n');
43 }
44 }
45 result
46 }
47
48 pub fn serialize_request(request: &model::Request) -> String {
50 let mut result = String::new();
51 let comments_string = request
52 .comments
53 .iter()
54 .map(|comment| comment.to_string())
55 .collect::<Vec<String>>()
56 .join("\n");
57
58 if !comments_string.is_empty() {
59 result.push_str(&comments_string);
60 result.push('\n');
61 }
62
63 if let Some(ref name) = request.name {
64 result.push_str(&format!("# @name={}\n", name));
65 }
66
67 result.push_str(&request.settings.serialized());
68
69 if let Some(pre_request_script) = &request.pre_request_script {
70 result.push_str(&pre_request_script.to_string());
71 result.push('\n');
72 }
73
74 if !request.request_line.target.is_missing() {
78 if let WithDefault::Some(method) = &request.request_line.method {
79 result.push_str(&method.to_string());
80 result.push(' ');
81 }
82 result.push_str(&request.request_line.target.to_string());
83 if let WithDefault::Some(ref http_version) = request.request_line.http_version {
84 result.push(' ');
85 result.push_str(&http_version.to_string());
86 }
87 }
88
89 if !request.headers.is_empty() {
90 result.push('\n');
91 let headers = request
92 .headers
93 .iter()
94 .map(|header| header.to_string())
95 .collect::<Vec<String>>()
96 .join("\n");
97 result.push_str(&headers);
98 result.push('\n')
100 }
101
102 if request.body.is_present() {
103 result.push('\n');
104 result.push_str(&request.body.to_string());
105 }
106
107 if let Some(response_handler) = &request.response_handler {
108 result.push_str("\n\n");
109 let string = match response_handler {
110 ResponseHandler::FromFilepath(path) => format!("> {}", path),
111 ResponseHandler::Script(script) => format!("> {{%{}%}}", script),
112 };
113 result.push_str(&string);
114 }
115
116 if let Some(ref save_response) = request.save_response {
117 result.push_str("\n\n");
118 let string = match save_response {
119 model::SaveResponse::RewriteFile(path) => format!(">>! {}", path.to_string_lossy()),
120 model::SaveResponse::NewFileIfExists(path) => {
121 format!(">> {}", path.to_string_lossy())
122 }
123 };
124 result.push_str(&string);
125 }
126
127 result
128 }
129}
130#[cfg(test)]
131mod tests {
132 use std::path::PathBuf;
133
134 use super::*;
135 use crate::{model::*, Parser};
136 use pretty_assertions::assert_eq;
137
138 #[test]
139 pub fn serialize_comments() {
140 let request = Request {
141 name: Some("RequestName".to_string()),
142 headers: vec![],
143 comments: vec![Comment {
144 value: "The Request".to_string(),
145 kind: CommentKind::RequestSeparator,
146 }],
147 settings: RequestSettings {
148 no_redirect: Some(true),
149 no_log: Some(true),
150 no_cookie_jar: Some(true),
151 },
152 request_line: RequestLine {
153 method: WithDefault::Some(HttpMethod::GET),
154 target: RequestTarget::from("https://httpbin.org"),
155 http_version: WithDefault::default(),
156 },
157 body: RequestBody::None,
158 pre_request_script: None,
159 response_handler: None,
160 save_response: None,
161 };
162 let expected = r"### The Request
163# @name=RequestName
164# @no-redirect
165# @no-log
166# @no-cookie-jar
167GET https://httpbin.org";
168
169 let serialized = Serializer::serialize_requests(&[&request]);
170 assert_eq!(serialized, expected);
171 }
172
173 #[test]
174 pub fn serialize_only_url() {
175 let request = Request {
176 name: None,
177 headers: vec![],
178 comments: vec![],
179 settings: RequestSettings {
180 no_redirect: None,
181 no_log: None,
182 no_cookie_jar: None,
183 },
184 request_line: RequestLine {
185 method: WithDefault::default(),
186 target: RequestTarget::from("https://httpbin.org"),
187 http_version: WithDefault::default(),
188 },
189 body: RequestBody::None,
190 pre_request_script: None,
191 response_handler: None,
192 save_response: None,
193 };
194 let expected = r"https://httpbin.org";
195
196 let serialized = Serializer::serialize_requests(&[&request]);
197 assert_eq!(serialized, expected);
198 }
199
200 #[test]
201 pub fn serialize_method_url() {
202 let request = Request {
203 name: None,
204 headers: vec![],
205 comments: vec![],
206 settings: RequestSettings {
207 no_redirect: None,
208 no_log: None,
209 no_cookie_jar: None,
210 },
211 request_line: RequestLine {
212 method: WithDefault::Some(HttpMethod::GET),
213 target: RequestTarget::from("https://httpbin.org"),
214 http_version: WithDefault::default(),
215 },
216 body: RequestBody::None,
217 pre_request_script: None,
218 response_handler: None,
219 save_response: None,
220 };
221 let expected = r"GET https://httpbin.org";
222
223 let serialized = Serializer::serialize_requests(&[&request]);
224 assert_eq!(serialized, expected);
225 }
226
227 #[test]
228 pub fn serialize_method_url_http_version() {
229 let request = Request {
230 name: None,
231 headers: vec![],
232 comments: vec![],
233 settings: RequestSettings {
234 no_redirect: None,
235 no_log: None,
236 no_cookie_jar: None,
237 },
238 request_line: RequestLine {
239 method: WithDefault::Some(HttpMethod::GET),
240 target: RequestTarget::from("https://httpbin.org"),
241 http_version: WithDefault::Some(HttpVersion { major: 1, minor: 1 }),
242 },
243 body: RequestBody::None,
244 pre_request_script: None,
245 response_handler: None,
246 save_response: None,
247 };
248 let expected = r"GET https://httpbin.org HTTP/1.1";
249
250 let serialized = Serializer::serialize_requests(&[&request]);
251 assert_eq!(serialized, expected);
252 }
253
254 #[test]
255 pub fn serialize_custom_method() {
256 let request = Request {
257 name: None,
258 headers: vec![],
259 comments: vec![],
260 settings: RequestSettings {
261 no_redirect: None,
262 no_log: None,
263 no_cookie_jar: None,
264 },
265 request_line: RequestLine {
266 method: WithDefault::Some(HttpMethod::CUSTOM("CustomMethod".to_string())),
267 target: RequestTarget::from("https://httpbin.org"),
268 http_version: WithDefault::Some(HttpVersion { major: 2, minor: 1 }),
269 },
270 body: RequestBody::None,
271 pre_request_script: None,
272 response_handler: None,
273 save_response: None,
274 };
275 let expected = r"CustomMethod https://httpbin.org HTTP/2.1";
276 let serialized = Serializer::serialize_requests(&[&request]);
277 assert_eq!(serialized, expected);
278 }
279
280 #[test]
281 pub fn serialize_with_form_url_encoded() {
282 let request = Request {
283 name: None,
284 headers: vec![Header::new(
285 "Content-Type",
286 "application/x-www-form-urlencoded",
287 )],
288 request_line: RequestLine {
289 method: WithDefault::Some(HttpMethod::POST),
290 target: RequestTarget::from("https://httpbin.org/post"),
291 http_version: WithDefault::default(),
292 },
293 body: RequestBody::UrlEncoded {
294 url_encoded_params: vec![
295 UrlEncodedParam::new("abc", "def"),
296 UrlEncodedParam::new("ghi", "jkl"),
297 ],
298 },
299
300 ..Default::default()
301 };
302 let expected = r####"POST https://httpbin.org/post
303Content-Type: application/x-www-form-urlencoded
304
305abc=def&ghi=jkl"####;
306
307 let serialized = Serializer::serialize_requests(&[&request]);
308 assert_eq!(serialized, expected);
309 }
310
311 #[test]
312 pub fn serialize_with_text_body() {
313 let request = Request {
314 name: None,
315 headers: vec![Header::new("Content-Type", "application/json")],
316 comments: vec![],
317 settings: RequestSettings {
318 no_redirect: None,
319 no_log: None,
320 no_cookie_jar: None,
321 },
322 request_line: RequestLine {
323 method: WithDefault::Some(HttpMethod::POST),
324 target: RequestTarget::from("https://httpbin.org/post"),
325 http_version: WithDefault::default(),
326 },
327 body: RequestBody::Raw {
328 data: DataSource::Raw(
329 r####"{
330 "name": "John Doe",
331 "age": 30,
332 "email": "johndoe@example.com",
333 "address": {
334 "street": "123 Main St",
335 "city": "Anytown",
336 "state": "CA",
337 "zip": "12345"
338 },
339 "phoneNumbers": [
340 {
341 "type": "home",
342 "number": "555-555-1234"
343 },
344 {
345 "type": "work",
346 "number": "555-555-5678"
347 }
348 ],
349 "isActive": true
350}"####
351 .to_string(),
352 ),
353 },
354 pre_request_script: None,
355 response_handler: None,
356 save_response: None,
357 };
358 let expected = r####"POST https://httpbin.org/post
359Content-Type: application/json
360
361{
362 "name": "John Doe",
363 "age": 30,
364 "email": "johndoe@example.com",
365 "address": {
366 "street": "123 Main St",
367 "city": "Anytown",
368 "state": "CA",
369 "zip": "12345"
370 },
371 "phoneNumbers": [
372 {
373 "type": "home",
374 "number": "555-555-1234"
375 },
376 {
377 "type": "work",
378 "number": "555-555-5678"
379 }
380 ],
381 "isActive": true
382}"####;
383
384 let serialized = Serializer::serialize_requests(&[&request]);
385 assert_eq!(serialized, expected);
386 }
387
388 #[test]
389 pub fn serialize_with_file() {
390 let request = Request {
391 name: None,
392 headers: vec![Header::new("Content-Type", "application/json")],
393 comments: vec![],
394 settings: RequestSettings {
395 no_redirect: None,
396 no_log: None,
397 no_cookie_jar: None,
398 },
399 request_line: RequestLine {
400 method: WithDefault::Some(HttpMethod::POST),
401 target: RequestTarget::from("https://httpbin.org/post"),
402 http_version: WithDefault::default(),
403 },
404 body: RequestBody::Raw {
405 data: DataSource::FromFilepath("/path/to/file.json".to_string()),
406 },
407 pre_request_script: None,
408 response_handler: None,
409 save_response: None,
410 };
411 let expected = r####"POST https://httpbin.org/post
412Content-Type: application/json
413
414< /path/to/file.json"####;
415
416 let serialized = Serializer::serialize_requests(&[&request]);
417 assert_eq!(serialized, expected);
418 }
419
420 #[test]
421 pub fn serialize_with_redirect() {
422 let request = Request {
423 name: None,
424 headers: vec![Header::new("Content-Type", "application/json")],
425 comments: vec![],
426 settings: RequestSettings {
427 no_redirect: None,
428 no_log: None,
429 no_cookie_jar: None,
430 },
431 request_line: RequestLine {
432 method: WithDefault::Some(HttpMethod::POST),
433 target: RequestTarget::from("https://httpbin.org/post"),
434 http_version: WithDefault::default(),
435 },
436 body: RequestBody::Raw {
437 data: DataSource::FromFilepath("/path/to/file.json".to_string()),
438 },
439 pre_request_script: None,
440 response_handler: None,
441 save_response: Some(SaveResponse::NewFileIfExists(PathBuf::from(
442 "./path/to/out.json",
443 ))),
444 };
445 let expected = r####"POST https://httpbin.org/post
446Content-Type: application/json
447
448< /path/to/file.json
449
450>> ./path/to/out.json"####;
451
452 let serialized = Serializer::serialize_requests(&[&request]);
453 assert_eq!(serialized, expected);
454 }
455
456 #[test]
457 pub fn serialize_with_headers() {
458 let request = Request {
459 name: None,
460 headers: vec![Header::new("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36")
461, Header::new("Accept-Language", "en-US,en;q=0.9,es;q=0.8"),
462 Header::new("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"),
464Header::new("Cache-Control", "max-age=3600")
465 ],
466 comments: vec![],
467 settings: RequestSettings {
468 no_redirect: None,
469 no_log: None,
470 no_cookie_jar: None,
471 },
472 request_line: RequestLine {
473 method: WithDefault::Some(HttpMethod::POST),
474 target: RequestTarget::from("https://httpbin.org/post"),
475 http_version: WithDefault::default(),
476 },
477 body: RequestBody::None,
478 pre_request_script: None,
479 response_handler: None,
480 save_response: None,
481 };
482 let expected = r"POST https://httpbin.org/post
484User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
485Accept-Language: en-US,en;q=0.9,es;q=0.8
486Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
487Cache-Control: max-age=3600
488";
489 let serialized = Serializer::serialize_requests(&[&request]);
490 assert_eq!(serialized, expected);
491 }
492
493 #[test]
494 pub fn serialize_all() {
495 let request = Request {
496 name: Some("RequestName".to_string()),
497 headers: vec![Header::new("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36")
498, Header::new("Accept-Language", "en-US,en;q=0.9,es;q=0.8"),
499 Header::new("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"),
501Header::new("Cache-Control", "max-age=3600"),
502 Header::new("Content-Type", "application/json")
503 ],
504comments: vec![Comment {
505 value: "The Request".to_string(),
506 kind: CommentKind::RequestSeparator,
507 }],
508 settings: RequestSettings {
509 no_redirect: Some(true),
510 no_log: Some(true),
511 no_cookie_jar: Some(true),
512 },
513 request_line: RequestLine {
514 method: WithDefault::Some(HttpMethod::POST),
515 target: RequestTarget::from("https://httpbin.org/post"),
516 http_version: WithDefault::Some(HttpVersion { major: 2, minor: 1 }),
517 },
518 body: RequestBody::Raw { data: DataSource::Raw(r####"{
519 "name": "John Doe",
520 "age": 30,
521 "email": "johndoe@example.com",
522 "address": {
523 "street": "123 Main St",
524 "city": "Anytown",
525 "state": "CA",
526 "zip": "12345"
527 },
528 "phoneNumbers": [
529 {
530 "type": "home",
531 "number": "555-555-1234"
532 },
533 {
534 "type": "work",
535 "number": "555-555-5678"
536 }
537 ],
538 "isActive": true
539}"####.to_string() )},
540 pre_request_script: Some(PreRequestScript::Script(r####" request.variables.set("firstname", "John") "####.to_string())),
541 response_handler: Some(ResponseHandler::FromFilepath(r####"/path/to/responseHandler.js"####.to_string())),
542 save_response: Some(SaveResponse::RewriteFile(PathBuf::from("/path/to/out_file"))),
543 };
544
545 let expected = r####"### The Request
547# @name=RequestName
548# @no-redirect
549# @no-log
550# @no-cookie-jar
551< {% request.variables.set("firstname", "John") %}
552POST https://httpbin.org/post HTTP/2.1
553User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
554Accept-Language: en-US,en;q=0.9,es;q=0.8
555Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
556Cache-Control: max-age=3600
557Content-Type: application/json
558
559{
560 "name": "John Doe",
561 "age": 30,
562 "email": "johndoe@example.com",
563 "address": {
564 "street": "123 Main St",
565 "city": "Anytown",
566 "state": "CA",
567 "zip": "12345"
568 },
569 "phoneNumbers": [
570 {
571 "type": "home",
572 "number": "555-555-1234"
573 },
574 {
575 "type": "work",
576 "number": "555-555-5678"
577 }
578 ],
579 "isActive": true
580}
581
582> /path/to/responseHandler.js
583
584>>! /path/to/out_file"####;
585 let serialized = Serializer::serialize_requests(&[&request]);
586 assert_eq!(serialized, expected);
587
588 let file_parse_result = Parser::parse(&serialized, false);
590 assert_eq!(file_parse_result.errs, vec![]);
591 assert_eq!(file_parse_result.requests.len(), 1);
592 assert_eq!(
593 file_parse_result.requests.iter().collect::<Vec<&Request>>(),
594 vec![&request]
595 );
596 }
597
598 #[test]
599 pub fn serialize_all_multipart() {
600 let request = Request {
601 name: Some("RequestName".to_string()),
602 headers: vec![Header::new("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36")
603, Header::new("Accept-Language", "en-US,en;q=0.9,es;q=0.8"),
604 Header::new("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"),
606Header::new("Cache-Control", "max-age=3600"),
607 Header::new("Content-Type", "multipart/form-data; boundary=WebAppBoundary")
608 ],
609comments: vec![Comment {
610 value: "The Request".to_string(),
611 kind: CommentKind::RequestSeparator,
612 }],
613 settings: RequestSettings {
614 no_redirect: Some(true),
615 no_log: Some(true),
616 no_cookie_jar: Some(true),
617 },
618 request_line: RequestLine {
619 method: WithDefault::Some(HttpMethod::POST),
620 target: RequestTarget::from("https://httpbin.org/post"),
621 http_version: WithDefault::Some(HttpVersion { major: 2, minor: 1 }),
622 },
623 body: model::RequestBody::Multipart {
624 boundary: "WebAppBoundary".to_string(),
625 parts: vec![
626 Multipart {
627 data: DataSource::Raw("Name".to_string()),
628 disposition: DispositionField::new("element-name"),
629 headers: vec![Header {
630 key: "Content-Type".to_string(),
631 value: "text/plain".to_string()
632 }]
633 },
634 Multipart {
635 disposition: DispositionField::new_with_filename("data", Some("data.json")),
636 data: DataSource::FromFilepath("./request-form-data.json".to_string()),
637 headers: vec![Header {
638 key: "Content-Type".to_string(),
639 value: "application/json".to_string()
640 }]
641 }
642 ]
643 },
644
645 pre_request_script: Some(PreRequestScript::Script("\nrequest.variables.set(\"firstname\", \"John\")\n".to_string())),
646 response_handler: Some(ResponseHandler::Script("\n client.global.set(\"my_cookie\", response.headers.valuesOf(\"Set-Cookie\")[0]);\n".to_string())),
647 save_response: Some(SaveResponse::NewFileIfExists(PathBuf::from("/path/to/out_file"))),
648 };
649
650 let expected = r####"### The Request
652# @name=RequestName
653# @no-redirect
654# @no-log
655# @no-cookie-jar
656< {%
657request.variables.set("firstname", "John")
658%}
659POST https://httpbin.org/post HTTP/2.1
660User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
661Accept-Language: en-US,en;q=0.9,es;q=0.8
662Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
663Cache-Control: max-age=3600
664Content-Type: multipart/form-data; boundary=WebAppBoundary
665
666--WebAppBoundary
667Content-Disposition: form-data; name="element-name"
668Content-Type: text/plain
669
670Name
671--WebAppBoundary
672Content-Disposition: form-data; name="data"; filename="data.json"
673Content-Type: application/json
674
675< ./request-form-data.json
676--WebAppBoundary--
677
678> {%
679 client.global.set("my_cookie", response.headers.valuesOf("Set-Cookie")[0]);
680%}
681
682>> /path/to/out_file"####;
683 let serialized = Serializer::serialize_requests(&[&request]);
684 assert_eq!(serialized, expected);
685
686 let file_parse_result = Parser::parse(&serialized, false);
688 assert_eq!(file_parse_result.errs, vec![]);
689 assert_eq!(file_parse_result.requests.len(), 1);
690 assert_eq!(
691 file_parse_result.requests.iter().collect::<Vec<&Request>>(),
692 vec![&request]
693 );
694 }
695}