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 tonic::{Request, Status};
13use tonic::codegen::InterceptedService;
14use tonic::metadata::{Ascii, MetadataValue};
15use tonic::service::Interceptor;
16use tonic::transport::Channel;
17use tracing::{debug, trace};
18
19use crate::child_process::ChildPluginProcess;
20use crate::proto::*;
21use crate::proto::pact_plugin_client::PactPluginClient;
22
23/// Type of plugin dependencies
24#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
25pub enum PluginDependencyType {
26  /// Required operating system package
27  OSPackage,
28  /// Dependency on another plugin
29  Plugin,
30  /// Dependency on a shared library
31  Library,
32  /// Dependency on an executable
33  Executable
34}
35
36impl Default for PluginDependencyType {
37  fn default() -> Self {
38    PluginDependencyType::Plugin
39  }
40}
41
42/// Plugin dependency
43#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
44#[serde(rename_all = "camelCase")]
45pub struct PluginDependency {
46  /// Dependency name
47  pub name: String,
48  /// Dependency version (semver format)
49  pub version: Option<String>,
50  /// Type of dependency
51  #[serde(default)]
52  pub dependency_type: PluginDependencyType
53}
54
55impl Display for PluginDependency {
56  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
57    if let Some(version) = &self.version {
58      write!(f, "{}:{}", self.name, version)
59    } else {
60      write!(f, "{}:*", self.name)
61    }
62  }
63}
64
65/// Manifest of a plugin
66#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
67#[serde(rename_all = "camelCase")]
68pub struct PactPluginManifest {
69  /// Directory were the plugin was loaded from
70  #[serde(skip)]
71  pub plugin_dir: String,
72
73  /// Interface version supported by the plugin
74  pub plugin_interface_version: u8,
75
76  /// Plugin name
77  pub name: String,
78
79  /// Plugin version in semver format
80  pub version: String,
81
82  /// Type if executable of the plugin
83  pub executable_type: String,
84
85  /// Minimum required version for the executable type
86  pub minimum_required_version: Option<String>,
87
88  /// How to invoke the plugin
89  pub entry_point: String,
90
91  /// Additional entry points for other operating systems (i.e. requiring a .bat file for Windows)
92  #[serde(default)]
93  pub entry_points: HashMap<String, String>,
94
95  /// Parameters to pass into the command line
96  pub args: Option<Vec<String>>,
97
98  /// Dependencies required to invoke the plugin
99  pub dependencies: Option<Vec<PluginDependency>>,
100
101  /// Plugin specific config
102  #[serde(default)]
103  pub plugin_config: HashMap<String, Value>
104}
105
106impl PactPluginManifest {
107  pub fn as_dependency(&self) -> PluginDependency {
108    PluginDependency {
109      name: self.name.clone(),
110      version: Some(self.version.clone()),
111      dependency_type: PluginDependencyType::Plugin
112    }
113  }
114}
115
116impl Default for PactPluginManifest {
117  fn default() -> Self {
118    PactPluginManifest {
119      plugin_dir: "".to_string(),
120      plugin_interface_version: 1,
121      name: "".to_string(),
122      version: "".to_string(),
123      executable_type: "".to_string(),
124      minimum_required_version: None,
125      entry_point: "".to_string(),
126      entry_points: Default::default(),
127      args: None,
128      dependencies: None,
129      plugin_config: Default::default()
130    }
131  }
132}
133
134/// Trait with remote-calling methods for a running plugin
135#[async_trait]
136pub trait PactPluginRpc {
137  /// Send an init request to the plugin process
138  async fn init_plugin(&mut self, request: InitPluginRequest) -> anyhow::Result<InitPluginResponse>;
139
140  /// Send a compare contents request to the plugin process
141  async fn compare_contents(&self, request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse>;
142
143  /// Send a configure contents request to the plugin process
144  async fn configure_interaction(&self, request: ConfigureInteractionRequest) -> anyhow::Result<ConfigureInteractionResponse>;
145
146  /// Send a generate content request to the plugin
147  async fn generate_content(&self, request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse>;
148
149  /// Start a mock server
150  async fn start_mock_server(&self, request: StartMockServerRequest) -> anyhow::Result<StartMockServerResponse>;
151
152  /// Shutdown a running mock server
153  async fn shutdown_mock_server(&self, request: ShutdownMockServerRequest) -> anyhow::Result<ShutdownMockServerResponse>;
154
155  /// Get the matching results from a running mock server
156  async fn get_mock_server_results(&self, request: MockServerRequest) -> anyhow::Result<MockServerResults>;
157
158  /// Prepare an interaction for verification. This should return any data required to construct any request
159  /// so that it can be amended before the verification is run.
160  async fn prepare_interaction_for_verification(&self, request: VerificationPreparationRequest) -> anyhow::Result<VerificationPreparationResponse>;
161
162  /// Execute the verification for the interaction.
163  async fn verify_interaction(&self, request: VerifyInteractionRequest) -> anyhow::Result<VerifyInteractionResponse>;
164
165  /// Updates the catalogue. This will be sent when the core catalogue has been updated (probably by a plugin loading).
166  async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()>;
167}
168
169/// Running plugin details
170#[derive(Debug, Clone)]
171pub struct PactPlugin {
172  /// Manifest for this plugin
173  pub manifest: PactPluginManifest,
174
175  /// Running child process
176  pub child: Arc<ChildPluginProcess>,
177
178  /// Count of access to the plugin. If this is ever zero, the plugin process will be shutdown
179  access_count: Arc<AtomicUsize>
180}
181
182#[async_trait]
183impl PactPluginRpc for PactPlugin {
184  /// Send an init request to the plugin process
185  async fn init_plugin(&mut self, request: InitPluginRequest) -> anyhow::Result<InitPluginResponse> {
186    let mut client = self.get_plugin_client().await?;
187    let response = client.init_plugin(Request::new(request)).await?;
188    Ok(response.get_ref().clone())
189  }
190
191  /// Send a compare contents request to the plugin process
192  async fn compare_contents(&self, request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse> {
193    let mut client = self.get_plugin_client().await?;
194    let response = client.compare_contents(tonic::Request::new(request)).await?;
195    Ok(response.get_ref().clone())
196  }
197
198  /// Send a configure contents request to the plugin process
199  async fn configure_interaction(&self, request: ConfigureInteractionRequest) -> anyhow::Result<ConfigureInteractionResponse> {
200    let mut client = self.get_plugin_client().await?;
201    let response = client.configure_interaction(tonic::Request::new(request)).await?;
202    Ok(response.get_ref().clone())
203  }
204
205  /// Send a generate content request to the plugin
206  async fn generate_content(&self, request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse> {
207    let mut client = self.get_plugin_client().await?;
208    let response = client.generate_content(tonic::Request::new(request)).await?;
209    Ok(response.get_ref().clone())
210  }
211
212  async fn start_mock_server(&self, request: StartMockServerRequest) -> anyhow::Result<StartMockServerResponse> {
213    let mut client = self.get_plugin_client().await?;
214    let response = client.start_mock_server(tonic::Request::new(request)).await?;
215    Ok(response.get_ref().clone())
216  }
217
218  async fn shutdown_mock_server(&self, request: ShutdownMockServerRequest) -> anyhow::Result<ShutdownMockServerResponse> {
219    let mut client = self.get_plugin_client().await?;
220    let response = client.shutdown_mock_server(tonic::Request::new(request)).await?;
221    Ok(response.get_ref().clone())
222  }
223
224  async fn get_mock_server_results(&self, request: MockServerRequest) -> anyhow::Result<MockServerResults> {
225    let mut client = self.get_plugin_client().await?;
226    let response = client.get_mock_server_results(tonic::Request::new(request)).await?;
227    Ok(response.get_ref().clone())
228  }
229
230  async fn prepare_interaction_for_verification(&self, request: VerificationPreparationRequest) -> anyhow::Result<VerificationPreparationResponse> {
231    let mut client = self.get_plugin_client().await?;
232    let response = client.prepare_interaction_for_verification(tonic::Request::new(request)).await?;
233    Ok(response.get_ref().clone())
234  }
235
236  async fn verify_interaction(&self, request: VerifyInteractionRequest) -> anyhow::Result<VerifyInteractionResponse> {
237    let mut client = self.get_plugin_client().await?;
238    let response = client.verify_interaction(tonic::Request::new(request)).await?;
239    Ok(response.get_ref().clone())
240  }
241
242  async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()> {
243    let mut client = self.get_plugin_client().await?;
244    client.update_catalogue(tonic::Request::new(request)).await?;
245    Ok(())
246  }
247}
248
249impl PactPlugin {
250  /// Create a new Plugin
251  pub fn new(manifest: &PactPluginManifest, child: ChildPluginProcess) -> Self {
252    PactPlugin {
253      manifest: manifest.clone(),
254      child: Arc::new(child),
255      access_count: Arc::new(AtomicUsize::new(1))
256    }
257  }
258
259  /// Port the plugin is running on
260  pub fn port(&self) -> u16 {
261    self.child.port()
262  }
263
264  /// Kill the running plugin process
265  pub fn kill(&self) {
266    self.child.kill();
267  }
268
269  /// Update the access of the plugin
270  pub fn update_access(&mut self) {
271    let count = self.access_count.fetch_add(1, Ordering::SeqCst);
272    trace!("update_access: Plugin {}/{} access is now {}", self.manifest.name,
273      self.manifest.version, count + 1);
274  }
275
276  /// Decrement and return the access count for the plugin
277  pub fn drop_access(&mut self) -> usize {
278    let check = self.access_count.fetch_update(Ordering::SeqCst,
279      Ordering::SeqCst, |count| {
280        if count > 0 {
281          Some(count - 1)
282        } else {
283          None
284        }
285      });
286    let count = if let Ok(v) = check {
287      if v > 0 { v - 1 } else { v }
288    } else {
289      0
290    };
291    trace!("drop_access: Plugin {}/{} access is now {}", self.manifest.name, self.manifest.version,
292      count);
293    count
294  }
295
296  async fn connect_channel(&self) -> anyhow::Result<Channel> {
297    let port = self.child.port();
298    match Channel::from_shared(format!("http://[::1]:{}", port))?.connect().await {
299      Ok(channel) => Ok(channel),
300      Err(err) => {
301        debug!("IP6 connection failed, will try IP4 address - {err}");
302        Channel::from_shared(format!("http://127.0.0.1:{}", port))?.connect().await
303          .map_err(|err| anyhow!(err))
304      }
305    }
306  }
307
308  async fn get_plugin_client(&self) -> anyhow::Result<PactPluginClient<InterceptedService<Channel, PactPluginInterceptor>>> {
309    let channel = self.connect_channel().await?;
310    let interceptor = PactPluginInterceptor::new(self.child.plugin_info.server_key.as_str())?;
311    Ok(PactPluginClient::with_interceptor(channel, interceptor))
312  }
313}
314
315/// Interceptor to inject the server key as an authorisation header
316#[derive(Clone, Debug)]
317struct PactPluginInterceptor {
318  /// Server key to inject
319  server_key: MetadataValue<Ascii>
320}
321
322impl PactPluginInterceptor {
323  fn new(server_key: &str) -> anyhow::Result<Self> {
324    let token = MetadataValue::try_from(server_key)?;
325    Ok(PactPluginInterceptor {
326      server_key: token
327    })
328  }
329}
330
331impl Interceptor for PactPluginInterceptor {
332  fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
333    request.metadata_mut().insert("authorization", self.server_key.clone());
334    Ok(request)
335  }
336}
337
338/// Plugin configuration to add to the matching context for an interaction
339#[derive(Clone, Debug, PartialEq)]
340pub struct PluginInteractionConfig {
341  /// Global plugin config (Pact level)
342  pub pact_configuration: HashMap<String, Value>,
343  /// Interaction plugin config
344  pub interaction_configuration: HashMap<String, Value>
345}
346
347#[cfg(test)]
348pub(crate) mod tests {
349  use std::sync::RwLock;
350
351  use async_trait::async_trait;
352
353  use crate::plugin_models::PactPluginRpc;
354  use crate::proto::*;
355  use crate::proto::verification_preparation_response::Response;
356
357  pub(crate) struct MockPlugin {
358    pub prepare_request: RwLock<VerificationPreparationRequest>,
359    pub verify_request: RwLock<VerifyInteractionRequest>
360  }
361
362  impl Default for MockPlugin {
363    fn default() -> Self {
364      MockPlugin {
365        prepare_request: RwLock::new(VerificationPreparationRequest::default()),
366        verify_request: RwLock::new(VerifyInteractionRequest::default())
367      }
368    }
369  }
370
371  #[async_trait]
372  impl PactPluginRpc for MockPlugin {
373    async fn init_plugin(&mut self, _request: InitPluginRequest) -> anyhow::Result<InitPluginResponse> {
374      unimplemented!()
375    }
376
377    async fn compare_contents(&self, _request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse> {
378      unimplemented!()
379    }
380
381    async fn configure_interaction(&self, _request: ConfigureInteractionRequest) -> anyhow::Result<ConfigureInteractionResponse> {
382      unimplemented!()
383    }
384
385    async fn generate_content(&self, _request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse> {
386      unimplemented!()
387    }
388
389    async fn start_mock_server(&self, _request: StartMockServerRequest) -> anyhow::Result<StartMockServerResponse> {
390      unimplemented!()
391    }
392
393    async fn shutdown_mock_server(&self, _request: ShutdownMockServerRequest) -> anyhow::Result<ShutdownMockServerResponse> {
394      unimplemented!()
395    }
396
397    async fn get_mock_server_results(&self, _request: MockServerRequest) -> anyhow::Result<MockServerResults> {
398      unimplemented!()
399    }
400
401    async fn prepare_interaction_for_verification(&self, request: VerificationPreparationRequest) -> anyhow::Result<VerificationPreparationResponse> {
402      let mut w = self.prepare_request.write().unwrap();
403      *w = request;
404      let data = InteractionData {
405        body: None,
406        metadata: Default::default()
407      };
408      Ok(VerificationPreparationResponse {
409        response: Some(Response::InteractionData(data))
410      })
411    }
412
413    async fn verify_interaction(&self, request: VerifyInteractionRequest) -> anyhow::Result<VerifyInteractionResponse> {
414      let mut w = self.verify_request.write().unwrap();
415      *w = request;
416      let result = VerificationResult {
417        success: false,
418        response_data: None,
419        mismatches: vec![],
420        output: vec![]
421      };
422      Ok(VerifyInteractionResponse {
423        response: Some(verify_interaction_response::Response::Result(result))
424      })
425    }
426
427    async fn update_catalogue(&self, _request: Catalogue) -> anyhow::Result<()> {
428      unimplemented!()
429    }
430  }
431}