use std::collections::HashMap;
use std::str::from_utf8;
use anyhow::anyhow;
use bytes::Bytes;
use maplit::hashmap;
use pact_models::bodies::OptionalBody;
use pact_models::content_types::ContentTypeHint;
use pact_models::matchingrules::{Category, MatchingRule, MatchingRuleCategory, RuleList};
use pact_models::path_exp::DocPath;
use pact_models::prelude::{ContentType, Generator, GeneratorCategory, Generators, RuleLogic};
use pact_models::plugins::PluginData;
use serde_json::Value;
use tracing::{debug, error};
use crate::catalogue_manager::{CatalogueEntry, CatalogueEntryProviderType};
use crate::plugin_manager::lookup_plugin;
use crate::plugin_models::{PactPluginManifest, PactPluginRpc, PluginInteractionConfig};
use crate::proto::{
Body,
CompareContentsRequest,
ConfigureInteractionRequest,
GenerateContentRequest,
PluginConfiguration as ProtoPluginConfiguration
};
use crate::proto::body;
use crate::proto::interaction_response::MarkupType;
use crate::utils::{proto_struct_to_json, proto_struct_to_map, to_proto_struct};
#[derive(Clone, Debug)]
pub struct ContentMatcher {
pub catalogue_entry: CatalogueEntry
}
impl ContentMatcher {
pub fn plugin(&self) -> Option<PactPluginManifest> {
self.catalogue_entry.plugin.clone()
}
}
#[derive(Clone, Debug)]
pub struct ContentMismatch {
pub expected: String,
pub actual: String,
pub mismatch: String,
pub path: String,
pub diff: Option<String>
}
#[derive(Clone, Debug)]
pub struct InteractionContents {
pub part_name: String,
pub body: OptionalBody,
pub rules: Option<MatchingRuleCategory>,
pub generators: Option<Generators>,
pub metadata: Option<HashMap<String, Value>>,
pub plugin_config: PluginConfiguration,
pub interaction_markup: String,
pub interaction_markup_type: String
}
impl Default for InteractionContents {
fn default() -> Self {
InteractionContents {
part_name: Default::default(),
body: Default::default(),
rules: None,
generators: None,
metadata: None,
plugin_config: Default::default(),
interaction_markup: Default::default(),
interaction_markup_type: Default::default()
}
}
}
#[derive(Clone, Debug)]
pub struct PluginConfiguration {
pub interaction_configuration: HashMap<String, Value>,
pub pact_configuration: HashMap<String, Value>
}
impl PluginConfiguration {
pub fn is_empty(&self) -> bool {
self.pact_configuration.is_empty() && self.interaction_configuration.is_empty()
}
}
impl Default for PluginConfiguration {
fn default() -> Self {
PluginConfiguration {
interaction_configuration: Default::default(),
pact_configuration: Default::default()
}
}
}
impl From<ProtoPluginConfiguration> for PluginConfiguration {
fn from(config: ProtoPluginConfiguration) -> Self {
PluginConfiguration {
interaction_configuration: config.interaction_configuration.as_ref().map(|c| proto_struct_to_map(c)).unwrap_or_default(),
pact_configuration: config.pact_configuration.as_ref().map(|c| proto_struct_to_map(c)).unwrap_or_default()
}
}
}
impl ContentMatcher {
pub fn is_core(&self) -> bool {
self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
}
pub fn catalogue_entry_key(&self) -> String {
if self.is_core() {
format!("core/content-matcher/{}", self.catalogue_entry.key)
} else {
format!("plugin/{}/content-matcher/{}", self.plugin_name(), self.catalogue_entry.key)
}
}
pub fn plugin_name(&self) -> String {
self.catalogue_entry.plugin.as_ref()
.map(|p| p.name.clone())
.unwrap_or("core".to_string())
}
pub fn plugin_version(&self) -> String {
self.catalogue_entry.plugin.as_ref()
.map(|p| p.version.clone())
.unwrap_or_default()
}
pub async fn configure_interation(
&self,
content_type: &ContentType,
definition: HashMap<String, Value>
) -> anyhow::Result<(Vec<InteractionContents>, Option<PluginConfiguration>)> {
debug!("Sending ConfigureContents request to plugin {:?}", self.catalogue_entry);
let request = ConfigureInteractionRequest {
content_type: content_type.to_string(),
contents_config: Some(to_proto_struct(&definition)),
};
let plugin_manifest = self.catalogue_entry.plugin.as_ref()
.expect("Plugin type is required");
match lookup_plugin(&plugin_manifest.as_dependency()) {
Some(plugin) => match plugin.configure_interaction(request).await {
Ok(response) => {
debug!("Got response: {:?}", response);
if response.error.is_empty() {
let mut results = vec![];
for response in response.interaction {
let body = match &response.contents {
Some(body) => {
let returned_content_type = ContentType::parse(body.content_type.as_str()).ok();
let contents = body.content.as_ref().cloned().unwrap_or_default();
OptionalBody::Present(Bytes::from(contents), returned_content_type,
Some(match body.content_type_hint() {
body::ContentTypeHint::Text => ContentTypeHint::TEXT,
body::ContentTypeHint::Binary => ContentTypeHint::BINARY,
body::ContentTypeHint::Default => ContentTypeHint::DEFAULT,
}))
},
None => OptionalBody::Missing
};
let rules = if !response.rules.is_empty() {
Some(MatchingRuleCategory {
name: Category::BODY,
rules: response.rules.iter().map(|(k, rules)| {
(DocPath::new(k).unwrap(), RuleList {
rules: rules.rule.iter().map(|rule| {
MatchingRule::create(rule.r#type.as_str(), &rule.values.as_ref().map(|rule| {
proto_struct_to_json(rule)
}).unwrap_or_default()).unwrap()
}).collect(),
rule_logic: RuleLogic::And,
cascaded: false
})
}).collect()
})
} else {
None
};
let generators = if !response.generators.is_empty() {
Some(Generators {
categories: hashmap! {
GeneratorCategory::BODY => response.generators.iter().map(|(k, gen)| {
(DocPath::new(k).unwrap(), Generator::create(gen.r#type.as_str(),
&gen.values.as_ref().map(|attr| proto_struct_to_json(attr)).unwrap_or_default()).unwrap())
}).collect()
}
})
} else {
None
};
let metadata = response.message_metadata.as_ref().map(|md| proto_struct_to_map(md));
let plugin_config = if let Some(plugin_configuration) = &response.plugin_configuration {
PluginConfiguration {
interaction_configuration: plugin_configuration.interaction_configuration.as_ref()
.map(|val| proto_struct_to_map(val)).unwrap_or_default(),
pact_configuration: plugin_configuration.pact_configuration.as_ref()
.map(|val| proto_struct_to_map(val)).unwrap_or_default()
}
} else {
PluginConfiguration::default()
};
debug!("body={}", body);
debug!("rules={:?}", rules);
debug!("generators={:?}", generators);
debug!("metadata={:?}", metadata);
debug!("pluginConfig={:?}", plugin_config);
results.push(InteractionContents {
part_name: response.part_name.clone(),
body,
rules,
generators,
metadata,
plugin_config,
interaction_markup: response.interaction_markup.clone(),
interaction_markup_type: match response.interaction_markup_type() {
MarkupType::Html => "HTML".to_string(),
_ => "COMMON_MARK".to_string(),
}
})
}
Ok((results, response.plugin_configuration.map(|config| PluginConfiguration::from(config))))
} else {
Err(anyhow!("Request to configure interaction failed: {}", response.error))
}
}
Err(err) => {
error!("Call to plugin failed - {}", err);
Err(anyhow!("Call to plugin failed - {}", err))
}
},
None => {
error!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry);
Err(anyhow!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry))
}
}
}
pub async fn match_contents(
&self,
expected: &OptionalBody,
actual: &OptionalBody,
context: &MatchingRuleCategory,
allow_unexpected_keys: bool,
plugin_config: Option<PluginInteractionConfig>
) -> Result<(), HashMap<String, Vec<ContentMismatch>>> {
let request = CompareContentsRequest {
expected: Some(Body {
content_type: expected.content_type().unwrap_or_default().to_string(),
content: expected.value().map(|b| b.to_vec()),
content_type_hint: body::ContentTypeHint::Default as i32
}),
actual: Some(Body {
content_type: actual.content_type().unwrap_or_default().to_string(),
content: actual.value().map(|b| b.to_vec()),
content_type_hint: body::ContentTypeHint::Default as i32
}),
allow_unexpected_keys,
rules: context.rules.iter().map(|(k, r)| {
(k.to_string(), crate::proto::MatchingRules {
rule: r.rules.iter().map(|rule|{
crate::proto::MatchingRule {
r#type: rule.name(),
values: Some(to_proto_struct(&rule.values().iter().map(|(k, v)| (k.to_string(), v.clone())).collect())),
}
}).collect()
})
}).collect(),
plugin_configuration: plugin_config.map(|config| ProtoPluginConfiguration {
interaction_configuration: Some(to_proto_struct(&config.interaction_configuration)),
pact_configuration: Some(to_proto_struct(&config.pact_configuration))
})
};
let plugin_manifest = self.catalogue_entry.plugin.as_ref()
.expect("Plugin type is required");
match lookup_plugin(&plugin_manifest.as_dependency()) {
Some(plugin) => match plugin.compare_contents(request).await {
Ok(response) => if let Some(mismatch) = response.type_mismatch {
Err(hashmap!{
String::default() => vec![
ContentMismatch {
expected: mismatch.expected.clone(),
actual: mismatch.actual.clone(),
mismatch: format!("Expected content type '{}' but got '{}'", mismatch.expected, mismatch.actual),
path: "".to_string(),
diff: None
}
]
})
} else if !response.error.is_empty() {
Err(hashmap! {
String::default() => vec![
ContentMismatch {
expected: Default::default(),
actual: Default::default(),
mismatch: response.error.clone(),
path: "".to_string(),
diff: None
}
]
})
} else if !response.results.is_empty() {
Err(response.results.iter().map(|(k, v)| {
(k.clone(), v.mismatches.iter().map(|mismatch| {
ContentMismatch {
expected: mismatch.expected.as_ref()
.map(|e| from_utf8(&e).unwrap_or_default().to_string())
.unwrap_or_default(),
actual: mismatch.actual.as_ref()
.map(|a| from_utf8(&a).unwrap_or_default().to_string())
.unwrap_or_default(),
mismatch: mismatch.mismatch.clone(),
path: mismatch.path.clone(),
diff: if mismatch.diff.is_empty() {
None
} else {
Some(mismatch.diff.clone())
}
}
}).collect())
}).collect())
} else {
Ok(())
}
Err(err) => {
error!("Call to plugin failed - {}", err);
Err(hashmap! {
String::default() => vec![
ContentMismatch {
expected: "".to_string(),
actual: "".to_string(),
mismatch: format!("Call to plugin failed = {}", err),
path: "".to_string(),
diff: None
}
]
})
}
},
None => {
error!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry);
Err(hashmap! {
String::default() => vec![
ContentMismatch {
expected: "".to_string(),
actual: "".to_string(),
mismatch: format!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry),
path: "".to_string(),
diff: None
}
]
})
}
}
}
}
#[derive(Clone, Debug)]
pub struct ContentGenerator {
pub catalogue_entry: CatalogueEntry
}
impl ContentGenerator {
pub fn is_core(&self) -> bool {
self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
}
pub fn catalogue_entry_key(&self) -> String {
if self.is_core() {
format!("core/content-generator/{}", self.catalogue_entry.key)
} else {
format!("plugin/{}/content-generator/{}", self.plugin_name(), self.catalogue_entry.key)
}
}
pub fn plugin_name(&self) -> String {
self.catalogue_entry.plugin.as_ref()
.map(|p| p.name.clone())
.unwrap_or("core".to_string())
}
pub async fn generate_content(
&self,
content_type: &ContentType,
generators: &HashMap<String, Generator>,
body: &OptionalBody,
plugin_data: &Vec<PluginData>,
interaction_data: &HashMap<String, HashMap<String, Value>>,
context: &HashMap<&str, Value>
) -> anyhow::Result<OptionalBody> {
let pact_plugin_manifest = self.catalogue_entry.plugin.clone().unwrap_or_default();
let plugin_data = plugin_data.iter().find_map(|pd| {
if pact_plugin_manifest.name == pd.name {
Some(pd.configuration.clone())
} else {
None
}
});
let interaction_data = interaction_data.get(&pact_plugin_manifest.name);
let request = GenerateContentRequest {
contents: Some(crate::proto::Body {
content_type: content_type.to_string(),
content: Some(body.value().unwrap_or_default().to_vec()),
content_type_hint: body::ContentTypeHint::Default as i32
}),
generators: generators.iter().map(|(k, v)| {
(k.clone(), crate::proto::Generator {
r#type: v.name(),
values: Some(to_proto_struct(&v.values().iter()
.map(|(k, v)| (k.to_string(), v.clone())).collect())),
})
}).collect(),
plugin_configuration: Some(ProtoPluginConfiguration {
pact_configuration: plugin_data.as_ref().map(to_proto_struct),
interaction_configuration: interaction_data.map(to_proto_struct),
.. ProtoPluginConfiguration::default()
}),
test_context: Some(to_proto_struct(&context.iter().map(|(k, v)| (k.to_string(), v.clone())).collect())),
.. GenerateContentRequest::default()
};
let plugin_manifest = self.catalogue_entry.plugin.as_ref()
.expect("Plugin type is required");
match lookup_plugin(&plugin_manifest.as_dependency()) {
Some(plugin) => {
debug!("Sending generateContent request to plugin {:?}", plugin_manifest);
match plugin.generate_content(request).await?.contents {
Some(contents) => {
Ok(OptionalBody::Present(
Bytes::from(contents.content.unwrap_or_default()),
ContentType::parse(contents.content_type.as_str()).ok(),
None
))
}
None => Ok(OptionalBody::Empty)
}
},
None => {
error!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry);
Err(anyhow!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry))
}
}
}
}