Skip to content

Commit

Permalink
refactor: Extract the DocPath resolving functions from the generators…
Browse files Browse the repository at this point in the history
… to the json_utils module
  • Loading branch information
rholshausen committed Feb 20, 2025
1 parent 0fd358d commit a4ecc0b
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 85 deletions.
89 changes: 4 additions & 85 deletions rust/pact_models/src/generators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ use std::convert::TryFrom;
use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::mem;
use std::ops::Index;
use std::str::FromStr;

use anyhow::anyhow;
#[cfg(feature = "datetime")] use chrono::{DateTime, Local};
use indextree::{Arena, NodeId};
use itertools::Itertools;
use maplit::hashmap;
#[cfg(not(target_family = "wasm"))] use onig::{Captures, Regex};
Expand All @@ -26,10 +24,10 @@ use uuid::Uuid;
use crate::bodies::OptionalBody;
use crate::expression_parser::{contains_expressions, DataType, DataValue, MapValueResolver, parse_expression};
#[cfg(feature = "datetime")] use crate::generators::datetime_expressions::{execute_date_expression, execute_datetime_expression, execute_time_expression};
use crate::json_utils::{get_field_as_string, json_to_string, JsonToNum};
use crate::json_utils::{get_field_as_string, json_to_string, JsonToNum, resolve_path};
use crate::matchingrules::{Category, MatchingRuleCategory};
use crate::PactSpecification;
use crate::path_exp::{DocPath, PathToken};
use crate::path_exp::DocPath;
#[cfg(feature = "datetime")] use crate::time_utils::{parse_pattern, to_chrono_pattern};

#[cfg(feature = "datetime")] pub mod datetime_expressions;
Expand Down Expand Up @@ -1227,71 +1225,6 @@ pub struct JsonHandler {
pub value: Value
}

impl JsonHandler {
fn query_object_graph(&self, path_exp: &Vec<PathToken>, tree: &mut Arena<String>, root: NodeId, body: Value) {
let mut body_cursor = body;
let mut it = path_exp.iter();
let mut node_cursor = root;
loop {
match it.next() {
Some(token) => {
match token {
&PathToken::Field(ref name) => {
match body_cursor.clone().as_object() {
Some(map) => match map.get(name) {
Some(val) => {
node_cursor = node_cursor.append_value(name.clone(), tree);
body_cursor = val.clone();
},
None => return
},
None => return
}
},
&PathToken::Index(index) => {
match body_cursor.clone().as_array() {
Some(list) => if list.len() > index {
node_cursor = node_cursor.append_value(format!("{}", index), tree);
body_cursor = list[index].clone();
},
None => return
}
}
&PathToken::Star => {
match body_cursor.clone().as_object() {
Some(map) => {
let remaining = it.by_ref().cloned().collect();
for (key, val) in map {
let node = node_cursor.append_value(key.clone(), tree);
body_cursor = val.clone();
self.query_object_graph(&remaining, tree, node, val.clone());
}
},
None => return
}
},
&PathToken::StarIndex => {
match body_cursor.clone().as_array() {
Some(list) => {
let remaining = it.by_ref().cloned().collect();
for (index, val) in list.iter().enumerate() {
let node = node_cursor.append_value(format!("{}", index), tree);
body_cursor = val.clone();
self.query_object_graph(&remaining, tree, node,val.clone());
}
},
None => return
}
},
_ => ()
}
},
None => break
}
}
}
}

