hitt_parser/
lib.rs

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