Skip to main content

over_there/core/msg/content/request/
transform.rs

1use crate::core::{Reply, Request};
2use derive_more::{Display, Error};
3use jsonpath_lib as jsonpath;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7/// Represents an error that can occur when transforming request replyd on
8/// prior results from a sequential operation
9#[derive(Debug, Display, Error)]
10pub enum TransformRequestError {
11    RequestToJsonFailed(serde_json::Error),
12    JsonToRequestFailed(serde_json::Error),
13    #[display(fmt = "{:?}", _0)]
14    ExtractingReplyValueFailed(#[error(ignore)] jsonpath::JsonPathError),
15    ReplyValueMissing {
16        path: String,
17    },
18    ReplyValueNotScalar {
19        path: String,
20    },
21    #[display(fmt = "{:?}", _0)]
22    ReplacementFailed(#[error(ignore)] jsonpath::JsonPathError),
23}
24
25/// Represents request that will be transformed at runtime replyd on some
26/// prior input
27#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
28pub struct LazilyTransformedRequest {
29    /// Represents collection of transformation rules to apply to raw request
30    pub rules: Vec<TransformRule>,
31
32    /// Represents request prior to being transformed
33    pub raw_request: Request,
34}
35
36impl LazilyTransformedRequest {
37    pub fn new(raw_request: Request, rules: Vec<TransformRule>) -> Self {
38        Self { rules, raw_request }
39    }
40
41    /// Converts to the raw request with no transformations applied
42    pub fn into_raw_request(self) -> Request {
43        self.raw_request
44    }
45
46    /// Performs the transformation of request by applying all rules in order
47    /// and returning the resulting request
48    pub fn transform_with_reply(
49        &self,
50        reply: &Reply,
51    ) -> Result<Request, TransformRequestError> {
52        let mut value = serde_json::to_value(&self.raw_request)
53            .map_err(TransformRequestError::RequestToJsonFailed)?;
54        let reply_value = serde_json::to_value(reply)
55            .map_err(TransformRequestError::RequestToJsonFailed)?;
56
57        for rule in self.rules.iter() {
58            // For now, we're assuming that the replacement value must be
59            // a singular value (not replacing with an array, object, etc)
60            let mut new_values = jsonpath::select(&reply_value, &rule.value)
61                .map_err(TransformRequestError::ExtractingReplyValueFailed)?;
62            if new_values.is_empty() {
63                return Err(TransformRequestError::ReplyValueMissing {
64                    path: rule.value.clone(),
65                });
66            } else if new_values.len() > 1 {
67                return Err(TransformRequestError::ReplyValueNotScalar {
68                    path: rule.value.clone(),
69                });
70            }
71            let new_value = new_values.drain(0..=0).last();
72
73            value = jsonpath::replace_with(value, &rule.path, &mut |_| {
74                new_value.cloned()
75            })
76            .map_err(TransformRequestError::ReplacementFailed)?;
77        }
78
79        serde_json::from_value(value)
80            .map_err(TransformRequestError::JsonToRequestFailed)
81    }
82}
83
84impl crate::core::SchemaInfo for LazilyTransformedRequest {}
85
86/// Represents a transformation to apply against some request; uses syntax
87/// like JSONPath in that $.field can be used to reference the fields of the
88/// objects
89#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
90pub struct TransformRule {
91    /// Represents the key (at a JSON level) to transform for some request;
92    /// this will be interpolated using $ to represent the root of the current
93    /// object (request)
94    pub path: String,
95
96    /// Represents the new value to apply to the key; this will be interpolated
97    /// replyd on a previous result if present using $ to represent the root
98    /// of the previous output request as a JSON object and dot notation for
99    /// the nested keys
100    pub value: String,
101}
102
103impl crate::core::SchemaInfo for TransformRule {}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::core::reply::{CustomArgs, FileOpenedArgs};
109
110    #[test]
111    fn transform_with_reply_should_fail_if_rule_value_not_found() {
112        let raw_request = Request::ReadFile(Default::default());
113        let reply = Reply::FileOpened(FileOpenedArgs {
114            id: 123,
115            sig: 456,
116            ..Default::default()
117        });
118
119        let lazy_request = LazilyTransformedRequest {
120            raw_request: raw_request.clone(),
121            rules: vec![TransformRule {
122                // Replace id of raw request
123                path: String::from("$.payload.id"),
124
125                // Apply missing field from reply request
126                value: String::from("$.payload.missing_field"),
127            }],
128        };
129
130        match lazy_request.transform_with_reply(&reply) {
131            Err(TransformRequestError::ReplyValueMissing { .. }) => (),
132            x => panic!("Unexpected request: {:?}", x),
133        }
134    }
135
136    #[test]
137    fn transform_with_reply_should_fail_if_rule_value_not_scalar() {
138        let raw_request = Request::ReadFile(Default::default());
139        let reply = Reply::Custom(CustomArgs {
140            data: vec![0, 1, 2],
141        });
142
143        let lazy_request = LazilyTransformedRequest {
144            raw_request: raw_request.clone(),
145            rules: vec![TransformRule {
146                // Replace id of raw request
147                path: String::from("$.payload.id"),
148
149                // Apply array data field from reply request
150                value: String::from("$.payload.data[*]"),
151            }],
152        };
153
154        match lazy_request.transform_with_reply(&reply) {
155            Err(TransformRequestError::ReplyValueNotScalar { .. }) => (),
156            x => panic!("Unexpected request: {:?}", x),
157        }
158    }
159
160    #[test]
161    fn transform_with_reply_should_fail_if_rule_value_not_same_type_as_path() {
162        let raw_request = Request::Custom(Default::default());
163        let reply = Reply::FileOpened(FileOpenedArgs {
164            id: 123,
165            sig: 456,
166            ..Default::default()
167        });
168
169        let lazy_request = LazilyTransformedRequest {
170            raw_request: raw_request.clone(),
171            rules: vec![TransformRule {
172                // Replace data of raw request
173                path: String::from("$.payload.data"),
174
175                // Apply id from reply request
176                value: String::from("$.payload.id"),
177            }],
178        };
179
180        match lazy_request.transform_with_reply(&reply) {
181            Err(TransformRequestError::JsonToRequestFailed(_)) => (),
182            x => panic!("Unexpected request: {:?}", x),
183        }
184    }
185
186    #[test]
187    fn transform_with_reply_should_return_raw_request_if_rule_path_missing() {
188        let raw_request = Request::ReadFile(Default::default());
189        let reply = Reply::FileOpened(FileOpenedArgs {
190            id: 123,
191            sig: 456,
192            ..Default::default()
193        });
194
195        let lazy_request = LazilyTransformedRequest {
196            raw_request: raw_request.clone(),
197            rules: vec![TransformRule {
198                // Replace missing field of raw request
199                path: String::from("$.payload.missing_field"),
200
201                // Apply id from reply request
202                value: String::from("$.payload.id"),
203            }],
204        };
205
206        match lazy_request.transform_with_reply(&reply) {
207            Ok(request) => {
208                assert_eq!(request, raw_request, "Raw request altered")
209            }
210            x => panic!("Unexpected request: {:?}", x),
211        }
212    }
213
214    #[test]
215    fn transform_with_reply_should_succeed_if_able_to_replace_path_with_value()
216    {
217        let raw_request = Request::ReadFile(Default::default());
218        let reply = Reply::FileOpened(FileOpenedArgs {
219            id: 123,
220            sig: 456,
221            ..Default::default()
222        });
223
224        let lazy_request = LazilyTransformedRequest {
225            raw_request: raw_request.clone(),
226            rules: vec![TransformRule {
227                // Replace id of raw request
228                path: String::from("$.payload.id"),
229
230                // Apply id from reply request
231                value: String::from("$.payload.id"),
232            }],
233        };
234
235        let transformed_request = lazy_request
236            .transform_with_reply(&reply)
237            .expect("Failed to transform");
238
239        match transformed_request {
240            Request::ReadFile(args) => {
241                assert_eq!(args.id, 123);
242                assert_ne!(args.sig, 456);
243            }
244            x => panic!("Unexpected request: {:?}", x),
245        }
246    }
247
248    #[test]
249    fn transform_with_reply_should_apply_rules_in_sequence() {
250        let raw_request = Request::ReadFile(Default::default());
251        let reply = Reply::FileOpened(FileOpenedArgs {
252            id: 123,
253            sig: 456,
254            ..Default::default()
255        });
256
257        let lazy_request = LazilyTransformedRequest {
258            raw_request: raw_request.clone(),
259            rules: vec![
260                TransformRule {
261                    // Replace id of raw request
262                    path: String::from("$.payload.id"),
263
264                    // Apply id from reply request
265                    value: String::from("$.payload.id"),
266                },
267                TransformRule {
268                    // Replace sig of raw request
269                    path: String::from("$.payload.sig"),
270
271                    // Apply sig from reply request
272                    value: String::from("$.payload.sig"),
273                },
274            ],
275        };
276
277        let transformed_request = lazy_request
278            .transform_with_reply(&reply)
279            .expect("Failed to transform");
280
281        match transformed_request {
282            Request::ReadFile(args) => {
283                assert_eq!(args.id, 123);
284                assert_eq!(args.sig, 456);
285            }
286            x => panic!("Unexpected request: {:?}", x),
287        }
288    }
289}