1#![warn(missing_docs)]
354
355use std::collections::{BTreeSet, HashMap, HashSet};
356use std::fmt::{Debug, Display};
357use std::fmt::Formatter;
358use std::hash::Hash;
359use std::panic::RefUnwindSafe;
360use std::str;
361use std::str::from_utf8;
362
363use ansi_term::*;
364use ansi_term::Colour::*;
365use anyhow::anyhow;
366use bytes::Bytes;
367use itertools::{Either, Itertools};
368use lazy_static::*;
369use maplit::{hashmap, hashset};
370#[cfg(feature = "plugins")] use pact_plugin_driver::catalogue_manager::find_content_matcher;
371#[cfg(feature = "plugins")] use pact_plugin_driver::plugin_models::PluginInteractionConfig;
372use serde_json::{json, Value};
373#[allow(unused_imports)] use tracing::{debug, error, info, instrument, trace, warn};
374
375use pact_models::bodies::OptionalBody;
376use pact_models::content_types::ContentType;
377use pact_models::generators::{apply_generators, GenerateValue, GeneratorCategory, GeneratorTestMode, VariantMatcher};
378use pact_models::http_parts::HttpPart;
379use pact_models::interaction::Interaction;
380use pact_models::json_utils::json_to_string;
381use pact_models::matchingrules::{Category, MatchingRule, MatchingRuleCategory, RuleList};
382use pact_models::pact::Pact;
383use pact_models::PactSpecification;
384use pact_models::path_exp::DocPath;
385use pact_models::v4::http_parts::{HttpRequest, HttpResponse};
386use pact_models::v4::message_parts::MessageContents;
387use pact_models::v4::sync_message::SynchronousMessage;
388
389use crate::generators::bodies::generators_process_body;
390use crate::generators::DefaultVariantMatcher;
391use crate::headers::{match_header_value, match_headers};
392#[cfg(feature = "plugins")] use crate::json::match_json;
393use crate::matchers::*;
394use crate::matchingrules::DisplayForMismatch;
395#[cfg(feature = "plugins")] use crate::plugin_support::{InteractionPart, setup_plugin_config};
396use crate::query::match_query_maps;
397
398#[macro_export]
400macro_rules! s {
401 ($e:expr) => ($e.to_string())
402}
403
404pub const PACT_RUST_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
406
407pub mod matchers;
408pub mod json;
409pub mod logging;
410pub mod matchingrules;
411pub mod metrics;
412pub mod generators;
413
414#[cfg(feature = "xml")] mod xml;
415pub mod binary_utils;
416pub mod headers;
417pub mod query;
418pub mod form_urlencoded;
419#[cfg(feature = "plugins")] mod plugin_support;
420
421#[cfg(not(feature = "plugins"))]
422#[derive(Clone, Debug, PartialEq)]
423pub struct PluginInteractionConfig {}
425
426pub trait MatchingContext: Debug {
428 fn matcher_is_defined(&self, path: &DocPath) -> bool;
430
431 fn select_best_matcher(&self, path: &DocPath) -> RuleList;
433
434 fn type_matcher_defined(&self, path: &DocPath) -> bool;
436
437 fn values_matcher_defined(&self, path: &DocPath) -> bool;
439
440 fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool;
442
443 fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>>;
445
446 fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig>;
448
449 fn matchers(&self) -> &MatchingRuleCategory;
451
452 fn config(&self) -> DiffConfig;
454
455 fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync>;
457}
458
459#[derive(Debug, Clone)]
460pub struct CoreMatchingContext {
462 pub matchers: MatchingRuleCategory,
464 pub config: DiffConfig,
466 pub matching_spec: PactSpecification,
468 pub plugin_configuration: HashMap<String, PluginInteractionConfig>
470}
471
472impl CoreMatchingContext {
473 pub fn new(
475 config: DiffConfig,
476 matchers: &MatchingRuleCategory,
477 plugin_configuration: &HashMap<String, PluginInteractionConfig>
478 ) -> Self {
479 CoreMatchingContext {
480 matchers: matchers.clone(),
481 config,
482 plugin_configuration: plugin_configuration.clone(),
483 .. CoreMatchingContext::default()
484 }
485 }
486
487 pub fn with_config(config: DiffConfig) -> Self {
489 CoreMatchingContext {
490 config,
491 .. CoreMatchingContext::default()
492 }
493 }
494
495 fn matchers_for_exact_path(&self, path: &DocPath) -> MatchingRuleCategory {
496 match self.matchers.name {
497 Category::HEADER | Category::QUERY => self.matchers.filter(|&(val, _)| {
498 path.len() == 1 && path.first_field() == val.first_field()
499 }),
500 Category::BODY => self.matchers.filter(|&(val, _)| {
501 let p = path.to_vec();
502 let p_slice = p.iter().map(|p| p.as_str()).collect_vec();
503 val.matches_path_exactly(p_slice.as_slice())
504 }),
505 _ => self.matchers.filter(|_| false)
506 }
507 }
508
509 #[allow(dead_code)]
510 pub(crate) fn clone_from(context: &(dyn MatchingContext + Send + Sync)) -> Self {
511 CoreMatchingContext {
512 matchers: context.matchers().clone(),
513 config: context.config().clone(),
514 plugin_configuration: context.plugin_configuration().clone(),
515 .. CoreMatchingContext::default()
516 }
517 }
518}
519
520impl Default for CoreMatchingContext {
521 fn default() -> Self {
522 CoreMatchingContext {
523 matchers: Default::default(),
524 config: DiffConfig::AllowUnexpectedKeys,
525 matching_spec: PactSpecification::V3,
526 plugin_configuration: Default::default()
527 }
528 }
529}
530
531impl MatchingContext for CoreMatchingContext {
532 #[instrument(level = "trace", ret, skip_all, fields(path, matchers = ?self.matchers))]
533 fn matcher_is_defined(&self, path: &DocPath) -> bool {
534 let path = path.to_vec();
535 let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
536 self.matchers.matcher_is_defined(path_slice.as_slice())
537 }
538
539 fn select_best_matcher(&self, path: &DocPath) -> RuleList {
540 let path = path.to_vec();
541 let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
542 self.matchers.select_best_matcher(path_slice.as_slice())
543 }
544
545 fn type_matcher_defined(&self, path: &DocPath) -> bool {
546 let path = path.to_vec();
547 let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
548 self.matchers.resolve_matchers_for_path(path_slice.as_slice()).type_matcher_defined()
549 }
550
551 fn values_matcher_defined(&self, path: &DocPath) -> bool {
552 self.matchers_for_exact_path(path).values_matcher_defined()
553 }
554
555 fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
556 let actual = self.matchers_for_exact_path(path);
557 if matchers.is_empty() {
558 actual.is_not_empty()
559 } else {
560 actual.as_rule_list().rules.iter().any(|r| matchers.contains(r.name().as_str()))
561 }
562 }
563
564 fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>> {
565 let mut expected_keys = expected.iter().cloned().collect::<Vec<String>>();
566 expected_keys.sort();
567 let mut actual_keys = actual.iter().cloned().collect::<Vec<String>>();
568 actual_keys.sort();
569 let missing_keys: Vec<String> = expected.iter().filter(|key| !actual.contains(*key)).cloned().collect();
570 let mut result = vec![];
571
572 if !self.direct_matcher_defined(path, &hashset! { "values", "each-value", "each-key" }) {
573 match self.config {
574 DiffConfig::AllowUnexpectedKeys if !missing_keys.is_empty() => {
575 result.push(CommonMismatch {
576 path: path.to_string(),
577 expected: expected.for_mismatch(),
578 actual: actual.for_mismatch(),
579 description: format!("Actual map is missing the following keys: {}", missing_keys.join(", ")),
580 });
581 }
582 DiffConfig::NoUnexpectedKeys if expected_keys != actual_keys => {
583 result.push(CommonMismatch {
584 path: path.to_string(),
585 expected: expected.for_mismatch(),
586 actual: actual.for_mismatch(),
587 description: format!("Expected a Map with keys [{}] but received one with keys [{}]",
588 expected_keys.join(", "), actual_keys.join(", ")),
589 });
590 }
591 _ => {}
592 }
593 }
594
595 if self.direct_matcher_defined(path, &Default::default()) {
596 let matchers = self.select_best_matcher(path);
597 for matcher in matchers.rules {
598 match matcher {
599 MatchingRule::EachKey(definition) => {
600 for sub_matcher in definition.rules {
601 match sub_matcher {
602 Either::Left(rule) => {
603 for key in &actual_keys {
604 let key_path = path.join(key);
605 if let Err(err) = String::default().matches_with(key, &rule, false) {
606 result.push(CommonMismatch {
607 path: key_path.to_string(),
608 expected: "".to_string(),
609 actual: key.clone(),
610 description: err.to_string(),
611 });
612 }
613 }
614 }
615 Either::Right(name) => {
616 result.push(CommonMismatch {
617 path: path.to_string(),
618 expected: expected.for_mismatch(),
619 actual: actual.for_mismatch(),
620 description: format!("Expected a matching rule, found an unresolved reference '{}'",
621 name.name),
622 });
623 }
624 }
625 }
626 }
627 _ => {}
628 }
629 }
630 }
631
632 if result.is_empty() {
633 Ok(())
634 } else {
635 Err(result)
636 }
637 }
638
639 fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
640 &self.plugin_configuration
641 }
642
643 fn matchers(&self) -> &MatchingRuleCategory {
644 &self.matchers
645 }
646
647 fn config(&self) -> DiffConfig {
648 self.config
649 }
650
651 fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync> {
652 Box::new(CoreMatchingContext {
653 matchers: matchers.clone(),
654 config: self.config.clone(),
655 matching_spec: self.matching_spec,
656 plugin_configuration: self.plugin_configuration.clone()
657 })
658 }
659}
660
661#[derive(Debug, Clone, Default)]
662pub struct HeaderMatchingContext {
664 inner_context: CoreMatchingContext
665}
666
667impl HeaderMatchingContext {
668 pub fn new(context: &(dyn MatchingContext + Send + Sync)) -> Self {
670 let matchers = context.matchers();
671 HeaderMatchingContext {
672 inner_context: CoreMatchingContext::new(
673 context.config(),
674 &MatchingRuleCategory {
675 name: matchers.name.clone(),
676 rules: matchers.rules.iter()
677 .map(|(path, rules)| {
678 (path.to_lower_case(), rules.clone())
679 })
680 .collect()
681 },
682 &context.plugin_configuration()
683 )
684 }
685 }
686}
687
688impl MatchingContext for HeaderMatchingContext {
689 fn matcher_is_defined(&self, path: &DocPath) -> bool {
690 self.inner_context.matcher_is_defined(path)
691 }
692
693 fn select_best_matcher(&self, path: &DocPath) -> RuleList {
694 self.inner_context.select_best_matcher(path)
695 }
696
697 fn type_matcher_defined(&self, path: &DocPath) -> bool {
698 self.inner_context.type_matcher_defined(path)
699 }
700
701 fn values_matcher_defined(&self, path: &DocPath) -> bool {
702 self.inner_context.values_matcher_defined(path)
703 }
704
705 fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
706 self.inner_context.direct_matcher_defined(path, matchers)
707 }
708
709 fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>> {
710 self.inner_context.match_keys(path, expected, actual)
711 }
712
713 fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
714 self.inner_context.plugin_configuration()
715 }
716
717 fn matchers(&self) -> &MatchingRuleCategory {
718 self.inner_context.matchers()
719 }
720
721 fn config(&self) -> DiffConfig {
722 self.inner_context.config()
723 }
724
725 fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync> {
726 Box::new(HeaderMatchingContext::new(
727 &CoreMatchingContext {
728 matchers: matchers.clone(),
729 config: self.inner_context.config.clone(),
730 matching_spec: self.inner_context.matching_spec,
731 plugin_configuration: self.inner_context.plugin_configuration.clone()
732 }
733 ))
734 }
735}
736
737lazy_static! {
738 static ref BODY_MATCHERS: [
739 (fn(content_type: &ContentType) -> bool,
740 fn(expected: &(dyn HttpPart + Send + Sync), actual: &(dyn HttpPart + Send + Sync), context: &(dyn MatchingContext + Send + Sync)) -> Result<(), Vec<Mismatch>>); 5]
741 = [
742 (|content_type| { content_type.is_json() }, json::match_json),
743 (|content_type| { content_type.is_xml() }, match_xml),
744 (|content_type| { content_type.main_type == "multipart" }, binary_utils::match_mime_multipart),
745 (|content_type| { content_type.base_type() == "application/x-www-form-urlencoded" }, form_urlencoded::match_form_urlencoded),
746 (|content_type| { content_type.is_binary() || content_type.base_type() == "application/octet-stream" }, binary_utils::match_octet_stream)
747 ];
748}
749
750fn match_xml(
751 expected: &(dyn HttpPart + Send + Sync),
752 actual: &(dyn HttpPart + Send + Sync),
753 context: &(dyn MatchingContext + Send + Sync)
754) -> Result<(), Vec<Mismatch>> {
755 #[cfg(feature = "xml")]
756 {
757 xml::match_xml(expected, actual, context)
758 }
759 #[cfg(not(feature = "xml"))]
760 {
761 warn!("Matching XML documents requires the xml feature to be enabled");
762 match_text(&expected.body().value(), &actual.body().value(), context)
763 }
764}
765
766#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
768pub struct CommonMismatch {
769 pub path: String,
771 expected: String,
773 actual: String,
775 description: String
777}
778
779impl CommonMismatch {
780 pub fn to_body_mismatch(&self) -> Mismatch {
782 Mismatch::BodyMismatch {
783 path: self.path.clone(),
784 expected: Some(self.expected.clone().into()),
785 actual: Some(self.actual.clone().into()),
786 mismatch: self.description.clone()
787 }
788 }
789
790 pub fn to_query_mismatch(&self) -> Mismatch {
792 Mismatch::QueryMismatch {
793 parameter: self.path.clone(),
794 expected: self.expected.clone(),
795 actual: self.actual.clone(),
796 mismatch: self.description.clone()
797 }
798 }
799
800 pub fn to_header_mismatch(&self) -> Mismatch {
802 Mismatch::HeaderMismatch {
803 key: self.path.clone(),
804 expected: self.expected.clone().into(),
805 actual: self.actual.clone().into(),
806 mismatch: self.description.clone()
807 }
808 }
809}
810
811impl Display for CommonMismatch {
812 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
813 write!(f, "{}", self.description)
814 }
815}
816
817impl PartialEq for CommonMismatch {
818 fn eq(&self, other: &CommonMismatch) -> bool {
819 self.path == other.path && self.expected == other.expected && self.actual == other.actual
820 }
821}
822
823impl From<Mismatch> for CommonMismatch {
824 fn from(value: Mismatch) -> Self {
825 match value {
826 Mismatch::MethodMismatch { expected, actual } => CommonMismatch {
827 path: "".to_string(),
828 expected: expected.clone(),
829 actual: actual.clone(),
830 description: "Method mismatch".to_string()
831 },
832 Mismatch::PathMismatch { expected, actual, mismatch } => CommonMismatch {
833 path: "".to_string(),
834 expected: expected.clone(),
835 actual: actual.clone(),
836 description: mismatch.clone()
837 },
838 Mismatch::StatusMismatch { expected, actual, mismatch } => CommonMismatch {
839 path: "".to_string(),
840 expected: expected.to_string(),
841 actual: actual.to_string(),
842 description: mismatch.clone()
843 },
844 Mismatch::QueryMismatch { parameter, expected, actual, mismatch } => CommonMismatch {
845 path: parameter.clone(),
846 expected: expected.clone(),
847 actual: actual.clone(),
848 description: mismatch.clone()
849 },
850 Mismatch::HeaderMismatch { key, expected, actual, mismatch } => CommonMismatch {
851 path: key.clone(),
852 expected: expected.clone(),
853 actual: actual.clone(),
854 description: mismatch.clone()
855 },
856 Mismatch::BodyTypeMismatch { expected, actual, mismatch, .. } => CommonMismatch {
857 path: "".to_string(),
858 expected: expected.clone(),
859 actual: actual.clone(),
860 description: mismatch.clone()
861 },
862 Mismatch::BodyMismatch { path, expected, actual, mismatch } => CommonMismatch {
863 path: path.clone(),
864 expected: String::from_utf8_lossy(expected.unwrap_or_default().as_ref()).to_string(),
865 actual: String::from_utf8_lossy(actual.unwrap_or_default().as_ref()).to_string(),
866 description: mismatch.clone()
867 },
868 Mismatch::MetadataMismatch { key, expected, actual, mismatch } => CommonMismatch {
869 path: key.clone(),
870 expected: expected.clone(),
871 actual: actual.clone(),
872 description: mismatch.clone()
873 }
874 }
875 }
876}
877
878#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
880pub enum Mismatch {
881 MethodMismatch {
883 expected: String,
885 actual: String
887 },
888 PathMismatch {
890 expected: String,
892 actual: String,
894 mismatch: String
896 },
897 StatusMismatch {
899 expected: u16,
901 actual: u16,
903 mismatch: String
905 },
906 QueryMismatch {
908 parameter: String,
910 expected: String,
912 actual: String,
914 mismatch: String
916 },
917 HeaderMismatch {
919 key: String,
921 expected: String,
923 actual: String,
925 mismatch: String
927 },
928 BodyTypeMismatch {
930 expected: String,
932 actual: String,
934 mismatch: String,
936 expected_body: Option<Bytes>,
938 actual_body: Option<Bytes>
940 },
941 BodyMismatch {
943 path: String,
945 expected: Option<Bytes>,
947 actual: Option<Bytes>,
949 mismatch: String
951 },
952 MetadataMismatch {
954 key: String,
956 expected: String,
958 actual: String,
960 mismatch: String
962 }
963}
964
965impl Mismatch {
966 pub fn to_json(&self) -> serde_json::Value {
968 match self {
969 Mismatch::MethodMismatch { expected: e, actual: a } => {
970 json!({
971 "type" : "MethodMismatch",
972 "expected" : e,
973 "actual" : a
974 })
975 },
976 Mismatch::PathMismatch { expected: e, actual: a, mismatch: m } => {
977 json!({
978 "type" : "PathMismatch",
979 "expected" : e,
980 "actual" : a,
981 "mismatch" : m
982 })
983 },
984 Mismatch::StatusMismatch { expected: e, actual: a, mismatch: m } => {
985 json!({
986 "type" : "StatusMismatch",
987 "expected" : e,
988 "actual" : a,
989 "mismatch": m
990 })
991 },
992 Mismatch::QueryMismatch { parameter: p, expected: e, actual: a, mismatch: m } => {
993 json!({
994 "type" : "QueryMismatch",
995 "parameter" : p,
996 "expected" : e,
997 "actual" : a,
998 "mismatch" : m
999 })
1000 },
1001 Mismatch::HeaderMismatch { key: k, expected: e, actual: a, mismatch: m } => {
1002 json!({
1003 "type" : "HeaderMismatch",
1004 "key" : k,
1005 "expected" : e,
1006 "actual" : a,
1007 "mismatch" : m
1008 })
1009 },
1010 Mismatch::BodyTypeMismatch {
1011 expected,
1012 actual,
1013 mismatch,
1014 expected_body,
1015 actual_body
1016 } => {
1017 json!({
1018 "type" : "BodyTypeMismatch",
1019 "expected" : expected,
1020 "actual" : actual,
1021 "mismatch" : mismatch,
1022 "expectedBody": match expected_body {
1023 Some(v) => serde_json::Value::String(str::from_utf8(v)
1024 .unwrap_or("ERROR: could not convert to UTF-8 from bytes").into()),
1025 None => serde_json::Value::Null
1026 },
1027 "actualBody": match actual_body {
1028 Some(v) => serde_json::Value::String(str::from_utf8(v)
1029 .unwrap_or("ERROR: could not convert to UTF-8 from bytes").into()),
1030 None => serde_json::Value::Null
1031 }
1032 })
1033 },
1034 Mismatch::BodyMismatch { path, expected, actual, mismatch } => {
1035 json!({
1036 "type" : "BodyMismatch",
1037 "path" : path,
1038 "expected" : match expected {
1039 Some(v) => serde_json::Value::String(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes").into()),
1040 None => serde_json::Value::Null
1041 },
1042 "actual" : match actual {
1043 Some(v) => serde_json::Value::String(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes").into()),
1044 None => serde_json::Value::Null
1045 },
1046 "mismatch" : mismatch
1047 })
1048 }
1049 Mismatch::MetadataMismatch { key, expected, actual, mismatch } => {
1050 json!({
1051 "type" : "MetadataMismatch",
1052 "key" : key,
1053 "expected" : expected,
1054 "actual" : actual,
1055 "mismatch" : mismatch
1056 })
1057 }
1058 }
1059 }
1060
1061 pub fn mismatch_type(&self) -> &str {
1063 match *self {
1064 Mismatch::MethodMismatch { .. } => "MethodMismatch",
1065 Mismatch::PathMismatch { .. } => "PathMismatch",
1066 Mismatch::StatusMismatch { .. } => "StatusMismatch",
1067 Mismatch::QueryMismatch { .. } => "QueryMismatch",
1068 Mismatch::HeaderMismatch { .. } => "HeaderMismatch",
1069 Mismatch::BodyTypeMismatch { .. } => "BodyTypeMismatch",
1070 Mismatch::BodyMismatch { .. } => "BodyMismatch",
1071 Mismatch::MetadataMismatch { .. } => "MetadataMismatch"
1072 }
1073 }
1074
1075 pub fn summary(&self) -> String {
1077 match *self {
1078 Mismatch::MethodMismatch { expected: ref e, .. } => format!("is a {} request", e),
1079 Mismatch::PathMismatch { expected: ref e, .. } => format!("to path '{}'", e),
1080 Mismatch::StatusMismatch { expected: ref e, .. } => format!("has status code {}", e),
1081 Mismatch::QueryMismatch { ref parameter, expected: ref e, .. } => format!("includes parameter '{}' with value '{}'", parameter, e),
1082 Mismatch::HeaderMismatch { ref key, expected: ref e, .. } => format!("includes header '{}' with value '{}'", key, e),
1083 Mismatch::BodyTypeMismatch { .. } => "has a matching body".to_string(),
1084 Mismatch::BodyMismatch { .. } => "has a matching body".to_string(),
1085 Mismatch::MetadataMismatch { .. } => "has matching metadata".to_string()
1086 }
1087 }
1088
1089 pub fn description(&self) -> String {
1091 match self {
1092 Mismatch::MethodMismatch { expected: e, actual: a } => format!("expected {} but was {}", e, a),
1093 Mismatch::PathMismatch { mismatch, .. } => mismatch.clone(),
1094 Mismatch::StatusMismatch { mismatch, .. } => mismatch.clone(),
1095 Mismatch::QueryMismatch { mismatch, .. } => mismatch.clone(),
1096 Mismatch::HeaderMismatch { mismatch, .. } => mismatch.clone(),
1097 Mismatch::BodyTypeMismatch { expected: e, actual: a, .. } =>
1098 format!("Expected a body of '{}' but the actual content type was '{}'", e, a),
1099 Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", path, mismatch),
1100 Mismatch::MetadataMismatch { mismatch, .. } => mismatch.clone()
1101 }
1102 }
1103
1104 pub fn ansi_description(&self) -> String {
1106 match self {
1107 Mismatch::MethodMismatch { expected: e, actual: a } => format!("expected {} but was {}", Red.paint(e.clone()), Green.paint(a.clone())),
1108 Mismatch::PathMismatch { expected: e, actual: a, .. } => format!("expected '{}' but was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
1109 Mismatch::StatusMismatch { expected: e, actual: a, .. } => format!("expected {} but was {}", Red.paint(e.to_string()), Green.paint(a.to_string())),
1110 Mismatch::QueryMismatch { expected: e, actual: a, parameter: p, .. } => format!("Expected '{}' but received '{}' for query parameter '{}'",
1111 Red.paint(e.to_string()), Green.paint(a.to_string()), Style::new().bold().paint(p.clone())),
1112 Mismatch::HeaderMismatch { expected: e, actual: a, key: k, .. } => format!("Expected header '{}' to have value '{}' but was '{}'",
1113 Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string())),
1114 Mismatch::BodyTypeMismatch { expected: e, actual: a, .. } =>
1115 format!("expected a body of '{}' but the actual content type was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
1116 Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", Style::new().bold().paint(path.clone()), mismatch),
1117 Mismatch::MetadataMismatch { expected: e, actual: a, key: k, .. } => format!("Expected message metadata '{}' to have value '{}' but was '{}'",
1118 Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string()))
1119 }
1120 }
1121}
1122
1123impl PartialEq for Mismatch {
1124 fn eq(&self, other: &Mismatch) -> bool {
1125 match (self, other) {
1126 (Mismatch::MethodMismatch { expected: e1, actual: a1 },
1127 Mismatch::MethodMismatch { expected: e2, actual: a2 }) => {
1128 e1 == e2 && a1 == a2
1129 },
1130 (Mismatch::PathMismatch { expected: e1, actual: a1, .. },
1131 Mismatch::PathMismatch { expected: e2, actual: a2, .. }) => {
1132 e1 == e2 && a1 == a2
1133 },
1134 (Mismatch::StatusMismatch { expected: e1, actual: a1, .. },
1135 Mismatch::StatusMismatch { expected: e2, actual: a2, .. }) => {
1136 e1 == e2 && a1 == a2
1137 },
1138 (Mismatch::BodyTypeMismatch { expected: e1, actual: a1, .. },
1139 Mismatch::BodyTypeMismatch { expected: e2, actual: a2, .. }) => {
1140 e1 == e2 && a1 == a2
1141 },
1142 (Mismatch::QueryMismatch { parameter: p1, expected: e1, actual: a1, .. },
1143 Mismatch::QueryMismatch { parameter: p2, expected: e2, actual: a2, .. }) => {
1144 p1 == p2 && e1 == e2 && a1 == a2
1145 },
1146 (Mismatch::HeaderMismatch { key: p1, expected: e1, actual: a1, .. },
1147 Mismatch::HeaderMismatch { key: p2, expected: e2, actual: a2, .. }) => {
1148 p1 == p2 && e1 == e2 && a1 == a2
1149 },
1150 (Mismatch::BodyMismatch { path: p1, expected: e1, actual: a1, .. },
1151 Mismatch::BodyMismatch { path: p2, expected: e2, actual: a2, .. }) => {
1152 p1 == p2 && e1 == e2 && a1 == a2
1153 },
1154 (Mismatch::MetadataMismatch { key: p1, expected: e1, actual: a1, .. },
1155 Mismatch::MetadataMismatch { key: p2, expected: e2, actual: a2, .. }) => {
1156 p1 == p2 && e1 == e2 && a1 == a2
1157 },
1158 (_, _) => false
1159 }
1160 }
1161}
1162
1163impl Display for Mismatch {
1164 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1165 write!(f, "{}", self.description())
1166 }
1167}
1168
1169fn merge_result<T: Clone>(res1: Result<(), Vec<T>>, res2: Result<(), Vec<T>>) -> Result<(), Vec<T>> {
1170 match (&res1, &res2) {
1171 (Ok(_), Ok(_)) => res1.clone(),
1172 (Err(_), Ok(_)) => res1.clone(),
1173 (Ok(_), Err(_)) => res2.clone(),
1174 (Err(m1), Err(m2)) => {
1175 let mut mismatches = m1.clone();
1176 mismatches.extend_from_slice(&*m2);
1177 Err(mismatches)
1178 }
1179 }
1180}
1181
1182#[derive(Debug, Clone, PartialEq)]
1184pub enum BodyMatchResult {
1185 Ok,
1187 BodyTypeMismatch {
1189 expected_type: String,
1191 actual_type: String,
1193 message: String,
1195 expected: Option<Bytes>,
1197 actual: Option<Bytes>
1199 },
1200 BodyMismatches(HashMap<String, Vec<Mismatch>>)
1202}
1203
1204impl BodyMatchResult {
1205 pub fn mismatches(&self) -> Vec<Mismatch> {
1207 match self {
1208 BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected, actual } => {
1209 vec![Mismatch::BodyTypeMismatch {
1210 expected: expected_type.clone(),
1211 actual: actual_type.clone(),
1212 mismatch: message.clone(),
1213 expected_body: expected.clone(),
1214 actual_body: actual.clone()
1215 }]
1216 },
1217 BodyMatchResult::BodyMismatches(results) =>
1218 results.values().flatten().cloned().collect(),
1219 _ => vec![]
1220 }
1221 }
1222
1223 pub fn all_matched(&self) -> bool {
1225 match self {
1226 BodyMatchResult::BodyTypeMismatch { .. } => false,
1227 BodyMatchResult::BodyMismatches(results) =>
1228 results.values().all(|m| m.is_empty()),
1229 _ => true
1230 }
1231 }
1232}
1233
1234#[derive(Debug, Clone, PartialEq)]
1236pub struct RequestMatchResult {
1237 pub method: Option<Mismatch>,
1239 pub path: Option<Vec<Mismatch>>,
1241 pub body: BodyMatchResult,
1243 pub query: HashMap<String, Vec<Mismatch>>,
1245 pub headers: HashMap<String, Vec<Mismatch>>
1247}
1248
1249impl RequestMatchResult {
1250 pub fn mismatches(&self) -> Vec<Mismatch> {
1252 let mut m = vec![];
1253
1254 if let Some(ref mismatch) = self.method {
1255 m.push(mismatch.clone());
1256 }
1257 if let Some(ref mismatches) = self.path {
1258 m.extend_from_slice(mismatches.as_slice());
1259 }
1260 for mismatches in self.query.values() {
1261 m.extend_from_slice(mismatches.as_slice());
1262 }
1263 for mismatches in self.headers.values() {
1264 m.extend_from_slice(mismatches.as_slice());
1265 }
1266 m.extend_from_slice(self.body.mismatches().as_slice());
1267
1268 m
1269 }
1270
1271 pub fn score(&self) -> i8 {
1273 let mut score = 0;
1274 if self.method.is_none() {
1275 score += 1;
1276 } else {
1277 score -= 1;
1278 }
1279 if self.path.is_none() {
1280 score += 1
1281 } else {
1282 score -= 1
1283 }
1284 for mismatches in self.query.values() {
1285 if mismatches.is_empty() {
1286 score += 1;
1287 } else {
1288 score -= 1;
1289 }
1290 }
1291 for mismatches in self.headers.values() {
1292 if mismatches.is_empty() {
1293 score += 1;
1294 } else {
1295 score -= 1;
1296 }
1297 }
1298 match &self.body {
1299 BodyMatchResult::BodyTypeMismatch { .. } => {
1300 score -= 1;
1301 },
1302 BodyMatchResult::BodyMismatches(results) => {
1303 for mismatches in results.values() {
1304 if mismatches.is_empty() {
1305 score += 1;
1306 } else {
1307 score -= 1;
1308 }
1309 }
1310 },
1311 _ => ()
1312 }
1313 score
1314 }
1315
1316 pub fn all_matched(&self) -> bool {
1318 self.method.is_none() && self.path.is_none() &&
1319 self.query.values().all(|m| m.is_empty()) &&
1320 self.headers.values().all(|m| m.is_empty()) &&
1321 self.body.all_matched()
1322 }
1323
1324 pub fn method_or_path_mismatch(&self) -> bool {
1326 self.method.is_some() || self.path.is_some()
1327 }
1328}
1329
1330#[derive(Debug, Clone, Copy, PartialEq)]
1332pub enum DiffConfig {
1333 AllowUnexpectedKeys,
1335 NoUnexpectedKeys
1337}
1338
1339pub fn match_text(expected: &Option<Bytes>, actual: &Option<Bytes>, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
1341 let path = DocPath::root();
1342 if context.matcher_is_defined(&path) {
1343 let mut mismatches = vec![];
1344 let empty = Bytes::default();
1345 let expected_str = match from_utf8(expected.as_ref().unwrap_or(&empty)) {
1346 Ok(expected) => expected,
1347 Err(err) => {
1348 mismatches.push(Mismatch::BodyMismatch {
1349 path: "$".to_string(),
1350 expected: expected.clone(),
1351 actual: actual.clone(),
1352 mismatch: format!("Could not parse expected value as UTF-8 text: {}", err)
1353 });
1354 ""
1355 }
1356 };
1357 let actual_str = match from_utf8(actual.as_ref().unwrap_or(&empty)) {
1358 Ok(actual) => actual,
1359 Err(err) => {
1360 mismatches.push(Mismatch::BodyMismatch {
1361 path: "$".to_string(),
1362 expected: expected.clone(),
1363 actual: actual.clone(),
1364 mismatch: format!("Could not parse actual value as UTF-8 text: {}", err)
1365 });
1366 ""
1367 }
1368 };
1369 if let Err(messages) = match_values(&path, &context.select_best_matcher(&path), expected_str, actual_str) {
1370 for message in messages {
1371 mismatches.push(Mismatch::BodyMismatch {
1372 path: "$".to_string(),
1373 expected: expected.clone(),
1374 actual: actual.clone(),
1375 mismatch: message.clone()
1376 })
1377 }
1378 };
1379 if mismatches.is_empty() {
1380 Ok(())
1381 } else {
1382 Err(mismatches)
1383 }
1384 } else if expected != actual {
1385 let expected = expected.clone().unwrap_or_default();
1386 let actual = actual.clone().unwrap_or_default();
1387 let e = String::from_utf8_lossy(&expected);
1388 let a = String::from_utf8_lossy(&actual);
1389 let mismatch = format!("Expected body '{}' to match '{}' using equality but did not match", e, a);
1390 Err(vec![
1391 Mismatch::BodyMismatch {
1392 path: "$".to_string(),
1393 expected: Some(expected.clone()),
1394 actual: Some(actual.clone()),
1395 mismatch
1396 }
1397 ])
1398 } else {
1399 Ok(())
1400 }
1401}
1402
1403pub fn match_method(expected: &str, actual: &str) -> Result<(), Mismatch> {
1405 if expected.to_lowercase() != actual.to_lowercase() {
1406 Err(Mismatch::MethodMismatch { expected: expected.to_string(), actual: actual.to_string() })
1407 } else {
1408 Ok(())
1409 }
1410}
1411
1412pub fn match_path(expected: &str, actual: &str, context: &(dyn MatchingContext + Send + Sync)) -> Result<(), Vec<Mismatch>> {
1414 let path = DocPath::empty();
1415 let matcher_result = if context.matcher_is_defined(&path) {
1416 match_values(&path, &context.select_best_matcher(&path), expected.to_string(), actual.to_string())
1417 } else {
1418 expected.matches_with(actual, &MatchingRule::Equality, false).map_err(|err| vec![err])
1419 .map_err(|errors| errors.iter().map(|err| err.to_string()).collect())
1420 };
1421 matcher_result.map_err(|messages| messages.iter().map(|message| {
1422 Mismatch::PathMismatch {
1423 expected: expected.to_string(),
1424 actual: actual.to_string(), mismatch: message.clone()
1425 }
1426 }).collect())
1427}
1428
1429pub fn match_query(
1431 expected: Option<HashMap<String, Vec<Option<String>>>>,
1432 actual: Option<HashMap<String, Vec<Option<String>>>>,
1433 context: &(dyn MatchingContext + Send + Sync)
1434) -> HashMap<String, Vec<Mismatch>> {
1435 match (actual, expected) {
1436 (Some(aqm), Some(eqm)) => match_query_maps(eqm, aqm, context),
1437 (Some(aqm), None) => aqm.iter().map(|(key, value)| {
1438 let actual_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
1439 (key.clone(), vec![Mismatch::QueryMismatch {
1440 parameter: key.clone(),
1441 expected: "".to_string(),
1442 actual: format!("{:?}", actual_value),
1443 mismatch: format!("Unexpected query parameter '{}' received", key)
1444 }])
1445 }).collect(),
1446 (None, Some(eqm)) => eqm.iter().map(|(key, value)| {
1447 let expected_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
1448 (key.clone(), vec![Mismatch::QueryMismatch {
1449 parameter: key.clone(),
1450 expected: format!("{:?}", expected_value),
1451 actual: "".to_string(),
1452 mismatch: format!("Expected query parameter '{}' but was missing", key)
1453 }])
1454 }).collect(),
1455 (None, None) => hashmap!{}
1456 }
1457}
1458
1459fn group_by<I, F, K>(items: I, f: F) -> HashMap<K, Vec<I::Item>>
1460 where I: IntoIterator, F: Fn(&I::Item) -> K, K: Eq + Hash {
1461 let mut m = hashmap!{};
1462 for item in items {
1463 let key = f(&item);
1464 let values = m.entry(key).or_insert_with(Vec::new);
1465 values.push(item);
1466 }
1467 m
1468}
1469
1470#[instrument(level = "trace", ret, skip_all)]
1471pub(crate) async fn compare_bodies(
1472 content_type: &ContentType,
1473 expected: &(dyn HttpPart + Send + Sync),
1474 actual: &(dyn HttpPart + Send + Sync),
1475 context: &(dyn MatchingContext + Send + Sync)
1476) -> BodyMatchResult {
1477 let mut mismatches = vec![];
1478
1479 trace!(?content_type, "Comparing bodies");
1480
1481 #[cfg(feature = "plugins")]
1482 {
1483 match find_content_matcher(content_type) {
1484 Some(matcher) => {
1485 debug!("Using content matcher {} for content type '{}'", matcher.catalogue_entry_key(), content_type);
1486 if matcher.is_core() {
1487 if let Err(m) = match matcher.catalogue_entry_key().as_str() {
1488 "core/content-matcher/form-urlencoded" => form_urlencoded::match_form_urlencoded(expected, actual, context),
1489 "core/content-matcher/json" => match_json(expected, actual, context),
1490 "core/content-matcher/multipart-form-data" => binary_utils::match_mime_multipart(expected, actual, context),
1491 "core/content-matcher/text" => match_text(&expected.body().value(), &actual.body().value(), context),
1492 "core/content-matcher/xml" => {
1493 #[cfg(feature = "xml")]
1494 {
1495 xml::match_xml(expected, actual, context)
1496 }
1497 #[cfg(not(feature = "xml"))]
1498 {
1499 warn!("Matching XML bodies requires the xml feature to be enabled");
1500 match_text(&expected.body().value(), &actual.body().value(), context)
1501 }
1502 },
1503 "core/content-matcher/binary" => binary_utils::match_octet_stream(expected, actual, context),
1504 _ => {
1505 warn!("There is no core content matcher for entry {}", matcher.catalogue_entry_key());
1506 match_text(&expected.body().value(), &actual.body().value(), context)
1507 }
1508 } {
1509 mismatches.extend_from_slice(&*m);
1510 }
1511 } else {
1512 trace!(plugin_name = matcher.plugin_name(),"Content matcher is provided via a plugin");
1513 let plugin_config = context.plugin_configuration().get(&matcher.plugin_name()).cloned();
1514 trace!("Plugin config = {:?}", plugin_config);
1515 if let Err(map) = matcher.match_contents(expected.body(), actual.body(), &context.matchers(),
1516 context.config() == DiffConfig::AllowUnexpectedKeys, plugin_config).await {
1517 for (_key, list) in map {
1519 for mismatch in list {
1520 mismatches.push(Mismatch::BodyMismatch {
1521 path: mismatch.path.clone(),
1522 expected: Some(Bytes::from(mismatch.expected)),
1523 actual: Some(Bytes::from(mismatch.actual)),
1524 mismatch: mismatch.mismatch.clone()
1525 });
1526 }
1527 }
1528 }
1529 }
1530 }
1531 None => {
1532 debug!("No content matcher defined for content type '{}', using core matcher implementation", content_type);
1533 mismatches.extend(compare_bodies_core(content_type, expected, actual, context));
1534 }
1535 }
1536 }
1537
1538 #[cfg(not(feature = "plugins"))]
1539 {
1540 mismatches.extend(compare_bodies_core(content_type, expected, actual, context));
1541 }
1542
1543 if mismatches.is_empty() {
1544 BodyMatchResult::Ok
1545 } else {
1546 BodyMatchResult::BodyMismatches(group_by(mismatches, |m| match m {
1547 Mismatch::BodyMismatch { path: m, ..} => m.to_string(),
1548 _ => String::default()
1549 }))
1550 }
1551}
1552
1553fn compare_bodies_core(
1554 content_type: &ContentType,
1555 expected: &(dyn HttpPart + Send + Sync),
1556 actual: &(dyn HttpPart + Send + Sync),
1557 context: &(dyn MatchingContext + Send + Sync)
1558) -> Vec<Mismatch> {
1559 let mut mismatches = vec![];
1560 match BODY_MATCHERS.iter().find(|mt| mt.0(content_type)) {
1561 Some(match_fn) => {
1562 debug!("Using body matcher for content type '{}'", content_type);
1563 if let Err(m) = match_fn.1(expected, actual, context) {
1564 mismatches.extend_from_slice(&*m);
1565 }
1566 },
1567 None => {
1568 debug!("No body matcher defined for content type '{}', checking for a content type matcher", content_type);
1569 let path = DocPath::root();
1570 if context.matcher_is_defined(&path) && context.select_best_matcher(&path).rules
1571 .iter().any(|rule| if let MatchingRule::ContentType(_) = rule { true } else { false }) {
1572 debug!("Found a content type matcher");
1573 if let Err(m) = binary_utils::match_octet_stream(expected, actual, context) {
1574 mismatches.extend_from_slice(&*m);
1575 }
1576 } else {
1577 debug!("No body matcher defined for content type '{}', using plain text matcher", content_type);
1578 if let Err(m) = match_text(&expected.body().value(), &actual.body().value(), context) {
1579 mismatches.extend_from_slice(&*m);
1580 }
1581 }
1582 }
1583 };
1584 mismatches
1585}
1586
1587#[instrument(level = "trace", ret, skip_all, fields(%content_type, ?context))]
1588async fn match_body_content(
1589 content_type: &ContentType,
1590 expected: &(dyn HttpPart + Send + Sync),
1591 actual: &(dyn HttpPart + Send + Sync),
1592 context: &(dyn MatchingContext + Send + Sync)
1593) -> BodyMatchResult {
1594 let expected_body = expected.body();
1595 let actual_body = actual.body();
1596 match (expected_body, actual_body) {
1597 (&OptionalBody::Missing, _) => BodyMatchResult::Ok,
1598 (&OptionalBody::Null, &OptionalBody::Present(ref b, _, _)) => {
1599 BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
1600 mismatch: format!("Expected empty body but received {}", actual_body),
1601 path: s!("/")}]})
1602 },
1603 (&OptionalBody::Empty, &OptionalBody::Present(ref b, _, _)) => {
1604 BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
1605 mismatch: format!("Expected empty body but received {}", actual_body),
1606 path: s!("/")}]})
1607 },
1608 (&OptionalBody::Null, _) => BodyMatchResult::Ok,
1609 (&OptionalBody::Empty, _) => BodyMatchResult::Ok,
1610 (e, &OptionalBody::Missing) => {
1611 BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch {
1612 expected: e.value(),
1613 actual: None,
1614 mismatch: format!("Expected body {} but was missing", e),
1615 path: s!("/")}]})
1616 },
1617 (e, &OptionalBody::Empty) => {
1618 BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch {
1619 expected: e.value(),
1620 actual: None,
1621 mismatch: format!("Expected body {} but was empty", e),
1622 path: s!("/")}]})
1623 },
1624 (_, _) => compare_bodies(content_type, expected, actual, context).await
1625 }
1626}
1627
1628pub async fn match_body(
1630 expected: &(dyn HttpPart + Send + Sync),
1631 actual: &(dyn HttpPart + Send + Sync),
1632 context: &(dyn MatchingContext + Send + Sync),
1633 header_context: &(dyn MatchingContext + Send + Sync)
1634) -> BodyMatchResult {
1635 let expected_content_type = expected.content_type().unwrap_or_default();
1636 let actual_content_type = actual.content_type().unwrap_or_default();
1637 debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
1638 actual_content_type);
1639 let content_type_matcher = header_context.select_best_matcher(&DocPath::root().join("content-type"));
1640 debug!("content type header matcher = '{:?}'", content_type_matcher);
1641 if expected_content_type.is_unknown() || actual_content_type.is_unknown() ||
1642 expected_content_type.is_equivalent_to(&actual_content_type) ||
1643 expected_content_type.is_equivalent_to(&actual_content_type.base_type()) ||
1644 (!content_type_matcher.is_empty() &&
1645 match_header_value("Content-Type", 0, expected_content_type.to_string().as_str(),
1646 actual_content_type.to_string().as_str(), header_context, true
1647 ).is_ok()) {
1648 match_body_content(&expected_content_type, expected, actual, context).await
1649 } else if expected.body().is_present() {
1650 BodyMatchResult::BodyTypeMismatch {
1651 expected_type: expected_content_type.to_string(),
1652 actual_type: actual_content_type.to_string(),
1653 message: format!("Expected a body of '{}' but the actual content type was '{}'", expected_content_type,
1654 actual_content_type),
1655 expected: expected.body().value(),
1656 actual: actual.body().value()
1657 }
1658 } else {
1659 BodyMatchResult::Ok
1660 }
1661}
1662
1663#[allow(unused_variables)]
1665pub async fn match_request<'a>(
1666 expected: HttpRequest,
1667 actual: HttpRequest,
1668 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>,
1669 interaction: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>
1670) -> RequestMatchResult {
1671 debug!("comparing to expected {}", expected);
1672 debug!(" body: '{}'", expected.body.display_string());
1673 debug!(" matching_rules: {:?}", expected.matching_rules);
1674 debug!(" generators: {:?}", expected.generators);
1675
1676 #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{};
1677 #[cfg(feature = "plugins")]
1678 {
1679 plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Request);
1680 };
1681 trace!("plugin_data = {:?}", plugin_data);
1682
1683 let path_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1684 &expected.matching_rules.rules_for_category("path").unwrap_or_default(),
1685 &plugin_data);
1686 let body_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1687 &expected.matching_rules.rules_for_category("body").unwrap_or_default(),
1688 &plugin_data);
1689 let query_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1690 &expected.matching_rules.rules_for_category("query").unwrap_or_default(),
1691 &plugin_data);
1692 let header_context = HeaderMatchingContext::new(
1693 &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1694 &expected.matching_rules.rules_for_category("header").unwrap_or_default(),
1695 &plugin_data
1696 )
1697 );
1698 let result = RequestMatchResult {
1699 method: match_method(&expected.method, &actual.method).err(),
1700 path: match_path(&expected.path, &actual.path, &path_context).err(),
1701 body: match_body(&expected, &actual, &body_context, &header_context).await,
1702 query: match_query(expected.query, actual.query, &query_context),
1703 headers: match_headers(expected.headers, actual.headers, &header_context)
1704 };
1705
1706 debug!("--> Mismatches: {:?}", result.mismatches());
1707 result
1708}
1709
1710#[instrument(level = "trace")]
1712pub fn match_status(expected: u16, actual: u16, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
1713 let path = DocPath::empty();
1714 let result = if context.matcher_is_defined(&path) {
1715 match_values(&path, &context.select_best_matcher(&path), expected, actual)
1716 .map_err(|messages| messages.iter().map(|message| {
1717 Mismatch::StatusMismatch {
1718 expected,
1719 actual,
1720 mismatch: message.clone()
1721 }
1722 }).collect())
1723 } else if expected != actual {
1724 Err(vec![Mismatch::StatusMismatch {
1725 expected,
1726 actual,
1727 mismatch: format!("expected {} but was {}", expected, actual)
1728 }])
1729 } else {
1730 Ok(())
1731 };
1732 trace!(?result, "matching response status");
1733 result
1734}
1735
1736#[allow(unused_variables)]
1738pub async fn match_response<'a>(
1739 expected: HttpResponse,
1740 actual: HttpResponse,
1741 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>,
1742 interaction: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>
1743) -> Vec<Mismatch> {
1744 let mut mismatches = vec![];
1745
1746 debug!("comparing to expected response: {}", expected);
1747 #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{};
1748 #[cfg(feature = "plugins")]
1749 {
1750 plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Response);
1751 };
1752 trace!("plugin_data = {:?}", plugin_data);
1753
1754 let status_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1755 &expected.matching_rules.rules_for_category("status").unwrap_or_default(),
1756 &plugin_data);
1757 let body_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1758 &expected.matching_rules.rules_for_category("body").unwrap_or_default(),
1759 &plugin_data);
1760 let header_context = HeaderMatchingContext::new(
1761 &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1762 &expected.matching_rules.rules_for_category("header").unwrap_or_default(),
1763 &plugin_data
1764 )
1765 );
1766
1767 mismatches.extend_from_slice(match_body(&expected, &actual, &body_context, &header_context).await
1768 .mismatches().as_slice());
1769 if let Err(m) = match_status(expected.status, actual.status, &status_context) {
1770 mismatches.extend_from_slice(&m);
1771 }
1772 let result = match_headers(expected.headers, actual.headers,
1773 &header_context);
1774 for values in result.values() {
1775 mismatches.extend_from_slice(values.as_slice());
1776 }
1777
1778 trace!(?mismatches, "match response");
1779
1780 mismatches
1781}
1782
1783#[instrument(level = "trace")]
1785pub async fn match_message_contents(
1786 expected: &MessageContents,
1787 actual: &MessageContents,
1788 context: &(dyn MatchingContext + Send + Sync)
1789) -> Result<(), Vec<Mismatch>> {
1790 let expected_content_type = expected.message_content_type().unwrap_or_default();
1791 let actual_content_type = actual.message_content_type().unwrap_or_default();
1792 debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
1793 actual_content_type);
1794 if expected_content_type.is_equivalent_to(&actual_content_type) {
1795 let result = match_body_content(&expected_content_type, expected, actual, context).await;
1796 match result {
1797 BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected, actual } => {
1798 Err(vec![ Mismatch::BodyTypeMismatch {
1799 expected: expected_type,
1800 actual: actual_type,
1801 mismatch: message,
1802 expected_body: expected,
1803 actual_body: actual
1804 } ])
1805 },
1806 BodyMatchResult::BodyMismatches(results) => {
1807 Err(results.values().flat_map(|values| values.iter().cloned()).collect())
1808 },
1809 _ => Ok(())
1810 }
1811 } else if expected.contents.is_present() {
1812 Err(vec![ Mismatch::BodyTypeMismatch {
1813 expected: expected_content_type.to_string(),
1814 actual: actual_content_type.to_string(),
1815 mismatch: format!("Expected message with content type {} but was {}",
1816 expected_content_type, actual_content_type),
1817 expected_body: expected.contents.value(),
1818 actual_body: actual.contents.value()
1819 } ])
1820 } else {
1821 Ok(())
1822 }
1823}
1824
1825#[instrument(level = "trace")]
1827pub fn match_message_metadata(
1828 expected: &MessageContents,
1829 actual: &MessageContents,
1830 context: &dyn MatchingContext
1831) -> HashMap<String, Vec<Mismatch>> {
1832 debug!("Matching message metadata");
1833 let mut result = hashmap!{};
1834 let expected_metadata = &expected.metadata;
1835 let actual_metadata = &actual.metadata;
1836 debug!("Matching message metadata. Expected '{:?}', Actual '{:?}'", expected_metadata, actual_metadata);
1837
1838 if !expected_metadata.is_empty() || context.config() == DiffConfig::NoUnexpectedKeys {
1839 for (key, value) in expected_metadata {
1840 match actual_metadata.get(key) {
1841 Some(actual_value) => {
1842 result.insert(key.clone(), match_metadata_value(key, value,
1843 actual_value, context).err().unwrap_or_default());
1844 },
1845 None => {
1846 result.insert(key.clone(), vec![Mismatch::MetadataMismatch { key: key.clone(),
1847 expected: json_to_string(&value),
1848 actual: "".to_string(),
1849 mismatch: format!("Expected message metadata '{}' but was missing", key) }]);
1850 }
1851 }
1852 }
1853 }
1854 result
1855}
1856
1857#[instrument(level = "trace")]
1858fn match_metadata_value(
1859 key: &str,
1860 expected: &Value,
1861 actual: &Value,
1862 context: &dyn MatchingContext
1863) -> Result<(), Vec<Mismatch>> {
1864 debug!("Comparing metadata values for key '{}'", key);
1865 let path = DocPath::root().join(key);
1866 let matcher_result = if context.matcher_is_defined(&path) {
1867 match_values(&path, &context.select_best_matcher(&path), expected, actual)
1868 } else if key.to_ascii_lowercase() == "contenttype" || key.to_ascii_lowercase() == "content-type" {
1869 debug!("Comparing message context type '{}' => '{}'", expected, actual);
1870 headers::match_parameter_header(expected.as_str().unwrap_or_default(), actual.as_str().unwrap_or_default(),
1871 key, "metadata", 0, true)
1872 } else {
1873 expected.matches_with(actual, &MatchingRule::Equality, false).map_err(|err| vec![err.to_string()])
1874 };
1875 matcher_result.map_err(|messages| {
1876 messages.iter().map(|message| {
1877 Mismatch::MetadataMismatch {
1878 key: key.to_string(),
1879 expected: expected.to_string(),
1880 actual: actual.to_string(),
1881 mismatch: format!("Expected metadata key '{}' to have value '{}' but was '{}' - {}", key, expected, actual, message)
1882 }
1883 }).collect()
1884 })
1885}
1886
1887#[allow(unused_variables)]
1889pub async fn match_message<'a>(
1890 expected: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
1891 actual: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
1892 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>) -> Vec<Mismatch> {
1893 let mut mismatches = vec![];
1894
1895 if expected.is_message() && actual.is_message() {
1896 debug!("comparing to expected message: {:?}", expected);
1897 let expected_message = expected.as_message().unwrap();
1898 let actual_message = actual.as_message().unwrap();
1899
1900 let matching_rules = &expected_message.matching_rules;
1901 #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{};
1902 #[cfg(feature = "plugins")]
1903 {
1904 plugin_data = setup_plugin_config(pact, expected, InteractionPart::None);
1905 };
1906
1907 let body_context = if expected.is_v4() {
1908 CoreMatchingContext {
1909 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
1910 config: DiffConfig::AllowUnexpectedKeys,
1911 matching_spec: PactSpecification::V4,
1912 plugin_configuration: plugin_data.clone()
1913 }
1914 } else {
1915 CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1916 &matching_rules.rules_for_category("body").unwrap_or_default(),
1917 &plugin_data)
1918 };
1919
1920 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1921 &matching_rules.rules_for_category("metadata").unwrap_or_default(),
1922 &plugin_data);
1923 let contents = match_message_contents(&expected_message.as_message_content(), &actual_message.as_message_content(), &body_context).await;
1924
1925 mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
1926 for values in match_message_metadata(&expected_message.as_message_content(), &actual_message.as_message_content(), &metadata_context).values() {
1927 mismatches.extend_from_slice(values.as_slice());
1928 }
1929 } else {
1930 mismatches.push(Mismatch::BodyTypeMismatch {
1931 expected: "message".into(),
1932 actual: actual.type_of(),
1933 mismatch: format!("Cannot compare a {} with a {}", expected.type_of(), actual.type_of()),
1934 expected_body: None,
1935 actual_body: None
1936 });
1937 }
1938
1939 mismatches
1940}
1941
1942pub async fn match_sync_message<'a>(expected: SynchronousMessage, actual: SynchronousMessage, pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>) -> Vec<Mismatch> {
1944 let mut mismatches = match_sync_message_request(&expected, &actual, pact).await;
1945 let response_result = match_sync_message_response(&expected, &expected.response, &actual.response, pact).await;
1946 mismatches.extend_from_slice(&*response_result);
1947 mismatches
1948}
1949
1950#[allow(unused_variables)]
1952pub async fn match_sync_message_request<'a>(
1953 expected: &SynchronousMessage,
1954 actual: &SynchronousMessage,
1955 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>
1956) -> Vec<Mismatch> {
1957 debug!("comparing to expected message request: {:?}", expected);
1958
1959 let matching_rules = &expected.request.matching_rules;
1960 #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{};
1961 #[cfg(feature = "plugins")]
1962 {
1963 plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None);
1964 };
1965
1966 let body_context = CoreMatchingContext {
1967 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
1968 config: DiffConfig::AllowUnexpectedKeys,
1969 matching_spec: PactSpecification::V4,
1970 plugin_configuration: plugin_data.clone()
1971 };
1972
1973 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1974 &matching_rules.rules_for_category("metadata").unwrap_or_default(),
1975 &plugin_data);
1976 let contents = match_message_contents(&expected.request, &actual.request, &body_context).await;
1977
1978 let mut mismatches = vec![];
1979 mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
1980 for values in match_message_metadata(&expected.request, &actual.request, &metadata_context).values() {
1981 mismatches.extend_from_slice(values.as_slice());
1982 }
1983 mismatches
1984}
1985
1986#[allow(unused_variables)]
1988pub async fn match_sync_message_response<'a>(
1989 expected: &SynchronousMessage,
1990 expected_responses: &[MessageContents],
1991 actual_responses: &[MessageContents],
1992 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>
1993) -> Vec<Mismatch> {
1994 debug!("comparing to expected message responses: {:?}", expected_responses);
1995
1996 let mut mismatches = vec![];
1997
1998 if expected_responses.len() != actual_responses.len() {
1999 if !expected_responses.is_empty() && actual_responses.is_empty() {
2000 mismatches.push(Mismatch::BodyTypeMismatch {
2001 expected: "message response".into(),
2002 actual: "".into(),
2003 mismatch: "Expected a message with a response, but the actual response was empty".into(),
2004 expected_body: None,
2005 actual_body: None
2006 });
2007 } else if !expected_responses.is_empty() {
2008 mismatches.push(Mismatch::BodyTypeMismatch {
2009 expected: "message response".into(),
2010 actual: "".into(),
2011 mismatch: format!("Expected a message with {} responses, but the actual response had {}",
2012 expected_responses.len(), actual_responses.len()),
2013 expected_body: None,
2014 actual_body: None
2015 });
2016 }
2017 } else {
2018 #[allow(unused_mut, unused_assignments)] let mut plugin_data = hashmap!{};
2019 #[cfg(feature = "plugins")]
2020 {
2021 plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None);
2022 };
2023 for (expected_response, actual_response) in expected_responses.iter().zip(actual_responses) {
2024 let matching_rules = &expected_response.matching_rules;
2025 let body_context = CoreMatchingContext {
2026 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2027 config: DiffConfig::AllowUnexpectedKeys,
2028 matching_spec: PactSpecification::V4,
2029 plugin_configuration: plugin_data.clone()
2030 };
2031
2032 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2033 &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2034 &plugin_data);
2035 let contents = match_message_contents(expected_response, actual_response, &body_context).await;
2036
2037 mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2038 for values in match_message_metadata(expected_response, actual_response, &metadata_context).values() {
2039 mismatches.extend_from_slice(values.as_slice());
2040 }
2041 }
2042 }
2043 mismatches
2044}
2045
2046#[instrument(level = "trace")]
2049pub async fn generate_request(request: &HttpRequest, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpRequest {
2050 trace!(?request, ?mode, ?context, "generate_request");
2051 let mut request = request.clone();
2052
2053 let generators = request.build_generators(&GeneratorCategory::PATH);
2054 if !generators.is_empty() {
2055 debug!("Applying path generator...");
2056 apply_generators(mode, &generators, &mut |_, generator| {
2057 if let Ok(v) = generator.generate_value(&request.path, context, &DefaultVariantMatcher.boxed()) {
2058 request.path = v;
2059 }
2060 });
2061 }
2062
2063 let generators = request.build_generators(&GeneratorCategory::HEADER);
2064 if !generators.is_empty() {
2065 debug!("Applying header generators...");
2066 apply_generators(mode, &generators, &mut |key, generator| {
2067 if let Some(header) = key.first_field() {
2068 if let Some(ref mut headers) = request.headers {
2069 if headers.contains_key(header) {
2070 if let Ok(v) = generator.generate_value(&headers.get(header).unwrap().clone(), context, &DefaultVariantMatcher.boxed()) {
2071 headers.insert(header.to_string(), v);
2072 }
2073 } else {
2074 if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2075 headers.insert(header.to_string(), vec![ v.to_string() ]);
2076 }
2077 }
2078 } else {
2079 if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2080 request.headers = Some(hashmap!{
2081 header.to_string() => vec![ v.to_string() ]
2082 })
2083 }
2084 }
2085 }
2086 });
2087 }
2088
2089 let generators = request.build_generators(&GeneratorCategory::QUERY);
2090 if !generators.is_empty() {
2091 debug!("Applying query generators...");
2092 apply_generators(mode, &generators, &mut |key, generator| {
2093 if let Some(param) = key.first_field() {
2094 if let Some(ref mut parameters) = request.query {
2095 if let Some(parameter) = parameters.get_mut(param) {
2096 let mut generated = parameter.clone();
2097 for (index, val) in parameter.iter().enumerate() {
2098 let value = val.clone().unwrap_or_default();
2099 if let Ok(v) = generator.generate_value(&value, context, &DefaultVariantMatcher.boxed()) {
2100 generated[index] = Some(v);
2101 }
2102 }
2103 *parameter = generated;
2104 } else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2105 parameters.insert(param.to_string(), vec![ Some(v.to_string()) ]);
2106 }
2107 } else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2108 request.query = Some(hashmap!{
2109 param.to_string() => vec![ Some(v.to_string()) ]
2110 })
2111 }
2112 }
2113 });
2114 }
2115
2116 let generators = request.build_generators(&GeneratorCategory::BODY);
2117 if !generators.is_empty() && request.body.is_present() {
2118 debug!("Applying body generators...");
2119 match generators_process_body(mode, &request.body, request.content_type(),
2120 context, &generators, &DefaultVariantMatcher {}, &vec![], &hashmap!{}).await {
2121 Ok(body) => request.body = body,
2122 Err(err) => error!("Failed to generate the body, will use the original: {}", err)
2123 }
2124 }
2125
2126 request
2127}
2128
2129pub async fn generate_response(response: &HttpResponse, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpResponse {
2132 trace!(?response, ?mode, ?context, "generate_response");
2133 let mut response = response.clone();
2134 let generators = response.build_generators(&GeneratorCategory::STATUS);
2135 if !generators.is_empty() {
2136 debug!("Applying status generator...");
2137 apply_generators(mode, &generators, &mut |_, generator| {
2138 if let Ok(v) = generator.generate_value(&response.status, context, &DefaultVariantMatcher.boxed()) {
2139 debug!("Generated value for status: {}", v);
2140 response.status = v;
2141 }
2142 });
2143 }
2144 let generators = response.build_generators(&GeneratorCategory::HEADER);
2145 if !generators.is_empty() {
2146 debug!("Applying header generators...");
2147 apply_generators(mode, &generators, &mut |key, generator| {
2148 if let Some(header) = key.first_field() {
2149 if let Some(ref mut headers) = response.headers {
2150 if headers.contains_key(header) {
2151 if let Ok(v) = generator.generate_value(&headers.get(header).unwrap().clone(), context, &DefaultVariantMatcher.boxed()) {
2152 headers.insert(header.to_string(), v);
2153 }
2154 } else {
2155 if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2156 headers.insert(header.to_string(), vec![ v.to_string() ]);
2157 }
2158 }
2159 } else {
2160 if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2161 response.headers = Some(hashmap!{
2162 header.to_string() => vec![ v.to_string() ]
2163 })
2164 }
2165 }
2166 }
2167 });
2168 }
2169 let generators = response.build_generators(&GeneratorCategory::BODY);
2170 if !generators.is_empty() && response.body.is_present() {
2171 debug!("Applying body generators...");
2172 match generators_process_body(mode, &response.body, response.content_type(),
2173 context, &generators, &DefaultVariantMatcher{}, &vec![], &hashmap!{}).await {
2174 Ok(body) => response.body = body,
2175 Err(err) => error!("Failed to generate the body, will use the original: {}", err)
2176 }
2177 }
2178 response
2179}
2180
2181pub async fn match_interaction_request(
2183 expected: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2184 actual: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2185 pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2186 _spec_version: &PactSpecification
2187) -> anyhow::Result<RequestMatchResult> {
2188 if let Some(http_interaction) = expected.as_v4_http() {
2189 let request = actual.as_v4_http()
2190 .ok_or_else(|| anyhow!("Could not unpack actual request as a V4 Http Request"))?.request;
2191 Ok(match_request(http_interaction.request, request, &pact, &expected).await)
2192 } else {
2193 Err(anyhow!("match_interaction_request must be called with HTTP request/response interactions, got {}", expected.type_of()))
2194 }
2195}
2196
2197pub async fn match_interaction_response(
2199 expected: Box<dyn Interaction + Sync + RefUnwindSafe>,
2200 actual: Box<dyn Interaction + Sync + RefUnwindSafe>,
2201 pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2202 _spec_version: &PactSpecification
2203) -> anyhow::Result<Vec<Mismatch>> {
2204 if let Some(expected) = expected.as_v4_http() {
2205 let expected_response = expected.response.clone();
2206 let expected = expected.boxed();
2207 let response = actual.as_v4_http()
2208 .ok_or_else(|| anyhow!("Could not unpack actual response as a V4 Http Response"))?.response;
2209 Ok(match_response(expected_response, response, &pact, &expected).await)
2210 } else {
2211 Err(anyhow!("match_interaction_response must be called with HTTP request/response interactions, got {}", expected.type_of()))
2212 }
2213}
2214
2215pub async fn match_interaction(
2217 expected: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2218 actual: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2219 pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2220 _spec_version: &PactSpecification
2221) -> anyhow::Result<Vec<Mismatch>> {
2222 if let Some(expected) = expected.as_v4_http() {
2223 let expected_request = expected.request.clone();
2224 let expected_response = expected.response.clone();
2225 let expected = expected.boxed();
2226 let request = actual.as_v4_http()
2227 .ok_or_else(|| anyhow!("Could not unpack actual request as a V4 Http Request"))?.request;
2228 let request_result = match_request(expected_request, request, &pact, &expected).await;
2229 let response = actual.as_v4_http()
2230 .ok_or_else(|| anyhow!("Could not unpack actual response as a V4 Http Response"))?.response;
2231 let response_result = match_response(expected_response, response, &pact, &expected).await;
2232 let mut mismatches = request_result.mismatches();
2233 mismatches.extend_from_slice(&*response_result);
2234 Ok(mismatches)
2235 } else if expected.is_message() || expected.is_v4() {
2236 Ok(match_message(&expected, &actual, &pact).await)
2237 } else {
2238 Err(anyhow!("match_interaction must be called with either an HTTP request/response interaction or a Message, got {}", expected.type_of()))
2239 }
2240}
2241
2242#[cfg(test)]
2243mod tests;
2244#[cfg(test)]
2245mod generator_tests;