Skip to main content

harmony_jolt/
lib.rs

1mod spec;
2mod shift;
3mod default;
4mod remove;
5mod modifier;
6mod pointer;
7mod transform;
8mod error;
9pub mod dsl;
10pub mod fuzz;
11
12use serde_json::{Map, Value};
13use serde_json::map::Entry;
14use transform::Transform;
15
16use crate::default::default;
17use crate::remove::remove;
18use crate::modifier::modify;
19use crate::spec::SpecEntry;
20
21pub use spec::TransformSpec;
22use crate::pointer::JsonPointer;
23
24pub use error::{Error, Result};
25
26/// Perform JSON to JSON transformation where the "specification" is a JSON.
27///
28/// Inspired by Java library [Jolt](https://github.com/bazaarvoice/jolt).
29///
30/// The transformation can compose of many operations that are chained together.
31///
32/// ### Operations
33/// 1. [`shift`](TransformSpec#shift-operation): copy data from the input tree and put it the output tree
34/// 2. [`default`](TransformSpec#default-operation): apply default values to the tree
35/// 3. [`remove`](TransformSpec#remove-operation): remove data from the tree
36/// 4. [`modify`](TransformSpec#modify-operation): modify data using functions like concat, toLower, etc.
37///
38/// For example, if you want to repack your JSON record, you can do the following:
39/// ```
40/// use serde_json::{json, Value};
41/// use harmony_jolt::{transform, TransformSpec};
42///
43/// let input: Value = serde_json::from_str(r#"
44///     {
45///         "id": 1,
46///         "name": "John Smith",
47///         "account": {
48///             "id": 1000,
49///             "type": "Checking"
50///         }
51///     }
52/// "#).unwrap();
53///
54/// let spec: TransformSpec =
55/// serde_json::from_str(r#"[
56///     {
57///       "operation": "shift",
58///       "spec": {
59///         "name": "data.name",
60///         "account": "data.account"
61///       }
62///     }
63///   ]"#).unwrap();
64///
65/// let output = transform(input, &spec).unwrap();
66///
67/// assert_eq!(output, json!({
68///     "data" : {
69///       "name": "John Smith",
70///       "account": {
71///         "id": 1000,
72///         "type": "Checking"
73///       }
74///     }
75/// }));
76/// ```
77///
78/// Checkout supported operations in [TransformSpec] docs.
79pub fn transform(input: Value, spec: &TransformSpec) -> Result<Value> {
80    let mut result = input;
81    for entry in spec.entries() {
82        match entry {
83            SpecEntry::Shift(shift) => result = shift.apply(&result)?,
84            SpecEntry::Default(spec) => result = default(result, spec),
85            SpecEntry::Modify(spec) => result = modify(result, spec)?,
86            SpecEntry::Remove(spec) => result = remove(result, spec),
87        }
88    }
89    Ok(result)
90}
91
92pub(crate) fn insert(dest: &mut Value, position: JsonPointer, val: Value) {
93    let elements = position.iter();
94    let folded = elements
95        .skip(1)
96        .try_fold(dest, |target, token| match target {
97            Value::Object(map) => {
98                if let Entry::Vacant(entry) = map.entry(token) {
99                    entry.insert(Value::Object(Map::new()));
100                }
101                map.get_mut(token)
102            }
103            _ => None,
104        });
105    if let Some(pointer_mut) = folded {
106        merge(pointer_mut, val);
107    }
108}
109
110/// Merge one `Value` node into another if they are both `Value::Object`, otherwise overwrite.
111fn merge(dest: &mut Value, new_value: Value) {
112    match (dest, new_value) {
113        (Value::Object(dest), Value::Object(new_value)) => {
114            for (key, value) in new_value.into_iter() {
115                dest.insert(key, value);
116            }
117        }
118        (dest, new_value) => *dest = new_value,
119    };
120}
121
122pub(crate) fn delete(dest: &mut Value, position: &JsonPointer) -> Option<()> {
123    if let Some(Value::Object(map)) = dest.pointer_mut(position.parent().join_rfc6901().as_str()) {
124        map.remove(position.leaf_name());
125    }
126    Some(())
127}
128
129#[cfg(test)]
130mod test {
131
132    use serde_json::json;
133    use super::*;
134
135    #[test]
136    fn test_transform() {
137        let spec: TransformSpec = serde_json::from_value(json!(
138            [
139                {
140                  "operation": "shift",
141                  "spec": {
142                    "a": "a_new",
143                    "c": "c_new"
144                  }
145                }
146            ]
147        ))
148        .expect("parsed spec");
149
150        let source = json!({
151            "a": "b",
152            "c": "d"
153        });
154        let result = transform(source, &spec).unwrap();
155
156        assert_eq!(
157            result,
158            json!({
159                "a_new": "b",
160                "c_new": "d"
161            })
162        );
163    }
164
165    #[test]
166    fn test_insert_object_to_empty() {
167        //given
168        let mut empty_dest = Value::Object(Map::new());
169        let value = json!({
170            "a": "b",
171        });
172
173        insert(
174            &mut empty_dest,
175            JsonPointer::from_dot_notation("new"),
176            value,
177        );
178
179        assert_eq!(
180            empty_dest,
181            json!({
182                "new": {
183                    "a": "b"
184                }
185            })
186        );
187    }
188
189    #[test]
190    fn test_insert_object_to_non_empty() {
191        //given
192        let mut dest = json!({
193            "b": "bb",
194            "c": "cc",
195        });
196        let value = json!({
197            "a": "b",
198        });
199
200        insert(&mut dest, JsonPointer::from_dot_notation("new"), value);
201
202        assert_eq!(
203            dest,
204            json!({
205                "b": "bb",
206                "c": "cc",
207                "new": {
208                    "a": "b"
209                }
210            })
211        );
212    }
213
214    #[test]
215    fn test_insert_object_merged() {
216        //given
217        let mut dest = json!({
218            "some": {
219                "b": "bb",
220                "c": "cc",
221            }
222        });
223        let value = json!({
224            "a": "b",
225        });
226
227        insert(&mut dest, JsonPointer::from_dot_notation("some"), value);
228
229        assert_eq!(
230            dest,
231            json!({
232                "some": {
233                    "a": "b",
234                    "b": "bb",
235                    "c": "cc",
236                }
237            })
238        );
239    }
240
241    #[test]
242    fn test_insert_object_to_empty_non_root() {
243        //given
244        let mut empty_dest = Value::Object(Map::new());
245        let value = json!({
246            "a": "b",
247        });
248
249        insert(
250            &mut empty_dest,
251            JsonPointer::from_dot_notation("level1.level2.new"),
252            value,
253        );
254
255        assert_eq!(
256            empty_dest,
257            json!({
258                "level1": {
259                    "level2": {
260                        "new": {
261                            "a": "b"
262                        }
263                    }
264                }
265            })
266        );
267    }
268
269    #[test]
270    fn test_delete_empty_pointer() {
271        //given
272        let mut input = json!({
273            "a": "b",
274        });
275
276        //when
277        let _ = delete(&mut input, &JsonPointer::from_dot_notation(""));
278
279        //then
280        assert_eq!(
281            input,
282            json!({
283                "a": "b",
284            })
285        );
286    }
287
288    #[test]
289    fn test_delete_not_existing() {
290        //given
291        let mut input = json!({
292            "a": "b",
293        });
294
295        //when
296        let _ = delete(&mut input, &JsonPointer::from_dot_notation(".b"));
297
298        //then
299        assert_eq!(
300            input,
301            json!({
302                "a": "b",
303            })
304        );
305    }
306
307    #[test]
308    fn test_delete() {
309        //given
310        let mut input1 = json!({
311            "a": "b",
312        });
313        let mut input2 = json!({
314            "a": "b",
315            "b": "c",
316        });
317        //when
318        let _ = delete(&mut input1, &JsonPointer::from_dot_notation(".a"));
319        let _ = delete(&mut input2, &JsonPointer::from_dot_notation("b"));
320
321        //then
322        assert_eq!(input1, json!({}));
323        assert_eq!(
324            input2,
325            json!({
326                "a": "b",
327            })
328        );
329    }
330
331    #[test]
332    fn test_invalid_array_index_error_includes_raw_value() {
333        // Using wildcard LHS, the captured key (e.g. "not_numeric") is used as an array index in RHS via &(0,0)
334        // This should fail and the error should include the raw invalid value.
335        let spec: TransformSpec = serde_json::from_value(json!(
336            [
337                {
338                  "operation": "shift",
339                  "spec": {
340                    "*": "out[&(0,0)]"
341                  }
342                }
343            ]
344        ))
345        .expect("parsed spec");
346
347        let source = json!({
348            "not_numeric": 1
349        });
350
351        let result = transform(source, &spec);
352        assert!(result.is_err(), "Expected error when using non-numeric key as array index");
353
354        let err_msg = result.unwrap_err().to_string();
355        assert!(
356            err_msg.contains("not_numeric"),
357            "Error should include the invalid index value, got: {}",
358            err_msg
359        );
360        assert!(
361            err_msg.contains("Invalid index in expression"),
362            "Error should mention 'Invalid index in expression', got: {}",
363            err_msg
364        );
365    }
366}