use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::str::from_utf8;
use base64::encode;
use itertools::Itertools;
use maplit::hashmap;
use serde_json::{json, Value};
use tracing::warn;
use crate::{DifferenceType, PactSpecification};
use crate::bodies::OptionalBody;
use crate::generators::{Generators, generators_from_json, generators_to_json};
use crate::http_parts::HttpPart;
use crate::json_utils::{body_from_json, headers_from_json, headers_to_json};
use crate::matchingrules::{matchers_from_json, matchers_to_json, MatchingRules};
use crate::query_strings::{query_from_json, query_to_json, v3_query_from_json};
use crate::v4::http_parts::HttpRequest;
#[derive(Debug, Clone, Eq)]
pub struct Request {
pub method: String,
pub path: String,
pub query: Option<HashMap<String, Vec<String>>>,
pub headers: Option<HashMap<String, Vec<String>>>,
pub body: OptionalBody,
pub matching_rules: MatchingRules,
pub generators: Generators
}
impl HttpPart for Request {
fn headers(&self) -> &Option<HashMap<String, Vec<String>>> {
&self.headers
}
fn headers_mut(&mut self) -> &mut HashMap<String, Vec<String>> {
if self.headers.is_none() {
self.headers = Some(hashmap!{});
}
self.headers.as_mut().unwrap()
}
fn body(&self) -> &OptionalBody {
&self.body
}
fn body_mut(&mut self) -> &mut OptionalBody {
&mut self.body
}
fn matching_rules(&self) -> &MatchingRules {
&self.matching_rules
}
fn matching_rules_mut(&mut self) -> &mut MatchingRules {
&mut self.matching_rules
}
fn generators(&self) -> &Generators {
&self.generators
}
fn generators_mut(&mut self) -> &mut Generators {
&mut self.generators
}
fn lookup_content_type(&self) -> Option<String> {
self.lookup_header_value(&"content-type".to_string())
}
}
impl Hash for Request {
fn hash<H: Hasher>(&self, state: &mut H) {
self.method.hash(state);
self.path.hash(state);
if let Some(query) = &self.query {
for (k, v) in query.iter().sorted_by(|(a, _), (b, _)| Ord::cmp(a, b)) {
k.hash(state);
v.hash(state);
}
}
if let Some(headers) = &self.headers {
for (k, v) in headers.iter().sorted_by(|(a, _), (b, _)| Ord::cmp(a, b)) {
k.hash(state);
v.hash(state);
}
}
self.body.hash(state);
self.matching_rules.hash(state);
self.generators.hash(state);
}
}
impl PartialEq for Request {
fn eq(&self, other: &Self) -> bool {
self.method == other.method && self.path == other.path && self.query == other.query &&
self.headers == other.headers && self.body == other.body &&
self.matching_rules == other.matching_rules && self.generators == other.generators
}
fn ne(&self, other: &Self) -> bool {
self.method != other.method || self.path != other.path || self.query != other.query ||
self.headers != other.headers || self.body != other.body ||
self.matching_rules != other.matching_rules || self.generators != other.generators
}
}
impl Display for Request {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "Request ( method: {}, path: {}, query: {:?}, headers: {:?}, body: {} )",
self.method, self.path, self.query, self.headers, self.body)
}
}
impl Default for Request {
fn default() -> Self {
Request {
method: "GET".to_string(),
path: "/".to_string(),
query: None,
headers: None,
body: OptionalBody::Missing,
matching_rules: MatchingRules::default(),
generators: Generators::default()
}
}
}
impl Request {
pub fn from_json(request_json: &Value, spec_version: &PactSpecification
) -> anyhow::Result<Request> {
let method_val = match request_json.get("method") {
Some(v) => match *v {
Value::String(ref s) => s.to_uppercase(),
_ => v.to_string().to_uppercase()
},
None => "GET".to_string()
};
let path_val = match request_json.get("path") {
Some(v) => match *v {
Value::String(ref s) => s.clone(),
_ => v.to_string()
},
None => "/".to_string()
};
let query_val = match request_json.get("query") {
Some(v) => match spec_version {
&PactSpecification::V3 => v3_query_from_json(v, spec_version),
_ => query_from_json(v, spec_version)
},
None => None
};
let headers = headers_from_json(request_json);
Ok(Request {
method: method_val,
path: path_val,
query: query_val,
headers: headers.clone(),
body: body_from_json(request_json, "body", &headers),
matching_rules: matchers_from_json(request_json, &Some("requestMatchingRules".to_string()))?,
generators: generators_from_json(request_json)?,
})
}
pub fn to_json(&self, spec_version: &PactSpecification) -> Value {
let mut json = json!({
"method".to_string() : Value::String(self.method.to_uppercase()),
"path".to_string() : Value::String(self.path.clone())
});
{
let map = json.as_object_mut().unwrap();
if self.query.is_some() {
map.insert("query".to_string(), query_to_json(self.query.clone().unwrap(), spec_version));
}
if self.headers.is_some() {
map.insert("headers".to_string(), headers_to_json(&self.headers.clone().unwrap()));
}
match self.body {
OptionalBody::Present(ref body, _, _) => if self.content_type().unwrap_or_default().is_json() {
match serde_json::from_slice(body) {
Ok(json_body) => { map.insert("body".to_string(), json_body); },
Err(err) => {
warn!("Failed to parse json body: {}", err);
map.insert("body".to_string(), Value::String(encode(body)));
}
}
} else {
match from_utf8(body) {
Ok(s) => map.insert("body".to_string(), Value::String(s.to_string())),
Err(_) => map.insert("body".to_string(), Value::String(encode(body)))
};
},
OptionalBody::Empty => { map.insert("body".to_string(), Value::String(String::default())); },
OptionalBody::Missing => (),
OptionalBody::Null => { map.insert("body".to_string(), Value::Null); }
}
if self.matching_rules.is_not_empty() {
map.insert("matchingRules".to_string(), matchers_to_json(
&self.matching_rules.clone(), spec_version));
}
if self.generators.is_not_empty() {
map.insert("generators".to_string(), generators_to_json(
&self.generators.clone(), spec_version));
}
}
json
}
#[deprecated(since="0.6.0", note="please use `default()` from the standard Default trait instead")]
pub fn default_request() -> Request {
Request::default()
}
pub fn differences_from(&self, other: &Request) -> Vec<(DifferenceType, String)> {
let mut differences = vec![];
if self.method != other.method {
differences.push((DifferenceType::Method, format!("Request method {} != {}", self.method, other.method)));
}
if self.path != other.path {
differences.push((DifferenceType::Path, format!("Request path {} != {}", self.path, other.path)));
}
if self.query != other.query {
differences.push((DifferenceType::QueryParameters, format!("Request query {:?} != {:?}", self.query, other.query)));
}
let mut keys = self.headers.clone().map(|m| m.keys().cloned().collect_vec()).unwrap_or_default();
let mut other_keys = other.headers.clone().map(|m| m.keys().cloned().collect_vec()).unwrap_or_default();
keys.sort();
other_keys.sort();
if keys != other_keys {
differences.push((DifferenceType::Headers, format!("Request headers {:?} != {:?}", self.headers, other.headers)));
}
if self.body != other.body {
differences.push((DifferenceType::Body, format!("Request body '{:?}' != '{:?}'", self.body, other.body)));
}
if self.matching_rules != other.matching_rules {
differences.push((DifferenceType::MatchingRules, format!("Request matching rules {:?} != {:?}", self.matching_rules, other.matching_rules)));
}
differences
}
pub fn as_v4_request(&self) -> HttpRequest {
HttpRequest {
method: self.method.clone(),
path: self.path.clone(),
query: self.query.clone(),
headers: self.headers.clone(),
body: self.body.clone(),
matching_rules: self.matching_rules.clone(),
generators: self.generators.clone()
}
}
}
#[cfg(test)]
mod tests {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use expectest::prelude::*;
use maplit::hashmap;
use serde_json::Value;
use crate::bodies::OptionalBody;
use crate::content_types::{HTML, JSON, XML};
use crate::generators::{Generator, GeneratorCategory, Generators};
use crate::http_parts::HttpPart;
use crate::matchingrules::{Category, MatchingRule, MatchingRuleCategory, MatchingRules, RuleList, RuleLogic};
use crate::PactSpecification;
use crate::path_exp::DocPath;
use crate::request::Request;
#[test]
fn request_from_json_defaults_to_get() {
let request_json : serde_json::Value = serde_json::from_str(r#"
{
"path": "/",
"query": "",
"headers": {}
}
"#).unwrap();
let request = Request::from_json(&request_json, &PactSpecification::V1);
expect!(request.unwrap().method).to(be_equal_to("GET"));
}
#[test]
fn request_from_json_defaults_to_root_for_path() {
let request_json : serde_json::Value = serde_json::from_str(r#"
{
"method": "PUT",
"query": "",
"headers": {}
}
"#).unwrap();
println!("request_json: {}", request_json);
let request = Request::from_json(&request_json, &PactSpecification::V1_1);
assert_eq!(request.unwrap().path, "/".to_string());
}
#[test]
fn request_content_type_is_based_on_the_content_type_header() {
let request = Request {
method: "GET".to_string(),
path: "/".to_string(),
query: None,
headers: None,
body: OptionalBody::Missing,
..Request::default()
};
expect!(request.content_type().unwrap_or_default().to_string()).to(be_equal_to("*/*"));
expect!(Request {
headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/html".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("text/html"));
expect!(Request {
headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json; charset=UTF-8".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/json;charset=utf-8"));
expect!(Request {
headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/json"));
expect!(Request {
headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/json; charset=UTF-8".to_string()] }), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/json;charset=utf-8"));
expect!(Request {
body: OptionalBody::Present("{\"json\": true}".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/json"));
expect!(Request {
body: OptionalBody::Present("{}".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/json"));
expect!(Request {
body: OptionalBody::Present("[]".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/json"));
expect!(Request {
body: OptionalBody::Present("[1,2,3]".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/json"));
expect!(Request {
body: OptionalBody::Present("\"string\"".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/json"));
expect!(Request {
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())
.to(be_equal_to("application/xml"));
expect!(Request {
body: OptionalBody::Present("<json>false</json>".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("application/xml"));
expect!(Request {
body: OptionalBody::Present("this is not json".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("text/plain"));
expect!(Request {
body: OptionalBody::Present("<html><body>this is also not json</body></html>".into(), None, None), .. request.clone() }.content_type().unwrap_or_default().to_string())
.to(be_equal_to("text/html"));
}
#[test]
fn content_type_struct_test() {
let request = Request {
method: "GET".to_string(),
path: "/".to_string(),
query: None,
headers: None,
body: OptionalBody::Missing,
..Request::default()
};
expect!(request.content_type()).to(be_none());
expect!(Request {
headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/html".to_string()] }), .. request.clone() }.content_type())
.to(be_some().value(HTML.clone()));
expect!(Request {
headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/json".to_string()] }), .. request.clone() }.content_type())
.to(be_some().value(JSON.clone()));
expect!(Request {
headers: Some(hashmap!{ "Content-Type".to_string() => vec!["application/hal+json".to_string()] }), .. request.clone() }
.content_type().map(|c| c.base_type()))
.to(be_some().value(JSON.clone()));
expect!(Request {
headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/xml".to_string()] }), .. request.clone() }.content_type())
.to(be_some().value(XML.clone()));
expect!(Request {
headers: Some(hashmap!{ "CONTENT-TYPE".to_string() => vec!["application/stuff+xml".to_string()] }), ..
request.clone() }.content_type().map(|c| c.base_type()))
.to(be_some().value(XML.clone()));
}
#[test]
fn request_to_json_with_defaults() {
let request = Request::default();
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to("{\"method\":\"GET\",\"path\":\"/\"}"));
}
#[test]
fn request_to_json_converts_methods_to_upper_case() {
let request = Request { method: "post".to_string(), .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(be_equal_to("{\"method\":\"POST\",\"path\":\"/\"}"));
}
#[test]
fn request_to_json_with_a_query() {
let request = Request { query: Some(hashmap!{
"a".to_string() => vec!["1".to_string(), "2".to_string()],
"b".to_string() => vec!["3".to_string()]
}), .. Request::default() };
expect!(request.to_json(&PactSpecification::V2).to_string()).to(
be_equal_to(r#"{"method":"GET","path":"/","query":"a=1&a=2&b=3"}"#)
);
}
#[test]
fn request_to_json_with_a_query_must_encode_the_query() {
let request = Request { query: Some(hashmap!{
"datetime".to_string() => vec!["2011-12-03T10:15:30+01:00".to_string()],
"description".to_string() => vec!["hello world!".to_string()] }), .. Request::default() };
expect!(request.to_json(&PactSpecification::V2).to_string()).to(
be_equal_to(r#"{"method":"GET","path":"/","query":"datetime=2011-12-03T10%3a15%3a30%2b01%3a00&description=hello+world%21"}"#)
);
}
#[test]
fn request_to_json_with_a_query_must_encode_the_query_with_utf8_chars() {
let request = Request { query: Some(hashmap!{
"a".to_string() => vec!["b=c&d❤".to_string()]
}), .. Request::default() };
expect!(request.to_json(&PactSpecification::V2).to_string()).to(
be_equal_to(r#"{"method":"GET","path":"/","query":"a=b%3dc%26d%27%64"}"#)
);
}
#[test]
fn request_to_json_with_a_query_v3() {
let request = Request { query: Some(hashmap!{
"a".to_string() => vec!["1".to_string(), "2".to_string()],
"b".to_string() => vec!["3".to_string()]
}), .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to(r#"{"method":"GET","path":"/","query":{"a":["1","2"],"b":["3"]}}"#)
);
}
#[test]
fn request_to_json_with_a_query_v3_must_not_encode_the_query() {
let request = Request { query: Some(hashmap!{
"datetime".to_string() => vec!["2011-12-03T10:15:30+01:00".to_string()],
"description".to_string() => vec!["hello world!".to_string()] }), .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to(r#"{"method":"GET","path":"/","query":{"datetime":["2011-12-03T10:15:30+01:00"],"description":["hello world!"]}}"#)
);
}
#[test]
fn request_to_json_with_a_query_v3_must_not_encode_the_query_with_utf8_chars() {
let request = Request { query: Some(hashmap!{
"a".to_string() => vec!["b=c&d❤".to_string()]
}), .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to(r#"{"method":"GET","path":"/","query":{"a":["b=c&d❤"]}}"#)
);
}
#[test]
fn request_to_json_with_headers() {
let request = Request { headers: Some(hashmap!{
"HEADERA".to_string() => vec!["VALUEA".to_string()],
"HEADERB".to_string() => vec!["VALUEB1, VALUEB2".to_string()]
}), .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to(r#"{"headers":{"HEADERA":"VALUEA","HEADERB":"VALUEB1, VALUEB2"},"method":"GET","path":"/"}"#)
);
}
#[test]
fn request_to_json_with_json_body() {
let request = Request { headers: Some(hashmap!{
"Content-Type".to_string() => vec!["application/json".to_string()]
}), body: OptionalBody::Present(r#"{"key": "value"}"#.into(), None, None), .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to(r#"{"body":{"key":"value"},"headers":{"Content-Type":"application/json"},"method":"GET","path":"/"}"#)
);
}
#[test]
fn request_to_json_with_non_json_body() {
let request = Request { headers: Some(hashmap!{ "Content-Type".to_string() => vec!["text/plain".to_string()] }),
body: OptionalBody::Present("This is some text".into(), None, None), .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to(r#"{"body":"This is some text","headers":{"Content-Type":"text/plain"},"method":"GET","path":"/"}"#)
);
}
#[test]
fn request_to_json_with_empty_body() {
let request = Request { body: OptionalBody::Empty, .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to(r#"{"body":"","method":"GET","path":"/"}"#)
);
}
#[test]
fn request_to_json_with_null_body() {
let request = Request { body: OptionalBody::Null, .. Request::default() };
expect!(request.to_json(&PactSpecification::V3).to_string()).to(
be_equal_to(r#"{"body":null,"method":"GET","path":"/"}"#)
);
}
fn hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
s.finish()
}
#[test]
fn hash_for_request() {
let request1 = Request::default();
let request2 = Request { method: "POST".to_string(), .. Request::default() };
let request3 = Request { headers: Some(hashmap!{
"H1".to_string() => vec!["A".to_string()]
}), .. Request::default() };
let request4 = Request { headers: Some(hashmap!{
"H1".to_string() => vec!["B".to_string()]
}), .. Request::default() };
expect!(hash(&request1)).to(be_equal_to(hash(&request1)));
expect!(hash(&request3)).to(be_equal_to(hash(&request3)));
expect!(hash(&request1)).to_not(be_equal_to(hash(&request2)));
expect!(hash(&request3)).to_not(be_equal_to(hash(&request4)));
}
#[test]
fn request_headers_do_not_conflict_if_they_have_been_serialised_and_deserialised_to_json() {
let original_request = Request {
method: "".to_string(),
path: "".to_string(),
query: None,
headers: Some(hashmap! {
"accept".to_string() => vec!["application/xml".to_string(), "application/json".to_string()],
"user-agent".to_string() => vec!["test".to_string(), "test2".to_string()],
"content-type".to_string() => vec!["text/plain".to_string()]
}),
body: OptionalBody::Missing,
matching_rules: Default::default(),
generators: Default::default(),
};
let json = serde_json::to_string(&original_request
.to_json(&PactSpecification::V3)).expect("could not serialize");
let serialized_and_deserialized_request: Value =
serde_json::from_str(&json).expect("could not deserialize");
expect!(original_request
.differences_from(&Request::from_json(&serialized_and_deserialized_request, &PactSpecification::V3).unwrap())
.iter())
.to(be_empty());
}
#[test]
fn to_json_with_provider_state_generator_test() {
let request = Request {
method: "GET".to_string(),
path: "/data/42".to_string(),
matching_rules: MatchingRules {
rules: hashmap!{
Category::PATH => MatchingRuleCategory {
name: Category::PATH,
rules: hashmap!{
DocPath::root() => RuleList {
rules: vec![MatchingRule::Type],
rule_logic: RuleLogic::And,
cascaded: false
}
}
}
}
},
generators: Generators {
categories: hashmap!{
GeneratorCategory::PATH => hashmap!{
DocPath::root() => Generator::ProviderStateGenerator("/data/${id}".to_string(), None)
}
}
},
.. Request::default()
};
let json = request.to_json(&PactSpecification::V3);
let generators = json.get("generators").unwrap();
expect!(generators.to_string()).to_not(be_equal_to("{}"));
let rules = json.get("matchingRules").unwrap();
expect!(rules.to_string()).to_not(be_equal_to("{}"));
}
}