-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Got basic request matching test to pass
- Loading branch information
1 parent
3746acc
commit 5bb2b5a
Showing
6 changed files
with
881 additions
and
42 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
//! Types for supporting building and executing plans for bodies | ||
use std::fmt::Debug; | ||
use std::sync::{Arc, LazyLock, RwLock}; | ||
|
||
use bytes::Bytes; | ||
|
||
use pact_models::content_types::ContentType; | ||
use pact_models::path_exp::DocPath; | ||
|
||
use crate::engine::{ExecutionPlanNode, PlanMatchingContext}; | ||
|
||
/// Trait for implementations of builders for different types of bodies | ||
pub trait PlanBodyBuilder: Debug { | ||
/// If this builder supports the given content type | ||
fn supports_type(&self, content_type: &ContentType) -> bool; | ||
|
||
/// Build the plan for the expected body | ||
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![]; | ||
// TODO: Add default implementations here | ||
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() | ||
} | ||
|
||
/// Plan builder for plain text. This just sets up an equality matcher | ||
#[derive(Clone, Debug)] | ||
pub struct PlainTextBuilder; | ||
|
||
impl PlainTextBuilder { | ||
/// Create a new instance | ||
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(child_node); | ||
node.add(ExecutionPlanNode::value_node(text_content.to_string())); | ||
Ok(node) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
//! Traits and structs for dealing with the test context. | ||
use std::panic::RefUnwindSafe; | ||
|
||
use anyhow::anyhow; | ||
use base64::Engine; | ||
use base64::engine::general_purpose::STANDARD as BASE64; | ||
use tracing::{instrument, trace, Level}; | ||
|
||
use pact_models::matchingrules::MatchingRule; | ||
use pact_models::prelude::v4::{SynchronousHttp, V4Pact}; | ||
use pact_models::v4::interaction::V4Interaction; | ||
|
||
use crate::engine::{ExecutionPlanNode, NodeResult, NodeValue}; | ||
use crate::matchers::Matches; | ||
|
||
/// Context to store data for use in executing an execution plan. | ||
#[derive(Clone, Debug)] | ||
pub struct PlanMatchingContext { | ||
/// Pact the plan is for | ||
pub pact: V4Pact, | ||
/// Interaction that the plan id for | ||
pub interaction: Box<dyn V4Interaction + Send + Sync + RefUnwindSafe> | ||
} | ||
|
||
impl PlanMatchingContext { | ||
/// Execute the action | ||
#[instrument(ret, skip(self), level = Level::TRACE)] | ||
pub fn execute_action( | ||
&self, | ||
action: &str, | ||
arguments: &Vec<ExecutionPlanNode> | ||
) -> anyhow::Result<NodeResult> { | ||
trace!(%action, ?arguments, "Executing action"); | ||
match action { | ||
"upper-case" => { | ||
let value = validate_one_arg(arguments, action)?; | ||
let result = value.as_string() | ||
.unwrap_or_default(); | ||
Ok(NodeResult::VALUE(NodeValue::STRING(result.to_uppercase()))) | ||
} | ||
"match:equality" => { | ||
let (first, second) = validate_two_args(arguments, action)?; | ||
let first = first.as_value().unwrap_or_default(); | ||
let second = second.as_value().unwrap_or_default(); | ||
first.matches_with(second, &MatchingRule::Equality, false)?; | ||
Ok(NodeResult::VALUE(NodeValue::BOOL(true))) | ||
} | ||
"expect:empty" => { | ||
let arg = validate_one_arg(arguments, action)?; | ||
let arg_value = arg.as_value(); | ||
if let Some(value) = &arg_value { | ||
match value { | ||
NodeValue::NULL => Ok(NodeResult::VALUE(NodeValue::BOOL(true))), | ||
NodeValue::STRING(s) => if s.is_empty() { | ||
Ok(NodeResult::VALUE(NodeValue::BOOL(true))) | ||
} else { | ||
Err(anyhow!("Expected {:?} to be empty", value)) | ||
} | ||
NodeValue::BOOL(b) => Ok(NodeResult::VALUE(NodeValue::BOOL(*b))), | ||
NodeValue::MMAP(m) => if m.is_empty() { | ||
Ok(NodeResult::VALUE(NodeValue::BOOL(true))) | ||
} else { | ||
Err(anyhow!("Expected {:?} to be empty", value)) | ||
} | ||
NodeValue::SLIST(l) => if l.is_empty() { | ||
Ok(NodeResult::VALUE(NodeValue::BOOL(true))) | ||
} else { | ||
Err(anyhow!("Expected {:?} to be empty", value)) | ||
}, | ||
NodeValue::BARRAY(bytes) => if bytes.is_empty() { | ||
Ok(NodeResult::VALUE(NodeValue::BOOL(true))) | ||
} else { | ||
Err(anyhow!("Expected byte array ({} bytes) to be empty", bytes.len())) | ||
} | ||
} | ||
} else { | ||
// TODO: If the parameter value is an error, this should return an error? | ||
Ok(NodeResult::VALUE(NodeValue::BOOL(true))) | ||
} | ||
} | ||
"convert:UTF8" => { | ||
let arg = validate_one_arg(arguments, action)?; | ||
let arg_value = arg.as_value(); | ||
if let Some(value) = &arg_value { | ||
match value { | ||
NodeValue::NULL => Ok(NodeResult::VALUE(NodeValue::STRING("".to_string()))), | ||
NodeValue::STRING(s) => Ok(NodeResult::VALUE(NodeValue::STRING(s.clone()))), | ||
NodeValue::BARRAY(b) => Ok(NodeResult::VALUE(NodeValue::STRING(String::from_utf8_lossy(b).to_string()))), | ||
_ => Err(anyhow!("convert:UTF8 can not be used with {}", value.value_type())) | ||
} | ||
} else { | ||
Ok(NodeResult::VALUE(NodeValue::STRING("".to_string()))) | ||
} | ||
} | ||
"if" => { | ||
let (first, second) = validate_two_args(arguments, action)?; | ||
if first.is_truthy() { | ||
Ok(second) | ||
} else { | ||
Ok(NodeResult::VALUE(NodeValue::BOOL(false))) | ||
} | ||
} | ||
_ => Err(anyhow!("'{}' is not a valid action", action)) | ||
} | ||
} | ||
} | ||
|
||
fn validate_two_args(arguments: &Vec<ExecutionPlanNode>, action: &str) -> anyhow::Result<(NodeResult, NodeResult)> { | ||
if arguments.len() == 2 { | ||
let first = arguments[0].value().unwrap_or_default(); | ||
let second = arguments[1].value().unwrap_or_default(); | ||
Ok((first, second)) | ||
} else { | ||
Err(anyhow!("{} requires two arguments, got {}", action, arguments.len())) | ||
} | ||
} | ||
|
||
fn validate_one_arg(arguments: &Vec<ExecutionPlanNode>, action: &str) -> anyhow::Result<NodeResult> { | ||
if arguments.len() > 1 { | ||
Err(anyhow!("{} takes only one argument, got {}", action, arguments.len())) | ||
} else if let Some(argument) = arguments.first() { | ||
Ok(argument.value().unwrap_or_default()) | ||
} else { | ||
Err(anyhow!("{} requires one argument, got none", action)) | ||
} | ||
} | ||
|
||
impl Default for PlanMatchingContext { | ||
fn default() -> Self { | ||
PlanMatchingContext { | ||
pact: Default::default(), | ||
interaction: Box::new(SynchronousHttp::default()) | ||
} | ||
} | ||
} |
Oops, something went wrong.