expr-lang 2.0.0

Implementation of expr language in Rust
Documentation
use crate::ast::node::Node;
use crate::Rule;
use crate::{bail, Result};
use crate::{ContextProvider, Environment, Value};
use log::trace;
use pest::iterators::Pair;
use std::iter::once;

#[derive(Debug, Clone, strum::Display)]
pub enum PostfixOperator {
    Index { idx: Box<Node>, optional: bool },
    Range {
        start: Option<Box<Node>>,
        end: Option<Box<Node>>,
    },
    Default(Box<Node>),
    Pipe(Box<Node>),
    Ternary { left: Box<Node>, right: Box<Node> },
    Method { ident: String, args: Vec<Node> },
}

impl PostfixOperator {
    pub(crate) fn contains_hash_ident(&self) -> bool {
        match self {
            PostfixOperator::Index { idx, .. } => idx.contains_hash_ident(),
            PostfixOperator::Default(node) | PostfixOperator::Pipe(node) => {
                node.contains_hash_ident()
            }
            PostfixOperator::Ternary { left, right } => {
                left.contains_hash_ident() || right.contains_hash_ident()
            }
            PostfixOperator::Method { args, .. } => {
                args.iter().any(Node::contains_hash_ident)
            }
            PostfixOperator::Range { start, end } => start
                .iter()
                .chain(end)
                .any(|node| node.contains_hash_ident()),
        }
    }
}

impl From<Pair<'_, Rule>> for PostfixOperator {
    fn from(pair: Pair<Rule>) -> Self {
        trace!("{:?}={}", pair.as_rule(), pair.as_str());
        match pair.as_rule() {
            Rule::index_op => PostfixOperator::Index {
                idx: Box::new(pair.into_inner().into()),
                optional: false,
            },
            Rule::membership_op => PostfixOperator::Index {
                idx: Box::new(Node::Value(Value::String(
                    pair.into_inner().next().unwrap().as_str().to_string(),
                ))),
                optional: false,
            },
            Rule::opt_index_op => PostfixOperator::Index {
                idx: Box::new(pair.into_inner().into()),
                optional: true,
            },
            Rule::opt_membership_op => PostfixOperator::Index {
                idx: Box::new(Node::Value(Value::String(
                    pair.into_inner().next().unwrap().as_str().to_string(),
                ))),
                optional: true,
            },
            Rule::range_start_op => {
                let mut inner = pair.into_inner();
                let start = inner.next().map(|p| Box::new(p.into()));
                let end = inner.next().map(|p| Box::new(p.into()));
                PostfixOperator::Range { start, end }
            }
            Rule::range_end_op => {
                let mut inner = pair.into_inner();
                let end = inner.next().map(|p| Box::new(p.into()));
                PostfixOperator::Range { start: None, end }
            }
            Rule::default_op => PostfixOperator::Default(Box::new(pair.into_inner().into())),
            Rule::ternary => {
                let mut inner = pair.into_inner();
                let left = Box::new(inner.next().unwrap().into());
                let right = Box::new(inner.next().unwrap().into());
                PostfixOperator::Ternary { left, right }
            }
            Rule::pipe => PostfixOperator::Pipe(Box::new(pair.into_inner().into())),
            Rule::method_call => {
                let mut inner = pair.into_inner();
                PostfixOperator::Method {
                    ident: inner.next().expect("method name").as_str().to_string(),
                    args: inner.map(Node::from).collect(),
                }
            }
            rule => unreachable!("Unexpected rule: {rule:?}"),
        }
    }
}