impl ContentTypeHandler<Value> for JsonHandler {
fn process_body(
&mut self,
Expand All @@ -1316,21 +1249,7 @@ impl ContentTypeHandler<Value> for JsonHandler {
context: &HashMap<&str, Value>,
matcher: &Box<dyn VariantMatcher + Send + Sync>,
) {
let path_exp = key;
let mut tree = Arena::new();
let root = tree.new_node("".into());
self.query_object_graph(path_exp.tokens(), &mut tree, root, self.value.clone());
let expanded_paths = root.descendants(&tree).fold(Vec::<String>::new(), |mut acc, node_id| {
let node = tree.index(node_id);
if !node.get().is_empty() && node.first_child().is_none() {
let path: Vec<String> = node_id.ancestors(&tree).map(|n| format!("{}", tree.index(n).get())).collect();
if path.len() == path_exp.len() {
acc.push(path.iter().rev().join("/"));
}
}
acc
});

let expanded_paths = resolve_path(&self.value, key);
if !expanded_paths.is_empty() {
for pointer_str in expanded_paths {
match self.value.pointer_mut(&pointer_str) {
Expand All @@ -1341,7 +1260,7 @@ impl ContentTypeHandler<Value> for JsonHandler {
None => ()
}
}
} else if path_exp.len() == 1 {
} else if key.len() == 1 {
match generator.generate_value(&self.value.clone(), context, matcher) {
Ok(new_value) => self.value = new_value,
Err(_) => ()
Expand Down
173 changes: 173 additions & 0 deletions rust/pact_models/src/json_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
use std::collections::{BTreeMap, HashMap};
use std::hash::{Hash, Hasher};
use std::ops::Index;
use std::str::FromStr;

use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use indextree::{Arena, NodeId};
use itertools::Itertools;
use serde::Deserialize;
use serde_json::{self, json, Map, Value};

use crate::bodies::OptionalBody;
use crate::content_types::{ContentType, detect_content_type_from_string};
use crate::headers::parse_header;
use crate::path_exp::{DocPath, PathToken};

/// Trait to convert a JSON structure to a number
pub trait JsonToNum<T> {
Expand Down Expand Up @@ -228,6 +231,88 @@ pub fn is_empty(value: &Value) -> bool {
}
}

/// Resolve the path expression against the JSON value, returning a list of JSON pointer values
/// that match.
pub fn resolve_path(value: &Value, expression: &DocPath) -> Vec<String> {
let mut tree = Arena::new();
let root = tree.new_node("".into());
query_object_graph(expression.tokens(), &mut tree, root, value.clone());
let expanded_paths = root.descendants(&tree).fold(Vec::<String>::new(), |mut acc, node_id| {
let node = tree.index(node_id);
if !node.get().is_empty() && node.first_child().is_none() {
let path: Vec<String> = node_id.ancestors(&tree).map(|n| format!("{}", tree.index(n).get())).collect();
if path.len() == expression.len() {
acc.push(path.iter().rev().join("/"));
}
}
acc
});
expanded_paths
}

fn query_object_graph(path_exp: &Vec<PathToken>, tree: &mut Arena<String>, root: NodeId, body: Value) {
let mut body_cursor = body;
let mut it = path_exp.iter();
let mut node_cursor = root;
loop {
match it.next() {
Some(token) => {
match token {
&PathToken::Field(ref name) => {
match body_cursor.clone().as_object() {
Some(map) => match map.get(name) {
Some(val) => {
node_cursor = node_cursor.append_value(name.clone(), tree);
body_cursor = val.clone();
},
None => return
},
None => return
}
},
&PathToken::Index(index) => {
match body_cursor.clone().as_array() {
Some(list) => if list.len() > index {
node_cursor = node_cursor.append_value(format!("{}", index), tree);
body_cursor = list[index].clone();
},
None => return
}
}
&PathToken::Star => {
match body_cursor.clone().as_object() {
Some(map) => {
let remaining = it.by_ref().cloned().collect();
for (key, val) in map {
let node = node_cursor.append_value(key.clone(), tree);
body_cursor = val.clone();
query_object_graph(&remaining, tree, node, val.clone());
}
},
None => return
}
},
&PathToken::StarIndex => {
match body_cursor.clone().as_array() {
Some(list) => {
let remaining = it.by_ref().cloned().collect();
for (index, val) in list.iter().enumerate() {
let node = node_cursor.append_value(format!("{}", index), tree);
body_cursor = val.clone();
query_object_graph(&remaining, tree, node,val.clone());
}
},
None => return
}
},
_ => ()
}
},
None => break
}
}
}

#[cfg(test)]
mod tests {
use expectest::expect;
Expand Down Expand Up @@ -516,4 +601,92 @@ mod tests {
"Date".to_string() => vec!["Sun, 12 Mar 2023 01:21:35 GMT".to_string()]
}));
}

#[test]
fn resolve_path_with_root() {
expect!(resolve_path(&Value::Null, &DocPath::root())).to(be_equal_to::<Vec<String>>(vec![]));
expect!(resolve_path(&Value::Bool(true), &DocPath::root())).to(be_equal_to::<Vec<String>>(vec![]));
}

#[test]
fn resolve_path_with_field() {
let path = DocPath::new_unwrap("$.a");
let json = Value::Null;
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));

let json = Value::Bool(true);
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));

let json = json!({
"a": 100,
"b": 200
});
expect!(resolve_path(&json, &path)).to(be_equal_to(vec!["/a"]));

let json = json!([
{
"a": 100,
"b": 200
}
]);
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));
}

#[test]
fn resolve_path_with_index() {
let path = DocPath::new_unwrap("$[0]");
let json = Value::Null;
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));

let json = Value::Bool(true);
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));

let json = json!({
"a": 100,
"b": 200
});
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));

let json = json!([
{
"a": 100,
"b": 200
}
]);
expect!(resolve_path(&json, &path)).to(be_equal_to(vec!["/0"]));
let path = DocPath::new_unwrap("$[0].b");
expect!(resolve_path(&json, &path)).to(be_equal_to(vec!["/0/b"]));
}

#[test]
fn resolve_path_with_star() {
let path = DocPath::new_unwrap("$.*");
let json = Value::Null;
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));

let json = Value::Bool(true);
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));

let json = json!({
"a": 100,
"b": 200
});
expect!(resolve_path(&json, &path)).to(be_equal_to(vec!["/a", "/b"]));

let json = json!([
{
"a": 100,
"b": 200
},
{
"a": 200,
"b": 300
}
]);
expect!(resolve_path(&json, &path)).to(be_equal_to::<Vec<String>>(vec![]));
let path = DocPath::new_unwrap("$[*]");
expect!(resolve_path(&json, &path)).to(be_equal_to(vec!["/0", "/1"]));
let path = DocPath::new_unwrap("$[*].b");
expect!(resolve_path(&json, &path)).to(be_equal_to(vec!["/0/b", "/1/b"]));
}
}

0 comments on commit a4ecc0b

Please sign in to comment.