http_rest_file/
serializer.rs

1use crate::{
2    error::SerializeError,
3    model::{self, CommentKind, HttpRestFile, ResponseHandler, WithDefault},
4};
5
6pub struct Serializer {}
7
8impl Serializer {
9    /// Serializes the request models within a `HttpRestFile` to the `HttpRestFile.path`
10    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    /// Serialize all requests to a `String` delimited by the `crate::parser::REQUEST_SEPARATOR`
26    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 no request separator is present between the requests then create one
31            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            // insert new line between requests
41            if num_requests > 1 && index != num_requests - 1 {
42                result.push('\n');
43            }
44        }
45        result
46    }
47
48    /// Serialize a single `model::Request` to a `String`
49    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        // only output method and http version if there is some target url,
75        // otherwise when reparsing the method such as 'GET' will be parsed as the url!
76        // request line has format '[method] url [httpversion]'
77        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            // an empty newline is required after the headers
99            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                // fake token
463                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        // we expect a newline after the headers
483        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                // fake token
500                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        // we expect a newline after the headers
546        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        // reparsing should return the same model
589        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                // fake token
605                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        // we expect a newline after the headers
651        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        // reparsing should return the same model
687        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}