impl Environment<'_> {
    pub fn eval_postfix_operator(
        &self,
        ctx: &dyn ContextProvider,
        operator: &PostfixOperator,
        node: &Node,
    ) -> Result<Value> {
        let value = self.eval_expr(ctx, node)?;
        let result = match operator {
            PostfixOperator::Index { idx, optional } => {
                let key = self.eval_expr(ctx, idx)?;
                match (&key, value) {
                    (Value::Integer(idx), Value::Array(arr)) => {
                        let idx = i64_to_idx(*idx, arr.len());
                        arr.get(idx).cloned().unwrap_or(Value::Nil)
                    }
                    (Value::Integer(idx), Value::Bytes(bytes)) => {
                        let idx = i64_to_idx(*idx, bytes.len());
                        bytes
                            .get(idx)
                            .map(|byte| Value::Integer((*byte).into()))
                            .unwrap_or(Value::Nil)
                    }
                    (Value::String(key), Value::Map(map)) => {
                        map.get(key).cloned().unwrap_or(Value::Nil)
                    }
                    (key, Value::KeyedMap(map)) => map
                        .into_iter()
                        .find(|(candidate, _)| {
                            crate::ast::operator::map_keys_equal(key, candidate)
                        })
                        .map(|(_, value)| value)
                        .unwrap_or(Value::Nil),
                    (_, _) if *optional => Value::Nil,
                    _ => bail!("Invalid operand for operator []: {key:?}"),
                }
            },
            PostfixOperator::Range { start, end } => {
                let start = self.eval_slice_bound(ctx, start.as_deref())?;
                let end = self.eval_slice_bound(ctx, end.as_deref())?;
                match value {
                    Value::Array(arr) => {
                        let (start, end) = slice_bounds(start, end, arr.len());
                        let result = arr.get(start..end).unwrap_or_default().to_vec();
                        Value::Array(result)
                    }
                    Value::Bytes(bytes) => {
                        let (start, end) = slice_bounds(start, end, bytes.len());
                        Value::Bytes(bytes.get(start..end).unwrap_or_default().to_vec())
                    }
                    Value::String(string) => {
                        let (start, end) = slice_bounds(start, end, string.len());
                        Value::String(
                            String::from_utf8_lossy(&string.as_bytes()[start..end]).into(),
                        )
                    }
                    _ => bail!("Invalid operand for operator []"),
                }
            },
            PostfixOperator::Default(default) => match value {
                Value::Nil => self.eval_expr(ctx, default)?,
                value => value,
            },
            PostfixOperator::Ternary { left, right } => match value {
                Value::Bool(true) => self.eval_expr(ctx, left)?,
                Value::Bool(false) => self.eval_expr(ctx, right)?,
                value => bail!("Invalid condition for ?: {value:?}"),
            },
            PostfixOperator::Pipe(func) => {
                if let Node::Func {
                    ident,
                    args,
                    predicate,
                } = func.as_ref()
                {
                    let args = once(Ok(value))
                        .chain(args.iter().map(|arg| self.eval_expr(ctx, arg)))
                        .collect::<Result<Vec<Value>>>()?;
                    self.eval_func(ctx, ident, args, predicate.as_deref())?
                } else {
                    bail!("Invalid operand for operator |");
                }
            }
            PostfixOperator::Method { ident, args } => {
                let args = args
                    .iter()
                    .map(|arg| self.eval_expr(ctx, arg))
                    .collect::<Result<Vec<_>>>()?;
                crate::functions::temporal::eval_method(value, ident, args)?
            }
        };

        Ok(result)
    }

    fn eval_slice_bound(
        &self,
        ctx: &dyn ContextProvider,
        bound: Option<&Node>,
    ) -> Result<Option<i64>> {
        match bound {
            Some(bound) => match self.eval_expr(ctx, bound)? {
                Value::Integer(value) => Ok(Some(value)),
                value => bail!("Invalid slice index: {value:?}"),
            },
            None => Ok(None),
        }
    }
}

fn i64_to_idx(idx: i64, len: usize) -> usize {
    if idx < 0 {
        (len as i64 + idx) as usize
    } else {
        idx as usize
    }
}

fn slice_idx(idx: i64, len: usize) -> usize {
    if idx < 0 {
        len.saturating_sub(idx.unsigned_abs() as usize)
    } else {
        usize::try_from(idx).unwrap_or(usize::MAX).min(len)
    }
}

fn slice_bounds(start: Option<i64>, end: Option<i64>, len: usize) -> (usize, usize) {
    let start = slice_idx(start.unwrap_or(0), len);
    let end = slice_idx(end.unwrap_or(len as i64), len);
    (start.min(end), end)
}