cookie_cutter_core 0.2.0

A feature-rich template engine with context aware escaping and both runtime and compiletime compilation
Documentation
use std::{collections::BTreeMap, fmt::Display};

#[derive(Debug, Clone, PartialEq)]
/// A cookie cutter value used by the template engine to process data. Also used to provide data to
/// templates in the form of paramters on the initial call to a render* function of a [`crate::Templates`].
///
/// Can be obtained several ways:
/// - Constructed programmatically
/// - using the [`crate::value!`] value
/// - serializing a Rust value (if the serde feature is enabled)
pub enum Value {
    /// Some text (without text type since that information is erased by render time).
    Text(String),
    /// An integer.
    Int(i64),
    /// A bool.
    Bool(bool),
    /// A float.
    Float(f64),
    /// An array or a tuple (the information whether this value originated from or is destined for
    /// an array or a tuple has been erased by render time)
    ArrayOrTuple(Vec<Value>),
    /// A struct made up of key value pairs
    Struct(BTreeMap<String, Value>),
}

impl Value {
    /// Determines whether the given value can be turned into a string a.k.a is "printable".
    ///
    /// Anything besides a struct or any value containing structs are printable.
    pub fn is_printable(&self) -> bool {
        match self {
            Value::Text(_) | Value::Int(_) | Value::Bool(_) | Value::Float(_) => true,
            Value::ArrayOrTuple(value) => value.iter().all(Value::is_printable),
            Value::Struct(_) => false,
        }
    }
}

#[derive(Debug)]
/// An error that can occur when attempting to turn a value into a string that is not printable (as
/// defined by [`Value::is_printable`]).
pub struct ValueNotPrintableError;

impl Display for ValueNotPrintableError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("the value is not printable")
    }
}

impl std::error::Error for ValueNotPrintableError {}

impl TryInto<String> for Value {
    type Error = ValueNotPrintableError;
    fn try_into(self) -> Result<String, Self::Error> {
        Ok(match self {
            Value::Bool(value) => value.to_string(),
            Value::Float(value) => value.to_string(),
            Value::Int(value) => value.to_string(),
            Value::Text(value) => value.clone(),
            Value::ArrayOrTuple(value) => value
                .into_iter()
                .map(Value::try_into)
                .try_fold::<String, _, Result<String, ValueNotPrintableError>>(
                    String::new(),
                    |mut current, next: Result<String, ValueNotPrintableError>| {
                        current.push_str(&next?);
                        Ok(current)
                    },
                )?,
            Value::Struct(_) => return Err(ValueNotPrintableError),
        })
    }
}

#[cfg(feature = "serde")]
pub(crate) mod serde_values;

impl<'s> From<&'s str> for Value {
    fn from(value: &'s str) -> Self {
        Self::Text(value.to_string())
    }
}

impl From<String> for Value {
    fn from(value: String) -> Self {
        Self::Text(value)
    }
}

impl From<i64> for Value {
    fn from(value: i64) -> Self {
        Self::Int(value)
    }
}

impl From<bool> for Value {
    fn from(value: bool) -> Self {
        Self::Bool(value)
    }
}

impl From<f64> for Value {
    fn from(value: f64) -> Self {
        Self::Float(value)
    }
}

impl<V: Into<Value>, const N: usize> From<[V; N]> for Value {
    fn from(value: [V; N]) -> Self {
        Self::ArrayOrTuple(value.map(Into::into).to_vec())
    }
}

impl<V: Into<Value>> FromIterator<V> for Value {
    fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
        Self::ArrayOrTuple(iter.into_iter().map(Into::into).collect())
    }
}

impl<V: Into<Value>> From<Vec<V>> for Value {
    fn from(value: Vec<V>) -> Self {
        Self::ArrayOrTuple(value.into_iter().map(Into::into).collect())
    }
}

impl<'a, V: Into<Value>> FromIterator<(&'a str, V)> for Value {
    fn from_iter<T: IntoIterator<Item = (&'a str, V)>>(iter: T) -> Self {
        Self::Struct(
            iter.into_iter()
                .map(|(key, value)| (key.to_string(), value.into()))
                .collect::<BTreeMap<_, _>>(),
        )
    }
}

