zen_expression/intellisense/
entity_flow.rs1use 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}