1use 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#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
20pub enum PluginDependencyType {
21 OSPackage,
23 Plugin,
25 Library,
27 Executable,
29}
30
31impl Default for PluginDependencyType {
32 fn default() -> Self {
33 PluginDependencyType::Plugin
34 }
35}
36
37#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
39#[serde(rename_all = "camelCase")]
40pub struct PluginDependency {
41 pub name: String,
43 pub version: Option<String>,
45 #[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#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
62#[serde(rename_all = "camelCase")]
63pub struct PactPluginManifest {
64 #[serde(skip)]
66 pub plugin_dir: String,
67
68 pub plugin_interface_version: u8,
70
71 pub name: String,
73
74 pub version: String,
76
77 pub executable_type: String,
79
80 pub minimum_required_version: Option<String>,
82
83 pub entry_point: String,
85
86 #[serde(default)]
88 pub entry_points: HashMap<String, String>,
89
90 pub args: Option<Vec<String>>,
92
93 pub dependencies: Option<Vec<PluginDependency>>,
95
96 #[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#[async_trait]
163pub trait PactPluginRpc {
164 async fn init_plugin(&mut self, request: PluginInitRequest)
166 -> anyhow::Result<PluginInitResponse>;
167}
168
169#[async_trait]
174pub trait PluginInstance: std::fmt::Debug + Send + Sync {
175 fn manifest(&self) -> &PactPluginManifest;
177
178 fn instance_id(&self) -> &str;
180
181 fn has_capability(&self, capability: &str) -> bool;
183
184 fn kill(&self) {}
187
188 async fn compare_contents(
190 &self,
191 request: CompareContentsRequest,
192 ) -> anyhow::Result<CompareContentsResponse>;
193
194 async fn configure_interaction(
196 &self,
197 request: ConfigureInteractionRequest,
198 ) -> anyhow::Result<ConfigureInteractionResponse>;
199
200 async fn generate_content(
202 &self,
203 request: GenerateContentRequest,
204 ) -> anyhow::Result<GenerateContentResponse>;
205
206 async fn start_mock_server(
208 &self,
209 request: StartMockServerRequest,
210 ) -> anyhow::Result<StartMockServerResponse>;
211
212 async fn start_mock_server_v2(
214 &self,
215 request: proto_v2::StartMockServerRequest,
216 ) -> anyhow::Result<StartMockServerResponse> {
217 let _ = request;
218 Err(anyhow!("V2 interface not supported by this plugin"))
219 }
220
221 async fn shutdown_mock_server(
223 &self,
224 request: ShutdownMockServerRequest,
225 ) -> anyhow::Result<ShutdownMockServerResponse>;
226
227 async fn get_mock_server_results(
229 &self,
230 request: MockServerRequest,
231 ) -> anyhow::Result<MockServerResults>;
232
233 async fn prepare_interaction_for_verification(
235 &self,
236 request: VerificationPreparationRequest,
237 ) -> anyhow::Result<VerificationPreparationResponse>;
238
239 async fn prepare_interaction_for_verification_v2(
241 &self,
242 request: proto_v2::VerificationPreparationRequest,
243 ) -> anyhow::Result<VerificationPreparationResponse> {
244 let _ = request;
245 Err(anyhow!("V2 interface not supported by this plugin"))
246 }
247
248 async fn verify_interaction(
250 &self,
251 request: VerifyInteractionRequest,
252 ) -> anyhow::Result<VerifyInteractionResponse>;
253
254 async fn verify_interaction_v2(
256 &self,
257 request: proto_v2::VerifyInteractionRequest,
258 ) -> anyhow::Result<VerifyInteractionResponse> {
259 let _ = request;
260 Err(anyhow!("V2 interface not supported by this plugin"))
261 }
262
263 async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()>;
265}
266
267#[derive(Debug, Clone)]
269pub struct PactPlugin {
270 pub manifest: PactPluginManifest,
272
273 pub interface_version: PluginInterfaceVersion,
275
276 #[deprecated(
281 note = "Not all plugin types have a child process; use PluginInstance methods for plugin lifecycle"
282 )]
283 pub child: Arc<ChildPluginProcess>,
284
285 pub plugin_capabilities: Vec<String>,
287
288 pub instance_id: String,
290
291 access_count: Arc<AtomicUsize>,
293}
294
295impl PactPlugin {
296 #[allow(deprecated)]
298 pub fn new(manifest: &PactPluginManifest, child: ChildPluginProcess) -> anyhow::Result<Self> {
299 let instance_id = child.instance_id.clone();
300 Ok(PactPlugin {
301 manifest: manifest.clone(),
302 interface_version: PluginInterfaceVersion::try_from(manifest.plugin_interface_version)?,
303 instance_id,
304 child: Arc::new(child),
305 plugin_capabilities: vec![],
306 access_count: Arc::new(AtomicUsize::new(1)),
307 })
308 }
309
310 pub fn has_plugin_capability(&self, capability: &str) -> bool {
311 self.plugin_capabilities.iter().any(|value| value == capability)
312 }
313
314 pub fn has_capability(&self, capability: &str) -> bool {
316 self.plugin_capabilities.iter().any(|c| c == capability)
317 }
318
319 pub fn instance_id(&self) -> &str {
321 &self.instance_id
322 }
323
324 #[deprecated(note = "Port is specific to gRPC plugins; access it via GrpcPactPlugin")]
328 #[allow(deprecated)]
329 pub fn port(&self) -> u16 {
330 self.child.port()
331 }
332
333 #[deprecated(note = "Use PluginInstance::kill() instead")]
337 #[allow(deprecated)]
338 pub fn kill(&self) {
339 self.child.kill();
340 }
341
342 pub fn update_access(&self) {
344 let count = self.access_count.fetch_add(1, Ordering::SeqCst);
345 trace!(
346 "update_access: Plugin {}/{} access is now {}",
347 self.manifest.name,
348 self.manifest.version,
349 count + 1
350 );
351 }
352
353 pub fn drop_access(&self) -> usize {
355 let check = self
356 .access_count
357 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
358 if count > 0 { Some(count - 1) } else { None }
359 });
360 let count = if let Ok(v) = check {
361 if v > 0 { v - 1 } else { v }
362 } else {
363 0
364 };
365 trace!(
366 "drop_access: Plugin {}/{} access is now {}",
367 self.manifest.name, self.manifest.version, count
368 );
369 count
370 }
371}
372
373#[derive(Clone, Debug, PartialEq)]
375pub struct PluginInteractionConfig {
376 pub pact_configuration: HashMap<String, Value>,
378 pub interaction_configuration: HashMap<String, Value>,
380}
381
382#[cfg(test)]
383pub(crate) mod tests {
384 use std::sync::RwLock;
385
386 use async_trait::async_trait;
387
388 use crate::plugin_models::{PactPluginManifest, PluginInitRequest, PluginInitResponse, PluginInstance};
389 use crate::proto::verification_preparation_response::Response;
390 use crate::proto::*;
391
392 pub(crate) struct MockPlugin {
393 pub manifest: PactPluginManifest,
394 pub prepare_request: RwLock<VerificationPreparationRequest>,
395 pub verify_request: RwLock<VerifyInteractionRequest>,
396 }
397
398 impl std::fmt::Debug for MockPlugin {
399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400 f.debug_struct("MockPlugin")
401 .field("manifest", &self.manifest)
402 .finish()
403 }
404 }
405
406 impl Default for MockPlugin {
407 fn default() -> Self {
408 MockPlugin {
409 manifest: PactPluginManifest::default(),
410 prepare_request: RwLock::new(VerificationPreparationRequest::default()),
411 verify_request: RwLock::new(VerifyInteractionRequest::default()),
412 }
413 }
414 }
415
416 #[async_trait]
417 impl PluginInstance for MockPlugin {
418 fn manifest(&self) -> &PactPluginManifest {
419 &self.manifest
420 }
421
422 fn instance_id(&self) -> &str {
423 "test-instance"
424 }
425
426 fn has_capability(&self, _capability: &str) -> bool {
427 false
428 }
429
430 async fn compare_contents(
431 &self,
432 _request: CompareContentsRequest,
433 ) -> anyhow::Result<CompareContentsResponse> {
434 unimplemented!()
435 }
436
437 async fn configure_interaction(
438 &self,
439 _request: ConfigureInteractionRequest,
440 ) -> anyhow::Result<ConfigureInteractionResponse> {
441 unimplemented!()
442 }
443
444 async fn generate_content(
445 &self,
446 _request: GenerateContentRequest,
447 ) -> anyhow::Result<GenerateContentResponse> {
448 unimplemented!()
449 }
450
451 async fn start_mock_server(
452 &self,
453 _request: StartMockServerRequest,
454 ) -> anyhow::Result<StartMockServerResponse> {
455 unimplemented!()
456 }
457
458 async fn shutdown_mock_server(
459 &self,
460 _request: ShutdownMockServerRequest,
461 ) -> anyhow::Result<ShutdownMockServerResponse> {
462 unimplemented!()
463 }
464
465 async fn get_mock_server_results(
466 &self,
467 _request: MockServerRequest,
468 ) -> anyhow::Result<MockServerResults> {
469 unimplemented!()
470 }
471
472 async fn prepare_interaction_for_verification(
473 &self,
474 request: VerificationPreparationRequest,
475 ) -> anyhow::Result<VerificationPreparationResponse> {
476 let mut w = self.prepare_request.write().unwrap();
477 *w = request;
478 let data = InteractionData {
479 body: None,
480 metadata: Default::default(),
481 };
482 Ok(VerificationPreparationResponse {
483 response: Some(Response::InteractionData(data)),
484 })
485 }
486
487 async fn verify_interaction(
488 &self,
489 request: VerifyInteractionRequest,
490 ) -> anyhow::Result<VerifyInteractionResponse> {
491 let mut w = self.verify_request.write().unwrap();
492 *w = request;
493 let result = VerificationResult {
494 success: false,
495 response_data: None,
496 mismatches: vec![],
497 output: vec![],
498 };
499 Ok(VerifyInteractionResponse {
500 response: Some(verify_interaction_response::Response::Result(result)),
501 })
502 }
503
504 async fn update_catalogue(&self, _request: Catalogue) -> anyhow::Result<()> {
505 unimplemented!()
506 }
507 }
508
509 pub(crate) struct FailingInitPlugin {
510 pub error: String,
511 }
512
513 #[async_trait]
514 impl crate::plugin_models::PactPluginRpc for FailingInitPlugin {
515 async fn init_plugin(
516 &mut self,
517 _request: PluginInitRequest,
518 ) -> anyhow::Result<PluginInitResponse> {
519 Err(anyhow::anyhow!("{}", self.error))
520 }
521 }
522
523 pub(crate) struct InitRecordingPlugin {
524 pub request: RwLock<Option<PluginInitRequest>>,
525 }
526
527 impl Default for InitRecordingPlugin {
528 fn default() -> Self {
529 Self {
530 request: RwLock::new(None),
531 }
532 }
533 }
534
535 #[async_trait]
536 impl crate::plugin_models::PactPluginRpc for InitRecordingPlugin {
537 async fn init_plugin(
538 &mut self,
539 request: PluginInitRequest,
540 ) -> anyhow::Result<PluginInitResponse> {
541 *self.request.write().unwrap() = Some(request);
542 Ok(PluginInitResponse {
543 catalogue: vec![],
544 plugin_capabilities: vec!["interaction/request-response".to_string()],
545 })
546 }
547 }
548}