use std::cell::RefCell;
use bytes::Bytes;
use serde_json::Value;
use pact_models::matchingrules::MatchingRule;
use pact_models::path_exp::DocPath;
#[derive(Debug)]
pub(crate) struct FieldMatchScope;
thread_local! {
static FIELD_MATCH_PATH: RefCell<Vec<DocPath>> = const { RefCell::new(Vec::new()) };
static FIELD_MATCH_CATEGORY: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
#[derive(Debug)]
pub(crate) struct FieldMatchCategoryScope;
impl FieldMatchScope {
#[must_use]
pub(crate) fn path(path: &DocPath) -> FieldMatchScope {
FIELD_MATCH_PATH.with(|scope| scope.borrow_mut().push(path.clone()));
FieldMatchScope
}
#[must_use]
pub(crate) fn category(category: &str) -> FieldMatchCategoryScope {
FIELD_MATCH_CATEGORY.with(|scope| scope.borrow_mut().push(category.to_string()));
FieldMatchCategoryScope
}
}
impl Drop for FieldMatchScope {
fn drop(&mut self) {
FIELD_MATCH_PATH.with(|scope| { scope.borrow_mut().pop(); });
}
}
impl Drop for FieldMatchCategoryScope {
fn drop(&mut self) {
FIELD_MATCH_CATEGORY.with(|scope| { scope.borrow_mut().pop(); });
}
}
#[must_use]
pub(crate) fn scope_from_plan_path(plan_path: &[String]) -> (FieldMatchScope, FieldMatchCategoryScope) {
let path = plan_path.iter().rev()
.filter(|label| label.starts_with('$'))
.find_map(|label| DocPath::new(label).ok())
.unwrap_or_else(DocPath::root);
let category = plan_path.iter()
.find_map(|label| match label.as_str() {
"body" => Some("body"),
"headers" => Some("header"),
"query parameters" => Some("query"),
"metadata" => Some("metadata"),
"path" => Some("path"),
"status" => Some("status"),
_ => None
})
.unwrap_or("body");
(FieldMatchScope::path(&path), FieldMatchScope::category(category))
}
fn current_scope() -> (DocPath, String) {
let path = FIELD_MATCH_PATH.with(|scope| scope.borrow().last().cloned())
.unwrap_or_else(DocPath::root);
let category = FIELD_MATCH_CATEGORY.with(|scope| scope.borrow().last().cloned())
.unwrap_or_else(|| "body".to_string());
(path, category)
}
pub(crate) trait ToFieldValue {
fn to_field_value(&self) -> DriverFieldValue;
}
#[cfg(feature = "plugins")]
#[cfg(not(target_family = "wasm"))]
pub(crate) type DriverFieldValue = pact_plugin_driver::field::FieldValue;
#[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum DriverFieldValue {
Json(Value),
Binary(Bytes)
}
impl ToFieldValue for Value {
fn to_field_value(&self) -> DriverFieldValue {
DriverFieldValue::Json(self.clone())
}
}
impl ToFieldValue for &str {
fn to_field_value(&self) -> DriverFieldValue {
DriverFieldValue::Json(Value::String(self.to_string()))
}
}
impl ToFieldValue for String {
fn to_field_value(&self) -> DriverFieldValue {
DriverFieldValue::Json(Value::String(self.clone()))
}
}
impl ToFieldValue for u64 {
fn to_field_value(&self) -> DriverFieldValue {
DriverFieldValue::Json(Value::from(*self))
}
}
impl ToFieldValue for u16 {
fn to_field_value(&self) -> DriverFieldValue {
DriverFieldValue::Json(Value::from(*self))
}
}
impl ToFieldValue for Bytes {
fn to_field_value(&self) -> DriverFieldValue {
DriverFieldValue::Binary(self.clone())
}
}
#[cfg(feature = "plugins")]
#[cfg(not(target_family = "wasm"))]
pub(crate) fn apply_plugin_rule<T: ToFieldValue>(
rule: &MatchingRule,
name: &str,
expected: &T,
actual: &T
) -> anyhow::Result<()> {
use anyhow::anyhow;
use itertools::Itertools;
use pact_plugin_driver::field::{FieldContext, find_field_matcher};
use tracing::debug;
let (path, category) = current_scope();
let matcher = find_field_matcher(name)
.map_err(|err| anyhow!("Could not apply the '{}' matching rule - {}", name, err))?;
let context = FieldContext::new(&path, category.as_str());
debug!(%path, %category, "Applying the '{}' matching rule provided by {}", name, matcher.plugin_name());
match matcher.match_field_blocking(rule, &expected.to_field_value(), &actual.to_field_value(), &context) {
Ok(()) => Ok(()),
Err(mismatches) => Err(anyhow!("{}", mismatches.iter()
.map(|mismatch| mismatch.mismatch.as_str())
.join(", ")))
}
}
#[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
pub(crate) fn apply_plugin_rule<T: ToFieldValue>(
_rule: &MatchingRule,
name: &str,
_expected: &T,
_actual: &T
) -> anyhow::Result<()> {
Err(anyhow::anyhow!("'{}' is not a standard matching rule, and this build of pact_matching \
does not have plugin support enabled, so a plugin-provided rule can not be resolved", name))
}