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 pact_models::v4::V4InteractionType;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use tracing::trace;
14
15use crate::child_process::ChildPluginProcess;
16use crate::proto::*;
17use crate::proto_v2;
18
19#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
21pub enum PluginDependencyType {
22 OSPackage,
24 Plugin,
26 Library,
28 Executable,
30}
31
32impl Default for PluginDependencyType {
33 fn default() -> Self {
34 PluginDependencyType::Plugin
35 }
36}
37
38#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
40#[serde(rename_all = "camelCase")]
41pub struct PluginDependency {
42 pub name: String,
44 pub version: Option<String>,
46 #[serde(default)]
48 pub dependency_type: PluginDependencyType,
49}
50
51impl Display for PluginDependency {
52 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53 if let Some(version) = &self.version {
54 write!(f, "{}:{}", self.name, version)
55 } else {
56 write!(f, "{}:*", self.name)
57 }
58 }
59}
60
61#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
63#[serde(rename_all = "camelCase")]
64pub struct PactPluginManifest {
65 #[serde(skip)]
67 pub plugin_dir: String,
68
69 pub plugin_interface_version: u8,
71
72 pub name: String,
74
75 pub version: String,
77
78 pub executable_type: String,
80
81 pub minimum_required_version: Option<String>,
83
84 pub entry_point: String,
86
87 #[serde(default)]
89 pub entry_points: HashMap<String, String>,
90
91 pub args: Option<Vec<String>>,
93
94 pub dependencies: Option<Vec<PluginDependency>>,
96
97 #[serde(default)]
99 pub plugin_config: HashMap<String, Value>,
100}
101
102impl PactPluginManifest {
103 pub fn as_dependency(&self) -> PluginDependency {
104 PluginDependency {
105 name: self.name.clone(),
106 version: Some(self.version.clone()),
107 dependency_type: PluginDependencyType::Plugin,
108 }
109 }
110}
111
112impl Default for PactPluginManifest {
113 fn default() -> Self {
114 PactPluginManifest {
115 plugin_dir: "".to_string(),
116 plugin_interface_version: 1,
117 name: "".to_string(),
118 version: "".to_string(),
119 executable_type: "".to_string(),
120 minimum_required_version: None,
121 entry_point: "".to_string(),
122 entry_points: Default::default(),
123 args: None,
124 dependencies: None,
125 plugin_config: Default::default(),
126 }
127 }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum PluginInterfaceVersion {
132 V1,
133 V2,
134}
135
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct PluginInitRequest {
138 pub implementation: String,
139 pub version: String,
140 pub host_capabilities: Vec<String>,
141 pub plugin_instance_id: String,
142}
143
144#[derive(Clone, Debug, PartialEq)]
145pub struct PluginInitResponse {
146 pub catalogue: Vec<CatalogueEntry>,
147 pub plugin_capabilities: Vec<String>,
148}
149
150impl TryFrom<u8> for PluginInterfaceVersion {
151 type Error = anyhow::Error;
152
153 fn try_from(value: u8) -> Result<Self, Self::Error> {
154 match value {
155 1 => Ok(PluginInterfaceVersion::V1),
156 2 => Ok(PluginInterfaceVersion::V2),
157 _ => Err(anyhow!("Unsupported plugin interface version {}", value)),
158 }
159 }
160}
161
162pub fn interaction_type_capability(interaction_type: &V4InteractionType) -> &'static str {
169 match interaction_type {
170 V4InteractionType::Synchronous_HTTP => "interaction/request-response",
171 V4InteractionType::Asynchronous_Messages => "interaction/message",
172 V4InteractionType::Synchronous_Messages => "interaction/synchronous-message",
173 }
174}
175
176pub const ALL_INTERACTION_TYPES: [V4InteractionType; 3] = [
178 V4InteractionType::Synchronous_HTTP,
179 V4InteractionType::Asynchronous_Messages,
180 V4InteractionType::Synchronous_Messages,
181];
182
183pub fn check_interaction_type_capability(
194 plugin: &dyn PluginInstance,
195 interaction_type: V4InteractionType,
196) -> anyhow::Result<()> {
197 let declares_any = ALL_INTERACTION_TYPES
198 .iter()
199 .any(|interaction_type| plugin.has_capability(interaction_type_capability(interaction_type)));
200 if !declares_any {
201 return Ok(());
202 }
203
204 let required = interaction_type_capability(&interaction_type);
205 if plugin.has_capability(required) {
206 Ok(())
207 } else {
208 let manifest = plugin.manifest();
209 Err(anyhow!(
210 "Plugin {}/{} does not support {} interactions - it did not declare the '{}' capability",
211 manifest.name,
212 manifest.version,
213 interaction_type,
214 required
215 ))
216 }
217}
218
219#[async_trait]
221pub trait PactPluginRpc {
222 async fn init_plugin(&mut self, request: PluginInitRequest)
224 -> anyhow::Result<PluginInitResponse>;
225}
226
227#[async_trait]
232pub trait PluginInstance: std::fmt::Debug + Send + Sync {
233 fn manifest(&self) -> &PactPluginManifest;
235
236 fn instance_id(&self) -> &str;
238
239 fn has_capability(&self, capability: &str) -> bool;
241
242 fn kill(&self) {}
245
246 async fn compare_contents(
248 &self,
249 request: CompareContentsRequest,
250 ) -> anyhow::Result<CompareContentsResponse>;
251
252 async fn compare_contents_with_chain(
259 &self,
260 request: CompareContentsRequest,
261 chain_id: &str,
262 deadline_ms: u64,
263 ) -> anyhow::Result<CompareContentsResponse> {
264 let _ = (chain_id, deadline_ms);
265 self.compare_contents(request).await
266 }
267
268 async fn configure_interaction(
270 &self,
271 request: ConfigureInteractionRequest,
272 ) -> anyhow::Result<ConfigureInteractionResponse>;
273
274 async fn generate_content(
276 &self,
277 request: GenerateContentRequest,
278 ) -> anyhow::Result<GenerateContentResponse>;
279
280 async fn generate_content_with_chain(
284 &self,
285 request: GenerateContentRequest,
286 chain_id: &str,
287 deadline_ms: u64,
288 ) -> anyhow::Result<GenerateContentResponse> {
289 let _ = (chain_id, deadline_ms);
290 self.generate_content(request).await
291 }
292
293 async fn match_field(
297 &self,
298 request: proto_v2::MatchFieldRequest,
299 ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
300 let _ = request;
301 Err(anyhow!("Field-level matching rules are not supported by this plugin"))
302 }
303
304 async fn match_field_with_chain(
308 &self,
309 request: proto_v2::MatchFieldRequest,
310 chain_id: &str,
311 deadline_ms: u64,
312 ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
313 let _ = (chain_id, deadline_ms);
314 self.match_field(request).await
315 }
316
317 async fn generate_field(
320 &self,
321 request: proto_v2::GenerateFieldRequest,
322 ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
323 let _ = request;
324 Err(anyhow!("Field-level generators are not supported by this plugin"))
325 }
326
327 async fn generate_field_with_chain(
330 &self,
331 request: proto_v2::GenerateFieldRequest,
332 chain_id: &str,
333 deadline_ms: u64,
334 ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
335 let _ = (chain_id, deadline_ms);
336 self.generate_field(request).await
337 }
338
339 async fn start_mock_server(
341 &self,
342 request: StartMockServerRequest,
343 ) -> anyhow::Result<StartMockServerResponse>;
344
345 async fn start_mock_server_v2(
347 &self,
348 request: proto_v2::StartMockServerRequest,
349 ) -> anyhow::Result<StartMockServerResponse> {
350 let _ = request;
351 Err(anyhow!("V2 interface not supported by this plugin"))
352 }
353
354 async fn shutdown_mock_server(
356 &self,
357 request: ShutdownMockServerRequest,
358 ) -> anyhow::Result<ShutdownMockServerResponse>;
359
360 async fn get_mock_server_results(
362 &self,
363 request: MockServerRequest,
364 ) -> anyhow::Result<MockServerResults>;
365
366 async fn prepare_interaction_for_verification(
368 &self,
369 request: VerificationPreparationRequest,
370 ) -> anyhow::Result<VerificationPreparationResponse>;
371
372 async fn prepare_interaction_for_verification_v2(
374 &self,
375 request: proto_v2::VerificationPreparationRequest,
376 ) -> anyhow::Result<VerificationPreparationResponse> {
377 let _ = request;
378 Err(anyhow!("V2 interface not supported by this plugin"))
379 }
380
381 async fn verify_interaction(
383 &self,
384 request: VerifyInteractionRequest,
385 ) -> anyhow::Result<VerifyInteractionResponse>;
386
387 async fn verify_interaction_v2(
389 &self,
390 request: proto_v2::VerifyInteractionRequest,
391 ) -> anyhow::Result<VerifyInteractionResponse> {
392 let _ = request;
393 Err(anyhow!("V2 interface not supported by this plugin"))
394 }
395
396 async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()>;
398}
399
400#[derive(Debug, Clone)]
402pub struct PactPlugin {
403 pub manifest: PactPluginManifest,
405
406 pub interface_version: PluginInterfaceVersion,
408
409 #[deprecated(
414 note = "Not all plugin types have a child process; use PluginInstance methods for plugin lifecycle"
415 )]
416 pub child: Arc<ChildPluginProcess>,
417
418 pub plugin_capabilities: Vec<String>,
420
421 pub instance_id: String,
423
424 access_count: Arc<AtomicUsize>,
426}
427
428impl PactPlugin {
429 #[allow(deprecated)]
431 pub fn new(manifest: &PactPluginManifest, child: ChildPluginProcess) -> anyhow::Result<Self> {
432 let instance_id = child.instance_id.clone();
433 Ok(PactPlugin {
434 manifest: manifest.clone(),
435 interface_version: PluginInterfaceVersion::try_from(manifest.plugin_interface_version)?,
436 instance_id,
437 child: Arc::new(child),
438 plugin_capabilities: vec![],
439 access_count: Arc::new(AtomicUsize::new(1)),
440 })
441 }
442
443 pub fn has_plugin_capability(&self, capability: &str) -> bool {
444 self.plugin_capabilities.iter().any(|value| value == capability)
445 }
446
447 pub fn has_capability(&self, capability: &str) -> bool {
449 self.plugin_capabilities.iter().any(|c| c == capability)
450 }
451
452 pub fn instance_id(&self) -> &str {
454 &self.instance_id
455 }
456
457 #[deprecated(note = "Port is specific to gRPC plugins; access it via GrpcPactPlugin")]
461 #[allow(deprecated)]
462 pub fn port(&self) -> u16 {
463 self.child.port()
464 }
465
466 #[deprecated(note = "Use PluginInstance::kill() instead")]
470 #[allow(deprecated)]
471 pub fn kill(&self) {
472 self.child.kill();
473 }
474
475 pub fn update_access(&self) {
477 let count = self.access_count.fetch_add(1, Ordering::SeqCst);
478 trace!(
479 "update_access: Plugin {}/{} access is now {}",
480 self.manifest.name,
481 self.manifest.version,
482 count + 1
483 );
484 }
485
486 pub fn drop_access(&self) -> usize {
488 let check = self
489 .access_count
490 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
491 if count > 0 { Some(count - 1) } else { None }
492 });
493 let count = if let Ok(v) = check {
494 if v > 0 { v - 1 } else { v }
495 } else {
496 0
497 };
498 trace!(
499 "drop_access: Plugin {}/{} access is now {}",
500 self.manifest.name, self.manifest.version, count
501 );
502 count
503 }
504}
505
506#[derive(Clone, Debug, PartialEq)]
508pub struct PluginInteractionConfig {
509 pub pact_configuration: HashMap<String, Value>,
511 pub interaction_configuration: HashMap<String, Value>,
513}
514
515#[cfg(test)]
516pub(crate) mod tests {
517 use std::sync::RwLock;
518
519 use async_trait::async_trait;
520 use expectest::prelude::*;
521 use pact_models::v4::V4InteractionType;
522
523 use crate::plugin_models::{
524 ALL_INTERACTION_TYPES, PactPluginManifest, PluginInitRequest, PluginInitResponse,
525 PluginInstance, check_interaction_type_capability, interaction_type_capability,
526 };
527 use crate::proto::verification_preparation_response::Response;
528 use crate::proto::*;
529 use crate::proto_v2;
530
531 pub(crate) struct MockPlugin {
532 pub manifest: PactPluginManifest,
533 pub capabilities: Vec<String>,
534 pub prepare_request: RwLock<VerificationPreparationRequest>,
535 pub verify_request: RwLock<VerifyInteractionRequest>,
536 pub prepare_request_v2: RwLock<Option<proto_v2::VerificationPreparationRequest>>,
537 pub verify_request_v2: RwLock<Option<proto_v2::VerifyInteractionRequest>>,
538 }
539
540 impl std::fmt::Debug for MockPlugin {
541 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542 f.debug_struct("MockPlugin")
543 .field("manifest", &self.manifest)
544 .finish()
545 }
546 }
547
548 impl Default for MockPlugin {
549 fn default() -> Self {
550 MockPlugin {
551 manifest: PactPluginManifest::default(),
552 capabilities: vec![],
553 prepare_request: RwLock::new(VerificationPreparationRequest::default()),
554 verify_request: RwLock::new(VerifyInteractionRequest::default()),
555 prepare_request_v2: RwLock::new(None),
556 verify_request_v2: RwLock::new(None),
557 }
558 }
559 }
560
561 #[async_trait]
562 impl PluginInstance for MockPlugin {
563 fn manifest(&self) -> &PactPluginManifest {
564 &self.manifest
565 }
566
567 fn instance_id(&self) -> &str {
568 "test-instance"
569 }
570
571 fn has_capability(&self, capability: &str) -> bool {
572 self.capabilities.iter().any(|c| c == capability)
573 }
574
575 async fn compare_contents(
576 &self,
577 _request: CompareContentsRequest,
578 ) -> anyhow::Result<CompareContentsResponse> {
579 unimplemented!()
580 }
581
582 async fn configure_interaction(
583 &self,
584 _request: ConfigureInteractionRequest,
585 ) -> anyhow::Result<ConfigureInteractionResponse> {
586 unimplemented!()
587 }
588
589 async fn generate_content(
590 &self,
591 _request: GenerateContentRequest,
592 ) -> anyhow::Result<GenerateContentResponse> {
593 unimplemented!()
594 }
595
596 async fn start_mock_server(
597 &self,
598 _request: StartMockServerRequest,
599 ) -> anyhow::Result<StartMockServerResponse> {
600 unimplemented!()
601 }
602
603 async fn shutdown_mock_server(
604 &self,
605 _request: ShutdownMockServerRequest,
606 ) -> anyhow::Result<ShutdownMockServerResponse> {
607 unimplemented!()
608 }
609
610 async fn get_mock_server_results(
611 &self,
612 _request: MockServerRequest,
613 ) -> anyhow::Result<MockServerResults> {
614 unimplemented!()
615 }
616
617 async fn prepare_interaction_for_verification(
618 &self,
619 request: VerificationPreparationRequest,
620 ) -> anyhow::Result<VerificationPreparationResponse> {
621 let mut w = self.prepare_request.write().unwrap();
622 *w = request;
623 let data = InteractionData {
624 body: None,
625 metadata: Default::default(),
626 };
627 Ok(VerificationPreparationResponse {
628 response: Some(Response::InteractionData(data)),
629 })
630 }
631
632 async fn prepare_interaction_for_verification_v2(
633 &self,
634 request: proto_v2::VerificationPreparationRequest,
635 ) -> anyhow::Result<VerificationPreparationResponse> {
636 let mut w = self.prepare_request_v2.write().unwrap();
637 *w = Some(request);
638 let data = InteractionData {
639 body: None,
640 metadata: Default::default(),
641 };
642 Ok(VerificationPreparationResponse {
643 response: Some(Response::InteractionData(data)),
644 })
645 }
646
647 async fn verify_interaction(
648 &self,
649 request: VerifyInteractionRequest,
650 ) -> anyhow::Result<VerifyInteractionResponse> {
651 let mut w = self.verify_request.write().unwrap();
652 *w = request;
653 let result = VerificationResult {
654 success: false,
655 response_data: None,
656 mismatches: vec![],
657 output: vec![],
658 };
659 Ok(VerifyInteractionResponse {
660 response: Some(verify_interaction_response::Response::Result(result)),
661 })
662 }
663
664 async fn verify_interaction_v2(
665 &self,
666 request: proto_v2::VerifyInteractionRequest,
667 ) -> anyhow::Result<VerifyInteractionResponse> {
668 let mut w = self.verify_request_v2.write().unwrap();
669 *w = Some(request);
670 let result = VerificationResult {
671 success: false,
672 response_data: None,
673 mismatches: vec![],
674 output: vec![],
675 };
676 Ok(VerifyInteractionResponse {
677 response: Some(verify_interaction_response::Response::Result(result)),
678 })
679 }
680
681 async fn update_catalogue(&self, _request: Catalogue) -> anyhow::Result<()> {
682 unimplemented!()
683 }
684 }
685
686 pub(crate) struct FailingInitPlugin {
687 pub error: String,
688 }
689
690 #[async_trait]
691 impl crate::plugin_models::PactPluginRpc for FailingInitPlugin {
692 async fn init_plugin(
693 &mut self,
694 _request: PluginInitRequest,
695 ) -> anyhow::Result<PluginInitResponse> {
696 Err(anyhow::anyhow!("{}", self.error))
697 }
698 }
699
700 pub(crate) struct InitRecordingPlugin {
701 pub request: RwLock<Option<PluginInitRequest>>,
702 }
703
704 impl Default for InitRecordingPlugin {
705 fn default() -> Self {
706 Self {
707 request: RwLock::new(None),
708 }
709 }
710 }
711
712 #[async_trait]
713 impl crate::plugin_models::PactPluginRpc for InitRecordingPlugin {
714 async fn init_plugin(
715 &mut self,
716 request: PluginInitRequest,
717 ) -> anyhow::Result<PluginInitResponse> {
718 *self.request.write().unwrap() = Some(request);
719 Ok(PluginInitResponse {
720 catalogue: vec![],
721 plugin_capabilities: vec!["interaction/request-response".to_string()],
722 })
723 }
724 }
725
726 #[test]
730 fn interaction_type_capability_covers_every_interaction_type() {
731 let capabilities = ALL_INTERACTION_TYPES
732 .iter()
733 .map(|interaction_type| {
734 (
735 interaction_type.to_string(),
736 interaction_type_capability(interaction_type),
737 )
738 })
739 .collect::<Vec<_>>();
740
741 expect!(capabilities).to(be_equal_to(vec![
742 ("Synchronous/HTTP".to_string(), "interaction/request-response"),
743 ("Asynchronous/Messages".to_string(), "interaction/message"),
744 (
745 "Synchronous/Messages".to_string(),
746 "interaction/synchronous-message",
747 ),
748 ]));
749 }
750
751 #[test]
752 fn check_interaction_type_capability_allows_a_plugin_that_declared_none() {
753 let plugin = MockPlugin::default();
754 expect!(check_interaction_type_capability(
755 &plugin,
756 V4InteractionType::Synchronous_HTTP
757 ))
758 .to(be_ok());
759 }
760
761 #[test]
762 fn check_interaction_type_capability_rejects_an_undeclared_type() {
763 let plugin = MockPlugin {
764 capabilities: vec!["interaction/message".to_string()],
765 ..MockPlugin::default()
766 };
767
768 expect!(check_interaction_type_capability(
769 &plugin,
770 V4InteractionType::Asynchronous_Messages
771 ))
772 .to(be_ok());
773 expect!(
774 check_interaction_type_capability(&plugin, V4InteractionType::Synchronous_HTTP).is_err()
775 )
776 .to(be_true());
777 }
778}