#[macro_export]
/// Constructs a [`Value`] using a literal syntax.
///
/// The most general form is:
/// ```
/// # use cookie_cutter_core::{Value, value};
/// assert_eq!(value!(12), Value::Int(12));
/// assert_eq!(value!("Hello World"), Value::Text("Hello World".to_string()));
/// assert_eq!(value!(true), Value::Bool(true));
/// ```
///
/// But you can also write tuple/array and struct literals:
///
/// ```
/// # use std::collections::BTreeMap;
/// # use cookie_cutter_core::{Value, value};
/// assert_eq!(value![1, 5, -3], Value::ArrayOrTuple(vec![Value::Int(1), Value::Int(5), Value::Int(-3)]));
/// assert_eq!(
///     value!{name: "Peter", age: 25},
///     Value::Struct(BTreeMap::from_iter([
///         ("name".to_string(), Value::Text("Peter".to_string())),
///         ("age".to_string(), Value::Int(25)),
///     ]))
/// );
/// ```
///
/// It is fully recursive and you defined arbitrarily nested values using it:
/// ```
/// # use std::collections::BTreeMap;
/// # use cookie_cutter_core::{Value, value};
/// assert_eq!(
///     value!{
///         page: {
///             header: "Hi guys!",
///             content: "This is some nice text.",
///             footer: "Bye guys!"
///         },
///         hits: 32553,
///         sub_pages: ["about_us", "other_article", "something_else"],
///     },
///     Value::Struct(BTreeMap::from_iter([
///         ("page".to_string(), Value::Struct(BTreeMap::from_iter([
///             ("header".to_string(), Value::Text("Hi guys!".to_string())),
///             ("content".to_string(), Value::Text("This is some nice text.".to_string())),
///             ("footer".to_string(), Value::Text("Bye guys!".to_string())),
///         ]))),
///         ("hits".to_string(), Value::Int(32553)),
///         ("sub_pages".to_string(), Value::ArrayOrTuple(vec![
///             Value::Text("about_us".to_string()),
///             Value::Text("other_article".to_string()),
///             Value::Text("something_else".to_string()),
///         ]))
///     ]))
/// );
/// ```
///
/// You can also interpolate arbitrary Rust expressions and variables:
/// ```
/// # use std::collections::BTreeMap;
/// # use cookie_cutter_core::{Value, value};
/// let d = 3.3;
/// let name = "Bob";
/// assert_eq!(
///     value! {
///         a: 1,
///         b: "Hello",
///         c: 1 + 1,
///         d: format!("{name}, this is {d}"),
///     },
///     Value::Struct(BTreeMap::from_iter([
///         ("a".to_string(), Value::Int(1)),
///         ("b".to_string(), Value::Text("Hello".to_string())),
///         ("c".to_string(), Value::Int(2)),
///         ("d".to_string(), Value::Text("Bob, this is 3.3".to_string()))
///     ]))
/// );
/// ```
macro_rules! value {
    ([$($body:tt)*]) => {
        $crate::value!(@array ($($body)*) -> ())
    };
    ({$($body:tt)*}) => {
        $crate::value!(@struct ($($body)*) -> ())
    };
    ($key:ident: $($tail:tt)*) => {
        $crate::value!(@struct ($key: $($tail)*) -> ())
    };
    ($expr:expr, $($tail:tt)*) => {
        $crate::value!(@array ($($tail)*) -> ($crate::value!($expr),))
    };
    ([$($array_body:tt)*], $($tail:tt)*) => {
        $crate::value!(@array ($($tail)*) -> ($crate::value!(@array ($($array_body)*) -> ()),))
    };
    ({$($struct_body:tt)*}, $($tail:tt)*) => {
        $crate::value!(@array ($($tail)*) -> ($crate::value!(@struct ($($struct_body)*) -> ()),))
    };
    (@array () -> ($($accum:tt)*)) => {
        $crate::Value::ArrayOrTuple(::std::vec::Vec::from([$($accum)*]))
    };
    (@array ({$($struct_body:tt)*} $(, $($tail:tt)*)?) -> ($($accum:tt)*)) => {
        $crate::value!(@array ($($($tail)*)?) -> ($($accum)* $crate::value!(@struct ($($struct_body)*) -> ()),))
    };
    (@array ([$($array_body:tt)*] $(, $($tail:tt)*)?) -> ($($accum:tt)*)) => {
        $crate::value!(@array ($($($tail)*)?) -> ($($accum)* $crate::value!(@array ($($array_body)*) -> ()),))
    };
    (@array ($expr:expr $(, $($tail:tt)*)?) -> ($($accum:tt)*)) => {
        $crate::value!(@array ($($($tail)*)?) -> ($($accum)* $crate::value!($expr),))
    };
    ({$($body:tt)*}) => {
        $crate::value!(@struct ($($body)*) -> ())
    };
    (@struct () -> ($($accum:tt)*)) => {
        $crate::Value::Struct(::std::collections::BTreeMap::from([$($accum)*]))
    };
    (@struct ($key:ident: {$($struct_body:tt)*} $(, $($tail:tt)*)?) -> ($($accum:tt)*)) => {
        $crate::value!(@struct ($($($tail)*)?) -> ($($accum)* (::std::stringify!($key).to_string(), $crate::value!(@struct ($($struct_body)*) -> ())),))
    };
    (@struct ($key:ident: [$($array_body:tt)*] $(, $($tail:tt)*)?) -> ($($accum:tt)*)) => {
        $crate::value!(@struct ($($($tail)*)?) -> ($($accum)* (::std::stringify!($key).to_string(), $crate::value!(@array ($($array_body)*) -> ())),))
    };
    (@struct ($key:ident: $expr:expr $(, $($tail:tt)*)?) -> ($($accum:tt)*)) => {
        $crate::value!(@struct ($($($tail)*)?) -> ($($accum)* (::std::stringify!($key).to_string(), $crate::value!($expr)),))
    };
    ($expr:expr) => {
        $crate::Value::from($expr)
    };
}

