hitt_parser/
lib.rs

1use error::RequestParseError;
2use header::{parse_header, HeaderToken};
3use method::parse_method_input;
4use uri::parse_uri_input;
5use variables::{parse_variable, parse_variable_declaration};
6use version::parse_http_version;
7
8pub mod error;
9mod header;
10mod method;
11mod uri;
12mod variables;
13mod version;
14
15#[derive(Copy, Clone, PartialEq)]
16enum ParserMode {
17    Request,
18    Headers,
19    Body,
20}
21
22#[derive(Debug)]
23enum RequestToken {
24    Method(http::method::Method),
25    Uri(http::uri::Uri),
26    HttpVersion(http::version::Version),
27    Header(HeaderToken),
28    Body(Option<String>),
29}
30
31#[inline]
32fn to_enum_chars(input: &str) -> core::iter::Enumerate<core::str::Chars> {
33    input.chars().enumerate()
34}
35
36#[inline]
37fn tokenize(
38    buffer: &str,
39    input_variables: &std::collections::HashMap<String, String>,
40) -> Result<Vec<RequestToken>, RequestParseError> {
41    let mut tokens: Vec<RequestToken> = Vec::new();
42
43    let mut parser_mode = ParserMode::Request;
44
45    let mut body_parts: Vec<String> = Vec::new();
46
47    let mut vars = input_variables.to_owned();
48
49    for line in buffer.lines() {
50        let trimmed_line = line.trim();
51
52        // check if line is comment (#) OR requests break (###)
53        if trimmed_line.starts_with('#') {
54            if trimmed_line.starts_with("###") && parser_mode != ParserMode::Request {
55                if body_parts.is_empty() {
56                    tokens.push(RequestToken::Body(None));
57                } else {
58                    tokens.push(RequestToken::Body(Some(body_parts.join("\n"))));
59
60                    body_parts.clear();
61                };
62
63                parser_mode = ParserMode::Request;
64            }
65
66            if parser_mode == ParserMode::Request {
67                continue;
68            }
69        } else if trimmed_line.starts_with("//") {
70            // check if line is comment (//)
71            if parser_mode == ParserMode::Request {
72                continue;
73            }
74        }
75
76        match parser_mode {
77            ParserMode::Request => {
78                if trimmed_line.starts_with('@') {
79                    let mut chrs = to_enum_chars(trimmed_line);
80
81                    // move forward once since we don't care about the '@'
82                    chrs.next();
83
84                    if let Some((name, value)) = parse_variable_declaration(&mut chrs, &vars)? {
85                        vars.insert(name, value);
86                        continue;
87                    }
88                }
89
90                if !trimmed_line.is_empty() {
91                    let mut chrs = to_enum_chars(trimmed_line);
92                    let method = parse_method_input(&mut chrs, &vars)?;
93
94                    tokens.push(RequestToken::Method(method));
95
96                    let uri = parse_uri_input(&mut chrs, &vars)?;
97
98                    tokens.push(RequestToken::Uri(uri));
99
100                    if let Some(http_version) = parse_http_version(&mut chrs, &vars) {
101                        tokens.push(RequestToken::HttpVersion(http_version));
102                    }
103
104                    parser_mode = ParserMode::Headers;
105                }
106            }
107
108            ParserMode::Headers => {
109                if trimmed_line.is_empty() {
110                    parser_mode = ParserMode::Body;
111                } else if let Some(header_token) =
112                    parse_header(&mut to_enum_chars(trimmed_line), &vars)?
113                {
114                    tokens.push(RequestToken::Header(header_token));
115                }
116            }
117
118            ParserMode::Body => {
119                let mut current_line = String::new();
120                let mut chars = to_enum_chars(line);
121
122                while let Some((_, ch)) = chars.next() {
123                    if ch == '{' {
124                        // FIXME: remove cloning of enumerator
125                        if let Some((var, jumps)) = parse_variable(&mut chars.clone()) {
126                            if let Some(variable_value) = vars.get(&var) {
127                                current_line.push_str(variable_value);
128
129                                for _ in 0..jumps {
130                                    chars.next();
131                                }
132
133                                continue;
134                            }
135
136                            return Err(RequestParseError::VariableNotFound(var));
137                        }
138                    }
139
140                    current_line.push(ch);
141                }
142
143                body_parts.push(current_line);
144            }
145        };
146    }
147
148    if !body_parts.is_empty() {
149        tokens.push(RequestToken::Body(Some(body_parts.join("\n"))));
150    }
151
152    Ok(tokens)
153}
154
155#[cfg(test)]
156mod test_tokenize {
157    use core::fmt::Write as _;
158
159    use once_cell::sync::Lazy;
160
161    use crate::{error::RequestParseError, tokenize, RequestToken};
162
163    static EMPTY_VARS: Lazy<std::collections::HashMap<String, String>> =
164        Lazy::new(std::collections::HashMap::new);
165
166    #[test]
167    fn should_return_a_list_of_tokens() {
168        let method_input = "GET";
169        let uri_input = "https://mhouge.dk/";
170        let http_version = "HTTP/2";
171        let header1_key = "content-type";
172        let header1_value = "application/json";
173        let body_input = "{ \"key\": \"value\"  }";
174
175        let input_request =
176            format!("{method_input} {uri_input} {http_version}\n{header1_key}: {header1_value}\n\n{body_input}");
177
178        let tokens =
179            tokenize(&input_request, &EMPTY_VARS).expect("it to return Result<Vec<RequestToken>>");
180
181        assert_eq!(tokens.len(), 5);
182
183        for token in tokens {
184            match token {
185                RequestToken::Uri(uri_token) => assert_eq!(uri_input, uri_token.to_string()),
186                RequestToken::Method(method_token) => {
187                    assert_eq!(method_input, method_token.as_str());
188                }
189                RequestToken::Header(header_token) => {
190                    assert_eq!(header1_key, header_token.key.to_string());
191
192                    assert_eq!(
193                        header1_value,
194                        header_token
195                            .value
196                            .to_str()
197                            .expect("value to be a valid str")
198                    );
199                }
200
201                RequestToken::Body(body_token) => {
202                    assert!(body_token.is_some());
203
204                    let body_inner = body_token.expect("body to be defined");
205
206                    assert_eq!(body_input, body_inner);
207                }
208
209                RequestToken::HttpVersion(version_token) => {
210                    assert_eq!(version_token, http::version::Version::HTTP_2);
211                }
212            }
213        }
214    }
215
216    #[test]
217    fn it_should_be_able_to_parse_multiple_requests() {
218        let uri = "https://mhouge.dk/";
219
220        let methods = [
221            http::Method::GET,
222            http::Method::PUT,
223            http::Method::POST,
224            http::Method::PATCH,
225            http::Method::DELETE,
226            http::Method::OPTIONS,
227            http::Method::HEAD,
228            http::Method::TRACE,
229            http::Method::CONNECT,
230        ];
231
232        let versions = [
233            http::Version::HTTP_09,
234            http::Version::HTTP_10,
235            http::Version::HTTP_11,
236            http::Version::HTTP_2,
237            http::Version::HTTP_3,
238        ];
239
240        let header_key = "x-test-header";
241        let header_value = "test-value";
242
243        let body = "mads was here\n".to_owned();
244
245        let mut input = String::new();
246
247        let mut input_request_index: u16 = 0;
248
249        for method in &methods {
250            for version in &versions {
251                writeln!(input, "{method} {uri} {version:?}").expect("it to write");
252                writeln!(input, "{header_key}: {header_value}\n").expect("it to write");
253
254                if input_request_index % 2 == 0 {
255                    writeln!(input, "{body}").expect("it to write");
256                }
257
258                writeln!(input, "###\n").expect("it to write");
259
260                input_request_index += 1;
261            }
262        }
263
264        let tokens = tokenize(&input, &EMPTY_VARS).expect("it to return a list of tokens");
265
266        assert_eq!(tokens.len(), methods.len() * versions.len() * 5);
267
268        let mut output_request_index: u16 = 0;
269        let mut token_index = 0;
270
271        let body_option = Some(body);
272
273        for method in &methods {
274            for version in &versions {
275                let method_token = tokens.get(token_index).expect("it to be a method token");
276                assert!(matches!(method_token, RequestToken::Method(m) if m == method));
277                token_index += 1;
278
279                let uri_token = tokens.get(token_index).expect("it to be an uri token");
280                assert!(matches!(uri_token, RequestToken::Uri(u) if u == uri));
281                token_index += 1;
282
283                let version_token = tokens.get(token_index).expect("it to be a version token");
284                assert!(matches!(version_token, RequestToken::HttpVersion(v) if v == version));
285                token_index += 1;
286
287                let header_token = tokens.get(token_index).expect("it to be a header token");
288                assert!(
289                    matches!(header_token, RequestToken::Header(h) if h.key  == header_key && h.value == header_value)
290                );
291                token_index += 1;
292
293                let body_token = tokens.get(token_index).expect("it to be a body token");
294                if output_request_index % 2 == 0 {
295                    assert!(matches!(body_token, RequestToken::Body(b) if b == &body_option));
296                } else {
297                    assert!(matches!(body_token, RequestToken::Body(b) if b.is_none()));
298                }
299                token_index += 1;
300
301                output_request_index += 1;
302            }
303        }
304    }
305
306    #[test]
307    fn it_should_ignore_comments() {
308        let input = "
309// comment 1
310# comment 2
311
312DELETE https://mhouge.dk/";
313
314        let tokens = tokenize(input, &EMPTY_VARS).expect("it to parse successfully");
315
316        assert_eq!(tokens.len(), 2);
317
318        let method_token = tokens.first().expect("it to be some");
319
320        assert!(
321            matches!(method_token, RequestToken::Method(m) if m == http::method::Method::DELETE)
322        );
323
324        let uri_token = tokens.get(1).expect("it to be Some");
325
326        let expected_uri = "https://mhouge.dk/";
327
328        assert!(matches!(uri_token, RequestToken::Uri(uri) if uri == expected_uri));
329    }
330
331    #[test]
332    fn it_should_only_check_for_comments_when_parsermode_request() {
333        let url = "https://mhouge.dk/api/something/?refresh=true";
334        let method = "DELETE";
335
336        let status_line = format!("{method} {url}");
337
338        for comment_style in ["#", "//"] {
339            let body = format!("{comment_style} this is not a comment");
340
341            let hashtag = format!(
342                "{status_line}
343
344{body}"
345            );
346
347            let tokens = tokenize(&hashtag, &EMPTY_VARS).expect("it to parse successfully");
348
349            assert_eq!(tokens.len(), 3);
350
351            let method_token = tokens.first().expect("it to be some");
352
353            assert!(
354                matches!(method_token, RequestToken::Method(m) if m == http::method::Method::DELETE)
355            );
356
357            let uri_token = tokens.get(1).expect("it to be Some");
358
359            assert!(matches!(uri_token, RequestToken::Uri(u) if u == url));
360
361            let body_token = tokens.get(2).expect("it to be Some");
362
363            assert!(matches!(body_token, RequestToken::Body(b) if b == &Some(body)));
364        }
365    }
366
367    #[test]
368    fn it_should_support_variables() {
369        {
370            let input = "
371@method = GET
372@host = https://mhouge.dk
373@path = /api
374@query_value = mads@mhouge.dk
375@body_input  = { \"key\": \"value\" }
376
377{{method}} {{host}}{{path}}?email={{query_value}}
378
379{{ body_input }}";
380
381            let tokens = tokenize(input, &EMPTY_VARS).expect("it to tokenize successfully");
382
383            assert_eq!(tokens.len(), 3);
384
385            let method_token = tokens.first().expect("it to be some");
386
387            assert!(
388                matches!(method_token, RequestToken::Method(m) if m == http::method::Method::GET)
389            );
390
391            let uri_token = tokens.get(1).expect("it to be Some");
392
393            let expected_uri = "https://mhouge.dk/api?email=mads@mhouge.dk";
394
395            assert!(matches!(uri_token, RequestToken::Uri(uri) if uri == expected_uri));
396
397            let body_token = tokens.get(2).expect("it to be set");
398
399            let expected_body_value = "{ \"key\": \"value\" }";
400
401            assert!(matches!(
402                body_token,
403                RequestToken::Body(value)
404                if value.clone().expect( "value to exist") == expected_body_value
405            ));
406        };
407
408        {
409            let input = "
410GET https://mhouge.dk/
411
412{{ body_input }}";
413
414            let tokens = tokenize(input, &EMPTY_VARS).expect_err("it to return an error");
415
416            assert!(matches!(
417                tokens,
418                RequestParseError::VariableNotFound(var)
419                if var == "body_input"
420            ));
421        }
422    }
423
424    #[test]
425    fn it_should_support_input_variables() {
426        let vars = std::collections::HashMap::from([
427            ("method".to_owned(), "GET".to_owned()),
428            ("host".to_owned(), "https://mhouge.dk".to_owned()),
429            ("path".to_owned(), "/api".to_owned()),
430            ("query_value".to_owned(), "mads@mhouge.dk".to_owned()),
431            ("body_input".to_owned(), "{ \"key\": \"value\" }".to_owned()),
432        ]);
433
434        let input = "
435{{method}} {{host}}{{path}}?email={{query_value}}
436
437{{ body_input }}";
438
439        let tokens = tokenize(input, &vars).expect("it to tokenize successfully");
440
441        assert_eq!(tokens.len(), 3);
442
443        let method_token = tokens.first().expect("it to return token");
444
445        assert!(
446            matches!(method_token, RequestToken::Method(method) if method == http::Method::GET)
447        );
448
449        let expected_uri = "https://mhouge.dk/api?email=mads@mhouge.dk";
450
451        let uri_token = tokens.get(1).expect("it to return an uri");
452
453        assert!(matches!(uri_token, RequestToken::Uri(uri) if uri == expected_uri));
454
455        let body_token = tokens.get(2).expect("it to return a token");
456
457        let expected_body = "{ \"key\": \"value\" }";
458
459        assert!(
460            matches!(body_token, RequestToken::Body(Some(output_body)) if output_body == expected_body)
461        );
462    }
463
464    #[test]
465    fn it_should_raise_error_if_missing_variable() {
466        let input = "GET {{missing_variable}}";
467
468        let err = tokenize(input, &EMPTY_VARS).expect_err("it to be a missing variable error");
469
470        assert!(matches!(err, RequestParseError::VariableNotFound(v) if v == "missing_variable"));
471    }
472
473    #[test]
474    fn input_variables_should_be_overwritten_by_local_variables() {
475        let vars = std::collections::HashMap::from([("method".to_owned(), "PUT".to_owned())]);
476
477        let input = "
478@method = POST
479
480{{method}} https://mhouge.dk/";
481
482        let tokens = tokenize(input, &vars).expect("it to parse successfully");
483
484        assert_eq!(tokens.len(), 2);
485
486        let method_token = tokens.first().expect("it to return token");
487
488        assert!(
489            matches!(method_token, RequestToken::Method(method) if method == http::Method::POST)
490        );
491
492        let uri_token = tokens.get(1).expect("it to return token");
493
494        assert!(matches!(uri_token, RequestToken::Uri(uri) if uri == "https://mhouge.dk/"));
495    }
496
497    #[test]
498    fn it_should_ignore_triple_hashtag_when_in_parsermode_request() {
499        let input = "
500###
501
502###
503
504###
505
506OPTIONS https://mhouge.dk/
507###
508###
509###
510
511HEAD https://mhouge.dk/blog/";
512
513        let output = tokenize(input, &EMPTY_VARS).expect("it to parse");
514
515        assert_eq!(output.len(), 5);
516
517        {
518            let method = output.first().expect("it to return a method token");
519            assert!(matches!(method, RequestToken::Method(m) if m == http::Method::OPTIONS));
520
521            let uri = output.get(1).expect("it to return a uri token");
522            assert!(matches!(uri, RequestToken::Uri(u) if u == "https://mhouge.dk/"));
523
524            let body = output.get(2).expect("it to be an empty body token");
525            assert!(matches!(body, RequestToken::Body(b) if b.is_none()));
526        };
527
528        {
529            let method = output.get(3).expect("it to return a method token");
530            assert!(matches!(method, RequestToken::Method(m) if m == http::Method::HEAD));
531
532            let uri = output.get(4).expect("it to return a uri token");
533            assert!(matches!(uri, RequestToken::Uri(u) if u == "https://mhouge.dk/blog/"));
534        };
535    }
536}
537
538#[derive(Debug)]
539pub struct HittRequest {
540    pub method: http::method::Method,
541    pub uri: http::uri::Uri,
542    pub headers: http::HeaderMap,
543    pub body: Option<String>,
544    pub http_version: Option<http::version::Version>,
545}
546
547#[derive(Default)]
548struct PartialHittRequest {
549    method: Option<http::method::Method>,
550    uri: Option<http::uri::Uri>,
551    headers: http::HeaderMap,
552    body: Option<String>,
553    http_version: Option<http::version::Version>,
554}
555
556impl PartialHittRequest {
557    #[inline]
558    fn build(self) -> Result<HittRequest, RequestParseError> {
559        match self.method {
560            Some(method) => match self.uri {
561                Some(uri) => Ok(HittRequest {
562                    method,
563                    uri,
564                    headers: self.headers,
565                    body: self.body,
566                    http_version: self.http_version,
567                }),
568                None => Err(RequestParseError::MissingUri),
569            },
570            None => Err(RequestParseError::MissingMethod),
571        }
572    }
573}
574
575#[cfg(test)]
576mod test_partial_http_request {
577
578    use http::{HeaderMap, Uri};
579
580    use crate::{error::RequestParseError, PartialHittRequest};
581
582    #[test]
583    fn build_should_reject_if_no_uri() {
584        let request = PartialHittRequest {
585            uri: None,
586            method: Some(http::Method::GET),
587            http_version: None,
588            headers: HeaderMap::default(),
589            body: None,
590        }
591        .build()
592        .expect_err("it to raise RequestParseError::MissingUri");
593
594        assert!(matches!(request, RequestParseError::MissingUri));
595    }
596
597    #[test]
598    fn build_should_reject_if_no_method() {
599        let uri = Uri::from_static("https://mhouge.dk/");
600
601        let request = PartialHittRequest {
602            uri: Some(uri),
603            method: None,
604            http_version: None,
605            headers: HeaderMap::default(),
606            body: None,
607        }
608        .build()
609        .expect_err("it to raise RequestParseError::MissingMethod");
610
611        assert!(matches!(request, RequestParseError::MissingMethod));
612    }
613}
614
615#[inline]
616pub fn parse_requests(
617    buffer: &str,
618    input_variables: &std::collections::HashMap<String, String>,
619) -> Result<Vec<HittRequest>, RequestParseError> {
620    let mut requests = Vec::new();
621
622    let tokens = tokenize(buffer, input_variables)?;
623
624    let mut partial_request = PartialHittRequest::default();
625
626    for token in tokens {
627        match token {
628            RequestToken::Method(method) => {
629                partial_request.method = Some(method);
630            }
631
632            RequestToken::Uri(uri) => {
633                partial_request.uri = Some(uri);
634            }
635
636            RequestToken::Header(header) => {
637                partial_request.headers.insert(header.key, header.value);
638            }
639
640            RequestToken::Body(body) => {
641                partial_request.body = body;
642
643                requests.push(partial_request.build()?);
644
645                partial_request = PartialHittRequest::default();
646            }
647
648            RequestToken::HttpVersion(version_token) => {
649                partial_request.http_version = Some(version_token);
650            }
651        };
652    }
653
654    if partial_request.method.is_some() {
655        requests.push(partial_request.build()?);
656    };
657
658    Ok(requests)
659}
660
661#[cfg(test)]
662mod test_parse_requests {
663    use core::str::FromStr;
664
665    use once_cell::sync::Lazy;
666
667    use crate::{error::RequestParseError, parse_requests};
668
669    const HTTP_METHODS: [&str; 9] = [
670        "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "CONNECT", "TRACE",
671    ];
672
673    static EMPTY_VARS: Lazy<std::collections::HashMap<String, String>> =
674        Lazy::new(std::collections::HashMap::new);
675
676    #[test]
677    fn it_should_parse_http_method_correctly() {
678        let url = "https://mhouge.dk";
679
680        for method in &HTTP_METHODS {
681            let expected_method = http::Method::from_str(method).expect("m is a valid method");
682
683            let input = format!("{method} {url}");
684
685            let parsed_requests =
686                parse_requests(&input, &EMPTY_VARS).expect("request should be valid");
687
688            assert!(parsed_requests.len() == 1);
689
690            let first_request = parsed_requests.first().expect("it to be a request");
691
692            assert_eq!(expected_method, first_request.method);
693
694            let expected_uri = url.parse::<http::uri::Uri>().expect("url should be valid");
695
696            assert_eq!(expected_uri, first_request.uri);
697
698            assert_eq!(0, first_request.headers.len());
699
700            assert_eq!(None, first_request.body);
701        }
702    }
703
704    #[test]
705    fn it_should_be_able_to_parse_requests() {
706        let method_input = "GET";
707        let uri_input = "https://mhouge.dk/";
708
709        let header1_key = "content-type";
710        let header1_value = "application/json";
711        let body_input = "{ \"key\": \"value\"  }";
712
713        let input_request =
714            format!("{method_input} {uri_input}\n{header1_key}: {header1_value}\n\n{body_input}");
715
716        let result =
717            parse_requests(&input_request, &EMPTY_VARS).expect("it to return a list of requests");
718
719        assert!(result.len() == 1);
720
721        let request = result.first().expect("request len to be 1");
722
723        assert_eq!(method_input, request.method.as_str());
724
725        assert_eq!(uri_input, request.uri.to_string());
726
727        let body_inner = request.body.clone().expect("body to be defined");
728
729        assert_eq!(body_inner, body_input);
730
731        assert_eq!(1, request.headers.len());
732
733        let header1_output = request
734            .headers
735            .get(header1_key)
736            .expect("header1_key to exist");
737
738        assert_eq!(
739            header1_value,
740            header1_output.to_str().expect("it to be a valid header")
741        );
742
743        assert!(request.http_version.is_none());
744    }
745
746    #[test]
747    fn it_should_be_able_to_parse_multiple_requests() {
748        let input = "
749GET https://mhouge.dk/ HTTP/0.9
750
751###
752
753PUT https://mhouge.dk/ HTTP/1.0
754
755###
756
757POST https://mhouge.dk/ HTTP/1.1
758
759###
760
761PATCH https://mhouge.dk/ HTTP/2
762
763###
764
765DELETE https://mhouge.dk/ HTTP/3
766
767###
768";
769
770        let requests = parse_requests(input, &EMPTY_VARS).expect("to get a list of requests");
771
772        assert_eq!(5, requests.len());
773
774        {
775            let request = requests.first().expect("it to be exist");
776
777            assert_eq!(http::Method::GET, request.method);
778
779            assert_eq!("https://mhouge.dk/", request.uri.to_string());
780
781            assert!(request.headers.is_empty());
782
783            assert!(request.body.is_none());
784
785            assert_eq!(
786                http::Version::HTTP_09,
787                request.http_version.expect("http_version to be defined")
788            );
789        };
790
791        {
792            let request = requests.get(1).expect("it to be exist");
793
794            assert_eq!(http::Method::PUT, request.method);
795
796            assert_eq!("https://mhouge.dk/", request.uri.to_string());
797
798            assert!(request.headers.is_empty());
799
800            assert!(request.body.is_none());
801
802            assert_eq!(
803                http::Version::HTTP_10,
804                request.http_version.expect("http_version to be defined")
805            );
806        };
807
808        {
809            let request = requests.get(2).expect("it to be exist");
810
811            assert_eq!(http::Method::POST, request.method);
812
813            assert_eq!("https://mhouge.dk/", request.uri.to_string());
814
815            assert!(request.headers.is_empty());
816
817            assert!(request.body.is_none());
818
819            assert_eq!(
820                http::Version::HTTP_11,
821                request.http_version.expect("http_version to be defined")
822            );
823        };
824
825        {
826            let request = requests.get(3).expect("it to be exist");
827
828            assert_eq!(http::Method::PATCH, request.method);
829
830            assert_eq!("https://mhouge.dk/", request.uri.to_string());
831
832            assert!(request.headers.is_empty());
833
834            assert!(request.body.is_none());
835
836            assert_eq!(
837                http::Version::HTTP_2,
838                request.http_version.expect("http_version to be defined")
839            );
840        };
841
842        {
843            let request = requests.get(4).expect("it to be exist");
844
845            assert_eq!(http::Method::DELETE, request.method);
846
847            assert_eq!("https://mhouge.dk/", request.uri.to_string());
848
849            assert!(request.headers.is_empty());
850
851            assert!(request.body.is_none());
852
853            assert_eq!(
854                http::Version::HTTP_3,
855                request.http_version.expect("http_version to be defined")
856            );
857        };
858    }
859
860    #[test]
861    fn it_should_support_variables() {
862        let input = "
863@method = GET
864@host = https://mhouge.dk
865@path = /api
866@query_value = mads@mhouge.dk
867@body_input  = { \"key\": \"value\" }
868
869{{method}} {{host}}{{path}}?email={{query_value}}
870
871{{ body_input }}";
872
873        let requests = parse_requests(input, &EMPTY_VARS).expect("to get a list of requests");
874
875        assert_eq!(requests.len(), 1);
876
877        let request = requests.first().expect("it to have 1 request");
878
879        assert_eq!(request.method, http::method::Method::GET);
880
881        assert_eq!(request.uri, "https://mhouge.dk/api?email=mads@mhouge.dk");
882
883        assert_eq!(
884            "{ \"key\": \"value\" }",
885            request.body.clone().expect("body to be set"),
886        );
887    }
888
889    #[test]
890    fn it_should_support_variable_input() {
891        {
892            let mut vars = std::collections::HashMap::from([
893                ("method".to_owned(), "GET".to_owned()),
894                ("host".to_owned(), "https://mhouge.dk".to_owned()),
895                ("path".to_owned(), "/api".to_owned()),
896                ("query_value".to_owned(), "mads@mhouge.dk".to_owned()),
897                ("body_input".to_owned(), "{ \"key\": \"value\" }".to_owned()),
898            ]);
899
900            let input = "
901{{method}} {{host}}{{path}}?email={{query_value}}
902{{header_name}}: {{header_value}}
903
904{{ body_input }}";
905
906            for i in u8::MIN..u8::MAX {
907                let header_name = format!("mads-was-here{i}");
908                let header_value = format!("or was i{i}?");
909
910                vars.insert("header_name".to_owned(), header_name.clone());
911                vars.insert("header_value".to_owned(), header_value.clone());
912
913                let requests = parse_requests(input, &vars).expect("to get a list of requests");
914
915                assert_eq!(requests.len(), 1);
916
917                let request = requests.first().expect("it to have 1 request");
918
919                assert_eq!(request.method, http::method::Method::GET);
920
921                assert_eq!(request.uri, "https://mhouge.dk/api?email=mads@mhouge.dk");
922
923                assert_eq!(request.headers.len(), 1);
924
925                let result_header_value = request.headers.get(header_name).expect("it to exist");
926
927                assert_eq!(
928                    header_value,
929                    result_header_value
930                        .to_str()
931                        .expect("it to be a valid string"),
932                );
933
934                assert_eq!(
935                    "{ \"key\": \"value\" }",
936                    request.body.clone().expect("body to be set"),
937                );
938            }
939        }
940
941        {
942            let input = "
943GET https://mhouge.dk/
944
945{{ body_input }}";
946
947            let request = parse_requests(input, &EMPTY_VARS).expect_err("it to return an error");
948
949            assert!(matches!(
950                request,
951                RequestParseError::VariableNotFound(var)
952                if var == "body_input"
953            ));
954        }
955    }
956
957    #[test]
958    fn input_variables_should_be_overwritten_by_local_variables() {
959        let vars = std::collections::HashMap::from([("method".to_owned(), "PUT".to_owned())]);
960
961        let input = "
962@method = POST
963
964{{method}} https://mhouge.dk/";
965
966        let requests = parse_requests(input, &vars).expect("it to parse successfully");
967
968        assert_eq!(requests.len(), 1);
969
970        let request = requests.first().expect("it to exist");
971
972        assert_eq!(request.method, http::method::Method::POST);
973
974        assert_eq!(request.uri, "https://mhouge.dk/");
975
976        assert_eq!(request.headers.len(), 0);
977    }
978
979    #[test]
980    fn it_should_ignore_comments() {
981        let input = "
982// comment 1
983# comment 2
984
985DELETE https://mhouge.dk/";
986
987        let requests = parse_requests(input, &EMPTY_VARS).expect("it to parse successfully");
988
989        assert_eq!(requests.len(), 1);
990
991        let request = requests.first().expect("it to exist");
992
993        assert_eq!(request.method, http::method::Method::DELETE);
994
995        let expected_uri = "https://mhouge.dk/";
996
997        assert_eq!(request.uri, expected_uri);
998
999        assert!(request.headers.is_empty());
1000
1001        assert!(request.body.is_none());
1002    }
1003}