Skip to content

Commit

Permalink
chore: Added pipeline node and apply action
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Dec 9, 2024
1 parent 5db8db2 commit 36b46a7
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 12 deletions.
108 changes: 106 additions & 2 deletions rust/pact_matching/src/engine/bodies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ use std::fmt::Debug;
use std::sync::{Arc, LazyLock, RwLock};

use bytes::Bytes;
use nom::AsBytes;
use serde_json::Value;

use pact_models::content_types::ContentType;
use pact_models::path_exp::DocPath;

use crate::engine::{ExecutionPlanNode, PlanMatchingContext};
use crate::engine::{ExecutionPlanNode, NodeValue, PlanMatchingContext};

/// Trait for implementations of builders for different types of bodies
pub trait PlanBodyBuilder: Debug {
/// If this builder supports a namespace for nodes.
fn namespace(&self) -> Option<String> {
None
}

/// If this builder supports the given content type
fn supports_type(&self, content_type: &ContentType) -> bool;

Expand All @@ -20,8 +27,11 @@ pub trait PlanBodyBuilder: Debug {
}

static BODY_PLAN_BUILDERS: LazyLock<RwLock<Vec<Arc<dyn PlanBodyBuilder + Send + Sync>>>> = LazyLock::new(|| {
let mut builders = vec![];
let mut builders: Vec<Arc<dyn PlanBodyBuilder + Send + Sync>> = vec![];

// TODO: Add default implementations here
builders.push(Arc::new(JsonPlanBuilder::new()));

RwLock::new(builders)
});

Expand Down Expand Up @@ -58,3 +68,97 @@ impl PlanBodyBuilder for PlainTextBuilder {
Ok(node)
}
}

/// Plan builder for JSON bodies
#[derive(Clone, Debug)]
pub struct JsonPlanBuilder;

impl JsonPlanBuilder {
/// Create a new instance
pub fn new() -> Self {
JsonPlanBuilder{}
}
}

impl PlanBodyBuilder for JsonPlanBuilder {
fn namespace(&self) -> Option<String> {
Some("json".to_string())
}

fn supports_type(&self, content_type: &ContentType) -> bool {
content_type.is_json()
}

fn build_plan(&self, content: &Bytes, _context: &PlanMatchingContext) -> anyhow::Result<ExecutionPlanNode> {
let expected_json: Value = serde_json::from_slice(content.as_bytes())?;
let path = DocPath::root();
let mut apply_node = ExecutionPlanNode::apply();
apply_node
.add(ExecutionPlanNode::action("json:parse")
.add(ExecutionPlanNode::resolve_value(DocPath::new_unwrap("$.body"))));

match expected_json {
Value::Array(_) => { todo!() }
Value::Object(_) => { todo!() }
_ => {
apply_node.add(
ExecutionPlanNode::action("match:equality")
.add(ExecutionPlanNode::value_node(NodeValue::NAMESPACED("json".to_string(), expected_json.to_string())))
.add(ExecutionPlanNode::action("apply"))
);
}
}

Ok(apply_node)
}
}

#[cfg(test)]
mod tests {
use bytes::Bytes;
use pretty_assertions::assert_eq;
use serde_json::Value;

use crate::engine::bodies::{JsonPlanBuilder, PlanBodyBuilder};
use crate::engine::context::PlanMatchingContext;

#[test]
fn json_plan_builder_with_null() {
let builder = JsonPlanBuilder::new();
let context = PlanMatchingContext::default();
let content = Bytes::copy_from_slice(Value::Null.to_string().as_bytes());
let node = builder.build_plan(&content, &context).unwrap();
let mut buffer = String::new();
node.pretty_form(&mut buffer, 0);
assert_eq!(buffer,
r#"-> (
%json:parse (
$.body
),
%match:equality (
%apply (),
json:null
)
)"#);
}

#[test]
fn json_plan_builder_with_boolean() {
let builder = JsonPlanBuilder::new();
let context = PlanMatchingContext::default();
let content = Bytes::copy_from_slice(Value::Bool(true).to_string().as_bytes());
let node = builder.build_plan(&content, &context).unwrap();
let mut buffer = String::new();
node.pretty_form(&mut buffer, 0);
assert_eq!(buffer,
r#"-> (
%json:parse (
$.body
),
%match:equality (
%apply (),
json:true
)
)"#);
}
}
34 changes: 31 additions & 3 deletions rust/pact_matching/src/engine/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ 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>
pub interaction: Box<dyn V4Interaction + Send + Sync + RefUnwindSafe>,
/// Stack of intermediate values (used by the pipeline operator and apply action)
value_stack: Vec<Option<NodeResult>>
}

