Skip to main content

pact_plugin_driver/
field.rs

1//! Support for matching and generating individual field/element values.
2//!
3//! This is the field-level counterpart of [`crate::content`]: where a content matcher owns a whole
4//! content type, a field matcher applies one matching rule to one value inside somebody else's
5//! content - a field in a JSON body, a header, a message metadata value. See proposal 006
6//! (Field-level matchers and generators) for the design.
7//!
8//! The proto types used here are the V2 interface ones. Field-level operations were introduced in
9//! V2 and have no V1 equivalent, so a V1 plugin cannot provide them.
10
11use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14use anyhow::anyhow;
15use bytes::Bytes;
16use lazy_static::lazy_static;
17use pact_models::matchingrules::MatchingRule;
18use pact_models::path_exp::DocPath;
19use pact_models::prelude::Generator;
20use serde_json::Value;
21use tokio::runtime::Runtime;
22use tracing::{debug, error};
23
24use crate::catalogue_manager::{CatalogueEntry, CatalogueEntryProviderType, CatalogueEntryType, resolve_capability_entry};
25use crate::content::ContentMismatch;
26use crate::core_capabilities;
27use crate::plugin_manager::lookup_plugin;
28use crate::plugin_models::{PactPluginManifest, PluginInteractionConfig};
29use crate::proto_v2::{
30  FieldValue as ProtoFieldValue,
31  GenerateFieldRequest,
32  GenerateFieldResponse,
33  MatchFieldRequest,
34  MatchFieldResponse,
35  MatchingRule as ProtoMatchingRule,
36  Generator as ProtoGenerator,
37  PluginConfiguration as ProtoPluginConfiguration,
38  field_value
39};
40use crate::utils::{proto_value_to_json, to_proto_struct, to_proto_value};
41
42/// A single value being matched or generated at the field/element level - the driver-side
43/// counterpart of the proto `FieldValue`.
44///
45/// Binary-safe by construction: a value that is not representable as text is carried as bytes
46/// rather than being stringified into one. Scalar types survive the trip to a plugin intact,
47/// including the difference between a whole number and a decimal, which the `integer`, `decimal`
48/// and `type` matching rules all depend on - see [`FieldValue::to_proto`].
49#[derive(Clone, Debug, PartialEq)]
50pub enum FieldValue {
51  /// A JSON-like value
52  Json(Value),
53  /// Raw bytes, for a value that is not representable as JSON
54  Binary(Bytes)
55}
56
57impl FieldValue {
58  /// Convert to the protobuf form sent to a plugin or core handler.
59  ///
60  /// Each scalar type gets its own arm, so a matching rule on the other side sees the type the
61  /// value actually has - in particular a whole number stays whole, which `integer`, `decimal` and
62  /// `type` all depend on. Only maps and lists go across as a `google.protobuf.Value`.
63  pub fn to_proto(&self) -> ProtoFieldValue {
64    ProtoFieldValue {
65      value: Some(match self {
66        FieldValue::Binary(bytes) => field_value::Value::BinaryValue(bytes.to_vec()),
67        FieldValue::Json(Value::Null) => field_value::Value::NullValue(0),
68        FieldValue::Json(Value::Bool(value)) => field_value::Value::BooleanValue(*value),
69        FieldValue::Json(Value::String(value)) => field_value::Value::StringValue(value.clone()),
70        FieldValue::Json(Value::Number(number)) => match number.as_i64() {
71          Some(value) => field_value::Value::IntegerValue(value),
72          // A u64 above i64::MAX has nowhere exact to go; a double at least keeps the magnitude
73          None => field_value::Value::DecimalValue(number.as_f64().unwrap_or_default())
74        },
75        FieldValue::Json(value) => field_value::Value::StructuredValue(to_proto_value(value))
76      })
77    }
78  }
79
80  /// Convert from the protobuf form returned by a plugin or core handler. An unset value is
81  /// treated as null, matching how an absent `oneof` reads everywhere else in the interface.
82  pub fn from_proto(value: &ProtoFieldValue) -> FieldValue {
83    match &value.value {
84      Some(field_value::Value::NullValue(_)) | None => FieldValue::Json(Value::Null),
85      Some(field_value::Value::BooleanValue(value)) => FieldValue::Json(Value::Bool(*value)),
86      Some(field_value::Value::StringValue(value)) => FieldValue::Json(Value::String(value.clone())),
87      Some(field_value::Value::IntegerValue(value)) => FieldValue::Json(Value::Number((*value).into())),
88      Some(field_value::Value::DecimalValue(value)) => FieldValue::Json(
89        serde_json::Number::from_f64(*value)
90          .map(Value::Number)
91          // NaN and the infinities have no JSON representation
92          .unwrap_or(Value::Null)
93      ),
94      Some(field_value::Value::BinaryValue(bytes)) => FieldValue::Binary(Bytes::from(bytes.clone())),
95      Some(field_value::Value::StructuredValue(value)) => FieldValue::Json(proto_value_to_json(value))
96    }
97  }
98}
99
100impl From<Value> for FieldValue {
101  fn from(value: Value) -> Self {
102    FieldValue::Json(value)
103  }
104}
105
106impl From<Bytes> for FieldValue {
107  fn from(bytes: Bytes) -> Self {
108    FieldValue::Binary(bytes)
109  }
110}
111
112/// Where a value sits and what is known about it, shared by matching and generation.
113#[derive(Clone, Debug)]
114pub struct FieldContext {
115  /// Path to the value, as a Pact matching rule expression (`$.card.number`)
116  pub path: DocPath,
117  /// Part of the interaction the value came from: `body`, `header`, `metadata`, `query`, `path`,
118  /// `status`. Only affects how a mismatch is reported; generation ignores it.
119  pub category: String,
120  /// Plugin configuration persisted into the Pact file for this interaction
121  pub plugin_config: Option<PluginInteractionConfig>,
122  /// Context data provided by the test framework
123  pub test_context: HashMap<String, Value>
124}
125
126impl Default for FieldContext {
127  fn default() -> Self {
128    FieldContext {
129      path: DocPath::root(),
130      category: "body".to_string(),
131      plugin_config: None,
132      test_context: HashMap::default()
133    }
134  }
135}
136
137impl FieldContext {
138  /// A context for a value at the given path in the given part of the interaction
139  pub fn new(path: &DocPath, category: &str) -> FieldContext {
140    FieldContext {
141      path: path.clone(),
142      category: category.to_string(),
143      .. FieldContext::default()
144    }
145  }
146
147  /// Set the plugin configuration
148  pub fn with_plugin_config(self, plugin_config: Option<PluginInteractionConfig>) -> FieldContext {
149    FieldContext { plugin_config, .. self }
150  }
151
152  /// Set the test framework context data
153  pub fn with_test_context(self, test_context: HashMap<String, Value>) -> FieldContext {
154    FieldContext { test_context, .. self }
155  }
156}
157
158/// Matching rule for a single field/element value, provided by a plugin or by a handler the host
159/// framework registered (see [`crate::core_capabilities::CoreFieldMatcher`]).
160#[derive(Clone, Debug)]
161pub struct FieldMatcher {
162  /// Catalogue entry for this matching rule
163  pub catalogue_entry: CatalogueEntry
164}
165
166/// Generator for a single field/element value. See [`FieldMatcher`].
167#[derive(Clone, Debug)]
168pub struct FieldGenerator {
169  /// Catalogue entry for this generator
170  pub catalogue_entry: CatalogueEntry
171}
172
173/// Find the field-level matching rule with the given name. The name is resolved against the
174/// catalogue the same way any other capability key is - see
175/// [`crate::catalogue_manager::resolve_capability`] - so `creditcard` finds a plugin's own rule and
176/// `type` finds the core `v2-type` rule. Returns a descriptive error if the name matches nothing,
177/// matches more than one rule, or names something that is not a matching rule.
178pub fn find_field_matcher(name: &str) -> anyhow::Result<FieldMatcher> {
179  resolve_capability_entry(name, CatalogueEntryType::MATCHER)
180    .map(|catalogue_entry| FieldMatcher { catalogue_entry })
181}
182
183/// Find the field-level generator with the given name. See [`find_field_matcher`].
184pub fn find_field_generator(name: &str) -> anyhow::Result<FieldGenerator> {
185  resolve_capability_entry(name, CatalogueEntryType::GENERATOR)
186    .map(|catalogue_entry| FieldGenerator { catalogue_entry })
187}
188
189impl FieldMatcher {
190  /// If this is a matching rule provided by the core framework rather than a plugin
191  pub fn is_core(&self) -> bool {
192    self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
193  }
194
195  /// Catalogue entry key for this matching rule
196  pub fn catalogue_entry_key(&self) -> String {
197    if self.is_core() {
198      format!("core/matcher/{}", self.catalogue_entry.key)
199    } else {
200      format!("plugin/{}/matcher/{}", self.plugin_name(), self.catalogue_entry.key)
201    }
202  }
203
204  /// Plugin that provides this matching rule, if any
205  pub fn plugin(&self) -> Option<PactPluginManifest> {
206    self.catalogue_entry.plugin.clone()
207  }
208
209  /// Name of the plugin that provides this matching rule
210  pub fn plugin_name(&self) -> String {
211    self.catalogue_entry.plugin.as_ref()
212      .map(|plugin| plugin.name.clone())
213      .unwrap_or("core".to_string())
214  }
215
216  /// Apply this matching rule to a single value.
217  ///
218  /// The context carries where the value lives and which part of the interaction it came from;
219  /// both are echoed back on any mismatch that does not place itself. An empty result means the
220  /// value matched.
221  pub async fn match_field(
222    &self,
223    rule: &MatchingRule,
224    expected: &FieldValue,
225    actual: &FieldValue,
226    context: &FieldContext
227  ) -> Result<(), Vec<ContentMismatch>> {
228    let request = MatchFieldRequest {
229      key: self.catalogue_entry.key.clone(),
230      rule: Some(to_proto_matching_rule(rule)),
231      path: context.path.to_string(),
232      mismatch_type: context.category.clone(),
233      expected: Some(expected.to_proto()),
234      actual: Some(actual.to_proto()),
235      plugin_configuration: context.plugin_config.clone().map(to_proto_plugin_config),
236      test_context: Some(to_proto_struct(&context.test_context))
237    };
238
239    let response = if self.is_core() {
240      match core_capabilities::lookup_core_field_matcher(&self.catalogue_entry.key) {
241        Some(handler) => handler.match_field(request).await,
242        None => Err(anyhow!("No core field matcher registered for '{}'", self.catalogue_entry.key))
243      }
244    } else {
245      self.call_plugin(request).await
246    };
247
248    process_match_field_response(response, context)
249  }
250
251  async fn call_plugin(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
252    let manifest = self.catalogue_entry.plugin.as_ref()
253      .ok_or_else(|| anyhow!("Catalogue entry '{}' has no plugin manifest", self.catalogue_entry_key()))?;
254    let plugin = lookup_plugin(&manifest.as_dependency())
255      .ok_or_else(|| anyhow!("Plugin '{}' for matching rule '{}' is not currently running",
256        manifest.name, self.catalogue_entry.key))?;
257    debug!("Sending MatchField request to plugin {:?}", manifest.name);
258    let chain_id = crate::call_chain::new_call_chain_id();
259    let deadline_ms = crate::call_chain::default_deadline_ms();
260    plugin.match_field_with_chain(request, &chain_id, deadline_ms).await
261  }
262
263  /// Apply this matching rule to a single value from a synchronous call path.
264  ///
265  /// Every Rust host applies matching rules synchronously (`match_values` and the matching engine's
266  /// `execute_*_plan` functions), while a plugin call is async, so this bridge exists so each host
267  /// does not have to build its own - see [`block_on_field_call`] for why the obvious ways of doing
268  /// it do not work.
269  pub fn match_field_blocking(
270    &self,
271    rule: &MatchingRule,
272    expected: &FieldValue,
273    actual: &FieldValue,
274    context: &FieldContext
275  ) -> Result<(), Vec<ContentMismatch>> {
276    let matcher = self.clone();
277    let rule = rule.clone();
278    let expected = expected.clone();
279    let actual = actual.clone();
280    let call_context = context.clone();
281
282    block_on_field_call(async move {
283      matcher.match_field(&rule, &expected, &actual, &call_context).await
284    })
285    .unwrap_or_else(|err| Err(vec![mismatch_for(err.to_string(), context)]))
286  }
287}
288
289impl FieldGenerator {
290  /// If this is a generator provided by the core framework rather than a plugin
291  pub fn is_core(&self) -> bool {
292    self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
293  }
294
295  /// Catalogue entry key for this generator
296  pub fn catalogue_entry_key(&self) -> String {
297    if self.is_core() {
298      format!("core/generator/{}", self.catalogue_entry.key)
299    } else {
300      format!("plugin/{}/generator/{}", self.plugin_name(), self.catalogue_entry.key)
301    }
302  }
303
304  /// Plugin that provides this generator, if any
305  pub fn plugin(&self) -> Option<PactPluginManifest> {
306    self.catalogue_entry.plugin.clone()
307  }
308
309  /// Name of the plugin that provides this generator
310  pub fn plugin_name(&self) -> String {
311    self.catalogue_entry.plugin.as_ref()
312      .map(|plugin| plugin.name.clone())
313      .unwrap_or("core".to_string())
314  }
315
316  /// Generate a single value, replacing the example value from the Pact interaction.
317  pub async fn generate_field(
318    &self,
319    generator: &Generator,
320    example: &FieldValue,
321    mode: TestMode,
322    context: &FieldContext
323  ) -> anyhow::Result<FieldValue> {
324    let request = GenerateFieldRequest {
325      key: self.catalogue_entry.key.clone(),
326      generator: Some(to_proto_generator(generator)),
327      path: context.path.to_string(),
328      example_value: Some(example.to_proto()),
329      plugin_configuration: context.plugin_config.clone().map(to_proto_plugin_config),
330      test_context: Some(to_proto_struct(&context.test_context)),
331      test_mode: mode.to_proto() as i32
332    };
333
334    let response = if self.is_core() {
335      let handler = core_capabilities::lookup_core_field_generator(&self.catalogue_entry.key)
336        .ok_or_else(|| anyhow!("No core field generator registered for '{}'", self.catalogue_entry.key))?;
337      handler.generate_field(request).await?
338    } else {
339      self.call_plugin(request).await?
340    };
341
342    if !response.error.is_empty() {
343      return Err(anyhow!("Generator '{}' failed: {}", self.catalogue_entry.key, response.error));
344    }
345    match &response.value {
346      Some(value) => Ok(FieldValue::from_proto(value)),
347      None => Err(anyhow!("Generator '{}' returned no value", self.catalogue_entry.key))
348    }
349  }
350
351  async fn call_plugin(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse> {
352    let manifest = self.catalogue_entry.plugin.as_ref()
353      .ok_or_else(|| anyhow!("Catalogue entry '{}' has no plugin manifest", self.catalogue_entry_key()))?;
354    let plugin = lookup_plugin(&manifest.as_dependency())
355      .ok_or_else(|| anyhow!("Plugin '{}' for generator '{}' is not currently running",
356        manifest.name, self.catalogue_entry.key))?;
357    debug!("Sending GenerateField request to plugin {:?}", manifest.name);
358    let chain_id = crate::call_chain::new_call_chain_id();
359    let deadline_ms = crate::call_chain::default_deadline_ms();
360    plugin.generate_field_with_chain(request, &chain_id, deadline_ms).await
361  }
362
363  /// Generate a single value from a synchronous call path. See
364  /// [`FieldMatcher::match_field_blocking`].
365  pub fn generate_field_blocking(
366    &self,
367    generator: &Generator,
368    example: &FieldValue,
369    mode: TestMode,
370    context: &FieldContext
371  ) -> anyhow::Result<FieldValue> {
372    let field_generator = self.clone();
373    let generator = generator.clone();
374    let example = example.clone();
375    let context = context.clone();
376
377    block_on_field_call(async move {
378      field_generator.generate_field(&generator, &example, mode, &context).await
379    })?
380  }
381}
382
383/// Which side of the test a generator is running on, mirroring `GenerateContentRequest.TestMode`
384/// in the plugin interface.
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub enum TestMode {
387  /// Running on the consumer side
388  Consumer,
389  /// Running on the provider side
390  Provider,
391  /// Not known
392  Unknown
393}
394
395impl TestMode {
396  fn to_proto(self) -> crate::proto_v2::generate_content_request::TestMode {
397    use crate::proto_v2::generate_content_request::TestMode as ProtoTestMode;
398    match self {
399      TestMode::Consumer => ProtoTestMode::Consumer,
400      TestMode::Provider => ProtoTestMode::Provider,
401      TestMode::Unknown => ProtoTestMode::Unknown
402    }
403  }
404}
405
406lazy_static! {
407  /// Runtime the driver owns for field-level plugin calls made from a synchronous call path.
408  /// Built on first use and never dropped - dropping a Tokio runtime from inside an async context
409  /// panics, and by definition this one is reached from call paths that may be exactly that.
410  static ref FIELD_RUNTIME: Mutex<Option<Arc<Runtime>>> = Mutex::new(None);
411}
412
413fn field_runtime() -> anyhow::Result<Arc<Runtime>> {
414  let mut guard = FIELD_RUNTIME.lock()
415    .map_err(|err| anyhow!("FIELD_RUNTIME mutex poisoned - {}", err))?;
416  match guard.as_ref() {
417    Some(runtime) => Ok(runtime.clone()),
418    None => {
419      let runtime = Arc::new(tokio::runtime::Builder::new_multi_thread()
420        .worker_threads(1)
421        .enable_all()
422        .thread_name("pact-plugin-field")
423        .build()?);
424      *guard = Some(runtime.clone());
425      Ok(runtime)
426    }
427  }
428}
429
430/// Run a field-level plugin call to completion from a synchronous call path.
431///
432/// The obvious approaches do not work. `Handle::current().block_on(..)` panics ("Cannot start a
433/// runtime from within a runtime") because matching is reached from an async call path, so the
434/// calling thread is already driving tasks. `task::block_in_place` re-enters legitimately but
435/// panics on a `current_thread` runtime, and some Pact entry points use one.
436///
437/// So the future runs on a runtime the driver owns, and the calling thread waits on a channel. It
438/// does block a host thread for the duration, which is inherent to bridging sync and async, but the
439/// plugin call itself never depends on the host's runtime making progress: the driver opens a fresh
440/// gRPC channel per call (see `GrpcPactPlugin::connect_channel`), so the connection driving that
441/// call belongs to this runtime too.
442fn block_on_field_call<F, T>(future: F) -> anyhow::Result<T>
443where
444  F: std::future::Future<Output = T> + Send + 'static,
445  T: Send + 'static
446{
447  let runtime = field_runtime()?;
448  let deadline_ms = crate::call_chain::default_deadline_ms();
449  let (sender, receiver) = std::sync::mpsc::channel();
450  runtime.spawn(async move {
451    // A send error just means the caller already gave up waiting
452    let _ = sender.send(future.await);
453  });
454  receiver.recv_timeout(crate::call_chain::remaining(deadline_ms))
455    .map_err(|err| {
456      error!("Timed out waiting for a field-level plugin call to complete - {}", err);
457      anyhow!("Timed out waiting for the plugin call to complete - {}", err)
458    })
459}
460
461fn process_match_field_response(
462  response: anyhow::Result<MatchFieldResponse>,
463  context: &FieldContext
464) -> Result<(), Vec<ContentMismatch>> {
465  let path = context.path.to_string();
466  match response {
467    Ok(response) => if !response.error.is_empty() {
468      Err(vec![mismatch_for(response.error, context)])
469    } else if response.mismatches.is_empty() {
470      Ok(())
471    } else {
472      Err(response.mismatches.iter().map(|mismatch| ContentMismatch {
473        expected: mismatch.expected.as_ref()
474          .map(|bytes| String::from_utf8_lossy(bytes).to_string())
475          .unwrap_or_default(),
476        actual: mismatch.actual.as_ref()
477          .map(|bytes| String::from_utf8_lossy(bytes).to_string())
478          .unwrap_or_default(),
479        mismatch: mismatch.mismatch.clone(),
480        // A mismatch that does not place itself is reported against the value being matched
481        path: if mismatch.path.is_empty() { path.clone() } else { mismatch.path.clone() },
482        diff: if mismatch.diff.is_empty() { None } else { Some(mismatch.diff.clone()) },
483        mismatch_type: if mismatch.mismatch_type.is_empty() {
484          Some(context.category.clone())
485        } else {
486          Some(mismatch.mismatch_type.clone())
487        }
488      }).collect())
489    },
490    Err(err) => {
491      error!("Field-level match call failed - {}", err);
492      Err(vec![mismatch_for(err.to_string(), context)])
493    }
494  }
495}
496
497fn mismatch_for(message: String, context: &FieldContext) -> ContentMismatch {
498  ContentMismatch {
499    expected: Default::default(),
500    actual: Default::default(),
501    mismatch: message,
502    path: context.path.to_string(),
503    diff: None,
504    mismatch_type: Some(context.category.clone())
505  }
506}
507
508fn to_proto_matching_rule(rule: &MatchingRule) -> ProtoMatchingRule {
509  ProtoMatchingRule {
510    r#type: rule.name(),
511    values: Some(to_proto_struct(&rule.values().iter()
512      .map(|(k, v)| (k.to_string(), v.clone()))
513      .collect()))
514  }
515}
516
517fn to_proto_generator(generator: &Generator) -> ProtoGenerator {
518  ProtoGenerator {
519    r#type: generator.name(),
520    values: Some(to_proto_struct(&generator.values().iter()
521      .map(|(k, v)| (k.to_string(), v.clone()))
522      .collect()))
523  }
524}
525
526fn to_proto_plugin_config(config: PluginInteractionConfig) -> ProtoPluginConfiguration {
527  ProtoPluginConfiguration {
528    interaction_configuration: Some(to_proto_struct(&config.interaction_configuration)),
529    pact_configuration: Some(to_proto_struct(&config.pact_configuration))
530  }
531}
532
533#[cfg(test)]
534mod tests {
535  use async_trait::async_trait;
536  use expectest::prelude::*;
537  use maplit::hashmap;
538  use pact_models::matchingrules::MatchingRule;
539
540  use crate::catalogue_manager::{CatalogueEntryProviderType, register_core_entries};
541  use crate::core_capabilities::{
542    CoreFieldGenerator,
543    CoreFieldMatcher,
544    deregister_core_field_generator,
545    deregister_core_field_matcher,
546    register_core_field_generator,
547    register_core_field_matcher
548  };
549  use crate::proto_v2::ContentMismatch as ProtoContentMismatch;
550
551  use super::*;
552
553  #[test]
554  fn field_values_round_trip_through_the_proto_form() {
555    for value in [
556      FieldValue::Json(Value::String("4111111111111111".to_string())),
557      FieldValue::Json(serde_json::json!(100)),
558      FieldValue::Json(serde_json::json!(-100.5)),
559      FieldValue::Json(Value::Bool(true)),
560      FieldValue::Json(Value::Null),
561      FieldValue::Json(serde_json::json!({ "brand": "visa" })),
562      // A value that is not representable as JSON survives as bytes rather than being stringified
563      FieldValue::Binary(Bytes::from(vec![0u8, 159, 146, 150]))
564    ] {
565      expect!(FieldValue::from_proto(&value.to_proto())).to(be_equal_to(value));
566    }
567  }
568
569  #[test]
570  fn a_whole_number_stays_whole_and_a_decimal_stays_decimal() {
571    // The distinction the `integer`, `decimal` and `type` rules are built on, and the reason
572    // FieldValue does not put every value through a google.protobuf.Value
573    let integer = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100)).to_proto());
574    let decimal = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100.5)).to_proto());
575    let whole_decimal = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100.0)).to_proto());
576
577    expect!(integer.clone()).to(be_equal_to(FieldValue::Json(serde_json::json!(100))));
578    expect!(decimal).to(be_equal_to(FieldValue::Json(serde_json::json!(100.5))));
579    match integer {
580      FieldValue::Json(Value::Number(number)) => expect!(number.is_i64()).to(be_true()),
581      other => panic!("expected a JSON number, got {:?}", other)
582    };
583    // A decimal that happens to be whole stays a decimal - it is not quietly promoted to an
584    // integer, which would make `decimal` reject a value it should accept
585    expect!(whole_decimal.clone()).to(be_equal_to(FieldValue::Json(serde_json::json!(100.0))));
586    match whole_decimal {
587      FieldValue::Json(Value::Number(number)) => expect!(number.is_f64()).to(be_true()),
588      other => panic!("expected a JSON number, got {:?}", other)
589    };
590  }
591
592  #[test]
593  fn each_scalar_type_crosses_the_boundary_under_its_own_arm() {
594    let cases = [
595      (FieldValue::Json(Value::Null), "null"),
596      (FieldValue::Json(Value::Bool(true)), "boolean"),
597      (FieldValue::Json(serde_json::json!("4111111111111111")), "string"),
598      (FieldValue::Json(serde_json::json!(100)), "integer"),
599      (FieldValue::Json(serde_json::json!(100.5)), "decimal"),
600      (FieldValue::Binary(Bytes::from(vec![0u8, 159, 146, 150])), "binary"),
601      (FieldValue::Json(serde_json::json!({ "brand": "visa" })), "structured")
602    ];
603    for (value, expected_arm) in cases {
604      let arm = match value.to_proto().value {
605        Some(field_value::Value::NullValue(_)) => "null",
606        Some(field_value::Value::BooleanValue(_)) => "boolean",
607        Some(field_value::Value::StringValue(_)) => "string",
608        Some(field_value::Value::IntegerValue(_)) => "integer",
609        Some(field_value::Value::DecimalValue(_)) => "decimal",
610        Some(field_value::Value::BinaryValue(_)) => "binary",
611        Some(field_value::Value::StructuredValue(_)) => "structured",
612        None => "unset"
613      };
614      expect!(arm).to(be_equal_to(expected_arm));
615    }
616  }
617
618  #[test]
619  fn an_unset_proto_value_reads_as_json_null() {
620    expect!(FieldValue::from_proto(&ProtoFieldValue { value: None }))
621      .to(be_equal_to(FieldValue::Json(Value::Null)));
622  }
623
624  /// Records the request it was given, and answers with the mismatches it was built with
625  #[derive(Debug)]
626  struct TestCoreMatcher {
627    mismatches: Vec<ProtoContentMismatch>,
628    error: String
629  }
630
631  #[async_trait]
632  impl CoreFieldMatcher for TestCoreMatcher {
633    async fn match_field(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
634      // Prove the request carried what the caller passed in
635      assert_eq!(request.path, "$.card.number");
636      assert_eq!(request.mismatch_type, "body");
637      assert_eq!(request.rule.as_ref().unwrap().r#type, "regex");
638      Ok(MatchFieldResponse {
639        error: self.error.clone(),
640        mismatches: self.mismatches.clone()
641      })
642    }
643  }
644
645  #[derive(Debug)]
646  struct TestCoreGenerator;
647
648  #[async_trait]
649  impl CoreFieldGenerator for TestCoreGenerator {
650    async fn generate_field(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse> {
651      assert_eq!(request.path, "$.card.number");
652      assert_eq!(request.test_mode, TestMode::Consumer.to_proto() as i32);
653      Ok(GenerateFieldResponse {
654        error: String::default(),
655        value: Some(FieldValue::Json(Value::String("4012888888881881".to_string())).to_proto())
656      })
657    }
658  }
659
660  fn register_core_matcher_entry(key: &str, entry_type: CatalogueEntryType) {
661    register_core_entries(&vec![CatalogueEntry {
662      entry_type,
663      provider_type: CatalogueEntryProviderType::CORE,
664      plugin: None,
665      key: key.to_string(),
666      values: hashmap!{}
667    }]);
668  }
669
670  /// The driver forwards whatever rule the host hands it - `rule.name()` and `rule.values()` -
671  /// so any rule exercises the plumbing. Once pact_models grows the `Plugin` carrier variant
672  /// (proposal 006 section 4), a plugin's own rule name arrives here by exactly this path.
673  fn a_rule() -> MatchingRule {
674    MatchingRule::Regex("\\d{16}".to_string())
675  }
676
677  fn field_context() -> FieldContext {
678    FieldContext::new(&DocPath::new("$.card.number").unwrap(), "body")
679  }
680
681  #[test_log::test(tokio::test)]
682  async fn match_field_dispatches_to_a_registered_core_handler() {
683    let key = "match_field_dispatches_to_a_registered_core_handler";
684    register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
685    register_core_field_matcher(key, Arc::new(TestCoreMatcher {
686      mismatches: vec![],
687      error: String::default()
688    }));
689
690    let matcher = find_field_matcher(key).unwrap();
691    let result = matcher.match_field(
692      &a_rule(),
693      &FieldValue::Json(Value::String("4111111111111111".to_string())),
694      &FieldValue::Json(Value::String("4012888888881881".to_string())),
695      &field_context()
696    ).await;
697
698    deregister_core_field_matcher(key);
699
700    expect!(matcher.is_core()).to(be_true());
701    expect!(result).to(be_ok());
702  }
703
704  #[test_log::test(tokio::test)]
705  async fn match_field_reports_mismatches_against_the_requested_path() {
706    let key = "match_field_reports_mismatches_against_the_requested_path";
707    register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
708    register_core_field_matcher(key, Arc::new(TestCoreMatcher {
709      mismatches: vec![ProtoContentMismatch {
710        // Deliberately no path/mismatchType: the driver fills them in from the request
711        mismatch: "fails the Luhn check".to_string(),
712        expected: Some("4111111111111111".as_bytes().to_vec()),
713        actual: Some("4111111111111112".as_bytes().to_vec()),
714        .. ProtoContentMismatch::default()
715      }],
716      error: String::default()
717    }));
718
719    let matcher = find_field_matcher(key).unwrap();
720    let result = matcher.match_field(
721      &a_rule(),
722      &FieldValue::Json(Value::String("4111111111111111".to_string())),
723      &FieldValue::Json(Value::String("4111111111111112".to_string())),
724      &field_context()
725    ).await;
726
727    deregister_core_field_matcher(key);
728
729    let mismatches = result.expect_err("expected a mismatch");
730    expect!(mismatches.len()).to(be_equal_to(1));
731    expect!(mismatches[0].mismatch.clone()).to(be_equal_to("fails the Luhn check".to_string()));
732    expect!(mismatches[0].path.clone()).to(be_equal_to("$.card.number".to_string()));
733    expect!(mismatches[0].mismatch_type.clone()).to(be_some().value("body".to_string()));
734    expect!(mismatches[0].expected.clone()).to(be_equal_to("4111111111111111".to_string()));
735  }
736
737  #[test_log::test(tokio::test)]
738  async fn match_field_turns_a_handler_error_into_a_mismatch() {
739    let key = "match_field_turns_a_handler_error_into_a_mismatch";
740    register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
741    register_core_field_matcher(key, Arc::new(TestCoreMatcher {
742      mismatches: vec![],
743      error: "'amx' is not a brand this plugin knows about".to_string()
744    }));
745
746    let matcher = find_field_matcher(key).unwrap();
747    let result = matcher.match_field(
748      &a_rule(),
749      &FieldValue::Json(Value::String("4111111111111111".to_string())),
750      &FieldValue::Json(Value::String("4111111111111111".to_string())),
751      &field_context()
752    ).await;
753
754    deregister_core_field_matcher(key);
755
756    let mismatches = result.expect_err("expected the error to surface");
757    expect!(mismatches[0].mismatch.clone())
758      .to(be_equal_to("'amx' is not a brand this plugin knows about".to_string()));
759  }
760
761  #[test_log::test(tokio::test)]
762  async fn match_field_fails_clearly_when_no_core_handler_is_registered() {
763    let key = "match_field_fails_clearly_when_no_core_handler_is_registered";
764    register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
765
766    let matcher = find_field_matcher(key).unwrap();
767    let result = matcher.match_field(
768      &a_rule(),
769      &FieldValue::Json(Value::Null),
770      &FieldValue::Json(Value::Null),
771      &field_context()
772    ).await;
773
774    let mismatches = result.expect_err("expected an error for a registered entry with no handler");
775    expect!(mismatches[0].mismatch.contains("No core field matcher registered")).to(be_true());
776  }
777
778  #[test_log::test(tokio::test)]
779  async fn generate_field_dispatches_to_a_registered_core_handler() {
780    let key = "generate_field_dispatches_to_a_registered_core_handler";
781    register_core_matcher_entry(key, CatalogueEntryType::GENERATOR);
782    register_core_field_generator(key, Arc::new(TestCoreGenerator));
783
784    let generator = find_field_generator(key).unwrap();
785    let result = generator.generate_field(
786      &Generator::RandomString(16),
787      &FieldValue::Json(Value::String("4111111111111111".to_string())),
788      TestMode::Consumer,
789      &field_context()
790    ).await;
791
792    deregister_core_field_generator(key);
793
794    expect!(generator.is_core()).to(be_true());
795    expect!(result.unwrap()).to(be_equal_to(
796      FieldValue::Json(Value::String("4012888888881881".to_string()))
797    ));
798  }
799
800  #[test]
801  fn finding_a_rule_that_is_not_registered_says_so() {
802    let err = find_field_matcher("finding_a_rule_that_is_not_registered_says_so")
803      .expect_err("expected an error for an unregistered rule");
804    expect!(err.to_string().contains("No catalogue entry found")).to(be_true());
805  }
806
807  #[test]
808  fn finding_a_rule_that_is_a_generator_says_so() {
809    let key = "finding_a_rule_that_is_a_generator_says_so";
810    register_core_matcher_entry(key, CatalogueEntryType::GENERATOR);
811
812    let err = find_field_matcher(key).expect_err("expected an error for the wrong entry type");
813    expect!(err.to_string().contains("is a GENERATOR, not a MATCHER")).to(be_true());
814  }
815
816  #[test_log::test]
817  fn the_blocking_bridge_runs_a_call_from_a_synchronous_context() {
818    let key = "the_blocking_bridge_runs_a_call_from_a_synchronous_context";
819    register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
820    register_core_field_matcher(key, Arc::new(TestCoreMatcher {
821      mismatches: vec![],
822      error: String::default()
823    }));
824
825    let matcher = find_field_matcher(key).unwrap();
826    let result = matcher.match_field_blocking(
827      &a_rule(),
828      &FieldValue::Json(Value::String("4111111111111111".to_string())),
829      &FieldValue::Json(Value::String("4012888888881881".to_string())),
830      &field_context()
831    );
832
833    deregister_core_field_matcher(key);
834
835    expect!(result).to(be_ok());
836  }
837
838  #[test_log::test(tokio::test(flavor = "multi_thread"))]
839  async fn the_blocking_bridge_works_from_inside_a_runtime() {
840    // The case Handle::block_on panics on: the calling thread is already driving async tasks.
841    // Run it on a blocking thread, which is how a host's synchronous matching path reaches us.
842    let key = "the_blocking_bridge_works_from_inside_a_runtime";
843    register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
844    register_core_field_matcher(key, Arc::new(TestCoreMatcher {
845      mismatches: vec![],
846      error: String::default()
847    }));
848
849    let result = tokio::task::spawn_blocking(move || {
850      let matcher = find_field_matcher(key).unwrap();
851      matcher.match_field_blocking(
852        &a_rule(),
853        &FieldValue::Json(Value::String("4111111111111111".to_string())),
854        &FieldValue::Json(Value::String("4012888888881881".to_string())),
855        &field_context()
856      )
857    }).await.unwrap();
858
859    deregister_core_field_matcher(key);
860
861    expect!(result).to(be_ok());
862  }
863}