use std::fmt::Debug;
use std::sync::{Arc, LazyLock, RwLock};
use bytes::Bytes;
use pact_models::content_types::ContentType;
use pact_models::matchingrules::{MatchingRule, RuleList};
use pact_models::path_exp::{DocPath, PathToken};
use crate::engine::{ExecutionPlanNode, NodeValue, PlanMatchingContext};
use crate::engine::bodies::json::JsonPlanBuilder;
#[cfg(feature = "xml")] use crate::engine::bodies::xml::XMLPlanBuilder;
pub mod json;
#[cfg(feature = "xml")] pub mod xml;
pub trait PlanBodyBuilder: Debug {
fn namespace(&self) -> Option<String> {
None
}
fn supports_type(&self, content_type: &ContentType) -> bool;
fn build_plan(&self, content: &Bytes, context: &PlanMatchingContext) -> anyhow::Result<ExecutionPlanNode>;
}
static BODY_PLAN_BUILDERS: LazyLock<RwLock<Vec<Arc<dyn PlanBodyBuilder + Send + Sync>>>> = LazyLock::new(|| {
let mut builders: Vec<Arc<dyn PlanBodyBuilder + Send + Sync>> = vec![];
builders.push(Arc::new(JsonPlanBuilder::new()));
#[cfg(feature = "xml")]
builders.push(Arc::new(XMLPlanBuilder::new()));
RwLock::new(builders)
});
pub(crate) fn get_body_plan_builder(content_type: &ContentType) -> Option<Arc<dyn PlanBodyBuilder + Send + Sync>> {
let registered_builders = (*BODY_PLAN_BUILDERS).read().unwrap();
registered_builders.iter().find(|builder| builder.supports_type(content_type))
.cloned()
}
#[derive(Clone, Debug)]
pub struct PlainTextBuilder;
impl PlainTextBuilder {
pub fn new() -> Self {
PlainTextBuilder{}
}
}
impl PlanBodyBuilder for PlainTextBuilder {
fn supports_type(&self, content_type: &ContentType) -> bool {
content_type.is_text()
}
fn build_plan(&self, content: &Bytes, _context: &PlanMatchingContext) -> anyhow::Result<ExecutionPlanNode> {
let bytes = content.to_vec();
let text_content = String::from_utf8_lossy(&bytes);
let mut node = ExecutionPlanNode::action("match:equality");
let mut child_node = ExecutionPlanNode::action("convert:UTF8");
child_node.add(ExecutionPlanNode::resolve_value(DocPath::new_unwrap("$.body")));
node.add(ExecutionPlanNode::value_node(text_content.to_string()));
node.add(child_node);
node.add(ExecutionPlanNode::value_node(NodeValue::NULL));
node.add(ExecutionPlanNode::value_node(NodeValue::BOOL(false)));
Ok(node)
}
}
fn should_apply_to_map_entries(rules: &RuleList) -> bool {
rules.rules.iter().any(|rule| {
match rule {
MatchingRule::Values => true,
MatchingRule::EachKey(_) => true,
MatchingRule::EachValue(_) => true,
_ => false
}
})
}
fn drop_indices(path: &DocPath) -> DocPath {
DocPath::from_tokens(path.tokens()
.iter()
.filter(|token| match token {
PathToken::Index(_) | PathToken::StarIndex => false,
_ => true
})
.map(|token| {
if let PathToken::Field(name) = token {
if name.ends_with('*') {
PathToken::Field(name.trim_end_matches('*').to_string())
} else {
token.clone()
}
} else {
token.clone()
}
}))
}
fn remove_marker(path: &DocPath) -> DocPath {
DocPath::from_tokens(path.tokens()
.iter()
.flat_map(|token| {
if let PathToken::Field(name) = token {
if name.ends_with('*') {
vec![PathToken::Field(name.trim_end_matches('*').to_string()), PathToken::Index(0)]
} else {
vec![token.clone()]
}
} else {
vec![token.clone()]
}
}))
}