oca_bundle_semantics/state/oca/overlay/
conditional.rs

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