graphitepdf-utils 0.4.0

Lightweight utility functions for GraphitePDF and related Rust workflows.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use crate::value::{Keys, Object, Path, Value};

pub type Transform = Box<dyn Fn(&Value) -> Value + Send + Sync + 'static>;
pub type TransformMap = BTreeMap<String, Transform>;

pub fn get<'a>(value: &Value, path: impl Into<Path<'a>>, fallback: Value) -> Value {
    let mut current = value;
    let mut missing = false;

    path.into().for_each(|segment| {
        if missing {
            return;
        }

        current = match current {
            Value::Object(object) => match object.get(segment) {
                Some(value) => value,
                None => {
                    missing = true;
                    current
                }
            },
            _ => {
                missing = true;
                current
            }
        };
    });

    if missing { fallback } else { current.clone() }
}

pub fn map_values<F>(object: &Object, mut mapper: F) -> Object
where
    F: FnMut(&Value, &str) -> Value,
{
    object
        .iter()
        .map(|(key, value)| (key.clone(), mapper(value, key)))
        .collect()
}

pub fn pick<'a>(keys: impl Into<Keys<'a>>, object: &Object) -> Object {
    let mut result = Object::new();

    keys.into().for_each(|key| {
        if let Some(value) = object.get(key) {
            result.insert(key.to_string(), value.clone());
        }
    });

    result
}

pub fn omit<'a>(keys: impl Into<Keys<'a>>, object: &Object) -> Object {
    let mut excluded: BTreeSet<&str> = BTreeSet::new();
    keys.into().for_each(|key| {
        excluded.insert(key);
    });

    object
        .iter()
        .filter(|(key, _)| !excluded.contains(key.as_str()))
        .map(|(key, value)| (key.clone(), value.clone()))
        .collect()
}

pub fn evolve(object: &Object, transforms: &TransformMap) -> Object {
    object
        .iter()
        .map(|(key, value)| match transforms.get(key) {
            Some(transform) => (key.clone(), transform(value)),
            None => (key.clone(), value.clone()),
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn object(entries: [(&str, Value); 2]) -> Object {
        entries
            .into_iter()
            .map(|(key, value)| (key.to_string(), value))
            .collect()
    }

    #[test]
    fn gets_nested_value_from_path() {
        let nested = Value::from(object([
            ("b", Value::from(1_i32)),
            ("c", Value::from("ignored")),
        ]));
        let value = Value::from([("a".to_string(), nested)].into_iter().collect::<Object>());

        assert_eq!(get(&value, &["a", "b"], Value::Null), Value::from(1_i32));
        assert_eq!(
            get(&value, &["a", "missing"], Value::from(0_i32)),
            Value::from(0_i32)
        );
    }

    #[test]
    fn maps_and_filters_object_values() {
        let object = object([("a", Value::from(1_i32)), ("b", Value::from(2_i32))]);

        let doubled = map_values(&object, |value, _| match value {
            Value::Number(number) => Value::from(number * 2.0),
            other => other.clone(),
        });

        assert_eq!(pick("a", &doubled).len(), 1);
        assert_eq!(pick(&["a"], &doubled).len(), 1);
        assert_eq!(omit("b", &doubled).len(), 1);
        assert_eq!(omit(&["b"], &doubled).len(), 1);
    }

    #[test]
    fn evolves_values_with_matching_transformers() {
        let object = object([("count", Value::from(5_i32)), ("name", Value::from("item"))]);
        let transforms: TransformMap = [
            (
                "count".to_string(),
                Box::new(|value: &Value| match value {
                    Value::Number(number) => Value::from(number + 1.0),
                    other => other.clone(),
                }) as Transform,
            ),
            (
                "name".to_string(),
                Box::new(|value: &Value| match value {
                    Value::String(text) => Value::from(text.to_uppercase()),
                    other => other.clone(),
                }) as Transform,
            ),
        ]
        .into_iter()
        .collect();

        let evolved = evolve(&object, &transforms);

        assert_eq!(evolved.get("count"), Some(&Value::from(6.0_f64)));
        assert_eq!(evolved.get("name"), Some(&Value::from("ITEM")));
    }
}