Skip to main content

pact_plugin_driver/
plugin_models.rs

1//! Models for representing plugins
2
3use std::collections::HashMap;
4use std::fmt::{Display, Formatter};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicUsize, Ordering};
7
8use anyhow::anyhow;
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use tracing::trace;
13
14use crate::child_process::ChildPluginProcess;
15use crate::proto::*;
16use crate::proto_v2;
17
18/// Type of plugin dependencies
19#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
20pub enum PluginDependencyType {
21  /// Required operating system package
22  OSPackage,
23  /// Dependency on another plugin
24  Plugin,
25  /// Dependency on a shared library
26  Library,
27  /// Dependency on an executable
28  Executable,
29}
30
31impl Default for PluginDependencyType {
32  fn default() -> Self {
33    PluginDependencyType::Plugin
34  }
35}
36
37/// Plugin dependency
38#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
39#[serde(rename_all = "camelCase")]
40pub struct PluginDependency {
41  /// Dependency name
42  pub name: String,
43  /// Dependency version (semver format)
44  pub version: Option<String>,
45  /// Type of dependency
46  #[serde(default)]
47  pub dependency_type: PluginDependencyType,
48}
49
50impl Display for PluginDependency {
51  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
52    if let Some(version) = &self.version {
53      write!(f, "{}:{}", self.name, version)
54    } else {
55      write!(f, "{}:*", self.name)
56    }
57  }
58}
59
60/// Manifest of a plugin
61#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
62#[serde(rename_all = "camelCase")]
63pub struct PactPluginManifest {
64  /// Directory were the plugin was loaded from
65  #[serde(skip)]
66  pub plugin_dir: String,
67
68  /// Interface version supported by the plugin
69  pub plugin_interface_version: u8,
70
71  /// Plugin name
72  pub name: String,
73
74  /// Plugin version in semver format
75  pub version: String,
76
77  /// Type if executable of the plugin
78  pub executable_type: String,
79
80  /// Minimum required version for the executable type
81  pub minimum_required_version: Option<String>,
82
83  /// How to invoke the plugin
84  pub entry_point: String,
85
86  /// Additional entry points for other operating systems (i.e. requiring a .bat file for Windows)
87  #[serde(default)]
88  pub entry_points: HashMap<String, String>,
89
90  /// Parameters to pass into the command line
91  pub args: Option<Vec<String>>,
92
93  /// Dependencies required to invoke the plugin
94  pub dependencies: Option<Vec<PluginDependency>>,
95
96  /// Plugin specific config
97  #[serde(default)]
98  pub plugin_config: HashMap<String, Value>,
99}
100
101impl PactPluginManifest {
102  pub fn as_dependency(&self) -> PluginDependency {
103    PluginDependency {
104      name: self.name.clone(),
105      version: Some(self.version.clone()),
106      dependency_type: PluginDependencyType::Plugin,
107    }
108  }
109}
110
111impl Default for PactPluginManifest {
112  fn default() -> Self {
113    PactPluginManifest {
114      plugin_dir: "".to_string(),
115      plugin_interface_version: 1,
116      name: "".to_string(),
117      version: "".to_string(),
118      executable_type: "".to_string(),
119      minimum_required_version: None,
120      entry_point: "".to_string(),
121      entry_points: Default::default(),
122      args: None,
123      dependencies: None,
124      plugin_config: Default::default(),
125    }
126  }
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum PluginInterfaceVersion {
131  V1,
132  V2,
133}
134
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub struct PluginInitRequest {
137  pub implementation: String,
138  pub version: String,
139  pub host_capabilities: Vec<String>,
140  pub plugin_instance_id: String,
141}
142
143#[derive(Clone, Debug, PartialEq)]
144pub struct PluginInitResponse {
145  pub catalogue: Vec<CatalogueEntry>,
146  pub plugin_capabilities: Vec<String>,
147}
148
149impl TryFrom<u8> for PluginInterfaceVersion {
150  type Error = anyhow::Error;
151
152  fn try_from(value: u8) -> Result<Self, Self::Error> {
153    match value {
154      1 => Ok(PluginInterfaceVersion::V1),
155      2 => Ok(PluginInterfaceVersion::V2),
156      _ => Err(anyhow!("Unsupported plugin interface version {}", value)),
157    }
158  }
159}
160
161/// Trait for the plugin init handshake only (used by anything that can handle the init message)
162#[async_trait]
163pub trait PactPluginRpc {
164  /// Send an init request to the plugin process
165  async fn init_plugin(&mut self, request: PluginInitRequest)
166    -> anyhow::Result<PluginInitResponse>;
167}
168
169/// Trait for a running plugin instance.
170///
171/// Implementations include [`crate::grpc_plugin::GrpcPactPlugin`] for exec-type plugins
172/// that communicate via gRPC, and future embedded runtimes (Lua, Python, …).
173#[async_trait]
174pub trait PluginInstance: std::fmt::Debug + Send + Sync {
175  /// Return the manifest for this plugin.
176  fn manifest(&self) -> &PactPluginManifest;
177
178  /// Return the instance ID assigned to this plugin at startup.
179  fn instance_id(&self) -> &str;
180
181  /// Check whether the plugin declared a specific capability.
182  fn has_capability(&self, capability: &str) -> bool;
183
184  /// Terminate the running plugin. The default no-op suits embedded runtimes
185  /// that are not managed as a child process.
186  fn kill(&self) {}
187
188  /// Send a compare contents request to the plugin process
189  async fn compare_contents(
190    &self,
191    request: CompareContentsRequest,
192  ) -> anyhow::Result<CompareContentsResponse>;
193
194  /// Send a compare contents request to the plugin process, propagating call-chain cycle
195  /// detection and deadline metadata (see [`crate::call_chain`]) for transports that support it.
196  /// The default implementation ignores `chain_id`/`deadline_ms` and delegates to
197  /// [`PluginInstance::compare_contents`], which suits in-process runtimes (Lua, WASM) where a
198  /// cycle is already caught by the native call stack; [`crate::grpc_plugin::GrpcPactPlugin`]
199  /// overrides this to send the metadata over gRPC.
200  async fn compare_contents_with_chain(
201    &self,
202    request: CompareContentsRequest,
203    chain_id: &str,
204    deadline_ms: u64,
205  ) -> anyhow::Result<CompareContentsResponse> {
206    let _ = (chain_id, deadline_ms);
207    self.compare_contents(request).await
208  }
209
210  /// Send a configure contents request to the plugin process
211  async fn configure_interaction(
212    &self,
213    request: ConfigureInteractionRequest,
214  ) -> anyhow::Result<ConfigureInteractionResponse>;
215
216  /// Send a generate content request to the plugin
217  async fn generate_content(
218    &self,
219    request: GenerateContentRequest,
220  ) -> anyhow::Result<GenerateContentResponse>;
221
222  /// Send a generate content request to the plugin, propagating call-chain cycle detection and
223  /// deadline metadata (see [`crate::call_chain`]) for transports that support it. See
224  /// [`PluginInstance::compare_contents_with_chain`] for the default/override split.
225  async fn generate_content_with_chain(
226    &self,
227    request: GenerateContentRequest,
228    chain_id: &str,
229    deadline_ms: u64,
230  ) -> anyhow::Result<GenerateContentResponse> {
231    let _ = (chain_id, deadline_ms);
232    self.generate_content(request).await
233  }
234
235  /// Apply a plugin-provided matching rule to a single value (see proposal 006). Field-level
236  /// operations exist only on the V2 interface, so the default reports that this plugin cannot
237  /// provide them - the same shape as the other V2-only operations below.
238  async fn match_field(
239    &self,
240    request: proto_v2::MatchFieldRequest,
241  ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
242    let _ = request;
243    Err(anyhow!("Field-level matching rules are not supported by this plugin"))
244  }
245
246  /// Apply a plugin-provided matching rule to a single value, propagating call-chain cycle
247  /// detection and deadline metadata (see [`crate::call_chain`]) for transports that support it.
248  /// See [`PluginInstance::compare_contents_with_chain`] for the default/override split.
249  async fn match_field_with_chain(
250    &self,
251    request: proto_v2::MatchFieldRequest,
252    chain_id: &str,
253    deadline_ms: u64,
254  ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
255    let _ = (chain_id, deadline_ms);
256    self.match_field(request).await
257  }
258
259  /// Apply a plugin-provided generator to a single value (see proposal 006). See
260  /// [`PluginInstance::match_field`].
261  async fn generate_field(
262    &self,
263    request: proto_v2::GenerateFieldRequest,
264  ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
265    let _ = request;
266    Err(anyhow!("Field-level generators are not supported by this plugin"))
267  }
268
269  /// Apply a plugin-provided generator to a single value, propagating call-chain cycle detection
270  /// and deadline metadata. See [`PluginInstance::match_field_with_chain`].
271  async fn generate_field_with_chain(
272    &self,
273    request: proto_v2::GenerateFieldRequest,
274    chain_id: &str,
275    deadline_ms: u64,
276  ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
277    let _ = (chain_id, deadline_ms);
278    self.generate_field(request).await
279  }
280
281  /// Start a mock server
282  async fn start_mock_server(
283    &self,
284    request: StartMockServerRequest,
285  ) -> anyhow::Result<StartMockServerResponse>;
286
287  /// Start a mock server using V2 structured interaction data (no pact JSON).
288  async fn start_mock_server_v2(
289    &self,
290    request: proto_v2::StartMockServerRequest,
291  ) -> anyhow::Result<StartMockServerResponse> {
292    let _ = request;
293    Err(anyhow!("V2 interface not supported by this plugin"))
294  }
295
296  /// Shutdown a running mock server
297  async fn shutdown_mock_server(
298    &self,
299    request: ShutdownMockServerRequest,
300  ) -> anyhow::Result<ShutdownMockServerResponse>;
301
302  /// Get the matching results from a running mock server
303  async fn get_mock_server_results(
304    &self,
305    request: MockServerRequest,
306  ) -> anyhow::Result<MockServerResults>;
307
308  /// Prepare an interaction for verification.
309  async fn prepare_interaction_for_verification(
310    &self,
311    request: VerificationPreparationRequest,
312  ) -> anyhow::Result<VerificationPreparationResponse>;
313
314  /// Prepare an interaction for verification using V2 structured interaction data.
315  async fn prepare_interaction_for_verification_v2(
316    &self,
317    request: proto_v2::VerificationPreparationRequest,
318  ) -> anyhow::Result<VerificationPreparationResponse> {
319    let _ = request;
320    Err(anyhow!("V2 interface not supported by this plugin"))
321  }
322
323  /// Execute the verification for the interaction.
324  async fn verify_interaction(
325    &self,
326    request: VerifyInteractionRequest,
327  ) -> anyhow::Result<VerifyInteractionResponse>;
328
329  /// Execute the verification for the interaction using V2 structured interaction data.
330  async fn verify_interaction_v2(
331    &self,
332    request: proto_v2::VerifyInteractionRequest,
333  ) -> anyhow::Result<VerifyInteractionResponse> {
334    let _ = request;
335    Err(anyhow!("V2 interface not supported by this plugin"))
336  }
337
338  /// Updates the catalogue.
339  async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()>;
340}
341
342/// Running plugin details
343#[derive(Debug, Clone)]
344pub struct PactPlugin {
345  /// Manifest for this plugin
346  pub manifest: PactPluginManifest,
347
348  /// Interface version supported by the plugin
349  pub interface_version: PluginInterfaceVersion,
350
351  /// Running child process.
352  ///
353  /// Deprecated: not all plugin types have a child process; this field is now
354  /// owned by the gRPC layer. Access plugin lifecycle through [`PluginInstance`] methods instead.
355  #[deprecated(
356    note = "Not all plugin types have a child process; use PluginInstance methods for plugin lifecycle"
357  )]
358  pub child: Arc<ChildPluginProcess>,
359
360  /// Optional capabilities negotiated for this plugin instance
361  pub plugin_capabilities: Vec<String>,
362
363  /// UUID assigned by the driver at process start; used to correlate log output from this instance
364  pub instance_id: String,
365
366  /// Count of access to the plugin. If this is ever zero, the plugin process will be shutdown
367  access_count: Arc<AtomicUsize>,
368}
369
370impl PactPlugin {
371  /// Create a new Plugin
372  #[allow(deprecated)]
373  pub fn new(manifest: &PactPluginManifest, child: ChildPluginProcess) -> anyhow::Result<Self> {
374    let instance_id = child.instance_id.clone();
375    Ok(PactPlugin {
376      manifest: manifest.clone(),
377      interface_version: PluginInterfaceVersion::try_from(manifest.plugin_interface_version)?,
378      instance_id,
379      child: Arc::new(child),
380      plugin_capabilities: vec![],
381      access_count: Arc::new(AtomicUsize::new(1)),
382    })
383  }
384
385  pub fn has_plugin_capability(&self, capability: &str) -> bool {
386    self.plugin_capabilities.iter().any(|value| value == capability)
387  }
388
389  /// Check if this plugin has the given capability
390  pub fn has_capability(&self, capability: &str) -> bool {
391    self.plugin_capabilities.iter().any(|c| c == capability)
392  }
393
394  /// Return the instance ID for this plugin
395  pub fn instance_id(&self) -> &str {
396    &self.instance_id
397  }
398
399  /// Port the plugin is running on.
400  ///
401  /// Deprecated: port is a gRPC-specific concept; use `GrpcPactPlugin` directly if you need it.
402  #[deprecated(note = "Port is specific to gRPC plugins; access it via GrpcPactPlugin")]
403  #[allow(deprecated)]
404  pub fn port(&self) -> u16 {
405    self.child.port()
406  }
407
408  /// Kill the running plugin process.
409  ///
410  /// Deprecated: use [`PluginInstance::kill`] instead so non-gRPC plugin types are handled correctly.
411  #[deprecated(note = "Use PluginInstance::kill() instead")]
412  #[allow(deprecated)]
413  pub fn kill(&self) {
414    self.child.kill();
415  }
416
417  /// Update the access count of the plugin
418  pub fn update_access(&self) {
419    let count = self.access_count.fetch_add(1, Ordering::SeqCst);
420    trace!(
421      "update_access: Plugin {}/{} access is now {}",
422      self.manifest.name,
423      self.manifest.version,
424      count + 1
425    );
426  }
427
428  /// Decrement and return the access count for the plugin
429  pub fn drop_access(&self) -> usize {
430    let check = self
431      .access_count
432      .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
433        if count > 0 { Some(count - 1) } else { None }
434      });
435    let count = if let Ok(v) = check {
436      if v > 0 { v - 1 } else { v }
437    } else {
438      0
439    };
440    trace!(
441      "drop_access: Plugin {}/{} access is now {}",
442      self.manifest.name, self.manifest.version, count
443    );
444    count
445  }
446}
447
448/// Plugin configuration to add to the matching context for an interaction
449#[derive(Clone, Debug, PartialEq)]
450pub struct PluginInteractionConfig {
451  /// Global plugin config (Pact level)
452  pub pact_configuration: HashMap<String, Value>,
453  /// Interaction plugin config
454  pub interaction_configuration: HashMap<String, Value>,
455}
456
457#[cfg(test)]
458pub(crate) mod tests {
459  use std::sync::RwLock;
460
461  use async_trait::async_trait;
462
463  use crate::plugin_models::{PactPluginManifest, PluginInitRequest, PluginInitResponse, PluginInstance};
464  use crate::proto::verification_preparation_response::Response;
465  use crate::proto::*;
466
467  pub(crate) struct MockPlugin {
468    pub manifest: PactPluginManifest,
469    pub prepare_request: RwLock<VerificationPreparationRequest>,
470    pub verify_request: RwLock<VerifyInteractionRequest>,
471  }
472
473  impl std::fmt::Debug for MockPlugin {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475      f.debug_struct("MockPlugin")
476        .field("manifest", &self.manifest)
477        .finish()
478    }
479  }
480
481  impl Default for MockPlugin {
482    fn default() -> Self {
483      MockPlugin {
484        manifest: PactPluginManifest::default(),
485        prepare_request: RwLock::new(VerificationPreparationRequest::default()),
486        verify_request: RwLock::new(VerifyInteractionRequest::default()),
487      }
488    }
489  }
490
491  #[async_trait]
492  impl PluginInstance for MockPlugin {
493    fn manifest(&self) -> &PactPluginManifest {
494      &self.manifest
495    }
496
497    fn instance_id(&self) -> &str {
498      "test-instance"
499    }
500
501    fn has_capability(&self, _capability: &str) -> bool {
502      false
503    }
504
505    async fn compare_contents(
506      &self,
507      _request: CompareContentsRequest,
508    ) -> anyhow::Result<CompareContentsResponse> {
509      unimplemented!()
510    }
511
512    async fn configure_interaction(
513      &self,
514      _request: ConfigureInteractionRequest,
515    ) -> anyhow::Result<ConfigureInteractionResponse> {
516      unimplemented!()
517    }
518
519    async fn generate_content(
520      &self,
521      _request: GenerateContentRequest,
522    ) -> anyhow::Result<GenerateContentResponse> {
523      unimplemented!()
524    }
525
526    async fn start_mock_server(
527      &self,
528      _request: StartMockServerRequest,
529    ) -> anyhow::Result<StartMockServerResponse> {
530      unimplemented!()
531    }
532
533    async fn shutdown_mock_server(
534      &self,
535      _request: ShutdownMockServerRequest,
536    ) -> anyhow::Result<ShutdownMockServerResponse> {
537      unimplemented!()
538    }
539
540    async fn get_mock_server_results(
541      &self,
542      _request: MockServerRequest,
543    ) -> anyhow::Result<MockServerResults> {
544      unimplemented!()
545    }
546
547    async fn prepare_interaction_for_verification(
548      &self,
549      request: VerificationPreparationRequest,
550    ) -> anyhow::Result<VerificationPreparationResponse> {
551      let mut w = self.prepare_request.write().unwrap();
552      *w = request;
553      let data = InteractionData {
554        body: None,
555        metadata: Default::default(),
556      };
557      Ok(VerificationPreparationResponse {
558        response: Some(Response::InteractionData(data)),
559      })
560    }
561
562    async fn verify_interaction(
563      &self,
564      request: VerifyInteractionRequest,
565    ) -> anyhow::Result<VerifyInteractionResponse> {
566      let mut w = self.verify_request.write().unwrap();
567      *w = request;
568      let result = VerificationResult {
569        success: false,
570        response_data: None,
571        mismatches: vec![],
572        output: vec![],
573      };
574      Ok(VerifyInteractionResponse {
575        response: Some(verify_interaction_response::Response::Result(result)),
576      })
577    }
578
579    async fn update_catalogue(&self, _request: Catalogue) -> anyhow::Result<()> {
580      unimplemented!()
581    }
582  }
583
584  pub(crate) struct FailingInitPlugin {
585    pub error: String,
586  }
587
588  #[async_trait]
589  impl crate::plugin_models::PactPluginRpc for FailingInitPlugin {
590    async fn init_plugin(
591      &mut self,
592      _request: PluginInitRequest,
593    ) -> anyhow::Result<PluginInitResponse> {
594      Err(anyhow::anyhow!("{}", self.error))
595    }
596  }
597
598  pub(crate) struct InitRecordingPlugin {
599    pub request: RwLock<Option<PluginInitRequest>>,
600  }
601
602  impl Default for InitRecordingPlugin {
603    fn default() -> Self {
604      Self {
605        request: RwLock::new(None),
606      }
607    }
608  }
609
610  #[async_trait]
611  impl crate::plugin_models::PactPluginRpc for InitRecordingPlugin {
612    async fn init_plugin(
613      &mut self,
614      request: PluginInitRequest,
615    ) -> anyhow::Result<PluginInitResponse> {
616      *self.request.write().unwrap() = Some(request);
617      Ok(PluginInitResponse {
618        catalogue: vec![],
619        plugin_capabilities: vec!["interaction/request-response".to_string()],
620      })
621    }
622  }
623}