#[cfg(test)]
mod test_value_macro {
    use pretty_assertions::assert_eq;
    use std::collections::BTreeMap;

    use crate::Value;

    #[test]
    #[allow(clippy::approx_constant, clippy::nonminimal_bool)]
    fn test_primitive() {
        assert_eq!(value!(1), Value::Int(1));
        assert_eq!(value!(true), Value::Bool(true));
        assert_eq!(value!(6.28), Value::Float(6.28));
        assert_eq!(value!(0 - 1), Value::Int(-1));
        assert_eq!(value!("h".to_string() + "i"), Value::Text("hi".to_string()));
        assert_eq!(value!(!true), Value::Bool(false));
    }

    #[test]
    fn test_array() {
        assert_eq!(value!([]), Value::ArrayOrTuple(Vec::new()));
        assert_eq!(value!([1,]), Value::ArrayOrTuple(vec![Value::Int(1)]));
        assert_eq!(value!([1 + 1,]), Value::ArrayOrTuple(vec![Value::Int(2)]));
        assert_eq!(
            value!([1, 1 + 1, 1 + 1 + 1]),
            Value::ArrayOrTuple(vec![Value::Int(1), Value::Int(2), Value::Int(3)])
        );
        assert_eq!(
            value![1, 2, 3],
            Value::ArrayOrTuple(vec![Value::Int(1), Value::Int(2), Value::Int(3)])
        );
    }

    #[test]
    fn test_struct() {
        assert_eq!(value!({}), Value::Struct(BTreeMap::new()));
        assert_eq!(
            value!({name: "Josh"}),
            Value::Struct(BTreeMap::from([(
                "name".to_string(),
                Value::Text("Josh".to_string())
            )]))
        );
        assert_eq!(
            value!({name: "Josh", age: 21}),
            Value::Struct(BTreeMap::from([
                ("name".to_string(), Value::Text("Josh".to_string())),
                ("age".to_string(), Value::Int(21))
            ]))
        );
        assert_eq!(
            value! {name: "Josh", age: 21},
            Value::Struct(BTreeMap::from([
                ("name".to_string(), Value::Text("Josh".to_string())),
                ("age".to_string(), Value::Int(21))
            ]))
        );
    }

    #[test]
    fn test_complex() {
        assert_eq!(
            value! {person: {name: "Peter", favorite: {sport: "baseball", hobby: "coding"}}},
            Value::Struct(BTreeMap::from([(
                "person".to_string(),
                Value::Struct(BTreeMap::from([
                    ("name".to_string(), Value::Text("Peter".to_string())),
                    (
                        "favorite".to_string(),
                        Value::Struct(BTreeMap::from([
                            ("sport".to_string(), Value::Text("baseball".to_string())),
                            ("hobby".to_string(), Value::Text("coding".to_string()))
                        ]))
                    )
                ]))
            )]))
        );
        assert_eq!(
            value! {name: "Peter", age: 30 + 2, hobbies: ["table".to_string() + " tennis"]},
            Value::Struct(BTreeMap::from([
                ("name".to_string(), Value::Text("Peter".to_string())),
                ("age".to_string(), Value::Int(32)),
                (
                    "hobbies".to_string(),
                    Value::ArrayOrTuple(vec![Value::Text("table tennis".to_string())])
                )
            ]))
        );
    }
}