use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::fmt;
use std::panic::RefUnwindSafe;
use std::str::FromStr;
use anyhow::anyhow;
use serde_json::{json, Value};
use tracing::warn;
use crate::interaction::Interaction;
use crate::json_utils::json_to_string;
use crate::v4::async_message::AsynchronousMessage;
use crate::v4::sync_message::SynchronousMessage;
use crate::v4::synch_http::SynchronousHttp;
use crate::v4::V4InteractionType;
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct InteractionMarkup {
pub markup: String,
pub markup_type: String
}
impl InteractionMarkup {
pub fn from_json(json: &Value) -> InteractionMarkup {
match json {
Value::Object(values) => InteractionMarkup {
markup: values.get("markup").map(|val| json_to_string(val)).unwrap_or_default(),
markup_type: values.get("markupType").map(|val| json_to_string(val)).unwrap_or_default()
},
_ => {
warn!("{:?} is not a valid value for InteractionMarkup", json);
InteractionMarkup::default()
}
}
}
pub fn is_empty(&self) -> bool {
self.markup.is_empty()
}
pub fn to_json(&self) -> Value {
json!({
"markup": self.markup,
"markupType": self.markup_type
})
}
pub fn merge(&self, other: InteractionMarkup) -> InteractionMarkup {
if self.is_empty() {
other
} else if other.is_empty() {
self.clone()
} else {
if self.markup_type != other.markup_type {
warn!("Merging different markup types: {} and {}", self.markup_type, other.markup_type);
}
let mut buffer = String::new();
buffer.push_str(self.markup.as_str());
buffer.push('\n');
buffer.push_str(other.markup.as_str());
InteractionMarkup {
markup: buffer,
markup_type: self.markup_type.clone()
}
}
}
}
pub trait V4Interaction: Interaction + Send + Sync {
fn to_json(&self) -> Value;
fn to_super(&self) -> &(dyn Interaction + Send + Sync + RefUnwindSafe);
fn to_super_mut(&mut self) -> &mut (dyn Interaction + Send + Sync);
fn key(&self) -> Option<String>;
fn boxed_v4(&self) -> Box<dyn V4Interaction + Send + Sync + RefUnwindSafe>;
fn comments(&self) -> HashMap<String, Value>;
fn comments_mut(&mut self) -> &mut HashMap<String, Value>;
fn v4_type(&self) -> V4InteractionType;
fn plugin_config(&self) -> HashMap<String, HashMap<String, Value>>;
fn plugin_config_mut(&mut self) -> &mut HashMap<String, HashMap<String, Value>>;
fn interaction_markup(&self) -> InteractionMarkup;
fn interaction_markup_mut(&mut self) -> &mut InteractionMarkup;
fn transport(&self) -> Option<String>;
fn set_transport(&mut self, transport: Option<String>);
fn with_unique_key(&self) -> Box<dyn V4Interaction + Send + Sync + RefUnwindSafe>;
fn unique_key(&self) -> String;
}
impl Display for dyn V4Interaction {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(i) = self.as_v4_http() {
std::fmt::Display::fmt(&i, f)
} else if let Some(i) = self.as_v4_async_message() {
std::fmt::Display::fmt(&i, f)
} else if let Some(i) = self.as_v4_sync_message() {
std::fmt::Display::fmt(&i, f)
} else {
Err(fmt::Error)
}
}
}
impl Clone for Box<dyn V4Interaction + Send + Sync + RefUnwindSafe> {
fn clone(&self) -> Self {
if let Some(http) = self.as_v4_http() {
Box::new(http)
} else if let Some(message) = self.as_v4_async_message() {
Box::new(message)
} else if let Some(message) = self.as_v4_sync_message() {
Box::new(message)
} else {
panic!("Internal Error - Tried to clone an interaction that was not valid")
}
}
}
impl PartialEq for Box<dyn V4Interaction + Send + Sync + RefUnwindSafe> {
fn eq(&self, other: &Self) -> bool {
if let Some(http) = self.as_v4_http() {
if let Some(other) = other.as_v4_http() {
http == other
} else {
false
}
} else if let Some(message) = self.as_v4_async_message() {
if let Some(other) = other.as_v4_async_message() {
message == other
} else {
false
}
} else if let Some(message) = self.as_v4_sync_message() {
if let Some(other) = other.as_v4_sync_message() {
message == other
} else {
false
}
} else {
false
}
}
}
pub fn interactions_from_json(json: &Value, source: &str) -> anyhow::Result<Vec<Box<dyn V4Interaction + Send + Sync + RefUnwindSafe>>> {
match json.get("interactions") {
Some(Value::Array(array)) => {
let mut interactions = vec![];
for (index, ijson) in array.iter().enumerate() {
interactions.push(interaction_from_json(source, index, ijson)?);
}
Ok(interactions)
},
_ => Ok(vec![])
}
}
pub fn interaction_from_json(source: &str, index: usize, ijson: &Value) -> anyhow::Result<Box<dyn V4Interaction + Send + Sync + RefUnwindSafe>> {
match ijson.get("type") {
Some(i_type) => match FromStr::from_str(json_to_string(i_type).as_str()) {
Ok(i_type) => {
match i_type {
V4InteractionType::Synchronous_HTTP => SynchronousHttp::from_json(ijson, index).map(|i| i.boxed_v4()),
V4InteractionType::Asynchronous_Messages => AsynchronousMessage::from_json(ijson, index).map(|i| i.boxed_v4()),
V4InteractionType::Synchronous_Messages => SynchronousMessage::from_json(ijson, index).map(|i| i.boxed_v4())
}
},
Err(_) => {
warn!("Interaction {} has an incorrect type attribute '{}'. It will be ignored. Source: {}", index, i_type, source);
Err(anyhow!("Interaction {} has an incorrect type attribute '{}'. It will be ignored. Source: {}", index, i_type, source))
}
},
None => {
warn!("Interaction {} has no type attribute. It will be ignored. Source: {}", index, source);
Err(anyhow!("Interaction {} has no type attribute. It will be ignored. Source: {}", index, source))
}
}
}
pub(crate) fn parse_plugin_config(json: &Value) -> HashMap<String, HashMap<String, Value>> {
if let Some(config) = json.get("pluginConfiguration") {
match config {
Value::Object(map) => map.iter()
.map(|(k, v)| {
let inner_config = match v {
Value::Object(o) => o.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => {
warn!("Plugin {} configuration is not correctly formatted, ignoring it", k);
Default::default()
}
};
(k.clone(), inner_config)
}).collect(),
_ => {
warn!("Plugin configuration is not correctly formatted, ignoring it");
Default::default()
}
}
} else {
Default::default()
}
}
#[cfg(test)]
mod tests {
use expectest::prelude::*;
use maplit::hashmap;
use serde_json::json;
use crate::provider_states::ProviderState;
use crate::v4::interaction::interaction_from_json;
#[test]
fn loading_interaction_from_json() {
let interaction_json = json!({
"type": "Synchronous/HTTP",
"description": "String",
"providerStates": [{ "name": "provider state" }]
});
let interaction = interaction_from_json("", 0, &interaction_json).unwrap();
expect!(interaction.description()).to(be_equal_to("String"));
expect!(interaction.provider_states()).to(be_equal_to(vec![
ProviderState { name: "provider state".into(), params: hashmap!{} } ]));
}
#[test]
fn defaults_to_number_if_no_description() {
let interaction_json = json!({
"type": "Synchronous/HTTP"
});
let interaction = interaction_from_json("", 0, &interaction_json).unwrap();
expect!(interaction.description()).to(be_equal_to("Interaction 0"));
}
#[test]
fn defaults_to_empty_if_no_provider_state() {
let interaction_json = json!({
"type": "Synchronous/HTTP"
});
let interaction = interaction_from_json("", 0, &interaction_json).unwrap();
expect!(interaction.provider_states().iter()).to(be_empty());
}
#[test]
fn defaults_to_none_if_provider_state_null() {
let interaction_json = json!({
"type": "Synchronous/HTTP",
"description": "String",
"providerStates": null
});
let interaction = interaction_from_json("", 0, &interaction_json).unwrap();
expect!(interaction.provider_states().iter()).to(be_empty());
}
#[test]
fn interaction_from_json_sets_the_id_if_loaded_from_broker() {
let json = json!({
"type": "Synchronous/HTTP",
"_id": "123456789",
"description": "Test Interaction",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200
}
});
let interaction = interaction_from_json("", 0, &json).unwrap();
expect!(interaction.id()).to(be_some().value("123456789".to_string()));
}
}