oca_bundle/state/oca/overlay/
conditional.rs

1use crate::state::{attribute::Attribute, oca::Overlay};
2use oca_ast::ast::OverlayType;
3use piccolo::{Closure, Lua, Thread};
4use said::{sad::SerializationFormats, sad::SAD};
5use serde::{Deserialize, Serialize};
6use std::{any::Any, collections::BTreeMap, error::Error as StdError, fmt::Display};
7
8pub trait Conditionals {
9    fn set_condition(&mut self, condition: String);
10    fn check_condition(
11        &self,
12        dependency_values: BTreeMap<String, Box<dyn Display + 'static>>,
13    ) -> Result<bool, Vec<Error>>;
14}
15
16#[derive(Serialize, Deserialize, Debug)]
17#[serde(untagged)]
18pub enum Error {
19    Custom(String),
20}
21
22impl Conditionals for Attribute {
23    fn set_condition(&mut self, condition: String) {
24        let re = regex::Regex::new(r"\$\{([^}]*)\}").unwrap();
25        let mut dependencies = Vec::new();
26
27        let cond = re
28            .replace_all(&condition, |caps: &regex::Captures| {
29                let i = dependencies
30                    .iter()
31                    .position(|d| d == &caps[1])
32                    .unwrap_or_else(|| {
33                        dependencies.push(caps[1].to_string());
34                        dependencies.len() - 1
35                    });
36                format!("${{{}}}", i)
37            })
38            .to_string();
39
40        self.condition = Some(cond);
41        self.dependencies = Some(dependencies);
42    }
43
44    fn check_condition(
45        &self,
46        dependency_values: BTreeMap<String, Box<dyn Display + 'static>>,
47    ) -> Result<bool, Vec<Error>> {
48        let mut errors: Vec<Error> = vec![];
49
50        let condition = &self.condition.clone().ok_or(vec![Error::Custom(
51            "Attribute has no condition".to_string(),
52        )])?;
53        let condition_dependencies = &self.dependencies.clone().unwrap();
54        let re = regex::Regex::new(r"\$\{(\d+)\}").unwrap();
55        let attr = &self.name;
56        if condition_dependencies.contains(attr) {
57            errors.push(Error::Custom(format!(
58                "Attribute '{attr}' cannot be a dependency of itself"
59            )));
60        }
61        condition_dependencies.iter().for_each(|d| {
62            if !dependency_values.contains_key(d) {
63                errors.push(Error::Custom(format!("Missing dependency '{d}' value",)));
64            }
65        });
66
67        if !errors.is_empty() {
68            return Err(errors);
69        }
70
71        let script = re
72            .replace_all(condition, |caps: &regex::Captures| {
73                dependency_values
74                    .get(&condition_dependencies[caps[1].parse::<usize>().unwrap()].clone())
75                    .unwrap()
76                    .to_string()
77            })
78            .to_string();
79
80        let mut lua = Lua::new();
81        let thread_result = lua.try_run(|ctx| {
82            let closure = Closure::load(ctx, format!("return {script}").as_bytes())?;
83            let thread = Thread::new(&ctx);
84            thread.start(ctx, closure.into(), ())?;
85            Ok(ctx.state.registry.stash(&ctx, thread))
86        });
87
88        let mut result = None;
89        match thread_result {
90            Ok(thread) => match lua.run_thread::<bool>(&thread) {
91                Ok(r) => result = Some(r),
92                Err(e) => {
93                    errors.push(Error::Custom(format!(
94                        "Attribute '{attr}' has invalid condition: {}",
95                        e.source().unwrap()
96                    )));
97                }
98            },
99            Err(e) => {
100                errors.push(Error::Custom(format!(
101                    "Attribute '{attr}' has invalid condition: {}",
102                    e.source().unwrap()
103                )));
104            }
105        };
106        if errors.is_empty() {
107            match result.is_some() {
108                true => Ok(result.unwrap()),
109                false => Err(vec![Error::Custom(format!(
110                    "Attribute '{attr}' has invalid condition",
111                ))]),
112            }
113        } else {
114            Err(errors)
115        }
116    }
117}
118
119#[derive(SAD, Serialize, Deserialize, Debug, Clone)]
120pub struct ConditionalOverlay {
121    #[said]
122    #[serde(rename = "d")]
123    said: Option<said::SelfAddressingIdentifier>,
124    #[serde(rename = "type")]
125    overlay_type: OverlayType,
126    capture_base: Option<said::SelfAddressingIdentifier>,
127    pub attribute_conditions: BTreeMap<String, String>,
128    pub attribute_dependencies: BTreeMap<String, Vec<String>>,
129}
130
131impl Overlay for ConditionalOverlay {
132    fn as_any(&self) -> &dyn Any {
133        self
134    }
135    fn capture_base(&self) -> &Option<said::SelfAddressingIdentifier> {
136        &self.capture_base
137    }
138    fn set_capture_base(&mut self, said: &said::SelfAddressingIdentifier) {
139        self.capture_base = Some(said.clone());
140    }
141    fn said(&self) -> &Option<said::SelfAddressingIdentifier> {
142        &self.said
143    }
144    fn overlay_type(&self) -> &OverlayType {
145        &self.overlay_type
146    }
147    fn attributes(&self) -> Vec<&String> {
148        self.attribute_conditions.keys().collect::<Vec<&String>>()
149    }
150
151    fn add(&mut self, attribute: &Attribute) {
152        if attribute.condition.is_some() {
153            self.attribute_conditions.insert(
154                attribute.name.clone(),
155                attribute.condition.as_ref().unwrap().clone(),
156            );
157        }
158        if attribute.dependencies.is_some() {
159            self.attribute_dependencies.insert(
160                attribute.name.clone(),
161                attribute.dependencies.as_ref().unwrap().clone(),
162            );
163        }
164    }
165}
166impl ConditionalOverlay {
167    pub fn new() -> Self {
168        Self {
169            capture_base: None,
170            said: None,
171            overlay_type: OverlayType::Conditional,
172            attribute_conditions: BTreeMap::new(),
173            attribute_dependencies: BTreeMap::new(),
174        }
175    }
176}
177
178impl Default for ConditionalOverlay {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use oca_ast::ast::{AttributeType, NestedAttrType};
188
189    struct Dependency {
190        name: String,
191        value: Box<dyn Display + 'static>,
192    }
193    impl core::fmt::Debug for Dependency {
194        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
195            write!(f, "{}={}", self.name, self.value)
196        }
197    }
198    impl Dependency {
199        fn new(name: &str, value: Box<dyn Display + 'static>) -> Self {
200            Self {
201                name: name.to_string(),
202                value,
203            }
204        }
205    }
206
207    #[test]
208    fn test_checking_condition() {
209        let setting: Vec<(&str, Vec<Dependency>, bool)> = vec![
210            (
211                "${age} > 18 and ${age} < 30",
212                vec![
213                    Dependency::new("age", Box::new(20)),
214                    Dependency::new("name", Box::new("new")),
215                ],
216                true,
217            ),
218            (
219                "${age} > 18 and ${age} < 30",
220                vec![Dependency::new("age", Box::new(30))],
221                false,
222            ),
223            (
224                "${age} > 18 and ${age} < 30",
225                vec![Dependency::new("age", Box::new(18))],
226                false,
227            ),
228            (
229                "${age} > 18 and ${age} < 30",
230                vec![Dependency::new("age", Box::new(31))],
231                false,
232            ),
233            (
234                "${age} > 18 and ${age} < 30",
235                vec![Dependency::new("age", Box::new(17))],
236                false,
237            ),
238            (
239                "2021-01-01 > ${start_date}",
240                vec![Dependency::new("start_date", Box::new("2024-01-01"))],
241                false,
242            ),
243        ];
244
245        for (condition, values, expected) in setting {
246            let attribute = cascade! {
247                Attribute::new("name".to_string());
248                ..set_attribute_type(NestedAttrType::Value(AttributeType::Text));
249                ..set_condition(condition.to_string());
250            };
251
252            let mut dependency_values: BTreeMap<String, Box<dyn Display + 'static>> =
253                BTreeMap::new();
254            let mut dependency_values_str = vec![];
255            for value in values.into_iter() {
256                dependency_values_str.push(format!("{:?}", value));
257                dependency_values.insert(value.name, value.value);
258            }
259
260            let r = attribute.check_condition(dependency_values);
261            assert_eq!(
262                r.unwrap(),
263                expected,
264                "condition: \"{}\", values: [{}]",
265                condition,
266                dependency_values_str.join(", ")
267            );
268        }
269    }
270}