zen-expression 2.0.1

Zen Expression Language
Documentation
use std::rc::Rc;

use crate::functions::{ClosureFunction, FunctionKind};
use crate::lexer::{LogicalOperator, Operator};
use crate::parser::Node;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlowSource {
    pub path: Vec<Rc<str>>,
    pub element: bool,
}

impl FlowSource {
    pub(crate) fn from_node(node: &Node) -> Option<FlowSource> {
        match node {
            Node::Parenthesized(inner) => Self::from_node(inner),
            Node::Identifier(_) | Node::Root => Self::path_of(node).map(|path| FlowSource {
                path,
                element: false,
            }),
            Node::Member {
                node: base,
                property,
            } => match property {
                Node::String(_) => Self::path_of(node).map(|path| FlowSource {
                    path,
                    element: false,
                }),
                Node::Number(_) => {
                    let source = Self::from_node(base)?;
                    (!source.element).then_some(FlowSource {
                        path: source.path,
                        element: true,
                    })
                }
                _ => None,
            },
            Node::Slice { node: base, .. } => {
                let source = Self::from_node(base)?;
                (!source.element).then_some(source)
            }
            Node::FunctionCall {
                kind: FunctionKind::Closure(ClosureFunction::Filter),
                arguments,
            } => {
                let source = Self::from_node(arguments.first()?)?;
                (!source.element).then_some(source)
            }
            Node::Binary {
                left,
                operator: Operator::Logical(LogicalOperator::NullishCoalescing),
                right,
            } => Self::agreeing(left, right),
            Node::Conditional {
                on_true, on_false, ..
            } => Self::agreeing(on_true, on_false),
            _ => None,
        }
    }

    fn agreeing(a: &Node, b: &Node) -> Option<FlowSource> {
        let left = Self::from_node(a)?;
        let right = Self::from_node(b)?;
        (left == right).then_some(left)
    }

    fn path_of(node: &Node) -> Option<Vec<Rc<str>>> {
        match node {
            Node::Identifier(name) => Some(vec![Rc::from(*name)]),
            Node::Member {
                node: base,
                property,
            } => {
                let mut path = Self::path_of(base)?;
                match property {
                    Node::String(key) => {
                        path.push(Rc::from(*key));
                        Some(path)
                    }
                    _ => None,
                }
            }
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::intellisense::IntelliSense;

    fn flow(source: &str) -> Option<FlowSource> {
        IntelliSense::new().flow_source(source)
    }

    fn path(segments: &[&str]) -> Vec<Rc<str>> {
        segments.iter().map(|s| Rc::from(*s)).collect()
    }

    #[test]
    fn bare_path_is_identity() {
        assert_eq!(
            flow("customer.companies"),
            Some(FlowSource {
                path: path(&["customer", "companies"]),
                element: false,
            })
        );
    }

    #[test]
    fn filter_preserves_identity() {
        assert_eq!(
            flow("filter(customer.companies, $.revenue > 0)"),
            Some(FlowSource {
                path: path(&["customer", "companies"]),
                element: false,
            })
        );
    }

    #[test]
    fn index_yields_element() {
        assert_eq!(
            flow("customer.companies[0]"),
            Some(FlowSource {
                path: path(&["customer", "companies"]),
                element: true,
            })
        );
    }

    #[test]
    fn slice_preserves_array() {
        assert_eq!(
            flow("customer.companies[1:3]"),
            Some(FlowSource {
                path: path(&["customer", "companies"]),
                element: false,
            })
        );
    }

    #[test]
    fn filter_of_index_is_rejected() {
        assert_eq!(flow("filter(customer.companies[0], true)"), None);
    }

    #[test]
    fn index_of_index_is_rejected() {
        assert_eq!(flow("customer.companies[0][1]"), None);
    }

    #[test]
    fn nullish_with_agreeing_sides() {
        assert_eq!(
            flow("customer.companies ?? customer.companies"),
            Some(FlowSource {
                path: path(&["customer", "companies"]),
                element: false,
            })
        );
    }

    #[test]
    fn conditional_with_disagreeing_branches_is_rejected() {
        assert_eq!(
            flow("customer.age > 10 ? customer.companies : customer.orders"),
            None
        );
    }

    #[test]
    fn conditional_with_agreeing_branches() {
        assert_eq!(
            flow("customer.age > 10 ? customer.companies[0] : customer.companies[0]"),
            Some(FlowSource {
                path: path(&["customer", "companies"]),
                element: true,
            })
        );
    }

    #[test]
    fn map_erases_identity() {
        assert_eq!(flow("map(customer.companies as c, { name: c.name })"), None);
    }

    #[test]
    fn arithmetic_erases_identity() {
        assert_eq!(flow("customer.age * 2"), None);
    }

    #[test]
    fn filter_chain_through_member_path() {
        assert_eq!(
            flow("filter(customer.profitableCompanies, $.revenue > 100)[0]"),
            Some(FlowSource {
                path: path(&["customer", "profitableCompanies"]),
                element: true,
            })
        );
    }
}