femstache 0.1.5

Embrace the feminine mustache to unlock your templating dreams! Or something, I don't know. :3
Documentation
use std::collections::BTreeMap;

use femstache_types::{
    context::TyContext,
    vars::{Ty, TyId},
};

/// A representation of a value in the templating language
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
    /// The billion dollar mistake
    Null,
    /// A boolean value
    Bool(bool),
    /// A string
    String(String),
    /// A list of values
    List(Vec<Value>),
    /// A mapping of strings to values
    Dict(BTreeMap<String, Value>),
    /// A strict mapping of strings to values
    Struct(BTreeMap<String, Value>),
}

impl Value {
    /// Is this a billion dollar mistake?
    pub fn is_null(&self) -> bool {
        matches!(self, Value::Null)
    }

    /// Is this a boolean value?
    pub fn is_bool(&self) -> bool {
        matches!(self, Value::Bool(_))
    }

    /// Is this a string value?
    pub fn is_string(&self) -> bool {
        matches!(self, Value::String(_))
    }

    /// Is this a list value?
    pub fn is_list(&self) -> bool {
        matches!(self, Value::List(_))
    }

    /// Is this a dict value?
    pub fn is_dict(&self) -> bool {
        matches!(self, Value::Dict(_))
    }

    /// Is this a struct value?
    pub fn is_struct(&self) -> bool {
        matches!(self, Value::Struct(_))
    }

    /// Grab the inner [bool], if this is a bool
    pub fn as_bool(&self) -> Option<bool> {
        if let &Value::Bool(b) = self {
            Some(b)
        } else {
            None
        }
    }

    /// Borrow as a [str], if this is a string
    pub fn as_str(&self) -> Option<&str> {
        if let Value::String(s) = self {
            Some(s)
        } else {
            None
        }
    }

    /// Borrow as a slice, if this is a list
    pub fn as_list(&self) -> Option<&[Value]> {
        if let Value::List(l) = self {
            Some(l)
        } else {
            None
        }
    }

    /// Borrow as a [BTreeMap], if this is a dict
    pub fn as_dict(&self) -> Option<&BTreeMap<String, Value>> {
        if let Value::Dict(m) = self {
            Some(m)
        } else {
            None
        }
    }

    /// Borrow as a [BTreeMap], if this is a struct
    pub fn as_struct(&self) -> Option<&BTreeMap<String, Value>> {
        if let Value::Struct(m) = self {
            Some(m)
        } else {
            None
        }
    }

    /// Turn this into its inner [String], if it is a list
    pub fn into_string(self) -> Option<String> {
        if let Value::String(s) = self {
            Some(s)
        } else {
            None
        }
    }

    /// Turn this into its inner [Vec], if it is a list
    pub fn into_list(self) -> Option<Vec<Value>> {
        if let Value::List(l) = self {
            Some(l)
        } else {
            None
        }
    }

    /// Is this value truthy?
    ///
    /// Truthy values are:
    ///   - `true`
    ///   - non-empty string
    ///   - non-empty list
    ///   - non-empty dict
    ///   - non-empty struct
    ///
    /// The rest are falsy.
    pub fn is_truthy(&self) -> bool {
        match self {
            Value::Null => false,
            Value::Bool(b) => *b,
            Value::String(s) => !s.is_empty(),
            Value::List(l) => !l.is_empty(),
            Value::Dict(m) => !m.is_empty(),
            Value::Struct(s) => !s.is_empty(),
        }
    }

    pub fn conforms_to(&self, ctx: &TyContext, ty_id: TyId) -> bool {
        let ty = ctx.resolve(ty_id);
        match (self, ty) {
            (Value::Bool(_), Ty::Bool) => true,
            (Value::String(_), Ty::String) => true,
            (Value::Null, Ty::Optional(_)) => true,
            (Value::List(items), Ty::List(item_id)) => {
                items.iter().all(|i| i.conforms_to(ctx, item_id))
            }
            (Value::Dict(vals), Ty::Dict(val_id)) => {
                vals.values().all(|v| v.conforms_to(ctx, val_id))
            }
            (Value::Struct(fields), Ty::Struct(ty_fields, _)) => {
                for (fname, fty) in ty_fields {
                    // TODO: handle optional
                    let Some(value) = fields.get(&fname) else {
                        return false;
                    };
                    if !value.conforms_to(ctx, fty) {
                        return false;
                    }
                }
                true
            }
            (_, Ty::Var(_)) => true, /* TODO: this needs to be looked at in more detail */
            (_, _) => false,
        }
    }
}

/// Trait for types which can be turned into [Value]
pub trait ToValue {
    /// Turn this into a [Value]
    fn to_value(&self) -> Value;
}

impl ToValue for Value {
    fn to_value(&self) -> Value {
        self.clone()
    }
}

impl<T: ToValue> ToValue for &T {
    fn to_value(&self) -> Value {
        <T as ToValue>::to_value(self)
    }
}

impl<T: ToValue> ToValue for &[T] {
    fn to_value(&self) -> Value {
        Value::List(self.iter().map(|e| e.to_value()).collect())
    }
}

impl ToValue for &str {
    fn to_value(&self) -> Value {
        Value::String((*self).to_owned())
    }
}

impl ToValue for String {
    fn to_value(&self) -> Value {
        Value::String(self.clone())
    }
}

macro_rules! impl_to_value_via_to_string {
    ($name:ident) => {
        impl ToValue for $name {
            fn to_value(&self) -> Value {
                Value::String(<$name as ::std::string::ToString>::to_string(self))
            }
        }
    };
    ($($name:ident,)*) => {
        $(impl_to_value_via_to_string! { $name })*
    }
}

impl_to_value_via_to_string! {
    u8, u16, u32, u64, u128,
    i8, i16, i32, i64, i128,
    bool,
}