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 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 async fn configure_interaction(
212 &self,
213 request: ConfigureInteractionRequest,
214 ) -> anyhow::Result<ConfigureInteractionResponse>;
215
216 async fn generate_content(
218 &self,
219 request: GenerateContentRequest,
220 ) -> anyhow::Result<GenerateContentResponse>;
221
222 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 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 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 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 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 async fn start_mock_server(
283 &self,
284 request: StartMockServerRequest,
285 ) -> anyhow::Result<StartMockServerResponse>;
286
287 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 async fn shutdown_mock_server(
298 &self,
299 request: ShutdownMockServerRequest,
300 ) -> anyhow::Result<ShutdownMockServerResponse>;
301
302 async fn get_mock_server_results(
304 &self,
305 request: MockServerRequest,
306 ) -> anyhow::Result<MockServerResults>;
307
308 async fn prepare_interaction_for_verification(
310 &self,
311 request: VerificationPreparationRequest,
312 ) -> anyhow::Result<VerificationPreparationResponse>;
313
314 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 async fn verify_interaction(
325 &self,
326 request: VerifyInteractionRequest,
327 ) -> anyhow::Result<VerifyInteractionResponse>;
328
329 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 async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()>;
340}
341
342#[derive(Debug, Clone)]
344pub struct PactPlugin {
345 pub manifest: PactPluginManifest,
347
348 pub interface_version: PluginInterfaceVersion,
350
351 #[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 pub plugin_capabilities: Vec<String>,
362
363 pub instance_id: String,
365
366 access_count: Arc<AtomicUsize>,
368}
369
370impl PactPlugin {
371 #[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 pub fn has_capability(&self, capability: &str) -> bool {
391 self.plugin_capabilities.iter().any(|c| c == capability)
392 }
393
394 pub fn instance_id(&self) -> &str {
396 &self.instance_id
397 }
398
399 #[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 #[deprecated(note = "Use PluginInstance::kill() instead")]
412 #[allow(deprecated)]
413 pub fn kill(&self) {
414 self.child.kill();
415 }
416
417 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 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#[derive(Clone, Debug, PartialEq)]
450pub struct PluginInteractionConfig {
451 pub pact_configuration: HashMap<String, Value>,
453 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}