impl PlanMatchingContext {
Expand Down Expand Up @@ -72,7 +74,8 @@ impl PlanMatchingContext {
Ok(NodeResult::VALUE(NodeValue::BOOL(true)))
} else {
Err(anyhow!("Expected byte array ({} bytes) to be empty", bytes.len()))
}
},
NodeValue::NAMESPACED(_, _) => { todo!("Not Implemented: Need a way to resolve NodeValue::NAMESPACED") }
}
} else {
// TODO: If the parameter value is an error, this should return an error?
Expand Down Expand Up @@ -101,9 +104,33 @@ impl PlanMatchingContext {
Ok(first)
}
}
"apply" => if let Some(value) = self.value_stack.last() {
value.clone().ok_or_else(|| anyhow!("No value to apply (value on stack is empty)"))
} else {
Err(anyhow!("No value to apply (stack is empty)"))
}
_ => Err(anyhow!("'{}' is not a valid action", action))
}
}

/// Push a result value onto the value stack
pub fn push_result(&mut self, value: Option<NodeResult>) {
self.value_stack.push(value);
}

/// Replace the top value of the stack with the new value
pub fn update_result(&mut self, value: Option<NodeResult>) {
if let Some(current) = self.value_stack.last_mut() {
*current = value;
} else {
self.value_stack.push(value);
}
}

/// Return the value on the top if the stack
pub fn pop_result(&mut self) -> Option<NodeResult> {
self.value_stack.pop().flatten()
}
}

fn validate_two_args(arguments: &Vec<ExecutionPlanNode>, action: &str) -> anyhow::Result<(NodeResult, NodeResult)> {
Expand All @@ -130,7 +157,8 @@ impl Default for PlanMatchingContext {
fn default() -> Self {
PlanMatchingContext {
pact: Default::default(),
interaction: Box::new(SynchronousHttp::default())
interaction: Box::new(SynchronousHttp::default()),
value_stack: vec![]
}
}
}
104 changes: 97 additions & 7 deletions rust/pact_matching/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub enum PlanNodeType {
VALUE(NodeValue),
/// Leaf node that stores an expression to resolve against the test context
RESOLVE(DocPath),
/// Pipeline node (apply), which applies each node to the next as a pipeline returning the last
PIPELINE
}

/// Enum for the value stored in a leaf node
Expand All @@ -62,7 +64,9 @@ pub enum NodeValue {
/// List of String values
SLIST(Vec<String>),
/// Byte Array
BARRAY(Vec<u8>)
BARRAY(Vec<u8>),
/// Namespaced value
NAMESPACED(String, String)
}

impl NodeValue {
Expand Down Expand Up @@ -119,6 +123,13 @@ impl NodeValue {
buffer.push(')');
buffer
}
NodeValue::NAMESPACED(name, value) => {
let mut buffer = String::new();
buffer.push_str(name);
buffer.push(':');
buffer.push_str(value);
buffer
}
}
}

