use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use anyhow::anyhow;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tracing::{debug, trace};
use crate::child_process::ChildPluginProcess;
use crate::proto::*;
use crate::proto::pact_plugin_client::PactPluginClient;
#[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()
}
}
}
#[async_trait]
pub trait PactPluginRpc {
async fn init_plugin(&self, request: InitPluginRequest) -> anyhow::Result<InitPluginResponse>;
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 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 verify_interaction(&self, request: VerifyInteractionRequest) -> anyhow::Result<VerifyInteractionResponse>;
async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()>;
}
#[derive(Debug, Clone)]
pub struct PactPlugin {
pub manifest: PactPluginManifest,
pub child: Arc<ChildPluginProcess>,
access_count: usize
}
#[async_trait]
impl PactPluginRpc for PactPlugin {
async fn init_plugin(&self, request: InitPluginRequest) -> anyhow::Result<InitPluginResponse> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.init_plugin(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn compare_contents(&self, request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.compare_contents(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn configure_interaction(&self, request: ConfigureInteractionRequest) -> anyhow::Result<ConfigureInteractionResponse> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.configure_interaction(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn generate_content(&self, request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.generate_content(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn start_mock_server(&self, request: StartMockServerRequest) -> anyhow::Result<StartMockServerResponse> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.start_mock_server(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn shutdown_mock_server(&self, request: ShutdownMockServerRequest) -> anyhow::Result<ShutdownMockServerResponse> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.shutdown_mock_server(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn get_mock_server_results(&self, request: MockServerRequest) -> anyhow::Result<MockServerResults> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.get_mock_server_results(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn prepare_interaction_for_verification(&self, request: VerificationPreparationRequest) -> anyhow::Result<VerificationPreparationResponse> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.prepare_interaction_for_verification(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn verify_interaction(&self, request: VerifyInteractionRequest) -> anyhow::Result<VerifyInteractionResponse> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let response = client.verify_interaction(tonic::Request::new(request)).await?;
Ok(response.get_ref().clone())
}
async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()> {
let channel = self.connect_channel().await?;
let auth_str = self.child.plugin_info.server_key.as_str();
let token = MetadataValue::try_from(auth_str)?;
let mut client = PactPluginClient::with_interceptor(channel, move |mut req: tonic::Request<_>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
client.update_catalogue(tonic::Request::new(request)).await?;
Ok(())
}
}
impl PactPlugin {
pub fn new(manifest: &PactPluginManifest, child: ChildPluginProcess) -> Self {
PactPlugin { manifest: manifest.clone(), child: Arc::new(child), access_count: 1 }
}
pub fn port(&self) -> u16 {
self.child.port()
}
pub fn kill(&self) {
self.child.kill();
}
pub fn update_access(&mut self) {
self.access_count += 1;
trace!("update_access: Plugin {}/{} access is now {}", self.manifest.name, self.manifest.version,
self.access_count);
}
pub fn drop_access(&mut self) -> usize {
if self.access_count > 0 {
self.access_count -= 1;
}
trace!("drop_access: Plugin {}/{} access is now {}", self.manifest.name, self.manifest.version,
self.access_count);
self.access_count
}
async fn connect_channel(&self) -> anyhow::Result<Channel> {
let port = self.child.port();
match Channel::from_shared(format!("http://[::1]:{}", port))?.connect().await {
Ok(channel) => Ok(channel),
Err(err) => {
debug!("IP6 connection failed, will try IP4 address - {err}");
Channel::from_shared(format!("http://127.0.0.1:{}", port))?.connect().await
.map_err(|err| anyhow!(err))
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PluginInteractionConfig {
pub pact_configuration: HashMap<String, Value>,
pub interaction_configuration: HashMap<String, Value>
}