Skip to main content

graphitepdf_utils/
object.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::value::{Keys, Object, Path, Value};
4
5pub type Transform = Box<dyn Fn(&Value) -> Value + Send + Sync + 'static>;
6pub type TransformMap = BTreeMap<String, Transform>;
7
8pub fn get<'a>(value: &Value, path: impl Into<Path<'a>>, fallback: Value) -> Value {
9    let mut current = value;
10    let mut missing = false;
11
12    path.into().for_each(|segment| {
13        if missing {
14            return;
15        }
16
17        current = match current {
18            Value::Object(object) => match object.get(segment) {
19                Some(value) => value,
20                None => {
21                    missing = true;
22                    current
23                }
24            },
25            _ => {
26                missing = true;
27                current
28            }
29        };
30    });
31
32    if missing { fallback } else { current.clone() }
33}
34
35pub fn map_values<F>(object: &Object, mut mapper: F) -> Object
36where
37    F: FnMut(&Value, &str) -> Value,
38{
39    object
40        .iter()
41        .map(|(key, value)| (key.clone(), mapper(value, key)))
42        .collect()
43}
44
45pub fn pick<'a>(keys: impl Into<Keys<'a>>, object: &Object) -> Object {
46    let mut result = Object::new();
47
48    keys.into().for_each(|key| {
49        if let Some(value) = object.get(key) {
50            result.insert(key.to_string(), value.clone());
51        }
52    });
53
54    result
55}
56
57pub fn omit<'a>(keys: impl Into<Keys<'a>>, object: &Object) -> Object {
58    let mut excluded: BTreeSet<&str> = BTreeSet::new();
59    keys.into().for_each(|key| {
60        excluded.insert(key);
61    });
62
63    object
64        .iter()
65        .filter(|(key, _)| !excluded.contains(key.as_str()))
66        .map(|(key, value)| (key.clone(), value.clone()))
67        .collect()
68}
69
70pub fn evolve(object: &Object, transforms: &TransformMap) -> Object {
71    object
72        .iter()
73        .map(|(key, value)| match transforms.get(key) {
74            Some(transform) => (key.clone(), transform(value)),
75            None => (key.clone(), value.clone()),
76        })
77        .collect()
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    fn object(entries: [(&str, Value); 2]) -> Object {
85        entries
86            .into_iter()
87            .map(|(key, value)| (key.to_string(), value))
88            .collect()
89    }
90
91    #[test]
92    fn gets_nested_value_from_path() {
93        let nested = Value::from(object([
94            ("b", Value::from(1_i32)),
95            ("c", Value::from("ignored")),
96        ]));
97        let value = Value::from([("a".to_string(), nested)].into_iter().collect::<Object>());
98
99        assert_eq!(get(&value, &["a", "b"], Value::Null), Value::from(1_i32));
100        assert_eq!(
101            get(&value, &["a", "missing"], Value::from(0_i32)),
102            Value::from(0_i32)
103        );
104    }
105
106    #[test]
107    fn maps_and_filters_object_values() {
108        let object = object([("a", Value::from(1_i32)), ("b", Value::from(2_i32))]);
109
110        let doubled = map_values(&object, |value, _| match value {
111            Value::Number(number) => Value::from(number * 2.0),
112            other => other.clone(),
113        });
114
115        assert_eq!(pick("a", &doubled).len(), 1);
116        assert_eq!(pick(&["a"], &doubled).len(), 1);
117        assert_eq!(omit("b", &doubled).len(), 1);
118        assert_eq!(omit(&["b"], &doubled).len(), 1);
119    }
120
121    #[test]
122    fn evolves_values_with_matching_transformers() {
123        let object = object([("count", Value::from(5_i32)), ("name", Value::from("item"))]);
124        let transforms: TransformMap = [
125            (
126                "count".to_string(),
127                Box::new(|value: &Value| match value {
128                    Value::Number(number) => Value::from(number + 1.0),
129                    other => other.clone(),
130                }) as Transform,
131            ),
132            (
133                "name".to_string(),
134                Box::new(|value: &Value| match value {
135                    Value::String(text) => Value::from(text.to_uppercase()),
136                    other => other.clone(),
137                }) as Transform,
138            ),
139        ]
140        .into_iter()
141        .collect();
142
143        let evolved = evolve(&object, &transforms);
144
145        assert_eq!(evolved.get("count"), Some(&Value::from(6.0_f64)));
146        assert_eq!(evolved.get("name"), Some(&Value::from("ITEM")));
147    }
148}