use crate::{
cut::Register,
error::FrError,
frame::*,
utils::{new_mut_selector, select_value, MutSelector},
};
use serde::{Deserialize, Serialize};
use serde_hashkey::{
to_key_with_ordered_float as to_key, Error as HashError, Key, OrderedFloatPolicy as Hash,
};
use serde_json::{json, to_value, Map, Value};
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap, HashSet},
};
const INVALID_INSTRUCTION_TYPE_ERR: &str =
"Frame write instruction did not correspond to a string object";
const MISSING_SELECTION_ERR: &str = "selection missing from Frame body";
#[derive(Serialize, Clone, Deserialize, Debug)]
pub struct Response<'a> {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<Value>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub etc: Option<Value>, #[serde(skip_serializing)]
pub validation: Option<Validation<'a>>,
pub status: u32,
}
impl<'a> Response<'a> {
fn to_frame_value(&self) -> Result<Value, FrError> {
Ok(json!({"response":to_value(self)?}))
}
pub(crate) fn validate(&self) -> Result<(), FrError> {
if self.validation.is_none() {
return Ok(());
}
for k in self.validation.as_ref().unwrap().keys() {
if !k.trim_start_matches('.').starts_with("'response'.'body'") {
return Err(FrError::ReadInstruction(
"validation options currently only support the response body",
));
}
}
Ok(())
}
pub fn match_payload_response(
&self,
set: &'a InstructionSet,
payload_response: &Response,
) -> Result<Option<HashMap<&'a str, Value>>, FrError> {
let frame_response: Value = self.to_frame_value()?;
let payload_response: Value = payload_response.to_frame_value()?;
let mut write_matches: HashMap<&str, Value> = HashMap::new();
for (k, query) in set.writes.iter() {
let frame_str = match select_value(&frame_response, query) {
Ok(Value::String(v)) => Ok(v),
Ok(_) => Err(FrError::FrameParsef(
INVALID_INSTRUCTION_TYPE_ERR,
query.to_string(),
)),
Err(e) => Err(e),
}?;
let payload_val = select_value(&payload_response, query)?;
if let Value::String(payload_str) = &payload_val {
let write_match = Register::write_match(k, &frame_str, payload_str)?;
if let Some(mat) = write_match {
write_matches.insert(k, to_value(mat)?);
}
continue;
}
Register::expect_standalone_var(k, &frame_str)?;
write_matches.insert(k, payload_val);
}
if write_matches.iter().next().is_some() {
return Ok(Some(write_matches));
}
Ok(None)
}
pub fn apply_validation(&mut self, other: &mut Self) -> Result<(), FrError> {
if self.body.is_none() || other.body.is_none() || self.validation.is_none() {
return Ok(());
}
for (k, v) in self.validation.as_ref().unwrap().iter() {
if !v.partial && !v.unordered {
continue;
}
let selector = new_mut_selector(strip_query(k))?;
if v.unordered {
v.apply_unordered(
k,
&selector,
self.body.as_mut().unwrap(),
other.body.as_mut().unwrap(),
)?;
}
if v.partial {
v.apply_partial(
k,
&selector,
self.body.as_mut().unwrap(),
other.body.as_mut().unwrap(),
)?;
}
}
self.validation = None;
Ok(())
}
}
fn strip_query(query: &str) -> &str {
let body_query = query
.trim_start_matches('.')
.trim_start_matches("'response'.'body'");
if body_query.is_empty() {
return ".";
}
body_query
}
impl Default for Response<'_> {
fn default() -> Self {
Self {
body: None,
etc: Some(json!({})),
validation: None,
status: 0,
}
}
}
impl<'a> PartialEq for Response<'a> {
fn eq(&self, other: &Self) -> bool {
self.body.eq(&other.body) && self.etc.eq(&other.etc) && self.status.eq(&other.status)
}
}
impl<'a> Eq for Response<'a> {}
type Validation<'a> = BTreeMap<Cow<'a, str>, Validator>;
#[derive(Serialize, Clone, Deserialize, Default, Debug, PartialEq)]
#[serde(default)]
pub struct Validator {
partial: bool,
unordered: bool,
}
impl Validator {
fn apply_partial(
&self,
query: &str,
selector: &MutSelector,
self_body: &mut Value,
other_body: &mut Value,
) -> Result<(), FrError> {
let selection = selector(self_body)
.ok_or_else(|| FrError::ReadInstructionf(MISSING_SELECTION_ERR, query.to_string()))?;
match selection {
Value::Object(o) => {
let preserve_keys = o.keys().collect::<Vec<&String>>();
let other_selection = match selector(other_body) {
Some(Value::Object(o)) => o,
_ => return Ok(()),
};
for k in other_selection
.keys() .filter(|k| !preserve_keys.contains(k))
.cloned()
.collect::<Vec<String>>()
{
other_selection.remove(&k);
}
}
Value::Array(self_selection) => {
let other_selection = match selector(other_body) {
Some(Value::Array(o)) => o,
_ => return Ok(()),
};
let self_len = self_selection.len();
if self_len >= other_selection.len() {
return Ok(());
}
for i in (0..other_selection.len()).into_iter() {
if i + self_len > other_selection.len() {
return Ok(());
}
if &other_selection[i..i + self_len] == self_selection.as_slice() {
*other_selection = self_selection.clone();
return Ok(()); }
}
}
_ => {
return Err(FrError::ReadInstruction(
"validation selectors must point to a JSON object or array",
))
}
}
Ok(())
}
fn apply_unordered(
&self,
query: &str,
selector: &MutSelector,
self_body: &mut Value,
other_body: &mut Value,
) -> Result<(), FrError> {
let selection = selector(self_body)
.ok_or_else(|| FrError::ReadInstructionf(MISSING_SELECTION_ERR, query.to_string()))?;
match selection {
Value::Object(_) => Ok(()),
Value::Array(self_selection) => {
let other_selection = match selector(other_body) {
Some(Value::Array(o)) => o,
_ => return Ok(()),
};
let mut other_idx_map: HashMap<Key<Hash>, Vec<usize>> = HashMap::new();
other_selection
.iter()
.enumerate()
.try_for_each(|(i, v)| match hash_value(v) {
Ok(k) => {
other_idx_map.entry(k).or_insert_with(Vec::new).push(i);
Ok(())
}
Err(e) => Err(e),
})?;
let mut placeholder_indices: HashSet<usize> = HashSet::new();
let mut sink: BTreeMap<usize, Value> = BTreeMap::new();
for (to_idx, v) in self_selection.iter().enumerate() {
let v_hash = hash_value(v)?;
if let Some(other_indices) = other_idx_map.get_mut(&v_hash) {
let from_idx = other_indices.remove(0);
let mut to_value = Value::Null;
std::mem::swap(&mut to_value, &mut other_selection[from_idx]);
sink.insert(to_idx, to_value);
placeholder_indices.insert(from_idx);
}
}
if sink.is_empty() {
return Ok(());
}
let mut i = 0;
other_selection.retain(|_| (!placeholder_indices.contains(&i), i += 1).0);
other_selection.splice(0..0, sink.into_iter().map(|(_, v)| v));
Ok(())
}
_ => Err(FrError::ReadInstruction(
"validation selectors must point to a JSON object or array",
)),
}
}
}
fn hash_value(value: &Value) -> Result<Key<Hash>, HashError> {
if let Value::Object(obj_map) = value {
let null_map: Map<String, Value> =
obj_map.keys().map(|k| (k.clone(), Value::Null)).collect();
return to_key(&null_map);
}
to_key(value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{from, to};
use rstest::*;
use serde_json::json;
#[test]
fn test_match_payload_response() {
let frame = Frame {
protocol: Protocol::GRPC,
cut: InstructionSet {
reads: from![],
writes: to! ({
"USER_ID"=> "'response'.'body'.'id'",
"CREATED"=> "'response'.'body'.'created'",
"ignore"=> "'response'.'body'.'array'.[0].'ignore'"
}),
hydrate_writes: true,
},
request: Request {
..Default::default()
},
response: Response {
body: Some(json!({
"id": "${USER_ID}",
"created": "${CREATED}",
"array": [{"ignore":"${ignore}"}]
})),
status: 0,
..Default::default()
},
};
let payload_response = Response {
body: Some(json!({
"id": "ID_010101",
"created": 101010,
"array": [{"ignore": "value"}]
})),
status: 0,
..Default::default()
};
let mat = frame
.response
.match_payload_response(&frame.cut, &payload_response)
.unwrap();
let mut expected_match = HashMap::new();
expected_match.insert("USER_ID", to_value("ID_010101").unwrap());
expected_match.insert("CREATED", to_value(101010).unwrap());
expected_match.insert("ignore", to_value("value").unwrap());
assert_eq!(expected_match, mat.unwrap());
}
const SIMPLE_FRAME: &str = r#"{ "body": %s, "status": 200 }"#;
const PARTIAL_FRAME: &str = r#"
{
"validation": {
"'response'.'body'": {
"partial": true
}
},
"body": %s,
"status": 200
}
"#;
fn partial_case(case: u32) -> (&'static str, &'static str, bool) {
let with_obj = r#"{"A":true,"B":true,"C":true}"#;
let with_arr = r#"["A","B","C"]"#;
match case {
1 => (with_obj, r#"{"A":true,"B":true,"C":true}"#, true),
2 => (with_obj, r#"{"A":true,"B":true,"C":true,"D":true}"#, true),
3 => (with_obj, r#"{"B":true,"C":true,"D":true}"#, false),
4 => (
r#"{"validation":{"'response'.'body'":{"partial":false}},
"body":{"A": true,"B": true, "C": true}}"#,
r#"{"B": true,"C": true, "D": true}"#,
false,
),
5 => (with_arr, r#"["A", "B", "C"]"#, true),
6 => (with_arr, r#"["other_value", false, "A", "B", "C"]"#, true),
7 => (with_arr, r#"["other_value", false, "B", "C"]"#, false),
_ => panic!(),
}
}
#[rstest(
t_case,
case(partial_case(1)),
case(partial_case(2)),
case(partial_case(3)),
case(partial_case(4)),
case(partial_case(5)),
case(partial_case(6)),
case(partial_case(7))
)]
fn test_partial_validation(t_case: (&str, &str, bool)) {
let self_response = str::replace(PARTIAL_FRAME, "%s", t_case.0);
let other_response = str::replace(SIMPLE_FRAME, "%s", t_case.1);
let mut frame: Response = serde_json::from_str(&self_response).unwrap();
let mut other_frame: Response = serde_json::from_str(&other_response).unwrap();
let should_match = t_case.2;
frame.apply_validation(&mut other_frame).unwrap();
if should_match {
pretty_assertions::assert_eq!(frame, other_frame);
} else {
pretty_assertions::assert_ne!(frame, other_frame);
}
}
const UNORDERED_FRAME: &str = r#"
{
"validation": {
"'response'.'body'": {
"unordered": true
}
},
"body": %s,
"status": 200
}
"#;
fn unordered_case(case: u32) -> (&'static str, &'static str, bool) {
let map_arr = r#"{"A":true,"B":true,"C":true}"#;
let string_arr = r#"["A","B","C"]"#;
let with_f32 = r#"["A","B","C",13.37]"#;
let with_dupes = r#"["A","B","C","A","A"]"#;
match case {
1 => (map_arr, r#"{"A":true,"B":true,"C":true}"#, true),
2 => (map_arr, r#"{"A":true,"B":false,"C":true}"#, false),
3 => (map_arr, r#"{"A":true,"B":true,"C":true,"D":true}"#, false),
4 => (map_arr, r#"{"A":true,"B":true}"#, false),
5 => (map_arr, r#"{"B":true,"C":true,"A":true}"#, true),
6 => (string_arr, r#"["A","B","C"]"#, true),
7 => (string_arr, r#"["other_value",false,"A","B","C"]"#, false),
8 => (string_arr, r#"[false,false,"A","B","C"]"#, false),
9 => (string_arr, r#"["B","A","C"]"#, true),
10 => (string_arr, r#"["B","A","D","C"]"#, false),
11 => (with_f32, r#"["C",13.37,"B","A"]"#, true),
12 => (with_dupes, r#"["A","C","A","B","A"]"#, true),
_ => panic!(),
}
}
#[rstest(
t_case,
case(unordered_case(1)),
case(unordered_case(2)),
case(unordered_case(3)),
case(unordered_case(4)),
case(unordered_case(5)),
case(unordered_case(6)),
case(unordered_case(7)),
case(unordered_case(8)),
case(unordered_case(9)),
case(unordered_case(10)),
case(unordered_case(11)),
case(unordered_case(12))
)]
fn test_unordered_validation(t_case: (&str, &str, bool)) {
let self_response = str::replace(UNORDERED_FRAME, "%s", t_case.0);
let other_response = str::replace(SIMPLE_FRAME, "%s", t_case.1);
let mut frame: Response = serde_json::from_str(&self_response).unwrap();
let mut other_frame: Response = serde_json::from_str(&other_response).unwrap();
let should_match = t_case.2;
frame.apply_validation(&mut other_frame).unwrap();
if should_match {
pretty_assertions::assert_eq!(frame, other_frame);
} else {
pretty_assertions::assert_ne!(frame, other_frame);
}
}
const PARTIAL_UNORDERED: &str = r#"
{
"validation": {
"'response'.'body'": {
"partial": true,
"unordered": true
}
},
"body": %s,
"status": 200
}
"#;
fn partial_unordered_case(case: u32) -> (&'static str, &'static str, &'static str) {
match case {
1 => (
r#"{"A":true,"B":true,"C":true}"#,
r#"{"A":true,"B":true,"C":true}"#,
r#"{"A":true,"B":true,"C":true}"#,
),
2 => (
r#"{"A":true,"B":[1,0],"C":true}"#,
r#"{"A":true,"C":true,"B":[0,1]}"#,
r#"{"A":true,"B":[0,1],"C":true}"#,
),
3 => (
r#"{"A":true,"B":true,"C":true}"#,
r#"{"D":true,"B":true,"C":true,"A":true}"#,
r#"{"A":true,"B":true,"C":true}"#,
),
4 => (
r#"{"A":true,"B":true,"C":true}"#,
r#"{"B":true,"A":true,"A":true}"#,
r#"{"A":true,"B":true}"#,
),
5 => (
r#"{"A":true,"B":true,"C":true}"#,
r#"{"B":true,"C":true,"A":true}"#,
r#"{"A":true,"B":true,"C":true}"#,
),
6 => (r#"["A","B","C"]"#, r#"["F","C","C"]"#, r#"["C","F","C"]"#),
7 => (
r#"["A","B","C"]"#,
r#"["other_value",false,"B","A","C","B"]"#,
r#"["A","B","C"]"#,
),
8 => (
r#"["A","B","C"]"#,
r#"[false,false,"A","B","C"]"#,
r#"["A","B","C"]"#,
),
9 => (
r#"[0,"A",0,"C"]"#,
r#"["B","B","A","C","C","A"]"#,
r#"["A","C","B","B","C","A"]"#,
),
10 => (
r#"["A","B","C"]"#,
r#"["B","A","D","C"]"#,
r#"["A","B","C"]"#,
),
11 => (
r#"["A","B","C",13.37]"#,
r#"["C",13.37,"B","A"]"#,
r#"["A","B","C",13.37]"#,
),
12 => (
r#"["A","B","C","A","A"]"#,
r#"["A","C","A","B","A"]"#,
r#"["A","B","C","A","A"]"#,
),
13 => (
r#"[0,{"A":1},1,4,5]"#,
r#"[1,{"A":0},0,2,3]"#,
r#"[0,{"A":0},1,2,3]"#,
),
14 => (
r#"[0,{"A":false,"B":true},1]"#,
r#"[1,{"B":true},0]"#,
r#"[0,1,{"B":true}]"#,
),
_ => panic!(),
}
}
#[rstest(
t_case,
case(partial_unordered_case(1)),
case(partial_unordered_case(2)),
case(partial_unordered_case(3)),
case(partial_unordered_case(4)),
case(partial_unordered_case(5)),
case(partial_unordered_case(6)),
case(partial_unordered_case(7)),
case(partial_unordered_case(8)),
case(partial_unordered_case(9)),
case(partial_unordered_case(10)),
case(partial_unordered_case(11)),
case(partial_unordered_case(12)),
case(partial_unordered_case(13)),
case(partial_unordered_case(14))
)]
fn test_partial_unordered_validation(t_case: (&str, &str, &str)) {
let self_response = str::replace(PARTIAL_UNORDERED, "%s", t_case.0);
let other_response = str::replace(SIMPLE_FRAME, "%s", t_case.1);
let expected_response = str::replace(SIMPLE_FRAME, "%s", t_case.2);
let mut frame: Response = serde_json::from_str(&self_response).unwrap();
let mut other_frame: Response = serde_json::from_str(&other_response).unwrap();
let expected_frame: Response = serde_json::from_str(&expected_response).unwrap();
frame.apply_validation(&mut other_frame).unwrap();
pretty_assertions::assert_eq!(other_frame, expected_frame);
}
}