pochoir-lang 0.12.2

Custom parser and interpreter for the pochoir template engine
Documentation
//! Defines the [`Context`] structure.
use indexmap::{indexmap, IndexMap};
use serde::Serialize;
use std::{borrow::Borrow, fmt, hash::Hash, sync::LazyLock};

use crate::{value, IntoValue, Object, Value};

#[allow(clippy::wildcard_imports)]
use crate::functions::*;

static GLOBAL_CONTEXT: LazyLock<IndexMap<String, Value>> = LazyLock::new(|| indexmap! {
    String::from("slugify") => Function::into_value(Function::new(slugify::slugify)),
    String::from("word_count") => Function::into_value(Function::new(word_count::word_count)),
    String::from("reading_time") => Function::into_value(Function::new(reading_time::reading_time)),
    String::from("len") => Function::into_value(Function::new(len::len)),
    String::from("to_string") => Function::into_value(Function::new(to_string::to_string)),
    String::from("enumerate") => Function::into_value(Function::new(enumerate::enumerate)),
    String::from("reverse") => Function::into_value(Function::new(reverse::reverse)),
    String::from("skip") => Function::into_value(Function::new(skip::skip)),
    String::from("split") => Function::into_value(Function::new(split::split)),
    String::from("take") => Function::into_value(Function::new(take::take)),
    String::from("iter") => Function::into_value(Function::new(iter::iter)),
    String::from("entries") => Function::into_value(Function::new(entries::entries)),
    String::from("first") => Function::into_value(Function::new(first::first)),
    String::from("last") => Function::into_value(Function::new(last::last)),
    String::from("trim") => Function::into_value(Function::new(trim::trim)),
    String::from("sort") => Function::into_value(Function::new(sort::sort)),
    String::from("sort_by") => Function::into_value(Function::new(sort::sort_by)),
    String::from("round") => Function::into_value(Function::new(round::round)),
    String::from("replace") => Function::into_value(Function::new(replace::replace)),
    String::from("starts_with") => Function::into_value(Function::new(starts_with::starts_with)),
    String::from("ends_with") => Function::into_value(Function::new(ends_with::ends_with)),
    String::from("capitalize") => Function::into_value(Function::new(capitalize::capitalize)),
});

#[derive(Debug, Clone, PartialEq)]
struct ContextValue {
    inner: Value,
    is_inherited: bool,
}

/// The struct that holds the context of a template rendering.
///
/// Light wrapper around an [`IndexMap`] for easier insertions of Serializable
/// values.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Context {
    inner: IndexMap<String, ContextValue>,
}

impl Context {
    /// Create a new [`Context`].
    pub fn new() -> Self {
        Self {
            inner: IndexMap::new(),
        }
    }

    /// Create a new [`Context`] from a Serializable object.
    ///
    /// Returns `None` if the value to serialize is not an object, if the implementation of
    /// `Serialize` failed or if it contains non-string keys.
    ///
    /// # Example
    ///
    /// To create a [`Context`] from a JSON string:
    ///
    /// ```ignore
    /// use pochoir_lang::{Context, Value};
    ///
    /// let json = r#"{ "key": "value" }"#;
    /// let value: serde_json::Value = serde_json::from_str(json).expect("provided JSON should be valid");
    /// let context = Context::from_serialize(value).expect("value extracted from JSON should be valid");
    ///
    /// assert_eq!(context.get("key"), Some(&Value::String("value".to_string())));
    /// ```
    pub fn from_serialize<T: Serialize>(val: T) -> Option<Self> {
        let val = value::serialize_to_value(val).ok()?;

        if let Value::Object(obj) = val {
            let mut map = IndexMap::new();

            for (key, val) in obj {
                map.insert(
                    key,
                    ContextValue {
                        inner: val,
                        is_inherited: false,
                    },
                );
            }

            Some(Self { inner: map })
        } else if Value::Null == val {
            Some(Self {
                inner: IndexMap::new(),
            })
        } else {
            None
        }
    }

    /// Create a new [`Context`] from an [`Object`].
    ///
    /// # Example
    ///
    /// To create a [`Context`] from a JSON string:
    ///
    /// ```
    /// use pochoir_lang::{Context, object, Value};
    ///
    /// let context = Context::from_object(object! {
    ///     "name" => "John",
    ///     "hobbies" => ["swimming", "hiking"],
    ///     "friends" => [
    ///         object! {
    ///             "name" => "Bob",
    ///             "hobbies" => ["playing guitar"],
    ///         },
    ///         object! {
    ///             "name" => "Sasha",
    ///             "hobbies" => ["painting"],
    ///         },
    ///     ],
    /// });
    ///
    /// assert_eq!(context.get("name"), Some(&Value::String("John".to_string())));
    /// assert_eq!(context.get("hobbies"), Some(&Value::Array(vec![
    ///     Value::String("swimming".to_string()),
    ///     Value::String("hiking".to_string()),
    /// ])));
    /// assert_eq!(context
    ///     .get("friends")
    ///     .and_then(|a| a.index(1).expect("friends should be an array"))
    ///     .and_then(|o| o.get("name").expect("each friend should be an object")),
    /// Some(&Value::String("Sasha".to_string())));
    /// ```
    pub fn from_object(obj: Object) -> Self {
        let mut map = IndexMap::new();

        for (key, val) in obj {
            map.insert(
                key,
                ContextValue {
                    inner: val,
                    is_inherited: false,
                },
            );
        }

        Self { inner: map }
    }

