Skip to main content

pact_plugin_driver/
core_capabilities.rs

1//! Registry of host-provided ("core") capability handlers, keyed by catalogue entry key.
2//!
3//! This generalises the [`crate::plugin_log_sink::PluginLogSink`] pattern from a single sink to
4//! one handler per capability shape: the driver defines a narrow trait per capability (matching
5//! an operation already defined for plugins), the embedding Pact framework implements it and
6//! registers an instance here at startup, and the driver never has a compile-time dependency on
7//! that implementation. See proposal 007 (Driver-plugin callback model) for the full design.
8//!
9//! Registration should happen alongside [`crate::catalogue_manager::register_core_entries`] for
10//! the corresponding `CatalogueEntryProviderType::CORE` entry, so an entry and its handler never
11//! drift apart. Callers resolve a capability via the catalogue entry's `key` (unprefixed, e.g.
12//! `"xml"` for `core/content-matcher/xml`), not the full catalogue key.
13
14use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16
17use async_trait::async_trait;
18use lazy_static::lazy_static;
19
20use crate::proto::{CompareContentsRequest, CompareContentsResponse, GenerateContentRequest, GenerateContentResponse};
21use crate::proto_v2::{GenerateFieldRequest, GenerateFieldResponse, MatchFieldRequest, MatchFieldResponse};
22
23/// A host-provided handler for the `CompareContents` capability shape. Implemented by the
24/// embedding Pact framework and registered via [`register_core_content_matcher`].
25#[async_trait]
26pub trait CoreContentMatcher: Send + Sync {
27  /// Compare the actual contents against the expected contents, returning any mismatches.
28  async fn compare_contents(&self, request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse>;
29}
30
31/// A host-provided handler for the `MatchField` capability shape - one of the standard Pact
32/// matching rules applied to a single value. Implemented by the embedding Pact framework and
33/// registered via [`register_core_field_matcher`]. See proposals 006 (Field-level matchers and
34/// generators) and 009 (Host-provided core matching and generation).
35///
36/// The request and response are the V2 interface types: field-level operations were introduced in
37/// V2 and have no V1 equivalent.
38#[async_trait]
39pub trait CoreFieldMatcher: Send + Sync {
40  /// Apply the matching rule to a single value, returning any mismatches.
41  async fn match_field(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse>;
42}
43
44/// A host-provided handler for the `GenerateField` capability shape - one of the standard Pact
45/// generators applied to a single value. Implemented by the embedding Pact framework and registered
46/// via [`register_core_field_generator`]. See [`CoreFieldMatcher`].
47#[async_trait]
48pub trait CoreFieldGenerator: Send + Sync {
49  /// Generate a single value, replacing the example value from the Pact interaction.
50  async fn generate_field(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse>;
51}
52
53/// A host-provided handler for the `GenerateContent` capability shape. Implemented by the
54/// embedding Pact framework and registered via [`register_core_content_generator`].
55#[async_trait]
56pub trait CoreContentGenerator: Send + Sync {
57  /// Generate contents using the provided generators.
58  async fn generate_content(&self, request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse>;
59}
60
61lazy_static! {
62  static ref CORE_CONTENT_MATCHERS: Mutex<HashMap<String, Arc<dyn CoreContentMatcher>>> = Mutex::new(HashMap::new());
63  static ref CORE_CONTENT_GENERATORS: Mutex<HashMap<String, Arc<dyn CoreContentGenerator>>> = Mutex::new(HashMap::new());
64  static ref CORE_FIELD_MATCHERS: Mutex<HashMap<String, Arc<dyn CoreFieldMatcher>>> = Mutex::new(HashMap::new());
65  static ref CORE_FIELD_GENERATORS: Mutex<HashMap<String, Arc<dyn CoreFieldGenerator>>> = Mutex::new(HashMap::new());
66}
67
68/// Register a handler for a host-provided content matcher capability, keyed by the catalogue
69/// entry key (e.g. `"xml"` for the `core/content-matcher/xml` entry). Replaces any handler
70/// previously registered under the same key.
71pub fn register_core_content_matcher(key: &str, handler: Arc<dyn CoreContentMatcher>) {
72  CORE_CONTENT_MATCHERS.lock()
73    .expect("CORE_CONTENT_MATCHERS mutex poisoned")
74    .insert(key.to_string(), handler);
75}
76
77/// Register a handler for a host-provided content generator capability, keyed by the catalogue
78/// entry key (e.g. `"xml"` for the `core/content-generator/xml` entry). Replaces any handler
79/// previously registered under the same key.
80pub fn register_core_content_generator(key: &str, handler: Arc<dyn CoreContentGenerator>) {
81  CORE_CONTENT_GENERATORS.lock()
82    .expect("CORE_CONTENT_GENERATORS mutex poisoned")
83    .insert(key.to_string(), handler);
84}
85
86/// Look up a registered core content matcher handler by catalogue entry key.
87pub fn lookup_core_content_matcher(key: &str) -> Option<Arc<dyn CoreContentMatcher>> {
88  CORE_CONTENT_MATCHERS.lock()
89    .expect("CORE_CONTENT_MATCHERS mutex poisoned")
90    .get(key).cloned()
91}
92
93/// Look up a registered core content generator handler by catalogue entry key.
94pub fn lookup_core_content_generator(key: &str) -> Option<Arc<dyn CoreContentGenerator>> {
95  CORE_CONTENT_GENERATORS.lock()
96    .expect("CORE_CONTENT_GENERATORS mutex poisoned")
97    .get(key).cloned()
98}
99
100/// Register a handler for a host-provided field matching rule, keyed by the catalogue entry key
101/// (e.g. `"v2-type"` for the `core/matcher/v2-type` entry). Replaces any handler previously
102/// registered under the same key.
103pub fn register_core_field_matcher(key: &str, handler: Arc<dyn CoreFieldMatcher>) {
104  CORE_FIELD_MATCHERS.lock()
105    .expect("CORE_FIELD_MATCHERS mutex poisoned")
106    .insert(key.to_string(), handler);
107}
108
109/// Register a handler for a host-provided field generator, keyed by the catalogue entry key
110/// (e.g. `"v3-date"` for the `core/generator/v3-date` entry). Replaces any handler previously
111/// registered under the same key.
112pub fn register_core_field_generator(key: &str, handler: Arc<dyn CoreFieldGenerator>) {
113  CORE_FIELD_GENERATORS.lock()
114    .expect("CORE_FIELD_GENERATORS mutex poisoned")
115    .insert(key.to_string(), handler);
116}
117
118/// Look up a registered core field matcher handler by catalogue entry key.
119pub fn lookup_core_field_matcher(key: &str) -> Option<Arc<dyn CoreFieldMatcher>> {
120  CORE_FIELD_MATCHERS.lock()
121    .expect("CORE_FIELD_MATCHERS mutex poisoned")
122    .get(key).cloned()
123}
124
125/// Look up a registered core field generator handler by catalogue entry key.
126pub fn lookup_core_field_generator(key: &str) -> Option<Arc<dyn CoreFieldGenerator>> {
127  CORE_FIELD_GENERATORS.lock()
128    .expect("CORE_FIELD_GENERATORS mutex poisoned")
129    .get(key).cloned()
130}
131
132/// Remove a registered core field matcher handler. Mainly useful for tests.
133pub fn deregister_core_field_matcher(key: &str) {
134  CORE_FIELD_MATCHERS.lock()
135    .expect("CORE_FIELD_MATCHERS mutex poisoned")
136    .remove(key);
137}
138
139/// Remove a registered core field generator handler. Mainly useful for tests.
140pub fn deregister_core_field_generator(key: &str) {
141  CORE_FIELD_GENERATORS.lock()
142    .expect("CORE_FIELD_GENERATORS mutex poisoned")
143    .remove(key);
144}
145
146/// Remove a registered core content matcher handler. Mainly useful for tests.
147pub fn deregister_core_content_matcher(key: &str) {
148  CORE_CONTENT_MATCHERS.lock()
149    .expect("CORE_CONTENT_MATCHERS mutex poisoned")
150    .remove(key);
151}
152
153/// Remove a registered core content generator handler. Mainly useful for tests.
154pub fn deregister_core_content_generator(key: &str) {
155  CORE_CONTENT_GENERATORS.lock()
156    .expect("CORE_CONTENT_GENERATORS mutex poisoned")
157    .remove(key);
158}
159
160#[cfg(test)]
161mod tests {
162  use expectest::prelude::*;
163
164  use crate::proto::{CompareContentsRequest, CompareContentsResponse, GenerateContentRequest, GenerateContentResponse};
165  use crate::proto_v2::{GenerateFieldRequest, GenerateFieldResponse, MatchFieldRequest, MatchFieldResponse};
166
167  use super::*;
168
169  #[derive(Debug)]
170  struct TestMatcher;
171
172  #[async_trait]
173  impl CoreContentMatcher for TestMatcher {
174    async fn compare_contents(&self, _request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse> {
175      Ok(CompareContentsResponse::default())
176    }
177  }
178
179  #[derive(Debug)]
180  struct TestGenerator;
181
182  #[async_trait]
183  impl CoreContentGenerator for TestGenerator {
184    async fn generate_content(&self, _request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse> {
185      Ok(GenerateContentResponse::default())
186    }
187  }
188
189  #[test_log::test]
190  fn returns_none_for_an_unregistered_key() {
191    expect!(lookup_core_content_matcher("unregistered-matcher-key").is_none()).to(be_true());
192    expect!(lookup_core_content_generator("unregistered-generator-key").is_none()).to(be_true());
193  }
194
195  #[test_log::test(tokio::test)]
196  async fn registers_and_looks_up_a_content_matcher() {
197    register_core_content_matcher("test-matcher-key", Arc::new(TestMatcher));
198
199    let handler = lookup_core_content_matcher("test-matcher-key");
200    deregister_core_content_matcher("test-matcher-key");
201
202    expect!(handler.is_some()).to(be_true());
203    let response = handler.unwrap().compare_contents(CompareContentsRequest::default()).await;
204    expect!(response.is_ok()).to(be_true());
205  }
206
207  #[test_log::test(tokio::test)]
208  async fn registers_and_looks_up_a_content_generator() {
209    register_core_content_generator("test-generator-key", Arc::new(TestGenerator));
210
211    let handler = lookup_core_content_generator("test-generator-key");
212    deregister_core_content_generator("test-generator-key");
213
214    expect!(handler.is_some()).to(be_true());
215    let response = handler.unwrap().generate_content(GenerateContentRequest::default()).await;
216    expect!(response.is_ok()).to(be_true());
217  }
218
219  #[test_log::test]
220  fn deregister_is_a_no_op_for_an_unknown_key() {
221    deregister_core_content_matcher("never-registered");
222    deregister_core_content_generator("never-registered");
223    deregister_core_field_matcher("never-registered");
224    deregister_core_field_generator("never-registered");
225  }
226
227  #[derive(Debug)]
228  struct TestFieldMatcher;
229
230  #[async_trait]
231  impl CoreFieldMatcher for TestFieldMatcher {
232    async fn match_field(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
233      Ok(MatchFieldResponse { error: request.key, .. MatchFieldResponse::default() })
234    }
235  }
236
237  #[derive(Debug)]
238  struct TestFieldGenerator;
239
240  #[async_trait]
241  impl CoreFieldGenerator for TestFieldGenerator {
242    async fn generate_field(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse> {
243      Ok(GenerateFieldResponse { error: request.key, .. GenerateFieldResponse::default() })
244    }
245  }
246
247  #[test_log::test]
248  fn returns_none_for_an_unregistered_field_key() {
249    expect!(lookup_core_field_matcher("unregistered-field-matcher-key").is_none()).to(be_true());
250    expect!(lookup_core_field_generator("unregistered-field-generator-key").is_none()).to(be_true());
251  }
252
253  #[test_log::test(tokio::test)]
254  async fn registers_and_looks_up_a_field_matcher() {
255    register_core_field_matcher("test-field-matcher-key", Arc::new(TestFieldMatcher));
256
257    let handler = lookup_core_field_matcher("test-field-matcher-key");
258    deregister_core_field_matcher("test-field-matcher-key");
259
260    expect!(handler.is_some()).to(be_true());
261    let response = handler.unwrap()
262      .match_field(MatchFieldRequest { key: "v2-type".to_string(), .. MatchFieldRequest::default() })
263      .await;
264    // The stub echoes the request key back, so this also proves the request reached the handler
265    expect!(response.unwrap().error).to(be_equal_to("v2-type".to_string()));
266  }
267
268  #[test_log::test(tokio::test)]
269  async fn registers_and_looks_up_a_field_generator() {
270    register_core_field_generator("test-field-generator-key", Arc::new(TestFieldGenerator));
271
272    let handler = lookup_core_field_generator("test-field-generator-key");
273    deregister_core_field_generator("test-field-generator-key");
274
275    expect!(handler.is_some()).to(be_true());
276    let response = handler.unwrap()
277      .generate_field(GenerateFieldRequest { key: "v3-date".to_string(), .. GenerateFieldRequest::default() })
278      .await;
279    expect!(response.unwrap().error).to(be_equal_to("v3-date".to_string()));
280  }
281}