Skip to content

Commit

Permalink
chore: Handle simple JSON arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Dec 9, 2024
1 parent 8ed227e commit cb57c9a
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 16 deletions.
161 changes: 157 additions & 4 deletions rust/pact_matching/src/engine/bodies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,53 @@ impl PlanBodyBuilder for JsonPlanBuilder {
content_type.is_json()
}

fn build_plan(&self, content: &Bytes, _context: &PlanMatchingContext) -> anyhow::Result<ExecutionPlanNode> {
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!() }
match &expected_json {
Value::Array(items) => {
// TODO: Deal with matching rules here
if context.matcher_is_defined(&path) {
todo!("Deal with matching rules here")
} else if items.is_empty() {
apply_node.add(
ExecutionPlanNode::action("expect:empty")
.add(ExecutionPlanNode::action("apply"))
);
} else {
apply_node.add(ExecutionPlanNode::action("push"));
apply_node.add(
ExecutionPlanNode::action("json:match:length")
.add(ExecutionPlanNode::value_node(items.len()))
.add(ExecutionPlanNode::action("apply"))
);
apply_node.add(ExecutionPlanNode::action("pop"));
let mut iter_node = ExecutionPlanNode::action("iter");
iter_node.add(ExecutionPlanNode::action("apply"));

for (index, item) in items.iter().enumerate() {
let item_path = path.join_index(index);
match item {
Value::Array(_) => { todo!() }
Value::Object(_) => { todo!() }
_ => {
iter_node.add(
ExecutionPlanNode::action("json:match:equality")
.add(ExecutionPlanNode::value_node(NodeValue::NAMESPACED("json".to_string(), item.to_string())))
.add(ExecutionPlanNode::resolve_value(item_path))
);
}
}
}

apply_node.add(iter_node);
}
}
Value::Object(_) => { todo!() }
_ => {
apply_node.add(
Expand All @@ -117,7 +154,7 @@ impl PlanBodyBuilder for JsonPlanBuilder {
mod tests {
use bytes::Bytes;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::{json, Value};

use crate::engine::bodies::{JsonPlanBuilder, PlanBodyBuilder};
use crate::engine::context::PlanMatchingContext;
Expand Down Expand Up @@ -159,6 +196,122 @@ r#"-> (
json:true,
%apply ()
)
)"#);
}

#[test]
fn json_plan_builder_with_string() {
let builder = JsonPlanBuilder::new();
let context = PlanMatchingContext::default();
let content = Bytes::copy_from_slice(Value::String("I am a string!".to_string()).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 (
json:"I am a string!",
%apply ()
)
)"#);
}

#[test]
fn json_plan_builder_with_int() {
let builder = JsonPlanBuilder::new();
let context = PlanMatchingContext::default();
let content = Bytes::copy_from_slice(json!(1000).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 (
json:1000,
%apply ()
)
)"#);
}

#[test]
fn json_plan_builder_with_float() {
let builder = JsonPlanBuilder::new();
let context = PlanMatchingContext::default();
let content = Bytes::copy_from_slice(json!(1000.3).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 (
json:1000.3,
%apply ()
)
)"#);
}

#[test]
fn json_plan_builder_with_empty_array() {
let builder = JsonPlanBuilder::new();
let context = PlanMatchingContext::default();
let content = Bytes::copy_from_slice(json!([]).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
),
%expect:empty (
%apply ()
)
)"#);
}

#[test]
fn json_plan_builder_with_array() {
let builder = JsonPlanBuilder::new();
let context = PlanMatchingContext::default();
let content = Bytes::copy_from_slice(json!([100, 200, 300]).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
),
%push (),
%json:match:length (
UINT(3),
%apply ()
),
%pop (),
%iter (
%apply (),
%json:match:equality (
json:100,
$[0]
),
%json:match:equality (
json:200,
$[1]
),
%json:match:equality (
json:300,
$[2]
)
)
)"#);
}
}
103 changes: 100 additions & 3 deletions rust/pact_matching/src/engine/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use std::panic::RefUnwindSafe;
use anyhow::anyhow;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use itertools::Itertools;
use tracing::{instrument, trace, Level};

use pact_models::matchingrules::MatchingRule;
use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory};
use pact_models::path_exp::DocPath;
use pact_models::prelude::v4::{SynchronousHttp, V4Pact};
use pact_models::v4::interaction::V4Interaction;

