Skip to content

Commit

Permalink
feat: Got basic request matching test to pass
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Oct 28, 2024
1 parent 3746acc commit 5bb2b5a
Show file tree
Hide file tree
Showing 6 changed files with 881 additions and 42 deletions.
72 changes: 71 additions & 1 deletion rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion rust/pact_matching/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ semver = "1.0.22"
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
serde_urlencoded = "0.7.1"
snailquote = "0.3.1"
sxd-document = { version = "0.3.2", optional = true }
tokio = { version = "1.37.0", features = ["full"] }
tracing = "0.1.40"
Expand All @@ -60,9 +61,10 @@ uuid = { version = "1.8.0", features = ["v4"] }
quickcheck = "1"
expectest = "0.12.0"
env_logger = "0.11.3"
googletest = "0.12.0"
hamcrest2 = "0.3.0"
test-log = { version = "0.2.15", features = ["trace"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "tracing-log", "fmt"] }
ntest = "0.9.0"
pretty_assertions = "1.4.0"
rstest = "0.22.0"
rstest = "0.23.0"
60 changes: 60 additions & 0 deletions rust/pact_matching/src/engine/bodies.rs
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)
}
}
136 changes: 136 additions & 0 deletions rust/pact_matching/src/engine/context.rs
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())
}
}
}
Loading

0 comments on commit 5bb2b5a

Please sign in to comment.