use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use anyhow::anyhow;
use bytes::Bytes;
use lazy_static::lazy_static;
use pact_models::matchingrules::MatchingRule;
use pact_models::path_exp::DocPath;
use pact_models::prelude::Generator;
use serde_json::Value;
use tokio::runtime::Runtime;
use tracing::{debug, error};
use crate::catalogue_manager::{CatalogueEntry, CatalogueEntryProviderType, CatalogueEntryType, resolve_capability_entry};
use crate::content::ContentMismatch;
use crate::core_capabilities;
use crate::plugin_manager::lookup_plugin;
use crate::plugin_models::{PactPluginManifest, PluginInteractionConfig};
use crate::proto_v2::{
FieldValue as ProtoFieldValue,
GenerateFieldRequest,
GenerateFieldResponse,
MatchFieldRequest,
MatchFieldResponse,
MatchingRule as ProtoMatchingRule,
Generator as ProtoGenerator,
PluginConfiguration as ProtoPluginConfiguration,
field_value
};
use crate::utils::{proto_value_to_json, to_proto_struct, to_proto_value};
#[derive(Clone, Debug, PartialEq)]
pub enum FieldValue {
Json(Value),
Binary(Bytes)
}
impl FieldValue {
pub fn to_proto(&self) -> ProtoFieldValue {
ProtoFieldValue {
value: Some(match self {
FieldValue::Binary(bytes) => field_value::Value::BinaryValue(bytes.to_vec()),
FieldValue::Json(Value::Null) => field_value::Value::NullValue(0),
FieldValue::Json(Value::Bool(value)) => field_value::Value::BooleanValue(*value),
FieldValue::Json(Value::String(value)) => field_value::Value::StringValue(value.clone()),
FieldValue::Json(Value::Number(number)) => match number.as_i64() {
Some(value) => field_value::Value::IntegerValue(value),
None => field_value::Value::DecimalValue(number.as_f64().unwrap_or_default())
},
FieldValue::Json(value) => field_value::Value::StructuredValue(to_proto_value(value))
})
}
}
pub fn from_proto(value: &ProtoFieldValue) -> FieldValue {
match &value.value {
Some(field_value::Value::NullValue(_)) | None => FieldValue::Json(Value::Null),
Some(field_value::Value::BooleanValue(value)) => FieldValue::Json(Value::Bool(*value)),
Some(field_value::Value::StringValue(value)) => FieldValue::Json(Value::String(value.clone())),
Some(field_value::Value::IntegerValue(value)) => FieldValue::Json(Value::Number((*value).into())),
Some(field_value::Value::DecimalValue(value)) => FieldValue::Json(
serde_json::Number::from_f64(*value)
.map(Value::Number)
.unwrap_or(Value::Null)
),
Some(field_value::Value::BinaryValue(bytes)) => FieldValue::Binary(Bytes::from(bytes.clone())),
Some(field_value::Value::StructuredValue(value)) => FieldValue::Json(proto_value_to_json(value))
}
}
}
impl From<Value> for FieldValue {
fn from(value: Value) -> Self {
FieldValue::Json(value)
}
}
impl From<Bytes> for FieldValue {
fn from(bytes: Bytes) -> Self {
FieldValue::Binary(bytes)
}
}
#[derive(Clone, Debug)]
pub struct FieldContext {
pub path: DocPath,
pub category: String,
pub plugin_config: Option<PluginInteractionConfig>,
pub test_context: HashMap<String, Value>
}
impl Default for FieldContext {
fn default() -> Self {
FieldContext {
path: DocPath::root(),
category: "body".to_string(),
plugin_config: None,
test_context: HashMap::default()
}
}
}
impl FieldContext {
pub fn new(path: &DocPath, category: &str) -> FieldContext {
FieldContext {
path: path.clone(),
category: category.to_string(),
.. FieldContext::default()
}
}
pub fn with_plugin_config(self, plugin_config: Option<PluginInteractionConfig>) -> FieldContext {
FieldContext { plugin_config, .. self }
}
pub fn with_test_context(self, test_context: HashMap<String, Value>) -> FieldContext {
FieldContext { test_context, .. self }
}
}
#[derive(Clone, Debug)]
pub struct FieldMatcher {
pub catalogue_entry: CatalogueEntry
}
#[derive(Clone, Debug)]
pub struct FieldGenerator {
pub catalogue_entry: CatalogueEntry
}
pub fn find_field_matcher(name: &str) -> anyhow::Result<FieldMatcher> {
resolve_capability_entry(name, CatalogueEntryType::MATCHER)
.map(|catalogue_entry| FieldMatcher { catalogue_entry })
}
pub fn find_field_generator(name: &str) -> anyhow::Result<FieldGenerator> {
resolve_capability_entry(name, CatalogueEntryType::GENERATOR)
.map(|catalogue_entry| FieldGenerator { catalogue_entry })
}
impl FieldMatcher {
pub fn is_core(&self) -> bool {
self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
}
pub fn catalogue_entry_key(&self) -> String {
if self.is_core() {
format!("core/matcher/{}", self.catalogue_entry.key)
} else {
format!("plugin/{}/matcher/{}", self.plugin_name(), self.catalogue_entry.key)
}
}
pub fn plugin(&self) -> Option<PactPluginManifest> {
self.catalogue_entry.plugin.clone()
}
pub fn plugin_name(&self) -> String {
self.catalogue_entry.plugin.as_ref()
.map(|plugin| plugin.name.clone())
.unwrap_or("core".to_string())
}
pub async fn match_field(
&self,
rule: &MatchingRule,
expected: &FieldValue,
actual: &FieldValue,
context: &FieldContext
) -> Result<(), Vec<ContentMismatch>> {
let request = MatchFieldRequest {
key: self.catalogue_entry.key.clone(),
rule: Some(to_proto_matching_rule(rule)),
path: context.path.to_string(),
mismatch_type: context.category.clone(),
expected: Some(expected.to_proto()),
actual: Some(actual.to_proto()),
plugin_configuration: context.plugin_config.clone().map(to_proto_plugin_config),
test_context: Some(to_proto_struct(&context.test_context))
};
let response = if self.is_core() {
match core_capabilities::lookup_core_field_matcher(&self.catalogue_entry.key) {
Some(handler) => handler.match_field(request).await,
None => Err(anyhow!("No core field matcher registered for '{}'", self.catalogue_entry.key))
}
} else {
self.call_plugin(request).await
};
process_match_field_response(response, context)
}
async fn call_plugin(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
let manifest = self.catalogue_entry.plugin.as_ref()
.ok_or_else(|| anyhow!("Catalogue entry '{}' has no plugin manifest", self.catalogue_entry_key()))?;
let plugin = lookup_plugin(&manifest.as_dependency())
.ok_or_else(|| anyhow!("Plugin '{}' for matching rule '{}' is not currently running",
manifest.name, self.catalogue_entry.key))?;
debug!("Sending MatchField request to plugin {:?}", manifest.name);
let chain_id = crate::call_chain::new_call_chain_id();
let deadline_ms = crate::call_chain::default_deadline_ms();
plugin.match_field_with_chain(request, &chain_id, deadline_ms).await
}
pub fn match_field_blocking(
&self,
rule: &MatchingRule,
expected: &FieldValue,
actual: &FieldValue,
context: &FieldContext
) -> Result<(), Vec<ContentMismatch>> {
let matcher = self.clone();
let rule = rule.clone();
let expected = expected.clone();
let actual = actual.clone();
let call_context = context.clone();
block_on_field_call(async move {
matcher.match_field(&rule, &expected, &actual, &call_context).await
})
.unwrap_or_else(|err| Err(vec![mismatch_for(err.to_string(), context)]))
}
}
impl FieldGenerator {
pub fn is_core(&self) -> bool {
self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
}
pub fn catalogue_entry_key(&self) -> String {
if self.is_core() {
format!("core/generator/{}", self.catalogue_entry.key)
} else {
format!("plugin/{}/generator/{}", self.plugin_name(), self.catalogue_entry.key)
}
}
pub fn plugin(&self) -> Option<PactPluginManifest> {
self.catalogue_entry.plugin.clone()
}
pub fn plugin_name(&self) -> String {
self.catalogue_entry.plugin.as_ref()
.map(|plugin| plugin.name.clone())
.unwrap_or("core".to_string())
}
pub async fn generate_field(
&self,
generator: &Generator,
example: &FieldValue,
mode: TestMode,
context: &FieldContext
) -> anyhow::Result<FieldValue> {
let request = GenerateFieldRequest {
key: self.catalogue_entry.key.clone(),
generator: Some(to_proto_generator(generator)),
path: context.path.to_string(),
example_value: Some(example.to_proto()),
plugin_configuration: context.plugin_config.clone().map(to_proto_plugin_config),
test_context: Some(to_proto_struct(&context.test_context)),
test_mode: mode.to_proto() as i32
};
let response = if self.is_core() {
let handler = core_capabilities::lookup_core_field_generator(&self.catalogue_entry.key)
.ok_or_else(|| anyhow!("No core field generator registered for '{}'", self.catalogue_entry.key))?;
handler.generate_field(request).await?
} else {
self.call_plugin(request).await?
};
if !response.error.is_empty() {
return Err(anyhow!("Generator '{}' failed: {}", self.catalogue_entry.key, response.error));
}
match &response.value {
Some(value) => Ok(FieldValue::from_proto(value)),
None => Err(anyhow!("Generator '{}' returned no value", self.catalogue_entry.key))
}
}
async fn call_plugin(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse> {
let manifest = self.catalogue_entry.plugin.as_ref()
.ok_or_else(|| anyhow!("Catalogue entry '{}' has no plugin manifest", self.catalogue_entry_key()))?;
let plugin = lookup_plugin(&manifest.as_dependency())
.ok_or_else(|| anyhow!("Plugin '{}' for generator '{}' is not currently running",
manifest.name, self.catalogue_entry.key))?;
debug!("Sending GenerateField request to plugin {:?}", manifest.name);
let chain_id = crate::call_chain::new_call_chain_id();
let deadline_ms = crate::call_chain::default_deadline_ms();
plugin.generate_field_with_chain(request, &chain_id, deadline_ms).await
}
pub fn generate_field_blocking(
&self,
generator: &Generator,
example: &FieldValue,
mode: TestMode,
context: &FieldContext
) -> anyhow::Result<FieldValue> {
let field_generator = self.clone();
let generator = generator.clone();
let example = example.clone();
let context = context.clone();
block_on_field_call(async move {
field_generator.generate_field(&generator, &example, mode, &context).await
})?
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TestMode {
Consumer,
Provider,
Unknown
}
impl TestMode {
fn to_proto(self) -> crate::proto_v2::generate_content_request::TestMode {
use crate::proto_v2::generate_content_request::TestMode as ProtoTestMode;
match self {
TestMode::Consumer => ProtoTestMode::Consumer,
TestMode::Provider => ProtoTestMode::Provider,
TestMode::Unknown => ProtoTestMode::Unknown
}
}
}
lazy_static! {
static ref FIELD_RUNTIME: Mutex<Option<Arc<Runtime>>> = Mutex::new(None);
}
fn field_runtime() -> anyhow::Result<Arc<Runtime>> {
let mut guard = FIELD_RUNTIME.lock()
.map_err(|err| anyhow!("FIELD_RUNTIME mutex poisoned - {}", err))?;
match guard.as_ref() {
Some(runtime) => Ok(runtime.clone()),
None => {
let runtime = Arc::new(tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.thread_name("pact-plugin-field")
.build()?);
*guard = Some(runtime.clone());
Ok(runtime)
}
}
}
fn block_on_field_call<F, T>(future: F) -> anyhow::Result<T>
where
F: std::future::Future<Output = T> + Send + 'static,
T: Send + 'static
{
let runtime = field_runtime()?;
let deadline_ms = crate::call_chain::default_deadline_ms();
let (sender, receiver) = std::sync::mpsc::channel();
runtime.spawn(async move {
let _ = sender.send(future.await);
});
receiver.recv_timeout(crate::call_chain::remaining(deadline_ms))
.map_err(|err| {
error!("Timed out waiting for a field-level plugin call to complete - {}", err);
anyhow!("Timed out waiting for the plugin call to complete - {}", err)
})
}
fn process_match_field_response(
response: anyhow::Result<MatchFieldResponse>,
context: &FieldContext
) -> Result<(), Vec<ContentMismatch>> {
let path = context.path.to_string();
match response {
Ok(response) => if !response.error.is_empty() {
Err(vec![mismatch_for(response.error, context)])
} else if response.mismatches.is_empty() {
Ok(())
} else {
Err(response.mismatches.iter().map(|mismatch| ContentMismatch {
expected: mismatch.expected.as_ref()
.map(|bytes| String::from_utf8_lossy(bytes).to_string())
.unwrap_or_default(),
actual: mismatch.actual.as_ref()
.map(|bytes| String::from_utf8_lossy(bytes).to_string())
.unwrap_or_default(),
mismatch: mismatch.mismatch.clone(),
path: if mismatch.path.is_empty() { path.clone() } else { mismatch.path.clone() },
diff: if mismatch.diff.is_empty() { None } else { Some(mismatch.diff.clone()) },
mismatch_type: if mismatch.mismatch_type.is_empty() {
Some(context.category.clone())
} else {
Some(mismatch.mismatch_type.clone())
}
}).collect())
},
Err(err) => {
error!("Field-level match call failed - {}", err);
Err(vec![mismatch_for(err.to_string(), context)])
}
}
}
fn mismatch_for(message: String, context: &FieldContext) -> ContentMismatch {
ContentMismatch {
expected: Default::default(),
actual: Default::default(),
mismatch: message,
path: context.path.to_string(),
diff: None,
mismatch_type: Some(context.category.clone())
}
}
fn to_proto_matching_rule(rule: &MatchingRule) -> ProtoMatchingRule {
ProtoMatchingRule {
r#type: rule.name(),
values: Some(to_proto_struct(&rule.value_map()))
}
}
fn to_proto_generator(generator: &Generator) -> ProtoGenerator {
ProtoGenerator {
r#type: generator.name(),
values: Some(to_proto_struct(&generator.value_map()))
}
}
fn to_proto_plugin_config(config: PluginInteractionConfig) -> ProtoPluginConfiguration {
ProtoPluginConfiguration {
interaction_configuration: Some(to_proto_struct(&config.interaction_configuration)),
pact_configuration: Some(to_proto_struct(&config.pact_configuration))
}
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use expectest::prelude::*;
use maplit::hashmap;
use pact_models::matchingrules::MatchingRule;
use crate::catalogue_manager::{CatalogueEntryProviderType, register_core_entries};
use crate::core_capabilities::{
CoreFieldGenerator,
CoreFieldMatcher,
deregister_core_field_generator,
deregister_core_field_matcher,
register_core_field_generator,
register_core_field_matcher
};
use crate::proto_v2::ContentMismatch as ProtoContentMismatch;
use super::*;
#[test]
fn field_values_round_trip_through_the_proto_form() {
for value in [
FieldValue::Json(Value::String("4111111111111111".to_string())),
FieldValue::Json(serde_json::json!(100)),
FieldValue::Json(serde_json::json!(-100.5)),
FieldValue::Json(Value::Bool(true)),
FieldValue::Json(Value::Null),
FieldValue::Json(serde_json::json!({ "brand": "visa" })),
FieldValue::Binary(Bytes::from(vec![0u8, 159, 146, 150]))
] {
expect!(FieldValue::from_proto(&value.to_proto())).to(be_equal_to(value));
}
}
#[test]
fn a_whole_number_stays_whole_and_a_decimal_stays_decimal() {
let integer = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100)).to_proto());
let decimal = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100.5)).to_proto());
let whole_decimal = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100.0)).to_proto());
expect!(integer.clone()).to(be_equal_to(FieldValue::Json(serde_json::json!(100))));
expect!(decimal).to(be_equal_to(FieldValue::Json(serde_json::json!(100.5))));
match integer {
FieldValue::Json(Value::Number(number)) => expect!(number.is_i64()).to(be_true()),
other => panic!("expected a JSON number, got {:?}", other)
};
expect!(whole_decimal.clone()).to(be_equal_to(FieldValue::Json(serde_json::json!(100.0))));
match whole_decimal {
FieldValue::Json(Value::Number(number)) => expect!(number.is_f64()).to(be_true()),
other => panic!("expected a JSON number, got {:?}", other)
};
}
#[test]
fn each_scalar_type_crosses_the_boundary_under_its_own_arm() {
let cases = [
(FieldValue::Json(Value::Null), "null"),
(FieldValue::Json(Value::Bool(true)), "boolean"),
(FieldValue::Json(serde_json::json!("4111111111111111")), "string"),
(FieldValue::Json(serde_json::json!(100)), "integer"),
(FieldValue::Json(serde_json::json!(100.5)), "decimal"),
(FieldValue::Binary(Bytes::from(vec![0u8, 159, 146, 150])), "binary"),
(FieldValue::Json(serde_json::json!({ "brand": "visa" })), "structured")
];
for (value, expected_arm) in cases {
let arm = match value.to_proto().value {
Some(field_value::Value::NullValue(_)) => "null",
Some(field_value::Value::BooleanValue(_)) => "boolean",
Some(field_value::Value::StringValue(_)) => "string",
Some(field_value::Value::IntegerValue(_)) => "integer",
Some(field_value::Value::DecimalValue(_)) => "decimal",
Some(field_value::Value::BinaryValue(_)) => "binary",
Some(field_value::Value::StructuredValue(_)) => "structured",
None => "unset"
};
expect!(arm).to(be_equal_to(expected_arm));
}
}
#[test]
fn an_unset_proto_value_reads_as_json_null() {
expect!(FieldValue::from_proto(&ProtoFieldValue { value: None }))
.to(be_equal_to(FieldValue::Json(Value::Null)));
}
#[test]
fn a_plugin_rules_configuration_crosses_the_boundary() {
let rule = MatchingRule::Plugin {
name: "creditcard".to_string(),
values: serde_json::json!({ "brand": "visa" })
};
let proto = to_proto_matching_rule(&rule);
expect!(proto.r#type.as_str()).to(be_equal_to("creditcard"));
expect!(proto.values.unwrap().fields.get("brand").cloned()).to(
be_some().value(crate::utils::to_proto_value(&Value::String("visa".to_string()))));
}
#[test]
fn a_plugin_generators_configuration_crosses_the_boundary() {
let generator = Generator::Plugin {
name: "creditcard".to_string(),
values: serde_json::json!({ "brand": "visa" })
};
let proto = to_proto_generator(&generator);
expect!(proto.r#type.as_str()).to(be_equal_to("creditcard"));
expect!(proto.values.unwrap().fields.get("brand").cloned()).to(
be_some().value(crate::utils::to_proto_value(&Value::String("visa".to_string()))));
}
#[derive(Debug)]
struct TestCoreMatcher {
mismatches: Vec<ProtoContentMismatch>,
error: String
}
#[async_trait]
impl CoreFieldMatcher for TestCoreMatcher {
async fn match_field(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
assert_eq!(request.path, "$.card.number");
assert_eq!(request.mismatch_type, "body");
assert_eq!(request.rule.as_ref().unwrap().r#type, "regex");
Ok(MatchFieldResponse {
error: self.error.clone(),
mismatches: self.mismatches.clone()
})
}
}
#[derive(Debug)]
struct TestCoreGenerator;
#[async_trait]
impl CoreFieldGenerator for TestCoreGenerator {
async fn generate_field(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse> {
assert_eq!(request.path, "$.card.number");
assert_eq!(request.test_mode, TestMode::Consumer.to_proto() as i32);
Ok(GenerateFieldResponse {
error: String::default(),
value: Some(FieldValue::Json(Value::String("4012888888881881".to_string())).to_proto())
})
}
}
fn register_core_matcher_entry(key: &str, entry_type: CatalogueEntryType) {
register_core_entries(&vec![CatalogueEntry {
entry_type,
provider_type: CatalogueEntryProviderType::CORE,
plugin: None,
key: key.to_string(),
values: hashmap!{}
}]);
}
fn a_rule() -> MatchingRule {
MatchingRule::Regex("\\d{16}".to_string())
}
fn field_context() -> FieldContext {
FieldContext::new(&DocPath::new("$.card.number").unwrap(), "body")
}
#[test_log::test(tokio::test)]
async fn match_field_dispatches_to_a_registered_core_handler() {
let key = "match_field_dispatches_to_a_registered_core_handler";
register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
register_core_field_matcher(key, Arc::new(TestCoreMatcher {
mismatches: vec![],
error: String::default()
}));
let matcher = find_field_matcher(key).unwrap();
let result = matcher.match_field(
&a_rule(),
&FieldValue::Json(Value::String("4111111111111111".to_string())),
&FieldValue::Json(Value::String("4012888888881881".to_string())),
&field_context()
).await;
deregister_core_field_matcher(key);
expect!(matcher.is_core()).to(be_true());
expect!(result).to(be_ok());
}
#[test_log::test(tokio::test)]
async fn match_field_reports_mismatches_against_the_requested_path() {
let key = "match_field_reports_mismatches_against_the_requested_path";
register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
register_core_field_matcher(key, Arc::new(TestCoreMatcher {
mismatches: vec![ProtoContentMismatch {
mismatch: "fails the Luhn check".to_string(),
expected: Some("4111111111111111".as_bytes().to_vec()),
actual: Some("4111111111111112".as_bytes().to_vec()),
.. ProtoContentMismatch::default()
}],
error: String::default()
}));
let matcher = find_field_matcher(key).unwrap();
let result = matcher.match_field(
&a_rule(),
&FieldValue::Json(Value::String("4111111111111111".to_string())),
&FieldValue::Json(Value::String("4111111111111112".to_string())),
&field_context()
).await;
deregister_core_field_matcher(key);
let mismatches = result.expect_err("expected a mismatch");
expect!(mismatches.len()).to(be_equal_to(1));
expect!(mismatches[0].mismatch.clone()).to(be_equal_to("fails the Luhn check".to_string()));
expect!(mismatches[0].path.clone()).to(be_equal_to("$.card.number".to_string()));
expect!(mismatches[0].mismatch_type.clone()).to(be_some().value("body".to_string()));
expect!(mismatches[0].expected.clone()).to(be_equal_to("4111111111111111".to_string()));
}
#[test_log::test(tokio::test)]
async fn match_field_turns_a_handler_error_into_a_mismatch() {
let key = "match_field_turns_a_handler_error_into_a_mismatch";
register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
register_core_field_matcher(key, Arc::new(TestCoreMatcher {
mismatches: vec![],
error: "'amx' is not a brand this plugin knows about".to_string()
}));
let matcher = find_field_matcher(key).unwrap();
let result = matcher.match_field(
&a_rule(),
&FieldValue::Json(Value::String("4111111111111111".to_string())),
&FieldValue::Json(Value::String("4111111111111111".to_string())),
&field_context()
).await;
deregister_core_field_matcher(key);
let mismatches = result.expect_err("expected the error to surface");
expect!(mismatches[0].mismatch.clone())
.to(be_equal_to("'amx' is not a brand this plugin knows about".to_string()));
}
#[test_log::test(tokio::test)]
async fn match_field_fails_clearly_when_no_core_handler_is_registered() {
let key = "match_field_fails_clearly_when_no_core_handler_is_registered";
register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
let matcher = find_field_matcher(key).unwrap();
let result = matcher.match_field(
&a_rule(),
&FieldValue::Json(Value::Null),
&FieldValue::Json(Value::Null),
&field_context()
).await;
let mismatches = result.expect_err("expected an error for a registered entry with no handler");
expect!(mismatches[0].mismatch.contains("No core field matcher registered")).to(be_true());
}
#[test_log::test(tokio::test)]
async fn generate_field_dispatches_to_a_registered_core_handler() {
let key = "generate_field_dispatches_to_a_registered_core_handler";
register_core_matcher_entry(key, CatalogueEntryType::GENERATOR);
register_core_field_generator(key, Arc::new(TestCoreGenerator));
let generator = find_field_generator(key).unwrap();
let result = generator.generate_field(
&Generator::RandomString(16),
&FieldValue::Json(Value::String("4111111111111111".to_string())),
TestMode::Consumer,
&field_context()
).await;
deregister_core_field_generator(key);
expect!(generator.is_core()).to(be_true());
expect!(result.unwrap()).to(be_equal_to(
FieldValue::Json(Value::String("4012888888881881".to_string()))
));
}
#[test]
fn finding_a_rule_that_is_not_registered_says_so() {
let err = find_field_matcher("finding_a_rule_that_is_not_registered_says_so")
.expect_err("expected an error for an unregistered rule");
expect!(err.to_string().contains("No catalogue entry found")).to(be_true());
}
#[test]
fn finding_a_rule_that_is_a_generator_says_so() {
let key = "finding_a_rule_that_is_a_generator_says_so";
register_core_matcher_entry(key, CatalogueEntryType::GENERATOR);
let err = find_field_matcher(key).expect_err("expected an error for the wrong entry type");
expect!(err.to_string().contains("is a GENERATOR, not a MATCHER")).to(be_true());
}
#[test_log::test]
fn the_blocking_bridge_runs_a_call_from_a_synchronous_context() {
let key = "the_blocking_bridge_runs_a_call_from_a_synchronous_context";
register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
register_core_field_matcher(key, Arc::new(TestCoreMatcher {
mismatches: vec![],
error: String::default()
}));
let matcher = find_field_matcher(key).unwrap();
let result = matcher.match_field_blocking(
&a_rule(),
&FieldValue::Json(Value::String("4111111111111111".to_string())),
&FieldValue::Json(Value::String("4012888888881881".to_string())),
&field_context()
);
deregister_core_field_matcher(key);
expect!(result).to(be_ok());
}
#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn the_blocking_bridge_works_from_inside_a_runtime() {
let key = "the_blocking_bridge_works_from_inside_a_runtime";
register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
register_core_field_matcher(key, Arc::new(TestCoreMatcher {
mismatches: vec![],
error: String::default()
}));
let result = tokio::task::spawn_blocking(move || {
let matcher = find_field_matcher(key).unwrap();
matcher.match_field_blocking(
&a_rule(),
&FieldValue::Json(Value::String("4111111111111111".to_string())),
&FieldValue::Json(Value::String("4012888888881881".to_string())),
&field_context()
)
}).await.unwrap();
deregister_core_field_matcher(key);
expect!(result).to(be_ok());
}
}