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