#![warn(missing_docs)]
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::fmt::Formatter;
use std::hash::Hash;
use std::str;
use std::str::from_utf8;
use ansi_term::*;
use ansi_term::Colour::*;
use lazy_static::*;
use log::*;
use maplit::hashmap;
use serde_json::{json, Value};
use crate::headers::match_headers;
use crate::matchers::*;
use crate::models::{HttpPart, Interaction, PactSpecification};
use crate::models::content_types::ContentType;
use crate::models::generators::*;
use crate::models::matchingrules::*;
#[macro_export]
macro_rules! s {
($e:expr) => ($e.to_string())
}
#[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;
mod headers;
#[derive(Debug, Clone)]
pub struct MatchingContext {
pub matchers: MatchingRuleCategory,
pub config: DiffConfig,
pub matching_spec: PactSpecification
}
impl MatchingContext {
pub fn new(config: DiffConfig, matchers: &MatchingRuleCategory) -> Self {
MatchingContext {
matchers: matchers.clone(),
config: config.clone(),
.. MatchingContext::default()
}
}
pub fn with_config(config: DiffConfig) -> Self {
MatchingContext {
config: config.clone(),
.. MatchingContext::default()
}
}
pub fn clone_with(&self, matchers: &MatchingRuleCategory) -> Self {
MatchingContext {
matchers: matchers.clone(),
config: self.config.clone(),
matching_spec: self.matching_spec.clone()
}
}
pub fn matcher_is_defined(&self, path: &Vec<&str>) -> bool {
self.matchers.matcher_is_defined(path)
}
pub fn select_best_matcher(&self, path: &Vec<&str>) -> Option<RuleList> {
self.matchers.select_best_matcher(path)
}
pub fn wildcard_matcher_is_defined(&self, path: &Vec<&str>) -> bool {
!self.matchers_for_exact_path(path).filter(|&(val, _)| val.ends_with(".*")).is_empty()
}
fn matchers_for_exact_path(&self, path: &Vec<&str>) -> MatchingRuleCategory {
if self.matchers.name == "body" {
self.matchers.filter(|&(val, _)| {
calc_path_weight(val, path).0 > 0 && path_length(val) == path.len()
})
} else if self.matchers.name == "header" || self.matchers.name == "query" {
self.matchers.filter(|&(val, _)| {
path.len() == 1 && path[0] == *val
})
} else {
self.matchers.filter(|_| false)
}
}
pub fn type_matcher_defined(&self, path: &Vec<&str>) -> bool {
self.matchers.resolve_matchers_for_path(path).type_matcher_defined()
}
pub fn match_keys<T: Display + Debug>(&self, path: &Vec<&str>, expected: &HashMap<String, T>, actual: &HashMap<String, T>) -> Result<(), Vec<Mismatch>> {
let mut p = path.to_vec();
p.push("any");
if !self.wildcard_matcher_is_defined(&p) {
let mut expected_keys = expected.keys().cloned().collect::<Vec<String>>();
expected_keys.sort();
let mut actual_keys = actual.keys().cloned().collect::<Vec<String>>();
actual_keys.sort();
let missing_keys: Vec<String> = expected.keys().filter(|key| !actual.contains_key(*key)).cloned().collect();
match self.config {
DiffConfig::AllowUnexpectedKeys if !missing_keys.is_empty() => {
Err(vec![Mismatch::BodyMismatch {
path: path.join("."),
expected: Some(expected.for_mismatch().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: format!("Actual map is missing the following keys: {}", missing_keys.join(", ")),
}])
}
DiffConfig::NoUnexpectedKeys if expected_keys != actual_keys => {
Err(vec![Mismatch::BodyMismatch {
path: path.join("."),
expected: Some(expected.for_mismatch().into()),
actual: Some(actual.for_mismatch().into()),
mismatch: format!("Expected a Map with keys {} but received one with keys {}",
expected_keys.join(", "), actual_keys.join(", ")),
}])
}
_ => Ok(())
}
} else {
Ok(())
}
}
}
impl Default for MatchingContext {
fn default() -> Self {
MatchingContext {
matchers: Default::default(),
config: DiffConfig::AllowUnexpectedKeys,
matching_spec: PactSpecification::V3
}
}
}
lazy_static! {
static ref BODY_MATCHERS: [
(fn(content_type: &ContentType) -> bool,
fn(expected: &dyn models::HttpPart, actual: &dyn models::HttpPart, context: &MatchingContext) -> Result<(), Vec<Mismatch>>); 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)
];
}
#[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
},
MetadataMismatch {
key: String,
expected: String,
actual: String,
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)
})
},
&Mismatch::MetadataMismatch { key: ref k, expected: ref e, actual: ref a, mismatch: ref m } => {
json!({
s!("type") : json!("MetadataMismatch"),
s!("key") : json!(k),
s!("expected") : json!(e),
s!("actual") : json!(a),
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"),
Mismatch::MetadataMismatch { .. } => s!("MetadataMismatch")
}
}
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"),
Mismatch::MetadataMismatch { .. } => s!("has matching metadata")
}
}
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),
Mismatch::MetadataMismatch { ref mismatch, .. } => mismatch.clone()
}
}
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),
Mismatch::MetadataMismatch { expected: ref e, actual: ref a, key: ref k, .. } => format!("Expected message metadata '{}' to have value '{}' but was '{}'",
Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string()))
}
}
}
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
},
(&Mismatch::MetadataMismatch{ key: ref p1, expected: ref e1, actual: ref a1, mismatch: _ },
&Mismatch::MetadataMismatch{ key: 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())
}
}
fn merge_result(res1: Result<(), Vec<Mismatch>>, res2: Result<(), Vec<Mismatch>>) -> Result<(), Vec<Mismatch>> {
match (&res1, &res2) {
(Ok(_), Ok(_)) => res1.clone(),
(Err(_), Ok(_)) => res1.clone(),
(Ok(_), Err(_)) => res2.clone(),
(Err(m1), Err(m2)) => {
let mut mismatches = m1.clone();
mismatches.extend_from_slice(&*m2);
Err(mismatches)
}
}
}
#[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>, context: &MatchingContext) -> Result<(), Vec<Mismatch>> {
let path = vec!["$"];
if context.matcher_is_defined(&path) {
let mut mismatches = vec![];
let expected_str = match from_utf8(expected) {
Ok(expected) => expected,
Err(err) => {
mismatches.push(Mismatch::BodyMismatch {
path: s!("$"),
expected: Some(expected.clone()),
actual: Some(actual.clone()),
mismatch: format!("Could not parse expected value as UTF-8 text: {}", err)
});
""
}
};
let actual_str = match from_utf8(actual) {
Ok(actual) => actual,
Err(err) => {
mismatches.push(Mismatch::BodyMismatch {
path: s!("$"),
expected: Some(expected.clone()),
actual: Some(actual.clone()),
mismatch: format!("Could not parse actual value as UTF-8 text: {}", err)
});
""
}
};
if let Err(messages) = match_values(&path, context, &expected_str, &actual_str) {
for message in messages {
mismatches.push(Mismatch::BodyMismatch {
path: s!("$"),
expected: Some(expected.clone()),
actual: Some(actual.clone()),
mismatch: message.clone()
})
}
};
if mismatches.is_empty() {
Ok(())
} else {
Err(mismatches)
}
} else if expected != actual {
Err(vec![ Mismatch::BodyMismatch { path: s!("$"), expected: Some(expected.clone()),
actual: Some(actual.clone()),
mismatch: format!("Expected text '{:?}' but received '{:?}'", expected, actual) } ])
} else {
Ok(())
}
}
pub fn match_method(expected: &String, actual: &String) -> Result<(), Mismatch> {
if expected.to_lowercase() != actual.to_lowercase() {
Err(Mismatch::MethodMismatch { expected: expected.clone(), actual: actual.clone() })
} else {
Ok(())
}
}
pub fn match_path(expected: &String, actual: &String, context: &MatchingContext) -> Result<(), Vec<Mismatch>> {
let path = vec![];
let matcher_result = if context.matcher_is_defined(&path) {
match_values(&path, context, &expected.to_string(), &actual.to_string())
} else {
expected.matches(actual, &MatchingRule::Equality).map_err(|err| vec![err])
};
matcher_result.map_err(|messages| messages.iter().map(|message| {
Mismatch::PathMismatch {
expected: expected.to_string(),
actual: actual.to_string(), mismatch: message.clone()
}
}).collect())
}
fn compare_query_parameter_value(key: &String, expected: &String, actual: &String, index: usize,
context: &MatchingContext) -> Result<(), Vec<Mismatch>> {
let index = index.to_string();
let path = vec!["$", key.as_str(), index.as_str()];
let matcher_result = if context.matcher_is_defined(&path) {
matchers::match_values(&path, context, expected, actual)
} else {
expected.matches(actual, &MatchingRule::Equality).map_err(|err| vec![err])
};
matcher_result.map_err(|messages| {
messages.iter().map(|message| {
Mismatch::QueryMismatch {
parameter: key.clone(),
expected: expected.clone(),
actual: actual.clone(),
mismatch: message.clone(),
}
}).collect()
})
}
fn compare_query_parameter_values(key: &String, expected: &Vec<String>, actual: &Vec<String>,
context: &MatchingContext) -> Result<(), Vec<Mismatch>> {
let result: Vec<Mismatch> = expected.iter().enumerate().flat_map(|(index, val)| {
if index < actual.len() {
match compare_query_parameter_value(key, val, &actual[index], index, context) {
Ok(_) => vec![],
Err(errors) => errors
}
} else {
vec![ Mismatch::QueryMismatch {
parameter: key.clone(),
expected: format!("{:?}", expected),
actual: format!("{:?}", actual),
mismatch: format!("Expected query parameter '{}' value '{}' but was missing", key, val)
} ]
}
}).collect();
if result.is_empty() {
Ok(())
} else {
Err(result)
}
}
fn match_query_values(key: &String, expected: &Vec<String>, actual: &Vec<String>, context: &MatchingContext) -> Result<(), Vec<Mismatch>> {
if expected.is_empty() && !actual.is_empty() {
Err(vec![ Mismatch::QueryMismatch { parameter: key.clone(),
expected: format!("{:?}", expected),
actual: format!("{:?}", actual),
mismatch: format!("Expected an empty parameter list for '{}' but received {:?}", key, actual) } ])
} else {
let mismatch = if expected.len() != actual.len() {
Err(vec![ 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()) } ])
} else {
Ok(())
};
merge_result(compare_query_parameter_values(key, expected, actual, context), mismatch)
}
}
fn match_query_maps(expected: HashMap<String, Vec<String>>, actual: HashMap<String, Vec<String>>, context: &MatchingContext) -> 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, context);
let v = result.entry(key.clone()).or_default();
v.extend(matches.err().unwrap_or_default());
},
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
}
pub fn match_query(expected: Option<HashMap<String, Vec<String>>>, actual: Option<HashMap<String, Vec<String>>>, context: &MatchingContext) -> HashMap<String, Vec<Mismatch>> {
match (actual, expected) {
(Some(aqm), Some(eqm)) => match_query_maps(eqm, aqm, context),
(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 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, context: &MatchingContext) -> 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);
if let Err(m) = match_fn.1(expected, actual, &context) {
mismatches.extend_from_slice(&*m);
}
},
None => {
debug!("No body matcher defined for content type '{}', using plain text matcher", content_type);
if let Err(m) = match_text(&expected.body().value(), &actual.body().value(), &context) {
mismatches.extend_from_slice(&*m);
}
}
};
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, context: &MatchingContext) -> 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, context)
}
}
pub fn match_body(expected: &dyn models::HttpPart, actual: &dyn models::HttpPart, context: &MatchingContext) -> BodyMatchResult {
let expected_content_type = expected.content_type().unwrap_or_default();
let actual_content_type = actual.content_type().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.is_equivalent_to(&actual_content_type) {
match_body_content(&expected_content_type, expected, actual, context)
} 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
}
}
pub fn match_request(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 path_context = MatchingContext::new(DiffConfig::NoUnexpectedKeys,
&expected.matching_rules.rules_for_category("path").unwrap_or_default());
let body_context = MatchingContext::new(DiffConfig::NoUnexpectedKeys,
&expected.matching_rules.rules_for_category("body").unwrap_or_default());
let query_context = MatchingContext::new(DiffConfig::NoUnexpectedKeys,
&expected.matching_rules.rules_for_category("query").unwrap_or_default());
let header_context = MatchingContext::new(DiffConfig::NoUnexpectedKeys,
&expected.matching_rules.rules_for_category("header").unwrap_or_default());
let result = RequestMatchResult {
method: match_method(&expected.method, &actual.method).err(),
path: match_path(&expected.path, &actual.path, &path_context).err(),
body: match_body(&expected, &actual, &body_context),
query: match_query(expected.query, actual.query, &query_context),
headers: match_headers(expected.headers, actual.headers, &header_context)
};
log::debug!("--> Mismatches: {:?}", result.mismatches());
result
}
pub fn match_status(expected: u16, actual: u16) -> Result<(), Mismatch> {
if expected != actual {
Err(Mismatch::StatusMismatch { expected, actual })
} else {
Ok(())
}
}
pub fn match_response(expected: models::Response, actual: models::Response) -> Vec<Mismatch> {
let mut mismatches = vec![];
log::info!("comparing to expected response: {}", expected);
let body_context = MatchingContext::new(DiffConfig::AllowUnexpectedKeys,
&expected.matching_rules.rules_for_category("body").unwrap_or_default());
let header_context = MatchingContext::new(DiffConfig::AllowUnexpectedKeys,
&expected.matching_rules.rules_for_category("header").unwrap_or_default());
mismatches.extend_from_slice(match_body(&expected, &actual, &body_context)
.mismatches().as_slice());
if let Err(mismatch) = match_status(expected.status, actual.status) {
mismatches.push(mismatch);
}
let result = match_headers(expected.headers, actual.headers, &header_context);
for values in result.values() {
mismatches.extend_from_slice(values.as_slice());
}
mismatches
}
pub fn match_message_contents(
expected: &models::message::Message,
actual: &models::message::Message,
context: &MatchingContext
) -> Result<(), Vec<Mismatch>> {
let expected_content_type = models::Interaction::content_type(expected).unwrap_or_default();
let actual_content_type = models::Interaction::content_type(actual).unwrap_or_default();
debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
actual_content_type);
if expected_content_type.is_equivalent_to(&actual_content_type) {
match match_body_content(&expected_content_type, expected, actual, context) {
BodyMatchResult::BodyTypeMismatch(expected, actual, message) => {
Err(vec![ Mismatch::BodyTypeMismatch {
expected, actual, mismatch: message,
} ])
},
BodyMatchResult::BodyMismatches(results) => {
Err(results.values().flat_map(|values| values.iter().cloned()).collect())
},
_ => Ok(())
}
} else if expected.contents.is_present() {
Err(vec![ 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),
} ])
} else {
Ok(())
}
}
pub fn match_message_metadata(
expected: &models::message::Message,
actual: &models::message::Message,
context: &MatchingContext
) -> HashMap<String, Vec<Mismatch>> {
debug!("Matching message metadata for '{}'", expected.description);
let mut result = hashmap!{};
if !expected.metadata.is_empty() || context.config == DiffConfig::NoUnexpectedKeys {
for (key, value) in &expected.metadata {
match actual.metadata.get(key) {
Some(actual_value) => {
result.insert(key.clone(), match_metadata_value(key, value,
actual_value, context).err().unwrap_or_default());
},
None => {
result.insert(key.clone(), vec![Mismatch::MetadataMismatch { key: key.clone(),
expected: value.clone(),
actual: "".to_string(),
mismatch: format!("Expected message metadata '{}' but was missing", key) }]);
}
}
}
}
result
}
fn match_metadata_value(key: &str, expected: &str, actual: &str, context: &MatchingContext) -> Result<(), Vec<Mismatch>> {
debug!("Comparing metadata values for key '{}'", key);
let path = vec![key];
let matcher_result = if context.matcher_is_defined(&path) {
matchers::match_values(&path, context, &expected.to_string(), &actual.to_string())
} else if key.to_ascii_lowercase() == "contenttype" || key.to_ascii_lowercase() == "content-type" {
debug!("Comparing message context type '{}' => '{}'", expected, actual);
headers::match_parameter_header(expected, actual, key, "metadata")
} else {
expected.to_string().matches(&actual.to_string(), &MatchingRule::Equality).map_err(|err| vec![err])
};
matcher_result.map_err(|messages| {
messages.iter().map(|message| {
Mismatch::MetadataMismatch {
key: key.to_string(),
expected: expected.to_string(),
actual: actual.to_string(),
mismatch: format!("Expected metadata key '{}' to have value '{}' but was '{}' - {}", key, expected, actual, message)
}
}).collect()
})
}
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);
let body_context = MatchingContext::new(DiffConfig::AllowUnexpectedKeys,
&expected.matching_rules.rules_for_category("body").unwrap_or_default());
let metadata_context = MatchingContext::new(DiffConfig::AllowUnexpectedKeys,
&expected.matching_rules.rules_for_category("metadata").unwrap_or_default());
mismatches.extend_from_slice(match_message_contents(expected, actual, &body_context).err().unwrap_or_default().as_slice());
for values in match_message_metadata(expected, actual, &metadata_context).values() {
mismatches.extend_from_slice(values.as_slice());
}
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| {
if let Ok(v) = generator.generate_value(&request.path, context) {
request.path = v;
}
});
generators.apply_generator(&GeneratorCategory::HEADER, |key, generator| {
if let Some(ref mut headers) = request.headers {
if headers.contains_key(key) {
if let Ok(v) = generator.generate_value(&headers.get(key).unwrap().clone(), context) {
headers.insert(key.clone(), v);
}
}
}
});
generators.apply_generator(&GeneratorCategory::QUERY, |key, generator| {
if let Some(ref mut parameters) = request.query {
if let Some(parameter) = parameters.get_mut(key) {
let mut generated = parameter.clone();
for (index, val) in parameter.iter().enumerate() {
if let Ok(v) = generator.generate_value(val, context) {
generated[index] = v;
}
}
*parameter = generated;
}
}
});
request.body = generators.apply_body_generators(&request.body, request.content_type(),
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| {
if let Ok(v) = generator.generate_value(&response.status, context) {
response.status = v;
}
});
generators.apply_generator(&GeneratorCategory::HEADER, |key, generator| {
if let Some(ref mut headers) = response.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
};
}
}
});
response.body = generators.apply_body_generators(&response.body, response.content_type(),
context);
response
}
pub fn match_interaction_request(expected: Box<dyn Interaction>, actual: Box<dyn Interaction>, _spec_version: &PactSpecification) -> Result<RequestMatchResult, String> {
if let Some(expected) = expected.as_request_response() {
Ok(match_request(expected.request, actual.as_request_response().unwrap().request))
} else {
Err(format!("match_interaction_request must be called with HTTP request/response interactions, got {}", expected.type_of()))
}
}
pub fn match_interaction_response(expected: Box<dyn Interaction>, actual: Box<dyn Interaction>, _spec_version: &PactSpecification) -> Result<Vec<Mismatch>, String> {
if let Some(expected) = expected.as_request_response() {
Ok(match_response(expected.response, actual.as_request_response().unwrap().response))
} else {
Err(format!("match_interaction_response must be called with HTTP request/response interactions, got {}", expected.type_of()))
}
}
pub fn match_interaction(expected: Box<dyn Interaction>, actual: Box<dyn Interaction>, _spec_version: &PactSpecification) -> Result<Vec<Mismatch>, String> {
if let Some(expected) = expected.as_request_response() {
let request_result = match_request(expected.request, actual.as_request_response().unwrap().request);
let response_result = match_response(expected.response, actual.as_request_response().unwrap().response);
let mut mismatches = request_result.mismatches();
mismatches.extend_from_slice(&*response_result);
Ok(mismatches)
} else if let Some(expected) = expected.as_message() {
Ok(match_message(&expected, &actual.as_message().unwrap()))
} else {
Err(format!("match_interaction must be called with either an HTTP request/response interaction or a Message, got {}", expected.type_of()))
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod generator_tests;