ftd/p2/
expression.rs

1#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
2#[serde(tag = "type")]
3pub enum Boolean {
4    // if: $caption is not null
5    IsNotNull {
6        value: ftd::PropertyValue,
7    },
8    // if: $caption is null
9    IsNull {
10        value: ftd::PropertyValue,
11    },
12    // if: $list is not empty
13    IsNotEmpty {
14        value: ftd::PropertyValue,
15    },
16    // if: $list is empty
17    IsEmpty {
18        value: ftd::PropertyValue,
19    },
20    // if: $caption == hello | if: $foo
21    Equal {
22        left: ftd::PropertyValue,
23        right: ftd::PropertyValue,
24    },
25    // if: $caption != hello
26    NotEqual {
27        left: ftd::PropertyValue,
28        right: ftd::PropertyValue,
29    },
30    // if: not $show_something
31    Not {
32        of: Box<Boolean>,
33    },
34    // if: false
35    Literal {
36        value: bool,
37    },
38    // if: $array is empty
39    ListIsEmpty {
40        value: ftd::PropertyValue,
41    },
42}
43
44impl Boolean {
45    pub fn to_condition(
46        &self,
47        line_number: usize,
48        doc: &ftd::p2::TDoc,
49    ) -> ftd::p1::Result<ftd::Condition> {
50        let (variable, value) = match self {
51            Self::Equal { left, right } => {
52                let variable = resolve_variable(left, line_number, doc)?;
53
54                let value = match right {
55                    ftd::PropertyValue::Value { value } => value.to_owned(),
56                    ftd::PropertyValue::Variable { name, .. } => doc.get_value(0, name)?,
57                    _ => {
58                        return ftd::p2::utils::e2(
59                            format!("{:?} must be value or argument", right),
60                            doc.name,
61                            line_number,
62                        );
63                    }
64                };
65
66                (variable, value)
67            }
68            Self::IsNotNull { value } => {
69                let variable = resolve_variable(value, line_number, doc)?;
70                (
71                    variable,
72                    ftd::Value::String {
73                        text: "$IsNotNull$".to_string(),
74                        source: ftd::TextSource::Header,
75                    },
76                )
77            }
78            Self::IsNull { value } => {
79                let variable = resolve_variable(value, line_number, doc)?;
80                (
81                    variable,
82                    ftd::Value::String {
83                        text: "$IsNull$".to_string(),
84                        source: ftd::TextSource::Header,
85                    },
86                )
87            }
88            _ => {
89                return ftd::p2::utils::e2(
90                    format!("{:?} must not happen", self),
91                    doc.name,
92                    line_number,
93                )
94            }
95        };
96        return match value.to_serde_value() {
97            None => {
98                return ftd::p2::utils::e2(
99                    format!(
100                        "expected value of type String, Integer, Decimal or Boolean, found: {:?}",
101                        value
102                    ),
103                    doc.name,
104                    line_number,
105                )
106            }
107            Some(value) => Ok(ftd::Condition { variable, value }),
108        };
109
110        fn resolve_variable(
111            value: &ftd::PropertyValue,
112            line_number: usize,
113            doc: &ftd::p2::TDoc,
114        ) -> ftd::p1::Result<String> {
115            match value {
116                ftd::PropertyValue::Variable { name, .. }
117                | ftd::PropertyValue::Reference { name, .. } => Ok(name.to_string()),
118                _ => ftd::p2::utils::e2(
119                    format!("{:?} must be variable or local variable", value),
120                    doc.name,
121                    line_number,
122                ),
123            }
124        }
125    }
126
127    pub fn boolean_left_right(
128        line_number: usize,
129        expr: &str,
130        doc_id: &str,
131    ) -> ftd::p1::Result<(String, String, Option<String>)> {
132        let expr: String = expr.split_whitespace().collect::<Vec<&str>>().join(" ");
133        if expr == "true" || expr == "false" {
134            return Ok(("Literal".to_string(), expr, None));
135        }
136        let (left, rest) = match expr.split_once(' ') {
137            None => return Ok(("Equal".to_string(), expr.to_string(), None)),
138            Some(v) => v,
139        };
140        if left == "not" {
141            return Ok(("NotEqual".to_string(), rest.to_string(), None));
142        }
143        Ok(match rest {
144            "is not null" => ("IsNotNull".to_string(), left.to_string(), None),
145            "is null" => ("IsNull".to_string(), left.to_string(), None),
146            "is not empty" => ("IsNotEmpty".to_string(), left.to_string(), None),
147            "is empty" => ("IsEmpty".to_string(), left.to_string(), None),
148            _ if rest.starts_with("==") => (
149                "Equal".to_string(),
150                left.to_string(),
151                Some(rest.replace("==", "").trim().to_string()),
152            ),
153            _ => {
154                return ftd::p2::utils::e2(
155                    format!("'{}' is not valid condition", rest),
156                    doc_id,
157                    line_number,
158                )
159            }
160        })
161    }
162
163    pub fn from_expression(
164        expr: &str,
165        doc: &ftd::p2::TDoc,
166        arguments: &ftd::Map<ftd::p2::Kind>,
167        left_right_resolved_property: (Option<ftd::PropertyValue>, Option<ftd::PropertyValue>),
168        line_number: usize,
169    ) -> ftd::p1::Result<Self> {
170        let (boolean, mut left, mut right) =
171            ftd::p2::Boolean::boolean_left_right(line_number, expr, doc.name)?;
172        left = doc.resolve_reference_name(line_number, left.as_str(), arguments)?;
173        if let Some(ref r) = right {
174            right = doc.resolve_reference_name(line_number, r, arguments).ok();
175        }
176        return Ok(match boolean.as_str() {
177            "Literal" => Boolean::Literal {
178                value: left == "true",
179            },
180            "IsNotNull" | "IsNull" => {
181                let value = if !left.starts_with("$PARENT") {
182                    let value = property_value(
183                        &left,
184                        None,
185                        doc,
186                        arguments,
187                        left_right_resolved_property.0,
188                        line_number,
189                    )?;
190                    if !value.kind().is_optional() {
191                        return ftd::p2::utils::e2(
192                            format!("'{}' is not to an optional", left),
193                            doc.name,
194                            line_number,
195                        );
196                    }
197                    value
198                } else {
199                    property_value(
200                        &left,
201                        None,
202                        doc,
203                        arguments,
204                        left_right_resolved_property.0,
205                        line_number,
206                    )
207                    .unwrap_or(ftd::PropertyValue::Variable {
208                        name: left.trim_start_matches('$').to_string(),
209                        kind: ftd::p2::Kind::Element,
210                    })
211                };
212                if boolean.as_str() == "IsNotNull" {
213                    Boolean::IsNotNull { value }
214                } else {
215                    Boolean::IsNull { value }
216                }
217            }
218            "IsNotEmpty" | "IsEmpty" => {
219                let value = property_value(
220                    &left,
221                    None,
222                    doc,
223                    arguments,
224                    left_right_resolved_property.0,
225                    line_number,
226                )?;
227                if !value.kind().is_list() {
228                    return ftd::p2::utils::e2(
229                        format!("'{}' is not to a list", left),
230                        doc.name,
231                        line_number,
232                    );
233                }
234                if boolean.as_str() == "IsNotEmpty" {
235                    Boolean::IsNotEmpty { value }
236                } else {
237                    Boolean::IsEmpty { value }
238                }
239            }
240            "NotEqual" | "Equal" => {
241                if let Some(right) = right {
242                    let left = property_value(
243                        &left,
244                        None,
245                        doc,
246                        arguments,
247                        left_right_resolved_property.0,
248                        line_number,
249                    )?;
250                    Boolean::Equal {
251                        left: left.to_owned(),
252                        right: property_value(
253                            &right,
254                            Some(left.kind()),
255                            doc,
256                            arguments,
257                            left_right_resolved_property.1,
258                            line_number,
259                        )?,
260                    }
261                } else {
262                    Boolean::Equal {
263                        left: property_value(
264                            &left,
265                            Some(ftd::p2::Kind::boolean()),
266                            doc,
267                            arguments,
268                            left_right_resolved_property.0,
269                            line_number,
270                        )?,
271                        right: ftd::PropertyValue::Value {
272                            value: ftd::Value::Boolean {
273                                value: boolean.as_str() == "Equal",
274                            },
275                        },
276                    }
277                }
278            }
279            _ => {
280                return ftd::p2::utils::e2(
281                    format!("'{}' is not valid condition", expr),
282                    doc.name,
283                    line_number,
284                )
285            }
286        });
287
288        fn property_value(
289            value: &str,
290            expected_kind: Option<ftd::p2::Kind>,
291            doc: &ftd::p2::TDoc,
292            arguments: &ftd::Map<ftd::p2::Kind>,
293            loop_already_resolved_property: Option<ftd::PropertyValue>,
294            line_number: usize,
295        ) -> ftd::p1::Result<ftd::PropertyValue> {
296            Ok(
297                match ftd::PropertyValue::resolve_value(
298                    line_number,
299                    value,
300                    expected_kind,
301                    doc,
302                    arguments,
303                    None,
304                ) {
305                    Ok(v) => v,
306                    Err(e) => match &loop_already_resolved_property {
307                        Some(ftd::PropertyValue::Variable { .. }) => {
308                            loop_already_resolved_property.clone().expect("")
309                        }
310                        _ if value.starts_with("$PARENT") => ftd::PropertyValue::Variable {
311                            name: value.trim_start_matches('$').to_string(),
312                            kind: ftd::p2::Kind::Element,
313                        },
314                        _ => return Err(e),
315                    },
316                },
317            )
318        }
319    }
320
321    pub fn is_constant(&self) -> bool {
322        let is_loop_constant = {
323            let mut constant = false;
324            if let ftd::p2::Boolean::Equal {
325                left: ftd::PropertyValue::Variable { name, .. },
326                right: ftd::PropertyValue::Value { .. },
327            } = self
328            {
329                if name.starts_with("$loop$") {
330                    constant = true;
331                }
332            }
333            constant
334        };
335        (!matches!(
336            self,
337            Self::Equal {
338                left: ftd::PropertyValue::Reference { .. },
339                right: ftd::PropertyValue::Value { .. },
340                ..
341            }
342        ) && !matches!(
343            self,
344            Self::Equal {
345                left: ftd::PropertyValue::Variable { .. },
346                right: ftd::PropertyValue::Value { .. },
347                ..
348            }
349        ) && !matches!(self, Self::IsNotNull { .. })
350            && !matches!(self, Self::IsNull { .. }))
351            || is_loop_constant
352    }
353
354    pub fn is_arg_constant(&self) -> bool {
355        let is_loop_constant = {
356            let mut constant = false;
357            if let ftd::p2::Boolean::Equal {
358                left: ftd::PropertyValue::Variable { name, .. },
359                right: ftd::PropertyValue::Value { .. },
360            } = self
361            {
362                if name.starts_with("$loop$") {
363                    constant = true;
364                }
365            }
366            constant
367        };
368        (!matches!(
369            self,
370            Self::Equal {
371                left: ftd::PropertyValue::Reference { .. },
372                right: ftd::PropertyValue::Value { .. },
373                ..
374            }
375        ) && !matches!(
376            self,
377            Self::Equal {
378                left: ftd::PropertyValue::Variable { .. },
379                right: ftd::PropertyValue::Value { .. },
380                ..
381            }
382        ) && !matches!(
383            self,
384            Self::Equal {
385                left: ftd::PropertyValue::Reference { .. },
386                right: ftd::PropertyValue::Variable { .. },
387                ..
388            }
389        ) && !matches!(
390            self,
391            Self::Equal {
392                left: ftd::PropertyValue::Variable { .. },
393                right: ftd::PropertyValue::Variable { .. },
394                ..
395            }
396        ) && !matches!(self, Self::IsNotNull { .. })
397            && !matches!(self, Self::IsNull { .. }))
398            || is_loop_constant
399    }
400
401    pub fn eval(&self, line_number: usize, doc: &ftd::p2::TDoc) -> ftd::p1::Result<bool> {
402        Ok(match self {
403            Self::Literal { value } => *value,
404            Self::IsNotNull { value } => !value.resolve(line_number, doc)?.is_null(),
405            Self::IsNull { value } => value.resolve(line_number, doc)?.is_null(),
406            Self::IsNotEmpty { value } => !value.resolve(line_number, doc)?.is_empty(),
407            Self::IsEmpty { value } => value.resolve(line_number, doc)?.is_empty(),
408            Self::Equal { left, right } => left
409                .resolve(line_number, doc)?
410                .is_equal(&right.resolve(line_number, doc)?),
411            _ => {
412                return ftd::p2::utils::e2(
413                    format!("unknown Boolean found: {:?}", self),
414                    doc.name,
415                    line_number,
416                )
417            }
418        })
419    }
420
421    pub fn set_null(&self, line_number: usize, doc_id: &str) -> ftd::p1::Result<bool> {
422        Ok(match self {
423            Self::Literal { .. } | Self::IsNotEmpty { .. } | Self::IsEmpty { .. } => true,
424            Self::Equal { left, right } => match (left, right) {
425                (ftd::PropertyValue::Value { .. }, ftd::PropertyValue::Value { .. })
426                | (ftd::PropertyValue::Value { .. }, ftd::PropertyValue::Variable { .. })
427                | (ftd::PropertyValue::Variable { .. }, ftd::PropertyValue::Value { .. })
428                | (ftd::PropertyValue::Variable { .. }, ftd::PropertyValue::Variable { .. }) => {
429                    true
430                }
431                _ => false,
432            },
433            Self::IsNotNull { .. } | Self::IsNull { .. } => false,
434            _ => {
435                return ftd::p2::utils::e2(
436                    format!("unimplemented for type: {:?}", self),
437                    doc_id,
438                    line_number,
439                )
440            }
441        })
442    }
443}