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