1use std::collections::HashMap;
4use std::fmt::{Display, Formatter};
5use std::hash::{Hash, Hasher};
6use std::str::from_utf8;
7
8use base64::Engine;
9use base64::engine::general_purpose::STANDARD as BASE64;
10use itertools::Itertools;
11use maplit::hashmap;
12use serde_json::{json, Value};
13use tracing::warn;
14
15use crate::{DifferenceType, PactSpecification};
16use crate::bodies::OptionalBody;
17use crate::generators::{Generators, generators_from_json, generators_to_json};
18use crate::http_parts::HttpPart;
19use crate::json_utils::{body_from_json, headers_from_json, headers_to_json};
20use crate::matchingrules::{matchers_from_json, matchers_to_json, MatchingRules};
21use crate::query_strings::{query_from_json, query_to_json, v3_query_from_json};
22use crate::v4::http_parts::HttpRequest;
23
24#[derive(Debug, Clone, Eq)]
26pub struct Request {
27 pub method: String,
29 pub path: String,
31 pub query: Option<HashMap<String, Vec<Option<String>>>>,
33 pub headers: Option<HashMap<String, Vec<String>>>,
35 pub body: OptionalBody,
37 pub matching_rules: MatchingRules,
39 pub generators: Generators
41}
42
43impl HttpPart for Request {
44 fn headers(&self) -> &Option<HashMap<String, Vec<String>>> {
45 &self.headers
46 }
47
48 fn headers_mut(&mut self) -> &mut HashMap<String, Vec<String>> {
49 if self.headers.is_none() {
50 self.headers = Some(hashmap!{});
51 }
52 self.headers.as_mut().unwrap()
53 }
54
55 fn body(&self) -> &OptionalBody {
56 &self.body
57 }
58
59 fn body_mut(&mut self) -> &mut OptionalBody {
60 &mut self.body
61 }
62
63 fn matching_rules(&self) -> &MatchingRules {
64 &self.matching_rules
65 }
66
67 fn matching_rules_mut(&mut self) -> &mut MatchingRules {
68 &mut self.matching_rules
69 }
70
71 fn generators(&self) -> &Generators {
72 &self.generators
73 }
74
75 fn generators_mut(&mut self) -> &mut Generators {
76 &mut self.generators
77 }
78
79 fn lookup_content_type(&self) -> Option<String> {
80 self.lookup_header_value(&"content-type".to_string())
81 }
82}
83
84impl Hash for Request {
85 fn hash<H: Hasher>(&self, state: &mut H) {
86 self.method.hash(state);
87 self.path.hash(state);
88 if let Some(query) = &self.query {
89 for (k, v) in query.iter().sorted_by(|(a, _), (b, _)| Ord::cmp(a, b)) {
90 k.hash(state);
91 v.hash(state);
92 }
93 }
94 if let Some(headers) = &self.headers {
95 for (k, v) in headers.iter().sorted_by(|(a, _), (b, _)| Ord::cmp(a, b)) {
96 k.hash(state);
97 v.hash(state);
98 }
99 }
100 self.body.hash(state);
101 self.matching_rules.hash(state);
102 self.generators.hash(state);
103 }
104}
105
106impl PartialEq for Request {
107 fn eq(&self, other: &Self) -> bool {
108 self.method == other.method && self.path == other.path && self.query == other.query &&
109 self.headers == other.headers && self.body == other.body &&
110 self.matching_rules == other.matching_rules && self.generators == other.generators
111 }
112
113 fn ne(&self, other: &Self) -> bool {
114 self.method != other.method || self.path != other.path || self.query != other.query ||
115 self.headers != other.headers || self.body != other.body ||
116 self.matching_rules != other.matching_rules || self.generators != other.generators
117 }
118}
119
120impl Display for Request {
121 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
122 write!(f, "Request ( method: {}, path: {}, query: {:?}, headers: {:?}, body: {} )",
123 self.method, self.path, self.query, self.headers, self.body)
124 }
125}
126
127impl Default for Request {
128 fn default() -> Self {
129 Request {
130 method: "GET".to_string(),
131 path: "/".to_string(),
132 query: None,
133 headers: None,
134 body: OptionalBody::Missing,
135 matching_rules: MatchingRules::default(),
136 generators: Generators::default()
137 }
138 }
139}
140
141impl Request {
142 pub fn from_json(request_json: &Value, spec_version: &PactSpecification
144 ) -> anyhow::Result<Request> {
145 let method_val = match request_json.get("method") {
146 Some(v) => match *v {
147 Value::String(ref s) => s.to_uppercase(),
148 _ => v.to_string().to_uppercase()
149 },
150 None => "GET".to_string()
151 };
152 let path_val = match request_json.get("path") {
153 Some(v) => match *v {
154 Value::String(ref s) => s.clone(),
155 _ => v.to_string()
156 },
157 None => "/".to_string()
158 };
159 let query_val = match request_json.get("query") {
160 Some(v) => match spec_version {
161 &PactSpecification::V3 => v3_query_from_json(v, spec_version),
162 _ => query_from_json(v, spec_version)
163 },
164 None => None
165 };
166 let headers = headers_from_json(request_json);
167 Ok(Request {
168 method: method_val,
169 path: path_val,
170 query: query_val,
171 headers: headers.clone(),
172 body: body_from_json(request_json, "body", &headers),
173 matching_rules: matchers_from_json(request_json, &Some("requestMatchingRules".to_string()))?,
174 generators: generators_from_json(request_json)?,
175 })
176 }
177
178 pub fn to_json(&self, spec_version: &PactSpecification) -> Value {
180 let mut json = json!({
181 "method".to_string() : Value::String(self.method.to_uppercase()),
182 "path".to_string() : Value::String(self.path.clone())
183 });
184 {
185 let map = json.as_object_mut().unwrap();
186
187 if self.query.is_some() {
188 map.insert("query".to_string(), query_to_json(self.query.clone().unwrap(), spec_version));
189 }
190
191 if self.headers.is_some() {
192 map.insert("headers".to_string(), headers_to_json(&self.headers.clone().unwrap()));
193 }
194
195 match self.body {
196 OptionalBody::Present(ref body, _, _) => if self.content_type().unwrap_or_default().is_json() {
197 match serde_json::from_slice(body) {
198 Ok(json_body) => { map.insert("body".to_string(), json_body); },
199 Err(err) => {
200 warn!("Failed to parse json body: {}", err);
201 map.insert("body".to_string(), Value::String(BASE64.encode(body)));
202 }
203 }
204 } else {
205 match from_utf8(body) {
206 Ok(s) => map.insert("body".to_string(), Value::String(s.to_string())),
207 Err(_) => map.insert("body".to_string(), Value::String(BASE64.encode(body)))
208 };
209 },
210 OptionalBody::Empty => { map.insert("body".to_string(), Value::String(String::default())); },
211 OptionalBody::Missing => (),
212 OptionalBody::Null => { map.insert("body".to_string(), Value::Null); }
213 }
214
215 if self.matching_rules.is_not_empty() {
216 map.insert("matchingRules".to_string(), matchers_to_json(
217 &self.matching_rules.clone(), spec_version));
218 }
219
220 if self.generators.is_not_empty() {
221 map.insert("generators".to_string(), generators_to_json(
222 &self.generators.clone(), spec_version));
223 }
224 }
225 json
226 }
227
228 #[deprecated(since="0.6.0", note="please use `default()` from the standard Default trait instead")]
230 pub fn default_request() -> Request {
231 Request::default()
232 }
233
234 pub fn differences_from(&self, other: &Request) -> Vec<(DifferenceType, String)> {
236 let mut differences = vec![];
237 if self.method != other.method {
238 differences.push((DifferenceType::Method, format!("Request method {} != {}", self.method, other.method)));
239 }
240 if self.path != other.path {
241 differences.push((DifferenceType::Path, format!("Request path {} != {}", self.path, other.path)));
242 }
243 if self.query != other.query {
244 differences.push((DifferenceType::QueryParameters, format!("Request query {:?} != {:?}", self.query, other.query)));
245 }
246 let mut keys = self.headers.clone().map(|m| m.keys().cloned().collect_vec()).unwrap_or_default();
247 let mut other_keys = other.headers.clone().map(|m| m.keys().cloned().collect_vec()).unwrap_or_default();
248 keys.sort();
249 other_keys.sort();
250 if keys != other_keys {
251 differences.push((DifferenceType::Headers, format!("Request headers {:?} != {:?}", self.headers, other.headers)));
252 }
253 if self.body != other.body {
254 differences.push((DifferenceType::Body, format!("Request body '{:?}' != '{:?}'", self.body, other.body)));
255 }
256 if self.matching_rules != other.matching_rules {
257 differences.push((DifferenceType::MatchingRules, format!("Request matching rules {:?} != {:?}", self.matching_rules, other.matching_rules)));
258 }
259 differences
260 }
261
262 pub fn as_v4_request(&self) -> HttpRequest {
264 HttpRequest {
265 method: self.method.clone(),
266 path: self.path.clone(),
267 query: self.query.clone(),
268 headers: self.headers.clone(),
269 body: self.body.clone(),
270 matching_rules: self.matching_rules.clone(),
271 generators: self.generators.clone()
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use std::collections::hash_map::DefaultHasher;
279 use std::hash::{Hash, Hasher};
280
281 use expectest::prelude::*;
282 use maplit::hashmap;
283 use serde_json::Value;
284
285 use crate::bodies::OptionalBody;
286 use crate::content_types::{HTML, JSON, XML};
287 use crate::generators::{Generator, GeneratorCategory, Generators};
288 use crate::http_parts::HttpPart;
289 use crate::matchingrules::{Category, MatchingRule, MatchingRuleCategory, MatchingRules, RuleList, RuleLogic};
290 use crate::PactSpecification;
291 use crate::path_exp::DocPath;
292 use crate::request::Request;
293
294 #[test]
295 fn request_from_json_defaults_to_get() {
296 let request_json : serde_json::Value = serde_json::from_str(r#"
297 {
298 "path": "/",
299 "query": "",
300 "headers": {}
301 }
302 "#).unwrap();
303 let request = Request::from_json(&request_json, &PactSpecification::V1);
304 expect!(request.unwrap().method).to(be_equal_to("GET"));
305 }
306
307 #[test]
308 fn request_from_json_defaults_to_root_for_path() {
309 let request_json : serde_json::Value = serde_json::from_str(r#"
310 {
311 "method": "PUT",
312 "query": "",
313 "headers": {}
314 }
315 "#).unwrap();
316 println!("request_json: {}", request_json);
317 let request = Request::from_json(&request_json, &PactSpecification::V1_1);
318 assert_eq!(request.unwrap().path, "/".to_string());
319 }
320
321 #[test]
322 fn request_content_type_is_based_on_the_content_type_header() {
323 let request = Request {
324 method: "GET".to_string(),
325 path: "/".to_string(),
326 query: None,
327 headers: None,
328 body: OptionalBody::Missing,
329 ..Request::default()
330 };
331 expect!(request.content_type().unwrap_or_default().to_string()).to(be_equal_to("*/*"));
332 expect!(Request {
333 headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/html".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
334 .to(be_equal_to("text/html"));
335 expect!(Request {
336 headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json; charset=UTF-8".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
337 .to(be_equal_to("application/json;charset=utf-8"));
338 expect!(Request {
339 headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
340 .to(be_equal_to("application/json"));
341 expect!(Request {
342 headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/json; charset=UTF-8".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
343 .to(be_equal_to("application/json;charset=utf-8"));
344 expect!(Request {
345 body: OptionalBody::Present("{\"json\": true}".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
346 .to(be_equal_to("application/json"));
347 expect!(Request {
348 body: OptionalBody::Present("{}".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
349 .to(be_equal_to("application/json"));
350 expect!(Request {
351 body: OptionalBody::Present("[]".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
352 .to(be_equal_to("application/json"));
353 expect!(Request {
354 body: OptionalBody::Present("[1,2,3]".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
355 .to(be_equal_to("application/json"));
356 expect!(Request {
357 body: OptionalBody::Present("\"string\"".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
358 .to(be_equal_to("application/json"));
359 expect!(Request {
360 body: OptionalBody::Present("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<json>false</json>".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
361 .to(be_equal_to("application/xml"));
362 expect!(Request {
363 body: OptionalBody::Present("<json>false</json>".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
364 .to(be_equal_to("application/xml"));
365 expect!(Request {
366 body: OptionalBody::Present("this is not json".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
367 .to(be_equal_to("text/plain"));
368 expect!(Request {
369 body: OptionalBody::Present("<html><body>this is also not json</body></html>".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
370 .to(be_equal_to("text/html"));
371 }
372
373 #[test]
374 fn content_type_struct_test() {
375 let request = Request {
376 method: "GET".to_string(),
377 path: "/".to_string(),
378 query: None,
379 headers: None,
380 body: OptionalBody::Missing,
381 ..Request::default()
382 };
383 expect!(request.content_type()).to(be_none());
384 expect!(Request {
385 headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/html".to_string()] }), .. request.clone() }.content_type())
386 .to(be_some().value(HTML.clone()));
387 expect!(Request {
388 headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] }), .. request.clone() }.content_type())
389 .to(be_some().value(JSON.clone()));
390 expect!(Request {
391 headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/hal+json".to_string()] }), .. request.clone() }
392 .content_type().map(|c| c.base_type()))
393 .to(be_some().value(JSON.clone()));
394 expect!(Request {
395 headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/xml".to_string()] }), .. request.clone() }.content_type())
396 .to(be_some().value(XML.clone()));
397 expect!(Request {
398 headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/stuff+xml".to_string()] }), ..
399 request.clone() }.content_type().map(|c| c.base_type()))
400 .to(be_some().value(XML.clone()));
401 }
402
403 #[test]
404 fn request_to_json_with_defaults() {
405 let request = Request::default();
406 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
407 be_equal_to("{\"method\":\"GET\",\"path\":\"/\"}"));
408 }
409
410 #[test]
411 fn request_to_json_converts_methods_to_upper_case() {
412 let request = Request { method: "post".to_string(), .. Request::default() };
413 expect!(request.to_json(&PactSpecification::V3).to_string()).to(be_equal_to("{\"method\":\"POST\",\"path\":\"/\"}"));
414 }
415
416 #[test]
417 fn request_to_json_with_a_query() {
418 let request = Request { query: Some(hashmap!{
419 "a".to_string() => vec![Some("1".to_string()), Some("2".to_string())],
420 "b".to_string() => vec![Some("3".to_string())]
421 }), .. Request::default() };
422 expect!(request.to_json(&PactSpecification::V2).to_string()).to(
423 be_equal_to(r#"{"method":"GET","path":"/","query":"a=1&a=2&b=3"}"#)
424 );
425 }
426
427 #[test]
428 fn request_to_json_with_a_query_must_encode_the_query() {
429 let request = Request { query: Some(hashmap!{
430 "datetime".to_string() => vec![Some("2011-12-03T10:15:30+01:00".to_string())],
431 "description".to_string() => vec![Some("hello world!".to_string())] }), .. Request::default() };
432 expect!(request.to_json(&PactSpecification::V2).to_string()).to(
433 be_equal_to(r#"{"method":"GET","path":"/","query":"datetime=2011-12-03T10%3a15%3a30%2b01%3a00&description=hello+world%21"}"#)
434 );
435 }
436
437 #[test]
438 fn request_to_json_with_a_query_must_encode_the_query_with_utf8_chars() {
439 let request = Request { query: Some(hashmap!{
440 "a".to_string() => vec![Some("b=c&d❤".to_string())]
441 }), .. Request::default() };
442 expect!(request.to_json(&PactSpecification::V2).to_string()).to(
443 be_equal_to(r#"{"method":"GET","path":"/","query":"a=b%3dc%26d%27%64"}"#)
444 );
445 }
446
447 #[test]
448 fn request_to_json_with_a_query_v3() {
449 let request = Request { query: Some(hashmap!{
450 "a".to_string() => vec![Some("1".to_string()), Some("2".to_string())],
451 "b".to_string() => vec![Some("3".to_string())]
452 }), .. Request::default() };
453 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
454 be_equal_to(r#"{"method":"GET","path":"/","query":{"a":["1","2"],"b":["3"]}}"#)
455 );
456 }
457
458 #[test]
459 fn request_to_json_with_a_query_v3_must_not_encode_the_query() {
460 let request = Request { query: Some(hashmap!{
461 "datetime".to_string() => vec![Some("2011-12-03T10:15:30+01:00".to_string())],
462 "description".to_string() => vec![Some("hello world!".to_string())] }), .. Request::default() };
463 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
464 be_equal_to(r#"{"method":"GET","path":"/","query":{"datetime":["2011-12-03T10:15:30+01:00"],"description":["hello world!"]}}"#)
465 );
466 }
467
468 #[test]
469 fn request_to_json_with_a_query_v3_must_not_encode_the_query_with_utf8_chars() {
470 let request = Request { query: Some(hashmap!{
471 "a".to_string() => vec![Some("b=c&d❤".to_string())]
472 }), .. Request::default() };
473 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
474 be_equal_to(r#"{"method":"GET","path":"/","query":{"a":["b=c&d❤"]}}"#)
475 );
476 }
477
478 #[test]
479 fn request_to_json_with_headers() {
480 let request = Request { headers: Some(hashmap!{
481 "HEADERA".to_string() => vec!["VALUEA".to_string()],
482 "HEADERB".to_string() => vec!["VALUEB1, VALUEB2".to_string()]
483 }), .. Request::default() };
484 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
485 be_equal_to(r#"{"headers":{"HEADERA":"VALUEA","HEADERB":"VALUEB1, VALUEB2"},"method":"GET","path":"/"}"#)
486 );
487 }
488
489 #[test]
490 fn request_to_json_with_json_body() {
491 let request = Request { headers: Some(hashmap!{
492 "Content-Type".to_string() => vec!["application/json".to_string()]
493 }), body: OptionalBody::Present(r#"{"key": "value"}"#.into(), None, None), .. Request::default() };
494 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
495 be_equal_to(r#"{"body":{"key":"value"},"headers":{"Content-Type":"application/json"},"method":"GET","path":"/"}"#)
496 );
497 }
498
499
500 #[test]
501 fn request_to_json_with_non_json_body() {
502 let request = Request { headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/plain".to_string()] }),
503 body: OptionalBody::Present("This is some text".into(), None, None), .. Request::default() };
504 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
505 be_equal_to(r#"{"body":"This is some text","headers":{"Content-Type":"text/plain"},"method":"GET","path":"/"}"#)
506 );
507 }
508
509 #[test]
510 fn request_to_json_with_empty_body() {
511 let request = Request { body: OptionalBody::Empty, .. Request::default() };
512 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
513 be_equal_to(r#"{"body":"","method":"GET","path":"/"}"#)
514 );
515 }
516
517 #[test]
518 fn request_to_json_with_null_body() {
519 let request = Request { body: OptionalBody::Null, .. Request::default() };
520 expect!(request.to_json(&PactSpecification::V3).to_string()).to(
521 be_equal_to(r#"{"body":null,"method":"GET","path":"/"}"#)
522 );
523 }
524
525 fn hash<T: Hash>(t: &T) -> u64 {
526 let mut s = DefaultHasher::new();
527 t.hash(&mut s);
528 s.finish()
529 }
530
531 #[test]
532 fn hash_for_request() {
533 let request1 = Request::default();
534 let request2 = Request { method: "POST".to_string(), .. Request::default() };
535 let request3 = Request { headers: Some(hashmap!{
536 "H1".to_string() => vec!["A".to_string()]
537 }), .. Request::default() };
538 let request4 = Request { headers: Some(hashmap!{
539 "H1".to_string() => vec!["B".to_string()]
540 }), .. Request::default() };
541 expect!(hash(&request1)).to(be_equal_to(hash(&request1)));
542 expect!(hash(&request3)).to(be_equal_to(hash(&request3)));
543 expect!(hash(&request1)).to_not(be_equal_to(hash(&request2)));
544 expect!(hash(&request3)).to_not(be_equal_to(hash(&request4)));
545 }
546
547 #[test]
548 fn request_headers_do_not_conflict_if_they_have_been_serialised_and_deserialised_to_json() {
549 let original_request = Request {
552 method: "".to_string(),
553 path: "".to_string(),
554 query: None,
555 headers: Some(hashmap! {
556 "accept".to_string() => vec!["application/xml".to_string(), "application/json".to_string()],
557 "user-agent".to_string() => vec!["test".to_string(), "test2".to_string()],
558 "content-type".to_string() => vec!["text/plain".to_string()]
559 }),
560 body: OptionalBody::Missing,
561 matching_rules: Default::default(),
562 generators: Default::default(),
563 };
564
565 let json = serde_json::to_string(&original_request
566 .to_json(&PactSpecification::V3)).expect("could not serialize");
567
568 let serialized_and_deserialized_request: Value =
569 serde_json::from_str(&json).expect("could not deserialize");
570
571 expect!(original_request
572 .differences_from(&Request::from_json(&serialized_and_deserialized_request, &PactSpecification::V3).unwrap())
573 .iter())
574 .to(be_empty());
575 }
576
577 #[test]
579 fn to_json_with_provider_state_generator_test() {
580 let request = Request {
581 method: "GET".to_string(),
582 path: "/data/42".to_string(),
583 matching_rules: MatchingRules {
584 rules: hashmap!{
585 Category::PATH => MatchingRuleCategory {
586 name: Category::PATH,
587 rules: hashmap!{
588 DocPath::root() => RuleList {
589 rules: vec![MatchingRule::Type],
590 rule_logic: RuleLogic::And,
591 cascaded: false
592 }
593 }
594 }
595 }
596 },
597 generators: Generators {
598 categories: hashmap!{
599 GeneratorCategory::PATH => hashmap!{
600 DocPath::root() => Generator::ProviderStateGenerator("/data/${id}".to_string(), None)
601 }
602 }
603 },
604 .. Request::default()
605 };
606
607 let json = request.to_json(&PactSpecification::V3);
608
609 let generators = json.get("generators").unwrap();
610 expect!(generators.to_string()).to_not(be_equal_to("{}"));
611
612 let rules = json.get("matchingRules").unwrap();
613 expect!(rules.to_string()).to_not(be_equal_to("{}"));
614 }
615}