cco 0.2.0

cascading configuration
Documentation
use super::{Expr, ExprEvaluator};
use crate::err::*;
use crate::eval::*;
use crate::value::*;
use std::path::Path;

#[derive(Debug, Clone)]
pub struct FuncCallExpr {
    pub namespace: Vec<String>,
    pub name: String,
    pub args: Vec<Index>,
}

impl Expr for FuncCallExpr {
    fn resolve(
        &self,
        index: Index,
        evaluation: &mut dyn ExprEvaluator,
    ) -> Result<ValueOrReference, ErrorKind> {
        evaluation.ensure_resolved(self.args.to_vec())?;

        let namespace_ref: Vec<&str> = self.namespace.iter().map(|s| s.as_str()).collect();
        let values = self
            .args
            .iter()
            .map(|arg| evaluation.get_value(*arg))
            .collect::<Result<Vec<Value>, ErrorKind>>()?;

        let value: Value = match (
            namespace_ref.as_slice(),
            self.name.as_str(),
            values.as_slice(),
        ) {
            (["str"], "empty", [Value::String(value)]) => Ok(Value::from(value.is_empty())),
            (["str"], "len", [Value::String(value)]) => Ok(Value::from(value.len())),
            (["str"], "trim", [Value::String(value)]) => Ok(value.trim().into()),
            (["str"], "trim_start", [Value::String(value)]) => Ok(value.trim_start().into()),
            (["str"], "trim_end", [Value::String(value)]) => Ok(value.trim_end().into()),
            (["str"], "contains", [Value::String(value), Value::String(search)]) => {
                Ok(value.contains(search).into())
            }
            (["str"], "starts_with", [Value::String(value), Value::String(prefix)]) => {
                Ok(value.starts_with(prefix).into())
            }
            (["str"], "ends_with", [Value::String(value), Value::String(suffix)]) => {
                Ok(value.ends_with(suffix).into())
            }
            (["str"], "to_upper", [Value::String(value)]) => Ok(value.to_uppercase().into()),
            (["str"], "to_lower", [Value::String(value)]) => Ok(value.to_lowercase().into()),
            (["str"], "join", [values @ .., Value::String(sep)]) => {
                list_of_str(values, |list| list.join(sep))
            }
            (["str"], "concat", values) => list_of_str(values, |list| list.join("")),
            (["str"], "split", [Value::String(value), Value::String(delimiter)]) => {
                Ok(Value::List(
                    value
                        .split(delimiter)
                        .map(|s| Value::String(s.to_string()))
                        .collect(),
                ))
            }
            (
                ["str"],
                "replace",
                [Value::String(value), Value::String(old), Value::String(new)],
            ) => Ok(value.replace(old, new).into()),
            (["str"], "reverse", [Value::String(value)]) => {
                Ok(value.chars().rev().collect::<String>().into())
            }

            (["path"], "basename" | "file_name", [Value::String(value)]) => Ok(Path::new(value)
                .file_name()
                .map(|path| String::from_utf8_lossy(path.as_encoded_bytes()).to_string())
                .unwrap_or_default()
                .into()),
            (["path"], "file_stem", [Value::String(value)]) => Ok(Path::new(value)
                .file_stem()
                .map(|path| path.to_string_lossy().to_string())
                .unwrap_or_default()
                .into()),
            (["path"], "file_ext", [Value::String(value)]) => Ok(Path::new(value)
                .extension()
                .map(|path| path.to_string_lossy().to_string())
                .unwrap_or_default()
                .into()),
            (["path"], "dirname", [Value::String(value)]) => Ok(Path::new(value)
                .parent()
                .map(|p| p.to_string_lossy().to_string())
                .unwrap_or_default()
                .into()),
            (["path"], "join", values) => list_of_str(values, |list| {
                list.iter()
                    .fold(std::path::PathBuf::new(), |mut path, segment| {
                        path.push(segment);
                        path
                    })
                    .to_string_lossy()
                    .to_string()
            }),

            (["list"], "empty", [Value::List(value)]) => Ok(value.is_empty().into()),
            (["list"], "len", [Value::List(value)]) => Ok(value.len().into()),
            (["list"], "all", values) => list_of_bool(values, |list| list.iter().all(|b| *b)),
            (["list"], "any", values) => list_of_bool(values, |list| list.iter().any(|b| *b)),
            (["list"], "first", [Value::List(list)]) => list
                .first()
                .cloned()
                .ok_or_else(|| anyhow::anyhow!("list is empty")),
            (["list"], "last", [Value::List(list)]) => list
                .last()
                .cloned()
                .ok_or_else(|| anyhow::anyhow!("list is empty")),
            (["list"], "reverse", [Value::List(list)]) => {
                let mut reversed = list.clone();
                reversed.reverse();
                Ok(Value::List(reversed))
            }
            (["list"], "contains", [Value::List(list), value]) => Ok(list.contains(value).into()),

            (["map"], "empty", [Value::Map(value)]) => Ok(value.is_empty().into()),
            (["map"], "len", [Value::Map(value)]) => Ok(value.len().into()),
            (["map"], "keys", [Value::Map(map)]) => Ok(Value::List(
                map.keys().map(|k| Value::String(k.clone())).collect(),
            )),
            (["map"], "values", [Value::Map(map)]) => {
                Ok(Value::List(map.values().cloned().collect()))
            }
            (["map"], "has_key", [Value::Map(map), Value::String(key)]) => {
                Ok(map.contains_key(key).into())
            }
            (["map"], "merge", values) => list_of_maps(values, |maps| {
                let mut merged = indexmap::IndexMap::new();
                for map in maps {
                    for (k, v) in map.iter() {
                        merged.insert(k.clone(), v.clone());
                    }
                }
                merged
            }),

            (["int"], "abs", &[Value::Integer(num)]) => Ok(num.abs().into()),
            (["int"], "sum", values) => list_of_int(values, |list| list.iter().sum::<i64>()),
            (["int"], "min", values) => {
                list_of_int(values, |list| *list.iter().min().unwrap_or(&0i64))
            }
            (["int"], "max", values) => {
                list_of_int(values, |list| *list.iter().max().unwrap_or(&0i64))
            }
            (
                ["int"],
                "clamp",
                [
                    Value::Integer(num),
                    Value::Integer(min),
                    Value::Integer(max),
                ],
            ) => Ok(Value::Integer(*num.clamp(min, max))),

            (["time"], "minutes", [Value::Integer(num)]) => Ok(Value::Integer(num * 60)),
            (["time"], "hours", [Value::Integer(num)]) => Ok(Value::Integer(num * 60 * 60)),
            (["time"], "days", [Value::Integer(num)]) => Ok(Value::Integer(num * 60 * 60 * 24)),
            (["time"], "weeks", [Value::Integer(num)]) => {
                Ok(Value::Integer(num * 60 * 60 * 24 * 7))
            }
            _ => {
                return Err(ErrorKind::UnknownFunction {
                    item: index,
                    name: self.name.clone(),
                    namespace: self.namespace.clone(),
                    arg_kinds: values.iter().map(Into::into).collect(),
                })?;
            }
        }
        .map_err(|inner| ErrorKind::Other(std::sync::Arc::new(inner)))?;

        Ok(ValueOrReference::Value(value))
    }

