use crate::core::{Reply, Request};
use derive_more::{Display, Error};
use jsonpath_lib as jsonpath;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Display, Error)]
pub enum TransformRequestError {
RequestToJsonFailed(serde_json::Error),
JsonToRequestFailed(serde_json::Error),
#[display(fmt = "{:?}", _0)]
ExtractingReplyValueFailed(#[error(ignore)] jsonpath::JsonPathError),
ReplyValueMissing {
path: String,
},
ReplyValueNotScalar {
path: String,
},
#[display(fmt = "{:?}", _0)]
ReplacementFailed(#[error(ignore)] jsonpath::JsonPathError),
}
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct LazilyTransformedRequest {
pub rules: Vec<TransformRule>,
pub raw_request: Request,
}
impl LazilyTransformedRequest {
pub fn new(raw_request: Request, rules: Vec<TransformRule>) -> Self {
Self { rules, raw_request }
}
pub fn into_raw_request(self) -> Request {
self.raw_request
}
pub fn transform_with_reply(
&self,
reply: &Reply,
) -> Result<Request, TransformRequestError> {
let mut value = serde_json::to_value(&self.raw_request)
.map_err(TransformRequestError::RequestToJsonFailed)?;
let reply_value = serde_json::to_value(reply)
.map_err(TransformRequestError::RequestToJsonFailed)?;
for rule in self.rules.iter() {
let mut new_values = jsonpath::select(&reply_value, &rule.value)
.map_err(TransformRequestError::ExtractingReplyValueFailed)?;
if new_values.is_empty() {
return Err(TransformRequestError::ReplyValueMissing {
path: rule.value.clone(),
});
} else if new_values.len() > 1 {
return Err(TransformRequestError::ReplyValueNotScalar {
path: rule.value.clone(),
});
}
let new_value = new_values.drain(0..=0).last();
value = jsonpath::replace_with(value, &rule.path, &mut |_| {
new_value.cloned()
})
.map_err(TransformRequestError::ReplacementFailed)?;
}
serde_json::from_value(value)
.map_err(TransformRequestError::JsonToRequestFailed)
}
}
impl crate::core::SchemaInfo for LazilyTransformedRequest {}
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct TransformRule {
pub path: String,
pub value: String,
}
impl crate::core::SchemaInfo for TransformRule {}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::reply::{CustomArgs, FileOpenedArgs};
#[test]
fn transform_with_reply_should_fail_if_rule_value_not_found() {
let raw_request = Request::ReadFile(Default::default());
let reply = Reply::FileOpened(FileOpenedArgs {
id: 123,
sig: 456,
..Default::default()
});
let lazy_request = LazilyTransformedRequest {
raw_request: raw_request.clone(),
rules: vec![TransformRule {
path: String::from("$.payload.id"),
value: String::from("$.payload.missing_field"),
}],
};
match lazy_request.transform_with_reply(&reply) {
Err(TransformRequestError::ReplyValueMissing { .. }) => (),
x => panic!("Unexpected request: {:?}", x),
}
}
#[test]
fn transform_with_reply_should_fail_if_rule_value_not_scalar() {
let raw_request = Request::ReadFile(Default::default());
let reply = Reply::Custom(CustomArgs {
data: vec![0, 1, 2],
});
let lazy_request = LazilyTransformedRequest {
raw_request: raw_request.clone(),
rules: vec![TransformRule {
path: String::from("$.payload.id"),
value: String::from("$.payload.data[*]"),
}],
};
match lazy_request.transform_with_reply(&reply) {
Err(TransformRequestError::ReplyValueNotScalar { .. }) => (),
x => panic!("Unexpected request: {:?}", x),
}
}
#[test]
fn transform_with_reply_should_fail_if_rule_value_not_same_type_as_path() {
let raw_request = Request::Custom(Default::default());
let reply = Reply::FileOpened(FileOpenedArgs {
id: 123,
sig: 456,
..Default::default()
});
let lazy_request = LazilyTransformedRequest {
raw_request: raw_request.clone(),
rules: vec![TransformRule {
path: String::from("$.payload.data"),
value: String::from("$.payload.id"),
}],
};
match lazy_request.transform_with_reply(&reply) {
Err(TransformRequestError::JsonToRequestFailed(_)) => (),
x => panic!("Unexpected request: {:?}", x),
}
}
#[test]
fn transform_with_reply_should_return_raw_request_if_rule_path_missing() {
let raw_request = Request::ReadFile(Default::default());
let reply = Reply::FileOpened(FileOpenedArgs {
id: 123,
sig: 456,
..Default::default()
});
let lazy_request = LazilyTransformedRequest {
raw_request: raw_request.clone(),
rules: vec![TransformRule {
path: String::from("$.payload.missing_field"),
value: String::from("$.payload.id"),
}],
};
match lazy_request.transform_with_reply(&reply) {
Ok(request) => {
assert_eq!(request, raw_request, "Raw request altered")
}
x => panic!("Unexpected request: {:?}", x),
}
}
#[test]
fn transform_with_reply_should_succeed_if_able_to_replace_path_with_value()
{
let raw_request = Request::ReadFile(Default::default());
let reply = Reply::FileOpened(FileOpenedArgs {
id: 123,
sig: 456,
..Default::default()
});
let lazy_request = LazilyTransformedRequest {
raw_request: raw_request.clone(),
rules: vec![TransformRule {
path: String::from("$.payload.id"),
value: String::from("$.payload.id"),
}],
};
let transformed_request = lazy_request
.transform_with_reply(&reply)
.expect("Failed to transform");
match transformed_request {
Request::ReadFile(args) => {
assert_eq!(args.id, 123);
assert_ne!(args.sig, 456);
}
x => panic!("Unexpected request: {:?}", x),
}
}
#[test]
fn transform_with_reply_should_apply_rules_in_sequence() {
let raw_request = Request::ReadFile(Default::default());
let reply = Reply::FileOpened(FileOpenedArgs {
id: 123,
sig: 456,
..Default::default()
});
let lazy_request = LazilyTransformedRequest {
raw_request: raw_request.clone(),
rules: vec![
TransformRule {
path: String::from("$.payload.id"),
value: String::from("$.payload.id"),
},
TransformRule {
path: String::from("$.payload.sig"),
value: String::from("$.payload.sig"),
},
],
};
let transformed_request = lazy_request
.transform_with_reply(&reply)
.expect("Failed to transform");
match transformed_request {
Request::ReadFile(args) => {
assert_eq!(args.id, 123);
assert_eq!(args.sig, 456);
}
x => panic!("Unexpected request: {:?}", x),
}
}
}