use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use anyhow::anyhow;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::trace;
use crate::child_process::ChildPluginProcess;
use crate::proto::*;
use crate::proto_v2;
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
pub enum PluginDependencyType {
OSPackage,
Plugin,
Library,
Executable,
}
impl Default for PluginDependencyType {
fn default() -> Self {
PluginDependencyType::Plugin
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
#[serde(rename_all = "camelCase")]
pub struct PluginDependency {
pub name: String,
pub version: Option<String>,
#[serde(default)]
pub dependency_type: PluginDependencyType,
}
impl Display for PluginDependency {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(version) = &self.version {
write!(f, "{}:{}", self.name, version)
} else {
write!(f, "{}:*", self.name)
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PactPluginManifest {
#[serde(skip)]
pub plugin_dir: String,
pub plugin_interface_version: u8,
pub name: String,
pub version: String,
pub executable_type: String,
pub minimum_required_version: Option<String>,
pub entry_point: String,
#[serde(default)]
pub entry_points: HashMap<String, String>,
pub args: Option<Vec<String>>,
pub dependencies: Option<Vec<PluginDependency>>,
#[serde(default)]
pub plugin_config: HashMap<String, Value>,
}
impl PactPluginManifest {
pub fn as_dependency(&self) -> PluginDependency {
PluginDependency {
name: self.name.clone(),
version: Some(self.version.clone()),
dependency_type: PluginDependencyType::Plugin,
}
}
}
impl Default for PactPluginManifest {
fn default() -> Self {
PactPluginManifest {
plugin_dir: "".to_string(),
plugin_interface_version: 1,
name: "".to_string(),
version: "".to_string(),
executable_type: "".to_string(),
minimum_required_version: None,
entry_point: "".to_string(),
entry_points: Default::default(),
args: None,
dependencies: None,
plugin_config: Default::default(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PluginInterfaceVersion {
V1,
V2,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginInitRequest {
pub implementation: String,
pub version: String,
pub host_capabilities: Vec<String>,
pub plugin_instance_id: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PluginInitResponse {
pub catalogue: Vec<CatalogueEntry>,
pub plugin_capabilities: Vec<String>,
}
impl TryFrom<u8> for PluginInterfaceVersion {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(PluginInterfaceVersion::V1),
2 => Ok(PluginInterfaceVersion::V2),
_ => Err(anyhow!("Unsupported plugin interface version {}", value)),
}
}
}
#[async_trait]
pub trait PactPluginRpc {
async fn init_plugin(&mut self, request: PluginInitRequest)
-> anyhow::Result<PluginInitResponse>;
}
#[async_trait]
pub trait PluginInstance: std::fmt::Debug + Send + Sync {
fn manifest(&self) -> &PactPluginManifest;
fn instance_id(&self) -> &str;
fn has_capability(&self, capability: &str) -> bool;
fn kill(&self) {}
async fn compare_contents(
&self,
request: CompareContentsRequest,
) -> anyhow::Result<CompareContentsResponse>;
async fn configure_interaction(
&self,
request: ConfigureInteractionRequest,
) -> anyhow::Result<ConfigureInteractionResponse>;
async fn generate_content(
&self,
request: GenerateContentRequest,
) -> anyhow::Result<GenerateContentResponse>;
async fn start_mock_server(
&self,
request: StartMockServerRequest,
) -> anyhow::Result<StartMockServerResponse>;
async fn start_mock_server_v2(
&self,
request: proto_v2::StartMockServerRequest,
) -> anyhow::Result<StartMockServerResponse> {
let _ = request;
Err(anyhow!("V2 interface not supported by this plugin"))
}
async fn shutdown_mock_server(
&self,
request: ShutdownMockServerRequest,
) -> anyhow::Result<ShutdownMockServerResponse>;
async fn get_mock_server_results(
&self,
request: MockServerRequest,
) -> anyhow::Result<MockServerResults>;
async fn prepare_interaction_for_verification(
&self,
request: VerificationPreparationRequest,
) -> anyhow::Result<VerificationPreparationResponse>;
async fn prepare_interaction_for_verification_v2(
&self,
request: proto_v2::VerificationPreparationRequest,
) -> anyhow::Result<VerificationPreparationResponse> {
let _ = request;
Err(anyhow!("V2 interface not supported by this plugin"))
}
async fn verify_interaction(
&self,
request: VerifyInteractionRequest,
) -> anyhow::Result<VerifyInteractionResponse>;
async fn verify_interaction_v2(
&self,
request: proto_v2::VerifyInteractionRequest,
) -> anyhow::Result<VerifyInteractionResponse> {
let _ = request;
Err(anyhow!("V2 interface not supported by this plugin"))
}
async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()>;
}
#[derive(Debug, Clone)]
pub struct PactPlugin {
pub manifest: PactPluginManifest,
pub interface_version: PluginInterfaceVersion,
#[deprecated(
note = "Not all plugin types have a child process; use PluginInstance methods for plugin lifecycle"
)]
pub child: Arc<ChildPluginProcess>,
pub plugin_capabilities: Vec<String>,
pub instance_id: String,
access_count: Arc<AtomicUsize>,
}
impl PactPlugin {
#[allow(deprecated)]
pub fn new(manifest: &PactPluginManifest, child: ChildPluginProcess) -> anyhow::Result<Self> {
let instance_id = child.instance_id.clone();
Ok(PactPlugin {
manifest: manifest.clone(),
interface_version: PluginInterfaceVersion::try_from(manifest.plugin_interface_version)?,
instance_id,
child: Arc::new(child),
plugin_capabilities: vec![],
access_count: Arc::new(AtomicUsize::new(1)),
})
}
pub fn has_plugin_capability(&self, capability: &str) -> bool {
self.plugin_capabilities.iter().any(|value| value == capability)
}
pub fn has_capability(&self, capability: &str) -> bool {
self.plugin_capabilities.iter().any(|c| c == capability)
}
pub fn instance_id(&self) -> &str {
&self.instance_id
}
#[deprecated(note = "Port is specific to gRPC plugins; access it via GrpcPactPlugin")]
#[allow(deprecated)]
pub fn port(&self) -> u16 {
self.child.port()
}
#[deprecated(note = "Use PluginInstance::kill() instead")]
#[allow(deprecated)]
pub fn kill(&self) {
self.child.kill();
}
pub fn update_access(&self) {
let count = self.access_count.fetch_add(1, Ordering::SeqCst);
trace!(
"update_access: Plugin {}/{} access is now {}",
self.manifest.name,
self.manifest.version,
count + 1
);
}
pub fn drop_access(&self) -> usize {
let check = self
.access_count
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
if count > 0 { Some(count - 1) } else { None }
});
let count = if let Ok(v) = check {
if v > 0 { v - 1 } else { v }
} else {
0
};
trace!(
"drop_access: Plugin {}/{} access is now {}",
self.manifest.name, self.manifest.version, count
);
count
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PluginInteractionConfig {
pub pact_configuration: HashMap<String, Value>,
pub interaction_configuration: HashMap<String, Value>,
}
#[cfg(test)]
pub(crate) mod tests {
use std::sync::RwLock;
use async_trait::async_trait;
use crate::plugin_models::{PactPluginManifest, PluginInitRequest, PluginInitResponse, PluginInstance};
use crate::proto::verification_preparation_response::Response;
use crate::proto::*;
pub(crate) struct MockPlugin {
pub manifest: PactPluginManifest,
pub prepare_request: RwLock<VerificationPreparationRequest>,
pub verify_request: RwLock<VerifyInteractionRequest>,
}
impl std::fmt::Debug for MockPlugin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MockPlugin")
.field("manifest", &self.manifest)
.finish()
}
}
impl Default for MockPlugin {
fn default() -> Self {
MockPlugin {
manifest: PactPluginManifest::default(),
prepare_request: RwLock::new(VerificationPreparationRequest::default()),
verify_request: RwLock::new(VerifyInteractionRequest::default()),
}
}
}
#[async_trait]
impl PluginInstance for MockPlugin {
fn manifest(&self) -> &PactPluginManifest {
&self.manifest
}
fn instance_id(&self) -> &str {
"test-instance"
}
fn has_capability(&self, _capability: &str) -> bool {
false
}
async fn compare_contents(
&self,
_request: CompareContentsRequest,
) -> anyhow::Result<CompareContentsResponse> {
unimplemented!()
}
async fn configure_interaction(
&self,
_request: ConfigureInteractionRequest,
) -> anyhow::Result<ConfigureInteractionResponse> {
unimplemented!()
}
async fn generate_content(
&self,
_request: GenerateContentRequest,
) -> anyhow::Result<GenerateContentResponse> {
unimplemented!()
}
async fn start_mock_server(
&self,
_request: StartMockServerRequest,
) -> anyhow::Result<StartMockServerResponse> {
unimplemented!()
}
async fn shutdown_mock_server(
&self,
_request: ShutdownMockServerRequest,
) -> anyhow::Result<ShutdownMockServerResponse> {
unimplemented!()
}
async fn get_mock_server_results(
&self,
_request: MockServerRequest,
) -> anyhow::Result<MockServerResults> {
unimplemented!()
}
async fn prepare_interaction_for_verification(
&self,
request: VerificationPreparationRequest,
) -> anyhow::Result<VerificationPreparationResponse> {
let mut w = self.prepare_request.write().unwrap();
*w = request;
let data = InteractionData {
body: None,
metadata: Default::default(),
};
Ok(VerificationPreparationResponse {
response: Some(Response::InteractionData(data)),
})
}
async fn verify_interaction(
&self,
request: VerifyInteractionRequest,
) -> anyhow::Result<VerifyInteractionResponse> {
let mut w = self.verify_request.write().unwrap();
*w = request;
let result = VerificationResult {
success: false,
response_data: None,
mismatches: vec![],
output: vec![],
};
Ok(VerifyInteractionResponse {
response: Some(verify_interaction_response::Response::Result(result)),
})
}
async fn update_catalogue(&self, _request: Catalogue) -> anyhow::Result<()> {
unimplemented!()
}
}
pub(crate) struct FailingInitPlugin {
pub error: String,
}
#[async_trait]
impl crate::plugin_models::PactPluginRpc for FailingInitPlugin {
async fn init_plugin(
&mut self,
_request: PluginInitRequest,
) -> anyhow::Result<PluginInitResponse> {
Err(anyhow::anyhow!("{}", self.error))
}
}
pub(crate) struct InitRecordingPlugin {
pub request: RwLock<Option<PluginInitRequest>>,
}
impl Default for InitRecordingPlugin {
fn default() -> Self {
Self {
request: RwLock::new(None),
}
}
}
#[async_trait]
impl crate::plugin_models::PactPluginRpc for InitRecordingPlugin {
async fn init_plugin(
&mut self,
request: PluginInitRequest,
) -> anyhow::Result<PluginInitResponse> {
*self.request.write().unwrap() = Some(request);
Ok(PluginInitResponse {
catalogue: vec![],
plugin_capabilities: vec!["interaction/request-response".to_string()],
})
}
}
}