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};
21
22/// A host-provided handler for the `CompareContents` capability shape. Implemented by the
23/// embedding Pact framework and registered via [`register_core_content_matcher`].
24#[async_trait]
25pub trait CoreContentMatcher: Send + Sync {
26  /// Compare the actual contents against the expected contents, returning any mismatches.
27  async fn compare_contents(&self, request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse>;
28}
29
30/// A host-provided handler for the `GenerateContent` capability shape. Implemented by the
31/// embedding Pact framework and registered via [`register_core_content_generator`].
32#[async_trait]
33pub trait CoreContentGenerator: Send + Sync {
34  /// Generate contents using the provided generators.
35  async fn generate_content(&self, request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse>;
36}
37
38lazy_static! {
39  static ref CORE_CONTENT_MATCHERS: Mutex<HashMap<String, Arc<dyn CoreContentMatcher>>> = Mutex::new(HashMap::new());
40  static ref CORE_CONTENT_GENERATORS: Mutex<HashMap<String, Arc<dyn CoreContentGenerator>>> = Mutex::new(HashMap::new());
41}
42
43/// Register a handler for a host-provided content matcher capability, keyed by the catalogue
44/// entry key (e.g. `"xml"` for the `core/content-matcher/xml` entry). Replaces any handler
45/// previously registered under the same key.
46pub fn register_core_content_matcher(key: &str, handler: Arc<dyn CoreContentMatcher>) {
47  CORE_CONTENT_MATCHERS.lock()
48    .expect("CORE_CONTENT_MATCHERS mutex poisoned")
49    .insert(key.to_string(), handler);
50}
51
52/// Register a handler for a host-provided content generator capability, keyed by the catalogue
53/// entry key (e.g. `"xml"` for the `core/content-generator/xml` entry). Replaces any handler
54/// previously registered under the same key.
55pub fn register_core_content_generator(key: &str, handler: Arc<dyn CoreContentGenerator>) {
56  CORE_CONTENT_GENERATORS.lock()
57    .expect("CORE_CONTENT_GENERATORS mutex poisoned")
58    .insert(key.to_string(), handler);
59}
60
61/// Look up a registered core content matcher handler by catalogue entry key.
62pub fn lookup_core_content_matcher(key: &str) -> Option<Arc<dyn CoreContentMatcher>> {
63  CORE_CONTENT_MATCHERS.lock()
64    .expect("CORE_CONTENT_MATCHERS mutex poisoned")
65    .get(key).cloned()
66}
67
68/// Look up a registered core content generator handler by catalogue entry key.
69pub fn lookup_core_content_generator(key: &str) -> Option<Arc<dyn CoreContentGenerator>> {
70  CORE_CONTENT_GENERATORS.lock()
71    .expect("CORE_CONTENT_GENERATORS mutex poisoned")
72    .get(key).cloned()
73}
74
75/// Remove a registered core content matcher handler. Mainly useful for tests.
76pub fn deregister_core_content_matcher(key: &str) {
77  CORE_CONTENT_MATCHERS.lock()
78    .expect("CORE_CONTENT_MATCHERS mutex poisoned")
79    .remove(key);
80}
81
82/// Remove a registered core content generator handler. Mainly useful for tests.
83pub fn deregister_core_content_generator(key: &str) {
84  CORE_CONTENT_GENERATORS.lock()
85    .expect("CORE_CONTENT_GENERATORS mutex poisoned")
86    .remove(key);
87}
88
89#[cfg(test)]
90mod tests {
91  use expectest::prelude::*;
92
93  use crate::proto::{CompareContentsRequest, CompareContentsResponse, GenerateContentRequest, GenerateContentResponse};
94
95  use super::*;
96
97  #[derive(Debug)]
98  struct TestMatcher;
99
100  #[async_trait]
101  impl CoreContentMatcher for TestMatcher {
102    async fn compare_contents(&self, _request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse> {
103      Ok(CompareContentsResponse::default())
104    }
105  }
106
107  #[derive(Debug)]
108  struct TestGenerator;
109
110  #[async_trait]
111  impl CoreContentGenerator for TestGenerator {
112    async fn generate_content(&self, _request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse> {
113      Ok(GenerateContentResponse::default())
114    }
115  }
116
117  #[test_log::test]
118  fn returns_none_for_an_unregistered_key() {
119    expect!(lookup_core_content_matcher("unregistered-matcher-key").is_none()).to(be_true());
120    expect!(lookup_core_content_generator("unregistered-generator-key").is_none()).to(be_true());
121  }
122
123  #[test_log::test(tokio::test)]
124  async fn registers_and_looks_up_a_content_matcher() {
125    register_core_content_matcher("test-matcher-key", Arc::new(TestMatcher));
126
127    let handler = lookup_core_content_matcher("test-matcher-key");
128    deregister_core_content_matcher("test-matcher-key");
129
130    expect!(handler.is_some()).to(be_true());
131    let response = handler.unwrap().compare_contents(CompareContentsRequest::default()).await;
132    expect!(response.is_ok()).to(be_true());
133  }
134
135  #[test_log::test(tokio::test)]
136  async fn registers_and_looks_up_a_content_generator() {
137    register_core_content_generator("test-generator-key", Arc::new(TestGenerator));
138
139    let handler = lookup_core_content_generator("test-generator-key");
140    deregister_core_content_generator("test-generator-key");
141
142    expect!(handler.is_some()).to(be_true());
143    let response = handler.unwrap().generate_content(GenerateContentRequest::default()).await;
144    expect!(response.is_ok()).to(be_true());
145  }
146
147  #[test_log::test]
148  fn deregister_is_a_no_op_for_an_unknown_key() {
149    deregister_core_content_matcher("never-registered");
150    deregister_core_content_generator("never-registered");
151  }
152}