graphitepdf_utils/
object.rs1use 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}