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 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 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 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 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}