    /// Insert a key/value pair in the context.
    ///
    /// Updates the value if the key already exists.
    ///
    /// This value will be inherited in all child components of the current component if used with
    /// `pochoir`.
    pub fn insert_inherited<K: Into<String>, V: IntoValue>(&mut self, key: K, val: V) {
        self.inner.insert(
            key.into(),
            ContextValue {
                inner: val.into_value(),
                is_inherited: true,
            },
        );
    }

    /// Insert a key/value pair in the context.
    ///
    /// Updates the value if the key already exists.
    pub fn insert<K: Into<String>, V: IntoValue>(&mut self, key: K, val: V) {
        self.inner.insert(
            key.into(),
            ContextValue {
                inner: val.into_value(),
                is_inherited: false,
            },
        );
    }

    /// Serialize the data into a [`Value`] and insert it with its key in the context.
    ///
    /// Updates the value if the key already exists.
    pub fn insert_serialize<K: Into<String>, V: Serialize>(&mut self, key: K, val: V) {
        if let Ok(val) = value::serialize_to_value(val) {
            self.inner.insert(
                key.into(),
                ContextValue {
                    inner: val,
                    is_inherited: false,
                },
            );
        }
    }

    /// Make a value inherited in all child components of the current component if used with
    /// `pochoir`.
    ///
    /// If the key does not exist, nothing will happen.
    pub fn make_inherited<K: Hash + Eq + ?Sized>(&mut self, k: &K)
    where
        String: Borrow<K>,
    {
        if let Some(v) = self.inner.get_mut(k) {
            v.is_inherited = true;
        }
    }

    /// Remove a key/value pair in the context.
    pub fn remove<K: Hash + Eq + ?Sized>(&mut self, key: &K) -> Option<Value>
    where
        String: Borrow<K>,
    {
        self.inner.swap_remove(key).map(|v| v.inner)
    }

    /// Returns whether the context contains a key.
    pub fn contains_key<K: Hash + Eq + ?Sized>(&self, key: &K) -> bool
    where
        String: Borrow<K>,
    {
        self.inner.contains_key(key)
    }

    /// Get a value of the context from a key.
    pub fn get<K: Hash + Eq + ?Sized>(&self, key: &K) -> Option<&Value>
    where
        String: Borrow<K>,
    {
        self.inner
            .get(key)
            .map_or_else(|| GLOBAL_CONTEXT.get(key), |v| Some(&v.inner))
    }

    /// Get a mutable reference to a value of the context from a key.
    pub fn get_mut<K: Hash + Eq + ?Sized>(&mut self, key: &K) -> Option<&mut Value>
    where
        String: Borrow<K>,
    {
        self.inner.get_mut(key).map(|v| &mut v.inner)
    }

    /// Returns if the value corresponding to the key is inherited in all child components if used
    /// with `pochoir`.
    ///
    /// Returns `None` if the key does not exist.
    pub fn is_inherited<K: Hash + Eq + ?Sized>(&self, key: &K) -> Option<bool>
    where
        String: Borrow<K>,
    {
        self.inner.get(key).map(|v| v.is_inherited)
    }

    /// Get an iterator over the entries of the context, sorted by key.
    pub fn iter(&self) -> impl Iterator<Item = (&String, &Value)> {
        self.inner.iter().map(|(k, v)| (k, &v.inner))
    }

    /// Get an iterator over the entries of the context, sorted by key.
    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&String, &mut Value)> {
        self.inner.iter_mut().map(|(k, v)| (k, &mut v.inner))
    }
}

impl FromIterator<(String, Value)> for Context {
    fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
        let mut context = Self::new();

        for (k, v) in iter {
            context.insert(k, v);
        }

        Self {
            inner: context.inner,
        }
    }
}

#[derive(Debug)]
pub struct Iter {
    iter: indexmap::map::IntoIter<String, ContextValue>,
}

impl Iterator for Iter {
    type Item = (String, Value);

    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().map(|(k, v)| (k, v.inner))
    }
}

impl IntoIterator for Context {
    type Item = (String, Value);

    type IntoIter = Iter;

    fn into_iter(self) -> Self::IntoIter {
        Iter {
            iter: self.inner.into_iter(),
        }
    }
}

impl Extend<(String, Value)> for Context {
    fn extend<T: IntoIterator<Item = (String, Value)>>(&mut self, iter: T) {
        for (k, v) in iter {
            self.insert(k, v);
        }
    }
}

impl fmt::Display for Context {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            Value::Object(
                self.inner
                    .iter()
                    .map(|(k, v)| (k.clone(), v.inner.clone()))
                    .collect()
            )
        )
    }
}