#![warn(missing_docs)]
#[macro_export]
macro_rules! s {
($e:expr) => ($e.to_string())
}
use std::collections::HashMap;
use std::iter::FromIterator;
use lazy_static::*;
use ansi_term::*;
use ansi_term::Colour::*;
use std::str;
use serde_json::*;
use log::*;
use maplit::hashmap;
#[macro_use] pub mod models;
mod path_exp;
mod timezone_db;
pub mod time_utils;
mod matchers;
pub mod json;
mod xml;
mod binary_utils;
use crate::models::HttpPart;
use crate::models::matchingrules::*;
use crate::models::generators::*;
use crate::matchers::*;
use std::fmt::Display;
use nom::lib::std::fmt::Formatter;
use crate::models::content_types::ContentType;
use std::hash::Hash;
fn strip_whitespace<'a, T: FromIterator<&'a str>>(val: &'a String, split_by: &'a str) -> T {
val.split(split_by).map(|v| v.trim()).collect()
}
lazy_static! {
static ref BODY_MATCHERS: [
(fn(content_type: &ContentType) -> bool,
fn(expected: &dyn models::HttpPart, actual: &dyn models::HttpPart, config: DiffConfig, mismatches: &mut Vec<Mismatch>, matchers: &MatchingRules)); 4]
= [
(|content_type| { content_type.is_json() }, json::match_json),
(|content_type| { content_type.is_xml() }, xml::match_xml),
(|content_type| { content_type.base_type() == "application/octet-stream" }, binary_utils::match_octet_stream),
(|content_type| { content_type.base_type() == "multipart/form-data" }, binary_utils::match_mime_multipart)
];
}
static PARAMETERISED_HEADER_TYPES: [&'static str; 2] = ["accept", "content-type"];
#[derive(Debug, Clone)]
pub enum Mismatch {
MethodMismatch {
expected: String,
actual: String
},
PathMismatch {
expected: String,
actual: String,
mismatch: String
},
StatusMismatch {
expected: u16,
actual: u16
},
QueryMismatch {
parameter: String,
expected: String,
actual: String,
mismatch: String
},
HeaderMismatch {
key: String,
expected: String,
actual: String,
mismatch: String
},
BodyTypeMismatch {
expected: String,
actual: String,
mismatch: String
},
BodyMismatch {
path: String,
expected: Option<Vec<u8>>,
actual: Option<Vec<u8>>,
mismatch: String
}
}
impl Mismatch {
pub fn to_json(&self) -> serde_json::Value {
match self {
&Mismatch::MethodMismatch { expected: ref e, actual: ref a } => {
json!({
s!("type") : json!("MethodMismatch"),
s!("expected") : json!(e),
s!("actual") : json!(a)
})
},
&Mismatch::PathMismatch { expected: ref e, actual: ref a, mismatch: ref m } => {
json!({
s!("type") : json!("PathMismatch"),
s!("expected") : json!(e),
s!("actual") : json!(a),
s!("mismatch") : json!(m)
})
},
&Mismatch::StatusMismatch { expected: ref e, actual: ref a } => {
json!({
s!("type") : json!("StatusMismatch"),
s!("expected") : json!(e),
s!("actual") : json!(a)
})
},
&Mismatch::QueryMismatch { parameter: ref p, expected: ref e, actual: ref a, mismatch: ref m } => {
json!({
s!("type") : json!("QueryMismatch"),
s!("parameter") : json!(p),
s!("expected") : json!(e),
s!("actual") : json!(a),
s!("mismatch") : json!(m)
})
},
&Mismatch::HeaderMismatch { key: ref k, expected: ref e, actual: ref a, mismatch: ref m } => {
json!({
s!("type") : json!("HeaderMismatch"),
s!("key") : json!(k),
s!("expected") : json!(e),
s!("actual") : json!(a),
s!("mismatch") : json!(m)
})
},
&Mismatch::BodyTypeMismatch { expected: ref e, actual: ref a, mismatch: ref m } => {
json!({
s!("type") : json!("BodyTypeMismatch"),
s!("expected") : json!(e),
s!("actual") : json!(a),
s!("mismatch") : json!(m)
})
},
&Mismatch::BodyMismatch { path: ref p, expected: ref e, actual: ref a, mismatch: ref m } => {
json!({
s!("type") : json!("BodyMismatch"),
s!("path") : json!(p),
s!("expected") : match e {
&Some(ref v) => json!(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes")),
&None => serde_json::Value::Null
},
s!("actual") : match a {
&Some(ref v) => json!(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes")),
&None => serde_json::Value::Null
},
s!("mismatch") : json!(m)
})
}
}
}
pub fn mismatch_type(&self) -> String {
match *self {
Mismatch::MethodMismatch { .. } => s!("MethodMismatch"),
Mismatch::PathMismatch { .. } => s!("PathMismatch"),
Mismatch::StatusMismatch { .. } => s!("StatusMismatch"),
Mismatch::QueryMismatch { .. } => s!("QueryMismatch"),
Mismatch::HeaderMismatch { .. } => s!("HeaderMismatch"),
Mismatch::BodyTypeMismatch { .. } => s!("BodyTypeMismatch"),
Mismatch::BodyMismatch { .. } => s!("BodyMismatch")
}
}
pub fn summary(&self) -> String {
match *self {
Mismatch::MethodMismatch { expected: ref e, .. } => format!("is a {} request", e),
Mismatch::PathMismatch { expected: ref e, .. } => format!("to path '{}'", e),
Mismatch::StatusMismatch { expected: ref e, .. } => format!("has status code {}", e),
Mismatch::QueryMismatch { ref parameter, expected: ref e, .. } => format!("includes parameter '{}' with value '{}'", parameter, e),
Mismatch::HeaderMismatch { ref key, expected: ref e, .. } => format!("includes header '{}' with value '{}'", key, e),
Mismatch::BodyTypeMismatch { .. } => s!("has a matching body"),
Mismatch::BodyMismatch { .. } => s!("has a matching body")
}
}
pub fn description(&self) -> String {
match *self {
Mismatch::MethodMismatch { expected: ref e, actual: ref a } => format!("expected {} but was {}", e, a),
Mismatch::PathMismatch { ref mismatch, .. } => mismatch.clone(),
Mismatch::StatusMismatch { expected: ref e, actual: ref a } => format!("expected {} but was {}", e, a),
Mismatch::QueryMismatch { ref mismatch, .. } => mismatch.clone(),
Mismatch::HeaderMismatch { ref mismatch, .. } => mismatch.clone(),
Mismatch::BodyTypeMismatch { expected: ref e, actual: ref a, .. } => format!("expected '{}' body but was '{}'", e, a),
Mismatch::BodyMismatch { ref path, ref mismatch, .. } => format!("{} -> {}", path, mismatch)
}
}
pub fn ansi_description(&self) -> String {
match *self {
Mismatch::MethodMismatch { expected: ref e, actual: ref a } => format!("expected {} but was {}", Red.paint(e.clone()), Green.paint(a.clone())),
Mismatch::PathMismatch { expected: ref e, actual: ref a, .. } => format!("expected '{}' but was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
Mismatch::StatusMismatch { expected: ref e, actual: ref a } => format!("expected {} but was {}", Red.paint(e.to_string()), Green.paint(a.to_string())),
Mismatch::QueryMismatch { expected: ref e, actual: ref a, parameter: ref p, .. } => format!("Expected '{}' but received '{}' for query parameter '{}'",
Red.paint(e.to_string()), Green.paint(a.to_string()), Style::new().bold().paint(p.clone())),
Mismatch::HeaderMismatch { expected: ref e, actual: ref a, key: ref k, .. } => format!("Expected header '{}' to have value '{}' but was '{}'",
Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string())),
Mismatch::BodyTypeMismatch { expected: ref e, actual: ref a, .. } => format!("expected '{}' body but was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
Mismatch::BodyMismatch { ref path, ref mismatch, .. } => format!("{} -> {}", Style::new().bold().paint(path.clone()), mismatch)
}
}
}
impl PartialEq for Mismatch {
fn eq(&self, other: &Mismatch) -> bool {
match (self, other) {
(&Mismatch::MethodMismatch{ expected: ref e1, actual: ref a1 },
&Mismatch::MethodMismatch{ expected: ref e2, actual: ref a2 }) => {
e1 == e2 && a1 == a2
},
(&Mismatch::PathMismatch{ expected: ref e1, actual: ref a1, mismatch: _ },
&Mismatch::PathMismatch{ expected: ref e2, actual: ref a2, mismatch: _ }) => {
e1 == e2 && a1 == a2
},
(&Mismatch::StatusMismatch{ expected: ref e1, actual: ref a1 },
&Mismatch::StatusMismatch{ expected: ref e2, actual: ref a2 }) => {
e1 == e2 && a1 == a2
},
(&Mismatch::BodyTypeMismatch{ expected: ref e1, actual: ref a1, mismatch: _ },
&Mismatch::BodyTypeMismatch{ expected: ref e2, actual: ref a2, mismatch: _ }) => {
e1 == e2 && a1 == a2
},
(&Mismatch::QueryMismatch{ parameter: ref p1, expected: ref e1, actual: ref a1, mismatch: _ },
&Mismatch::QueryMismatch{ parameter: ref p2, expected: ref e2, actual: ref a2, mismatch: _ }) => {
p1 == p2 && e1 == e2 && a1 == a2
},
(&Mismatch::HeaderMismatch{ key: ref p1, expected: ref e1, actual: ref a1, mismatch: _ },
&Mismatch::HeaderMismatch{ key: ref p2, expected: ref e2, actual: ref a2, mismatch: _ }) => {
p1 == p2 && e1 == e2 && a1 == a2
},
(&Mismatch::BodyMismatch{ path: ref p1, expected: ref e1, actual: ref a1, mismatch: _ },
&Mismatch::BodyMismatch{ path: ref p2, expected: ref e2, actual: ref a2, mismatch: _ }) => {
p1 == p2 && e1 == e2 && a1 == a2
},
(_, _) => false
}
}
}
impl Display for Mismatch {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum BodyMatchResult {
Ok,
BodyTypeMismatch(String, String, String),
BodyMismatches(HashMap<String, Vec<Mismatch>>)
}
impl BodyMatchResult {
pub fn mismatches(&self) -> Vec<Mismatch> {
match self {
BodyMatchResult::BodyTypeMismatch(expected, actual, message) => {
vec![Mismatch::BodyTypeMismatch {
expected: expected.clone(), actual: actual.clone(), mismatch: message.clone(),
}]
},
BodyMatchResult::BodyMismatches(results) =>
results.values().flatten().cloned().collect(),
_ => vec![]
}
}
pub fn all_matched(&self) -> bool {
match self {
BodyMatchResult::BodyTypeMismatch(_, _, _) => false,
BodyMatchResult::BodyMismatches(results) =>
results.values().all(|m| m.is_empty()),
_ => true
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RequestMatchResult {
pub method: Option<Mismatch>,
pub path: Option<Vec<Mismatch>>,
pub body: BodyMatchResult,
pub query: HashMap<String, Vec<Mismatch>>,
pub headers: HashMap<String, Vec<Mismatch>>
}
impl RequestMatchResult {
pub fn mismatches(&self) -> Vec<Mismatch> {
let mut m = vec![];
if let Some(ref mismatch) = self.method {
m.push(mismatch.clone());
}
if let Some(ref mismatches) = self.path {
m.extend_from_slice(mismatches.as_slice());
}
for mismatches in self.query.values() {
m.extend_from_slice(mismatches.as_slice());
}
for mismatches in self.headers.values() {
m.extend_from_slice(mismatches.as_slice());
}
m.extend_from_slice(self.body.mismatches().as_slice());
m
}
pub fn score(&self) -> i8 {
let mut score = 0;
if self.method.is_none() {
score += 1;
} else {
score -= 1;
}
if self.path.is_none() {
score += 1
} else {
score -= 1
}
for (_, mismatches) in &self.query {
if mismatches.is_empty() {
score += 1;
} else {
score -= 1;
}
}
for (_, mismatches) in &self.headers {
if mismatches.is_empty() {
score += 1;
} else {
score -= 1;
}
}
match &self.body {
BodyMatchResult::BodyTypeMismatch(_, _, _) => {
score -= 1;
},
BodyMatchResult::BodyMismatches(results) => {
for (_, mismatches) in results {
if mismatches.is_empty() {
score += 1;
} else {
score -= 1;
}
}
},
_ => ()
}
score
}
pub fn all_matched(&self) -> bool {
self.method.is_none() && self.path.is_none() &&
self.query.values().all(|m| m.is_empty()) &&
self.headers.values().all(|m| m.is_empty()) &&
self.body.all_matched()
}
pub fn method_or_path_mismatch(&self) -> bool {
self.method.is_some() || self.path.is_some()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DiffConfig {
AllowUnexpectedKeys,
NoUnexpectedKeys
}
pub fn match_text(expected: &Vec<u8>, actual: &Vec<u8>, mismatches: &mut Vec<Mismatch>, matchers: &MatchingRules) {
let path = vec![s!("$")];
if matchers.matcher_is_defined("body", &path) {
if let Err(messages) = match_values("body", &path, matchers.clone(), expected, actual) {
for message in messages {
mismatches.push(Mismatch::BodyMismatch {
path: s!("$"),
expected: Some(expected.clone()),
actual: Some(actual.clone()),
mismatch: message.clone()
})
}
}
} else if expected != actual {
mismatches.push(Mismatch::BodyMismatch { path: s!("$"), expected: Some(expected.clone()),
actual: Some(actual.clone()),
mismatch: format!("Expected text '{:?}' but received '{:?}'", expected, actual) });
};
}
#[deprecated(
since = "0.6.4",
note = "Use the version that returns a match result (match_method_result)"
)]
pub fn match_method(expected: String, actual: String, mismatches: &mut Vec<Mismatch>) {
if let Some(mismatch) = match_method_result(expected, actual) {
mismatches.push(mismatch);
}
}
pub fn match_method_result(expected: String, actual: String) -> Option<Mismatch> {
if expected.to_lowercase() != actual.to_lowercase() {
Some(Mismatch::MethodMismatch { expected, actual })
} else {
None
}
}
#[deprecated(
since = "0.6.4",
note = "Use the version that returns a match result (match_path_result)"
)]
pub fn match_path(expected: String, actual: String, mismatches: &mut Vec<Mismatch>,
matchers: &MatchingRules) {
if let Some(result) = match_path_result(expected, actual, matchers) {
for mismatch in result {
mismatches.push(mismatch);
}
}
}
pub fn match_path_result(expected: String, actual: String, matchers: &MatchingRules) -> Option<Vec<Mismatch>> {
let path = vec![];
let matcher_result = if matchers.matcher_is_defined("path", &path) {
matchers::match_values("path", &path, matchers.clone(), &expected, &actual)
} else {
expected.matches(&actual, &MatchingRule::Equality).map_err(|err| vec![err])
};
matcher_result.err().map(|messages| messages.iter().map(|message| {
Mismatch::PathMismatch {
expected: expected.clone(),
actual: actual.clone(), mismatch: message.clone()
}
}).collect())
}
fn compare_query_parameter_value(key: &String, expected: &String, actual: &String, index: usize,
mismatches: &mut Vec<Mismatch>, matchers: &MatchingRules) {
let path = vec![s!("$"), key.clone(), format!("{}", index)];
let matcher_result = if matchers.matcher_is_defined("query", &path) {
matchers::match_values("query", &path, matchers.clone(), expected, actual)
} else {
expected.matches(actual, &MatchingRule::Equality).map_err(|err| vec![err])
};
match matcher_result {
Err(messages) => {
for message in messages {
mismatches.push(Mismatch::QueryMismatch {
parameter: key.clone(),
expected: expected.clone(),
actual: actual.clone(),
mismatch: message
})
}
},
Ok(_) => ()
}
}
fn compare_query_parameter_values(key: &String, expected: &Vec<String>, actual: &Vec<String>,
mismatches: &mut Vec<Mismatch>, matchers: &MatchingRules) {
for (index, val) in expected.iter().enumerate() {
if index < actual.len() {
compare_query_parameter_value(key, val, &actual[index], index, mismatches, matchers);
} else {
mismatches.push(Mismatch::QueryMismatch { parameter: key.clone(),
expected: format!("{:?}", expected),
actual: format!("{:?}", actual),
mismatch: format!("Expected query parameter '{}' value '{}' but was missing", key, val) });
}
}
}
fn match_query_values(key: &String, expected: &Vec<String>, actual: &Vec<String>,
matchers: &MatchingRules) -> Vec<Mismatch> {
let mut mismatches = vec![];
if expected.is_empty() && !actual.is_empty() {
mismatches.push(Mismatch::QueryMismatch { parameter: key.clone(),
expected: format!("{:?}", expected),
actual: format!("{:?}", actual),
mismatch: format!("Expected an empty parameter list for '{}' but received {:?}", key, actual) });
} else {
if expected.len() != actual.len() {
mismatches.push(Mismatch::QueryMismatch { parameter: key.clone(),
expected: format!("{:?}", expected),
actual: format!("{:?}", actual),
mismatch: format!(
"Expected query parameter '{}' with {} value(s) but received {} value(s)",
key, expected.len(), actual.len()) });
}
compare_query_parameter_values(key, expected, actual, &mut mismatches, matchers);
};
mismatches
}
fn match_query_maps(expected: HashMap<String, Vec<String>>, actual: HashMap<String, Vec<String>>,
matchers: &MatchingRules) -> HashMap<String, Vec<Mismatch>> {
let mut result: HashMap<String, Vec<Mismatch>> = hashmap!{};
for (key, value) in &expected {
match actual.get(key) {
Some(actual_value) => {
let matches = match_query_values(key, value, actual_value, matchers);
let v = result.entry(key.clone()).or_default();
v.extend(matches);
},
None => result.entry(key.clone()).or_default().push(Mismatch::QueryMismatch { parameter: key.clone(),
expected: format!("{:?}", value),
actual: "".to_string(),
mismatch: format!("Expected query parameter '{}' but was missing", key) })
}
}
for (key, value) in &actual {
match expected.get(key) {
Some(_) => (),
None => result.entry(key.clone()).or_default().push(Mismatch::QueryMismatch { parameter: key.clone(),
expected: "".to_string(),
actual: format!("{:?}", value),
mismatch: format!("Unexpected query parameter '{}' received", key) })
}
}
result
}
#[deprecated(
since = "0.6.4",
note = "Use the version that returns a match result (match_query_result)"
)]
pub fn match_query(expected: Option<HashMap<String, Vec<String>>>,
actual: Option<HashMap<String, Vec<String>>>, mismatches: &mut Vec<Mismatch>,
matchers: &MatchingRules) {
let result = match_query_result(expected, actual, matchers);
for values in result.values() {
mismatches.extend_from_slice(values.as_slice());
}
}
pub fn match_query_result(expected: Option<HashMap<String, Vec<String>>>,
actual: Option<HashMap<String, Vec<String>>>, matchers: &MatchingRules) -> HashMap<String, Vec<Mismatch>> {
match (actual, expected) {
(Some(aqm), Some(eqm)) => match_query_maps(eqm, aqm, matchers),
(Some(aqm), None) => aqm.iter().map(|(key, value)| {
(key.clone(), vec![Mismatch::QueryMismatch { parameter: key.clone(),
expected: "".to_string(),
actual: format!("{:?}", value),
mismatch: format!("Unexpected query parameter '{}' received", key) }])
}).collect(),
(None, Some(eqm)) => eqm.iter().map(|(key, value)| {
(key.clone(), vec![Mismatch::QueryMismatch { parameter: key.clone(),
expected: format!("{:?}", value),
actual: "".to_string(),
mismatch: format!("Expected query parameter '{}' but was missing", key) }])
}).collect(),
(None, None) => hashmap!{}
}
}
fn parse_charset_parameters(parameters: &[&str]) -> HashMap<String, String> {
parameters.iter().map(|v| v.split("=").map(|p| p.trim()).collect::<Vec<&str>>())
.fold(HashMap::new(), |mut map, name_value| {
map.insert(name_value[0].to_string(), name_value[1].to_string());
map
})
}
fn match_parameter_header(expected: &String, actual: &String, header: &String) -> Vec<String> {
let expected_values: Vec<&str> = strip_whitespace(expected, ";");
let actual_values: Vec<&str> = strip_whitespace(actual, ";");
let expected_parameters = expected_values.as_slice().split_first().unwrap();
let actual_parameters = actual_values.as_slice().split_first().unwrap();
let header_mismatch = format!("Expected header '{}' to have value '{}' but was '{}'", header, expected, actual);
let mut mismatches = vec![];
if expected_parameters.0 == actual_parameters.0 {
let expected_parameter_map = parse_charset_parameters(expected_parameters.1);
let actual_parameter_map = parse_charset_parameters(actual_parameters.1);
for (k, v) in expected_parameter_map {
if actual_parameter_map.contains_key(&k) {
if v != *actual_parameter_map.get(&k).unwrap() {
mismatches.push(header_mismatch.clone());
}
} else {
mismatches.push(header_mismatch.clone());
}
}
} else {
mismatches.push(header_mismatch.clone());
}
mismatches
}
fn match_header_value(key: &String, expected: &String, actual: &String, matchers: &MatchingRules) -> Vec<Mismatch> {
let path = vec![s!("$"), key.clone()];
let expected = strip_whitespace::<String>(expected, ",");
let actual = strip_whitespace::<String>(actual, ",");
let matcher_result = if matchers.matcher_is_defined("header", &path) {
matchers::match_values("header",&path, matchers.clone(), &expected, &actual)
} else if PARAMETERISED_HEADER_TYPES.contains(&key.to_lowercase().as_str()) {
let result = match_parameter_header(&expected, &actual, &key);
if result.is_empty() {
Ok(())
} else {
Err(result)
}
} else {
expected.matches(&actual, &MatchingRule::Equality).map_err(|err| vec![err])
};
match matcher_result {
Err(messages) => messages.iter().map(|message| {
Mismatch::HeaderMismatch {
key: key.clone(),
expected: expected.clone(),
actual: actual.clone(),
mismatch: format!("Mismatch with header '{}': {}", key.clone(), message)
}
}).collect(),
Ok(_) => vec![]
}
}
fn find_entry<T>(map: &HashMap<String, T>, key: &String) -> Option<(String, T)> where T: Clone {
match map.keys().find(|k| k.to_lowercase() == key.to_lowercase() ) {
Some(k) => map.get(k).map(|v| (key.clone(), v.clone()) ),
None => None
}
}
fn match_header_maps(expected: HashMap<String, Vec<String>>, actual: HashMap<String, Vec<String>>,
matchers: &MatchingRules) -> HashMap<String, Vec<Mismatch>> {
let mut result = hashmap!{};
for (key, value) in &expected {
match find_entry(&actual, key) {
Some((_, actual_value)) => for (index, val) in value.iter().enumerate() {
result.insert(key.clone(), match_header_value(key, val,
actual_value.get(index).unwrap_or(&s!("")), matchers));
},
None => {
result.insert(key.clone(), vec![Mismatch::HeaderMismatch { key: key.clone(),
expected: format!("{:?}", value.join(", ")),
actual: "".to_string(),
mismatch: format!("Expected header '{}' but was missing", key) }]);
}
}
}
result
}
#[deprecated(
since = "0.6.4",
note = "Use the version that returns a match result (match_headers_result)"
)]
pub fn match_headers(expected: Option<HashMap<String, Vec<String>>>,
actual: Option<HashMap<String, Vec<String>>>, mismatches: &mut Vec<Mismatch>,
matchers: &MatchingRules) {
let result = match_headers_result(expected, actual, matchers);
for values in result.values() {
mismatches.extend_from_slice(values.as_slice());
}
}
pub fn match_headers_result(expected: Option<HashMap<String, Vec<String>>>,
actual: Option<HashMap<String, Vec<String>>>,
matchers: &MatchingRules) -> HashMap<String, Vec<Mismatch>> {
match (actual, expected) {
(Some(aqm), Some(eqm)) => match_header_maps(eqm, aqm, matchers),
(Some(_), None) => hashmap!{},
(None, Some(eqm)) => eqm.iter().map(|(key, value)| {
(key.clone(), vec![Mismatch::HeaderMismatch { key: key.clone(),
expected: format!("{:?}", value.join(", ")),
actual: "".to_string(),
mismatch: format!("Expected header '{}' but was missing", key) }])
}).collect(),
(None, None) => hashmap!{}
}
}
fn group_by<I, F, K>(items: I, f: F) -> HashMap<K, Vec<I::Item>>
where I: IntoIterator, F: Fn(&I::Item) -> K, K: Eq + Hash {
let mut m = hashmap!{};
for item in items {
let key = f(&item);
let values = m.entry(key).or_insert_with(|| vec![]);
values.push(item);
}
m
}
fn compare_bodies(content_type: &ContentType, expected: &dyn models::HttpPart, actual: &dyn models::HttpPart, config: DiffConfig,
matchers: &MatchingRules) -> BodyMatchResult {
let mut mismatches = vec![];
match BODY_MATCHERS.iter().find(|mt| mt.0(&content_type)) {
Some(ref match_fn) => {
debug!("Using body matcher for content type '{}'", content_type);
match_fn.1(expected, actual, config, &mut mismatches, matchers);
},
None => {
debug!("No body matcher defined for content type '{}', using plain text matcher", content_type);
match_text(&expected.body().value(), &actual.body().value(), &mut mismatches, matchers);
}
};
if mismatches.is_empty() {
BodyMatchResult::Ok
} else {
BodyMatchResult::BodyMismatches(group_by(mismatches, |m| match m {
Mismatch::BodyMismatch { path: m, ..} => m.to_string(),
_ => String::default()
}))
}
}
fn match_body_content(content_type: &ContentType, expected: &dyn models::HttpPart, actual: &dyn models::HttpPart,
config: DiffConfig, matchers: &MatchingRules) -> BodyMatchResult {
match (expected.body(), actual.body()) {
(&models::OptionalBody::Missing, _) => BodyMatchResult::Ok,
(&models::OptionalBody::Null, &models::OptionalBody::Present(ref b, _)) => {
BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
mismatch: format!("Expected empty body but received '{:?}'", b.clone()),
path: s!("/")}]})
},
(&models::OptionalBody::Empty, &models::OptionalBody::Present(ref b, _)) => {
BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
mismatch: format!("Expected empty body but received '{:?}'", b.clone()),
path: s!("/")}]})
},
(&models::OptionalBody::Null, _) => BodyMatchResult::Ok,
(&models::OptionalBody::Empty, _) => BodyMatchResult::Ok,
(e, &models::OptionalBody::Missing) => {
BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: Some(e.value()), actual: None,
mismatch: format!("Expected body '{:?}' but was missing", e.value()),
path: s!("/")}]})
},
(_, _) => {
compare_bodies(content_type, expected, actual, config, matchers)
}
}
}
#[deprecated(
since = "0.6.4",
note = "Use the version that returns a match result (match_body_result)"
)]
pub fn match_body(expected: &dyn models::HttpPart, actual: &dyn models::HttpPart, config: DiffConfig,
mismatches: &mut Vec<Mismatch>, matchers: &MatchingRules) {
mismatches.extend_from_slice(match_body_result(expected, actual, config, matchers).mismatches().as_slice());
}
pub fn match_body_result(expected: &dyn models::HttpPart, actual: &dyn models::HttpPart, config: DiffConfig,
matchers: &MatchingRules) -> BodyMatchResult {
let expected_content_type = expected.content_type_struct().unwrap_or_default();
let actual_content_type = actual.content_type_struct().unwrap_or_default();
debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
actual_content_type);
if expected_content_type.is_unknown() || actual_content_type.is_unknown() || expected_content_type == actual_content_type {
match_body_content(&expected_content_type, expected, actual, config, matchers)
} else if expected.body().is_present() {
BodyMatchResult::BodyTypeMismatch(expected_content_type.to_string(),
actual_content_type.to_string(),
format!("Expected body with content type {} but was {}", expected_content_type,
actual_content_type))
} else {
BodyMatchResult::Ok
}
}
#[deprecated(
since = "0.6.4",
note = "Use the version that returns a match result (match_request_result)"
)]
pub fn match_request(expected: models::Request, actual: models::Request) -> Vec<Mismatch> {
let mut mismatches = vec![];
log::info!("comparing to expected {}", expected);
log::debug!(" body: '{}'", expected.body.str_value());
log::debug!(" matching_rules: {:?}", expected.matching_rules);
log::debug!(" generators: {:?}", expected.generators);
match_method(expected.method.clone(), actual.method.clone(), &mut mismatches);
match_path(expected.path.clone(), actual.path.clone(), &mut mismatches, &expected.matching_rules);
match_body(&expected, &actual, DiffConfig::NoUnexpectedKeys, &mut mismatches, &expected.matching_rules);
match_query(expected.query, actual.query, &mut mismatches, &expected.matching_rules);
match_headers(expected.headers, actual.headers, &mut mismatches, &expected.matching_rules);
log::debug!("--> Mismatches: {:?}", mismatches);
mismatches
}
pub fn match_request_result(expected: models::Request, actual: models::Request) -> RequestMatchResult {
log::info!("comparing to expected {}", expected);
log::debug!(" body: '{}'", expected.body.str_value());
log::debug!(" matching_rules: {:?}", expected.matching_rules);
log::debug!(" generators: {:?}", expected.generators);
let result = RequestMatchResult {
method: match_method_result(expected.method.clone(), actual.method.clone()),
path: match_path_result(expected.path.clone(), actual.path.clone(), &expected.matching_rules),
body: match_body_result(&expected, &actual, DiffConfig::NoUnexpectedKeys, &expected.matching_rules),
query: match_query_result(expected.query, actual.query, &expected.matching_rules),
headers: match_headers_result(expected.headers, actual.headers, &expected.matching_rules)
};
log::debug!("--> Mismatches: {:?}", result.mismatches());
result
}
pub fn match_status(expected: u16, actual: u16, mismatches: &mut Vec<Mismatch>) {
if expected != actual {
mismatches.push(Mismatch::StatusMismatch { expected, actual });
}
}
pub fn match_response(expected: models::Response, actual: models::Response) -> Vec<Mismatch> {
let mut mismatches = vec![];
log::info!("comparing to expected response: {}", expected);
match_body(&expected, &actual, DiffConfig::AllowUnexpectedKeys, &mut mismatches, &expected.matching_rules);
match_status(expected.status, actual.status, &mut mismatches);
match_headers(expected.headers, actual.headers, &mut mismatches, &expected.matching_rules);
mismatches
}
pub fn match_message_contents(expected: &models::message::Message, actual: &models::message::Message, config: DiffConfig,
mismatches: &mut Vec<Mismatch>, matchers: &MatchingRules) {
let expected_content_type = expected.content_type().unwrap_or_default();
let actual_content_type = actual.content_type().unwrap_or_default();
if expected_content_type == actual_content_type {
match match_body_content(&expected_content_type, expected, actual, config, matchers) {
BodyMatchResult::BodyTypeMismatch(expected, actual, message) => {
mismatches.push(Mismatch::BodyTypeMismatch {
expected, actual, mismatch: message,
});
},
BodyMatchResult::BodyMismatches(results) => {
for values in results.values() {
mismatches.extend_from_slice(values.as_slice());
}
},
_ => ()
};
} else if expected.contents.is_present() {
mismatches.push(Mismatch::BodyTypeMismatch {
expected: expected_content_type.to_string(),
actual: actual_content_type.to_string(),
mismatch: format!("Expected message with content type {} but was {}",
expected_content_type, actual_content_type),
});
}
}
pub fn match_message(expected: models::message::Message, actual: models::message::Message) -> Vec<Mismatch> {
let mut mismatches = vec![];
log::info!("comparing to expected message: {:?}", expected);
match_message_contents(&expected, &actual, DiffConfig::AllowUnexpectedKeys, &mut mismatches, &expected.matching_rules);
mismatches
}
pub fn generate_request(request: &models::Request, context: &HashMap<String, Value>) -> models::Request {
let generators = request.generators.clone();
let mut request = request.clone();
generators.apply_generator(&GeneratorCategory::PATH, |_, generator| {
match generator.generate_value(&request.path, context) {
Ok(v) => request.path = v,
Err(_) => ()
}
});
generators.apply_generator(&GeneratorCategory::HEADER, |key, generator| {
match request.headers {
Some(ref mut headers) => if headers.contains_key(key) {
match generator.generate_value(&headers.get(key).unwrap().clone(), context) {
Ok(v) => headers.insert(key.clone(), v),
Err(_) => None
};
},
None => ()
}
});
generators.apply_generator(&GeneratorCategory::QUERY, |key, generator| {
match request.query {
Some(ref mut parameters) => match parameters.get_mut(key) {
Some(parameter) => {
let mut generated = parameter.clone();
for (index, val) in parameter.iter().enumerate() {
match generator.generate_value(val, context) {
Ok(v) => generated[index] = v,
Err(_) => ()
};
}
*parameter = generated;
},
None => ()
},
None => ()
}
});
request.body = generators.apply_body_generators(&request.body, request.content_type_struct(),
context);
request
}
pub fn generate_response(response: &models::Response, context: &HashMap<String, Value>) -> models::Response {
let generators = response.generators.clone();
let mut response = response.clone();
generators.apply_generator(&GeneratorCategory::STATUS, |_, generator| {
match generator.generate_value(&response.status, context) {
Ok(v) => response.status = v,
Err(_) => ()
}
});
generators.apply_generator(&GeneratorCategory::HEADER, |key, generator| {
match response.headers {
Some(ref mut headers) => if headers.contains_key(key) {
match generator.generate_value(&headers.get(key).unwrap().clone(), context) {
Ok(v) => headers.insert(key.clone(), v),
Err(_) => None
};
},
None => ()
}
});
response.body = generators.apply_body_generators(&response.body, response.content_type_struct(),
context);
response
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod generator_tests;