    fn traverse(
        &self,
        _evaluation: &dyn ExprEvaluator,
        expression: Index,
        _name: &Indexer,
    ) -> Result<Option<ValueOrReference>, ErrorKind> {
        Err(ErrorKind::DependenciesNotSatisfied {
            indices: vec![expression],
        })
    }
}

pub fn list_of_str<R>(values: &[Value], map: impl FnOnce(Vec<&str>) -> R) -> anyhow::Result<Value>
where
    R: Into<Value>,
{
    let values = if let [Value::List(values)] = values {
        values
    } else {
        values
    };

    values
        .iter()
        .map(|s| s.as_str())
        .collect::<Option<Vec<&str>>>()
        .ok_or_else(|| anyhow::anyhow!("only strings are allowed"))
        .map(map)
        .map(Into::into)
}

pub fn list_of_bool<R>(values: &[Value], map: impl FnOnce(Vec<bool>) -> R) -> anyhow::Result<Value>
where
    R: Into<Value>,
{
    let values = if let [Value::List(values)] = values {
        values
    } else {
        values
    };

    values
        .iter()
        .map(|s| s.as_boolean())
        .collect::<Option<Vec<bool>>>()
        .ok_or_else(|| anyhow::anyhow!("only booleans are allowed"))
        .map(map)
        .map(Into::into)
}

pub fn list_of_int<R>(values: &[Value], map: impl FnOnce(Vec<i64>) -> R) -> anyhow::Result<Value>
where
    R: Into<Value>,
{
    let values = if let [Value::List(values)] = values {
        values
    } else {
        values
    };

    values
        .iter()
        .map(|s| s.as_integer())
        .collect::<Option<Vec<i64>>>()
        .ok_or_else(|| anyhow::anyhow!("only integers are allowed"))
        .map(map)
        .map(Into::into)
}

pub fn list_of_maps<R>(
    values: &[Value],
    map: impl FnOnce(Vec<&indexmap::IndexMap<String, Value>>) -> R,
) -> anyhow::Result<Value>
where
    R: Into<Value>,
{
    let values = if let [Value::List(values)] = values {
        values
    } else {
        values
    };

    values
        .iter()
        .map(|s| s.as_map())
        .collect::<Option<Vec<_>>>()
        .ok_or_else(|| anyhow::anyhow!("only integers are allowed"))
        .map(map)
        .map(Into::into)
}