Skip to main content

zen_expression/intellisense/
entity_flow.rs

1use std::rc::Rc;
2
3use crate::functions::{ClosureFunction, FunctionKind};
4use crate::lexer::{LogicalOperator, Operator};
5use crate::parser::Node;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct FlowSource {
9    pub path: Vec<Rc<str>>,
10    pub element: bool,
11}
12
13impl FlowSource {
14    pub(crate) fn from_node(node: &Node) -> Option<FlowSource> {
15        match node {
16            Node::Parenthesized(inner) => Self::from_node(inner),
17            Node::Identifier(_) | Node::Root => Self::path_of(node).map(|path| FlowSource {
18                path,
19                element: false,
20            }),
21            Node::Member {
22                node: base,
23                property,
24            } => match property {
25                Node::String(_) => Self::path_of(node).map(|path| FlowSource {
26                    path,
27                    element: false,
28                }),
29                Node::Number(_) => {
30                    let source = Self::from_node(base)?;
31                    (!source.element).then_some(FlowSource {
32                        path: source.path,
33                        element: true,
34                    })
35                }
36                _ => None,
37            },
38            Node::Slice { node: base, .. } => {
39                let source = Self::from_node(base)?;
40                (!source.element).then_some(source)
41            }
42            Node::FunctionCall {
43                kind: FunctionKind::Closure(ClosureFunction::Filter),
44                arguments,
45            } => {
46                let source = Self::from_node(arguments.first()?)?;
47                (!source.element).then_some(source)
48            }
49            Node::Binary {
50                left,
51                operator: Operator::Logical(LogicalOperator::NullishCoalescing),
52                right,
53            } => Self::agreeing(left, right),
54            Node::Conditional {
55                on_true, on_false, ..
56            } => Self::agreeing(on_true, on_false),
57            _ => None,
58        }
59    }
60
61    fn agreeing(a: &Node, b: &Node) -> Option<FlowSource> {
62        let left = Self::from_node(a)?;
63        let right = Self::from_node(b)?;
64        (left == right).then_some(left)
65    }
66
67    fn path_of(node: &Node) -> Option<Vec<Rc<str>>> {
68        match node {
69            Node::Identifier(name) => Some(vec![Rc::from(*name)]),
70            Node::Member {
71                node: base,
72                property,
73            } => {
74                let mut path = Self::path_of(base)?;
75                match property {
76                    Node::String(key) => {
77                        path.push(Rc::from(*key));
78                        Some(path)
79                    }
80                    _ => None,
81                }
82            }
83            _ => None,
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::intellisense::IntelliSense;
92
93    fn flow(source: &str) -> Option<FlowSource> {
94        IntelliSense::new().flow_source(source)
95    }
96
97    fn path(segments: &[&str]) -> Vec<Rc<str>> {
98        segments.iter().map(|s| Rc::from(*s)).collect()
99    }
100
101    #[test]
102    fn bare_path_is_identity() {
103        assert_eq!(
104            flow("customer.companies"),
105            Some(FlowSource {
106                path: path(&["customer", "companies"]),
107                element: false,
108            })
109        );
110    }
111
112    #[test]
113    fn filter_preserves_identity() {
114        assert_eq!(
115            flow("filter(customer.companies, $.revenue > 0)"),
116            Some(FlowSource {
117                path: path(&["customer", "companies"]),
118                element: false,
119            })
120        );
121    }
122
123    #[test]
124    fn index_yields_element() {
125        assert_eq!(
126            flow("customer.companies[0]"),
127            Some(FlowSource {
128                path: path(&["customer", "companies"]),
129                element: true,
130            })
131        );
132    }
133
134    #[test]
135    fn slice_preserves_array() {
136        assert_eq!(
137            flow("customer.companies[1:3]"),
138            Some(FlowSource {
139                path: path(&["customer", "companies"]),
140                element: false,
141            })
142        );
143    }
144
145    #[test]
146    fn filter_of_index_is_rejected() {
147        assert_eq!(flow("filter(customer.companies[0], true)"), None);
148    }
149
150    #[test]
151    fn index_of_index_is_rejected() {
152        assert_eq!(flow("customer.companies[0][1]"), None);
153    }
154
155    #[test]
156    fn nullish_with_agreeing_sides() {
157        assert_eq!(
158            flow("customer.companies ?? customer.companies"),
159            Some(FlowSource {
160                path: path(&["customer", "companies"]),
161                element: false,
162            })
163        );
164    }
165
166    #[test]
167    fn conditional_with_disagreeing_branches_is_rejected() {
168        assert_eq!(
169            flow("customer.age > 10 ? customer.companies : customer.orders"),
170            None
171        );
172    }
173
174    #[test]
175    fn conditional_with_agreeing_branches() {
176        assert_eq!(
177            flow("customer.age > 10 ? customer.companies[0] : customer.companies[0]"),
178            Some(FlowSource {
179                path: path(&["customer", "companies"]),
180                element: true,
181            })
182        );
183    }
184
185    #[test]
186    fn map_erases_identity() {
187        assert_eq!(flow("map(customer.companies as c, { name: c.name })"), None);
188    }
189
190    #[test]
191    fn arithmetic_erases_identity() {
192        assert_eq!(flow("customer.age * 2"), None);
193    }
194
195    #[test]
196    fn filter_chain_through_member_path() {
197        assert_eq!(
198            flow("filter(customer.profitableCompanies, $.revenue > 100)[0]"),
199            Some(FlowSource {
200                path: path(&["customer", "profitableCompanies"]),
201                element: true,
202            })
203        );
204    }
205}