cookie_cutter_core 0.2.0

A feature-rich template engine with context aware escaping and both runtime and compiletime compilation
Documentation
use itertools::{Itertools, Position};

use maybe_sync::Rc;

use crate::rendertime::WRONG_TYPE_MESSAGE;
use crate::{Function, Type, Value};

struct Len;

impl Function for Len {
    fn accepts(&self, args: Vec<Type>) -> Option<Type> {
        if args.len() != 1 {
            return None;
        }

        matches!(args[0], Type::Array(_)).then_some(Type::Int)
    }

    fn run(
        &self,
        mut args: Vec<Value>,
    ) -> Result<Value, Box<maybe_sync::dyn_maybe_send_sync!(std::error::Error)>> {
        let Some(Value::ArrayOrTuple(inner)) = args.pop() else {
            panic!("wrong input type for builtin function len; {WRONG_TYPE_MESSAGE}");
        };

        Ok(Value::Int(
            inner
                .len()
                .try_into()
                .expect("array size too large in builtin function len"),
        ))
    }
}

struct Join;

impl Function for Join {
    fn accepts(&self, mut args: Vec<Type>) -> Option<Type> {
        if args.len() != 2 {
            return None;
        }

        let (Some(Type::Array(to_join_ty)), Some(Some(joiner_ty))) =
            (args.pop(), args.pop().map(|ty| ty.to_printable()))
        else {
            return None;
        };

        let Some(to_join_ty) = to_join_ty.as_deref().and_then(Type::to_printable).flatten() else {
            let Some(joiner_ty) = joiner_ty else {
                // we could neither figure out the arrays inner value nor the joiners text type;
                // how did we get here?
                return None;
            };

            return Some(Type::Text(joiner_ty));
        };

        joiner_ty
            .is_none_or(|joiner_ty| to_join_ty == joiner_ty)
            .then_some(Type::Text(to_join_ty))
    }

    fn run(
        &self,
        mut args: Vec<Value>,
    ) -> Result<Value, Box<maybe_sync::dyn_maybe_send_sync!(std::error::Error)>> {
        let (Some(Value::ArrayOrTuple(values)), Some(Ok(joiner))): (_, Option<Result<String, _>>) =
            (args.pop(), args.pop().map(Value::try_into))
        else {
            panic!("bad arguments ({args:?}) for builtin function join; {WRONG_TYPE_MESSAGE}");
        };

        let mut result = String::new();

        for (pos, value) in values.into_iter().with_position() {
            let Ok(str): Result<String, _> = value.clone().try_into() else {
                panic!("non printable value ({value:?}) in array in builtin function join; {WRONG_TYPE_MESSAGE}");
            };

            result.push_str(&str);

            if !matches!(pos, Position::Only | Position::Last) {
                result.push_str(&joiner);
            }
        }

        Ok(Value::Text(result))
    }
}

struct ToUnsafe;

impl Function for ToUnsafe {
    fn accepts(&self, mut args: Vec<Type>) -> Option<Type> {
        if args.len() != 1 {
            return None;
        }

        args.pop()
            .unwrap()
            .to_printable()
            .is_some()
            .then_some(Type::Text("unsafe".to_string()))
    }

    fn run(
        &self,
        mut args: Vec<Value>,
    ) -> Result<Value, Box<maybe_sync::dyn_maybe_send_sync!(std::error::Error)>> {
        let out: String = args
            .pop()
            .unwrap_or_else(|| {
                panic!(
                    "bad arguments ({args:?}) for builtin function toUnsafe; {WRONG_TYPE_MESSAGE}"
                )
            })
            .try_into()
            .unwrap_or_else(|_| {
                panic!("wrong input type for builtin function toUnsafe, {WRONG_TYPE_MESSAGE}")
            });
        Ok(Value::Text(out))
    }
}

#[must_use]
pub fn functions() -> Vec<(&'static str, Rc<maybe_sync::dyn_maybe_send_sync!(Function)>)> {
    vec![
        ("len", Rc::new(Len)),
        ("join", Rc::new(Join)),
        ("toUnsafe", Rc::new(ToUnsafe)),
    ]
}