Expand All @@ -139,7 +150,8 @@ impl NodeValue {
NodeValue::BOOL(_) => "Boolean",
NodeValue::MMAP(_) => "Multi-Value String Map",
NodeValue::SLIST(_) => "String List",
NodeValue::BARRAY(_) => "Byte Array"
NodeValue::BARRAY(_) => "Byte Array",
NodeValue::NAMESPACED(_, _) => "Namespaced Value"
}
}
}
Expand Down Expand Up @@ -234,7 +246,8 @@ impl NodeResult {
NodeValue::BOOL(b) => Some(b.to_string()),
NodeValue::MMAP(m) => Some(format!("{:?}", m)),
NodeValue::SLIST(list) => Some(format!("{:?}", list)),
NodeValue::BARRAY(bytes) => Some(BASE64.encode(bytes))
NodeValue::BARRAY(bytes) => Some(BASE64.encode(bytes)),
NodeValue::NAMESPACED(name, value) => Some(format!("{}:{}", name, value))
}
NodeResult::ERROR(_) => None
}
Expand All @@ -259,7 +272,8 @@ impl NodeResult {
NodeValue::BOOL(b) => *b,
NodeValue::MMAP(m) => !m.is_empty(),
NodeValue::SLIST(l) => !l.is_empty(),
NodeValue::BARRAY(b) => !b.is_empty()
NodeValue::BARRAY(b) => !b.is_empty(),
NodeValue::NAMESPACED(_, _) => false // TODO: Need a way to resolve this
}
NodeResult::ERROR(_) => false
}
Expand Down Expand Up @@ -342,6 +356,23 @@ impl ExecutionPlanNode {
buffer.push_str(pad.as_str());
buffer.push_str(str.to_string().as_str());

if let Some(result) = &self.result {
buffer.push_str(" ~ ");
buffer.push_str(result.to_string().as_str());
}
}
PlanNodeType::PIPELINE => {
buffer.push_str(pad.as_str());
buffer.push_str("->");
if self.is_empty() {
buffer.push_str(" ()");
} else {
buffer.push_str(" (\n");
self.pretty_form_children(buffer, indent);
buffer.push_str(pad.as_str());
buffer.push(')');
}

if let Some(result) = &self.result {
buffer.push_str(" ~ ");
buffer.push_str(result.to_string().as_str());
Expand Down Expand Up @@ -402,6 +433,17 @@ impl ExecutionPlanNode {
PlanNodeType::RESOLVE(str) => {
buffer.push_str(str.to_string().as_str());

if let Some(result) = &self.result {
buffer.push('~');
buffer.push_str(result.to_string().as_str());
}
}
PlanNodeType::PIPELINE => {
buffer.push_str("->");
buffer.push('(');
self.str_form_children(&mut buffer);
buffer.push(')');

if let Some(result) = &self.result {
buffer.push('~');
buffer.push_str(result.to_string().as_str());
Expand All @@ -424,9 +466,9 @@ impl ExecutionPlanNode {
}

/// Constructor for a container node
pub fn container(label: &str) -> ExecutionPlanNode {
pub fn container<S: Into<String>>(label: S) -> ExecutionPlanNode {
ExecutionPlanNode {
node_type: PlanNodeType::CONTAINER(label.to_string()),
node_type: PlanNodeType::CONTAINER(label.into()),
result: None,
children: vec![],
}
Expand Down Expand Up @@ -459,12 +501,26 @@ impl ExecutionPlanNode {
}
}

/// Constructor for an apply node
pub fn apply() -> ExecutionPlanNode {
ExecutionPlanNode {
node_type: PlanNodeType::PIPELINE,
result: None,
children: vec![],
}
}

/// Adds the node as a child
pub fn add<N>(&mut self, node: N) -> &mut Self where N: Into<ExecutionPlanNode> {
self.children.push(node.into());
self
}

/// Pushes the node onto the front of the list
pub fn push_node(&mut self, node: ExecutionPlanNode) {
self.children.insert(0, node.into());
}

/// If the node is a leaf node
pub fn is_empty(&self) -> bool {
match self.node_type {
Expand Down Expand Up @@ -711,7 +767,11 @@ fn walk_tree(
child_path.push(action.clone());
let mut result = vec![];
for child in &node.children {
let child_result = walk_tree(&child_path, child, value_resolver, context)?;
let child_result = if child.result.is_none() {
walk_tree(&child_path, child, value_resolver, context)?
} else {
child.clone()
};
result.push(child_result);
}
match context.execute_action(action.as_str(), &result) {
Expand Down Expand Up @@ -759,6 +819,36 @@ fn walk_tree(
}
}
}
PlanNodeType::PIPELINE => {
trace!(?path, "Apply pipeline node");

let child_path = path.to_vec();
context.push_result(None);

for child in &node.children {
let child_result = walk_tree(&child_path, child, value_resolver, context)?;
context.update_result(child_result.result);
}

let result = context.pop_result();
match result {
Some(value) => {
Ok(ExecutionPlanNode {
node_type: node.node_type.clone(),
result: Some(value),
children: vec![]
})
}
None => {
trace!(?path, "Value from stack is empty");
Ok(ExecutionPlanNode {
node_type: node.node_type.clone(),
result: Some(NodeResult::ERROR("Value from stack is empty".to_string())),
children: vec![]
})
}
}
}
}
}

Expand Down

0 comments on commit 36b46a7

Please sign in to comment.