Expand All @@ -22,7 +24,9 @@ pub struct PlanMatchingContext {
/// Interaction that the plan id for
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>>
value_stack: Vec<Option<NodeResult>>,
/// Matching rules to use
matching_rules: MatchingRuleCategory,
}

impl PlanMatchingContext {
Expand Down Expand Up @@ -76,6 +80,11 @@ impl PlanMatchingContext {
Err(anyhow!("Expected byte array ({} bytes) to be empty", bytes.len()))
},
NodeValue::NAMESPACED(_, _) => { todo!("Not Implemented: Need a way to resolve NodeValue::NAMESPACED") }
NodeValue::UINT(ui) => if *ui == 0 {
Ok(NodeResult::VALUE(NodeValue::BOOL(true)))
} else {
Err(anyhow!("Expected {:?} to be empty", value))
}
}
} else {
// TODO: If the parameter value is an error, this should return an error?
Expand Down Expand Up @@ -131,6 +140,93 @@ impl PlanMatchingContext {
pub fn pop_result(&mut self) -> Option<NodeResult> {
self.value_stack.pop().flatten()
}

/// If there is a matcher defined at the path in this context
pub fn matcher_is_defined(&self, path: &DocPath) -> bool {
let path = path.to_vec();
let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
self.matching_rules.matcher_is_defined(path_slice.as_slice())
}

/// Creates a clone of this context, but with the matching rules set for the Request Method
pub fn for_method(&self) -> Self {
let matching_rules = if let Some(req_res) = self.interaction.as_v4_http() {
req_res.request.matching_rules.rules_for_category("method").unwrap_or_default()
} else {
MatchingRuleCategory::default()
};

PlanMatchingContext {
pact: self.pact.clone(),
interaction: self.interaction.boxed_v4(),
value_stack: vec![],
matching_rules
}
}

/// Creates a clone of this context, but with the matching rules set for the Request Path
pub fn for_path(&self) -> Self {
let matching_rules = if let Some(req_res) = self.interaction.as_v4_http() {
req_res.request.matching_rules.rules_for_category("path").unwrap_or_default()
} else {
MatchingRuleCategory::default()
};

PlanMatchingContext {
pact: self.pact.clone(),
interaction: self.interaction.boxed_v4(),
value_stack: vec![],
matching_rules
}
}

/// Creates a clone of this context, but with the matching rules set for the Request Query Parameters
pub fn for_query(&self) -> Self {
let matching_rules = if let Some(req_res) = self.interaction.as_v4_http() {
req_res.request.matching_rules.rules_for_category("query").unwrap_or_default()
} else {
MatchingRuleCategory::default()
};

PlanMatchingContext {
pact: self.pact.clone(),
interaction: self.interaction.boxed_v4(),
value_stack: vec![],
matching_rules
}
}

/// Creates a clone of this context, but with the matching rules set for the Request Headers
pub fn for_headers(&self) -> Self {
let matching_rules = if let Some(req_res) = self.interaction.as_v4_http() {
req_res.request.matching_rules.rules_for_category("header").unwrap_or_default()
} else {
MatchingRuleCategory::default()
};

PlanMatchingContext {
pact: self.pact.clone(),
interaction: self.interaction.boxed_v4(),
value_stack: vec![],
matching_rules
}
}

/// Creates a clone of this context, but with the matching rules set for the Request Body
pub fn for_body(&self) -> Self {
let matching_rules = if let Some(req_res) = self.interaction.as_v4_http() {
req_res.request.matching_rules.rules_for_category("body").unwrap_or_default()
} else {
MatchingRuleCategory::default()
};

PlanMatchingContext {
pact: self.pact.clone(),
interaction: self.interaction.boxed_v4(),
value_stack: vec![],
matching_rules
}
}
}

fn validate_two_args(arguments: &Vec<ExecutionPlanNode>, action: &str) -> anyhow::Result<(NodeResult, NodeResult)> {
Expand Down Expand Up @@ -158,7 +254,8 @@ impl Default for PlanMatchingContext {
PlanMatchingContext {
pact: Default::default(),
interaction: Box::new(SynchronousHttp::default()),
value_stack: vec![]
value_stack: vec![],
matching_rules: Default::default()
}
}
}
Loading

0 comments on commit cb57c9a

Please sign in to comment.