expressive/context/
mod.rs

1//! A context defines methods to retrieve variable values and call functions for literals in an expression tree.
2//! If mutable, it also allows to assign to variables.
3//!
4//! This crate implements two basic variants, the `EmptyContext`, that returns `None` for each identifier and cannot be manipulated, and the `HashMapContext`, that stores its mappings in hash maps.
5//! The HashMapContext is type-safe and returns an error if the user tries to assign a value of a different type than before to an identifier.
6
7use std::{
8    collections::BTreeMap,
9    fmt::Display,
10    fs,
11    io::{Read, Write},
12    process::Command,
13};
14
15use crate::{
16    container,
17    function::Function,
18    time,
19    value::{value_type::ValueType, Value},
20    EvalexprError, EvalexprResult,
21};
22
23mod predefined;
24
25/// An immutable context.
26pub trait Context {
27    /// Returns the value that is linked to the given identifier.
28    fn get_value(&self, identifier: &str) -> Option<&Value>;
29
30    /// Calls the function that is linked to the given identifier with the given argument.
31    /// If no function with the given identifier is found, this method returns `EvalexprError::FunctionIdentifierNotFound`.
32    fn call_function(&self, identifier: &str, argument: &Value) -> EvalexprResult<Value>;
33}
34
35/// A context that allows to assign to variables.
36pub trait ContextWithMutableVariables: Context {
37    /// Sets the variable with the given identifier to the given value.
38    fn set_value(&mut self, _identifier: &str, _value: Value) -> EvalexprResult<()> {
39        Err(EvalexprError::ContextNotMutable)
40    }
41}
42
43/// A context that allows to assign to function identifiers.
44pub trait ContextWithMutableFunctions: Context {
45    /// Sets the function with the given identifier to the given function.
46    fn set_function(&mut self, _identifier: String, _function: Function) -> EvalexprResult<()> {
47        Err(EvalexprError::ContextNotMutable)
48    }
49}
50
51/// A context that stores its mappings in hash maps.
52#[derive(Clone, Debug, Default)]
53pub struct VariableMap {
54    variables: BTreeMap<String, Value>,
55}
56
57impl Display for VariableMap {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(f, "(")?;
60
61        for (key, value) in &self.variables {
62            write!(f, " {} = {};", key, value)?;
63        }
64
65        write!(f, " )")
66    }
67}
68
69impl PartialEq for VariableMap {
70    fn eq(&self, other: &Self) -> bool {
71        if self.variables.len() != other.variables.len() {
72            return false;
73        }
74
75        for variable in &self.variables {
76            for other in &other.variables {
77                if variable != other {
78                    return false;
79                }
80            }
81        }
82
83        true
84    }
85}
86
87impl VariableMap {
88    /// Create a new instace.
89    pub fn new() -> Self {
90        Default::default()
91    }
92}
93
94impl Context for VariableMap {
95    fn get_value(&self, identifier: &str) -> Option<&Value> {
96        let split = identifier.split_once(".");
97        if let Some((map_name, next_identifier)) = split {
98            let value = self.variables.get(map_name)?;
99            if let Value::Map(map) = value {
100                map.get_value(next_identifier)
101            } else {
102                None
103            }
104        } else {
105            self.variables.get(identifier).or_else(|| {
106                self.call_function(identifier, &Value::Empty);
107                None
108            })
109        }
110    }
111
112    fn call_function(&self, identifier: &str, argument: &Value) -> EvalexprResult<Value> {
113        call_whale_function(identifier, argument)
114    }
115}
116
117impl ContextWithMutableVariables for VariableMap {
118    fn set_value(&mut self, identifier: &str, value: Value) -> EvalexprResult<()> {
119        let split = identifier.split_once(".");
120
121        if let Some((map_name, next_identifier)) = split {
122            if let Some(map_value) = self.variables.get_mut(map_name) {
123                if let Value::Map(map) = map_value {
124                    map.set_value(next_identifier, value)
125                } else {
126                    return Err(EvalexprError::ExpectedMap {
127                        actual: map_value.clone(),
128                    });
129                }
130            } else {
131                let mut new_map = VariableMap {
132                    variables: BTreeMap::new(),
133                };
134
135                new_map.set_value(next_identifier, value)?;
136                self.variables
137                    .insert(map_name.to_string(), Value::Map(new_map));
138
139                Ok(())
140            }
141        } else if self.variables.contains_key(identifier) {
142            Err(EvalexprError::ExpectedMap {
143                actual: value.clone(),
144            })
145        } else {
146            self.variables.insert(identifier.to_string(), value);
147
148            Ok(())
149        }
150    }
151}
152
153impl ContextWithMutableFunctions for VariableMap {
154    fn set_function(&mut self, identifier: String, function: Function) -> EvalexprResult<()> {
155        todo!()
156    }
157}
158
159/// This macro provides a convenient syntax for creating a static context.
160///
161/// # Examples
162///
163/// ```rust
164/// use evalexpr::*;
165///
166/// let ctx = evalexpr::context_map! {
167///     "x" => 8,
168///     "f" => Function::new(|_| Ok(42.into()))
169/// }.unwrap(); // Do proper error handling here
170///
171/// assert_eq!(eval_with_context("x + f()", &ctx), Ok(50.into()));
172/// ```
173#[macro_export]
174macro_rules! context_map {
175    // Termination (allow missing comma at the end of the argument list)
176    ( ($ctx:expr) $k:expr => Function::new($($v:tt)*) ) =>
177        { $crate::context_map!(($ctx) $k => Function::new($($v)*),) };
178    ( ($ctx:expr) $k:expr => $v:expr ) =>
179        { $crate::context_map!(($ctx) $k => $v,)  };
180    // Termination
181    ( ($ctx:expr) ) => { Ok(()) };
182
183    // The user has to specify a literal 'Function::new' in order to create a function
184    ( ($ctx:expr) $k:expr => Function::new($($v:tt)*) , $($tt:tt)*) => {{
185        $crate::ContextWithMutableFunctions::set_function($ctx, $k.into(), $crate::Function::new($($v)*))
186            .and($crate::context_map!(($ctx) $($tt)*))
187    }};
188    // add a value, and chain the eventual error with the ones in the next values
189    ( ($ctx:expr) $k:expr => $v:expr , $($tt:tt)*) => {{
190        $crate::ContextWithMutableVariables::set_value($ctx, $k.into(), $v.into())
191            .and($crate::context_map!(($ctx) $($tt)*))
192    }};
193
194    // Create a context, then recurse to add the values in it
195    ( $($tt:tt)* ) => {{
196        let mut context = $crate::VariableMap::new();
197        $crate::context_map!((&mut context) $($tt)*)
198            .map(|_| context)
199    }};
200}
201
202fn call_whale_function(identifier: &str, argument: &Value) -> Result<Value, EvalexprError> {
203    match identifier {
204        "container::image::build" => container::image::build(argument),
205        "container::image::list" => container::image::list(argument),
206        "container::list" => container::list(argument),
207        "container::run" => container::run(argument),
208        "convert" => todo!(),
209        "dir::create" => {
210            let path = argument.as_string()?;
211            std::fs::create_dir_all(path).unwrap();
212
213            Ok(Value::Empty)
214        },
215        "dir::read" => {
216            let path = argument.as_string()?;
217            let files = std::fs::read_dir(path)
218                .unwrap()
219                .map(|entry| entry.unwrap().file_name().into_string().unwrap_or_default())
220                .collect();
221
222            Ok(Value::String(files))
223        },
224        "dir::remove" => {
225            let path = argument.as_string()?;
226            std::fs::remove_file(path).unwrap();
227
228            Ok(Value::Empty)
229        },
230        "dir::trash" => todo!(),
231        "dnf::copr" => {
232            let repo_name = if let Ok(string) = argument.as_string() {
233                string
234            } else if let Ok(tuple) = argument.as_tuple() {
235                let mut repos = String::new();
236
237                for repo in tuple {
238                    let repo = repo.as_string()?;
239
240                    repos.push_str(&repo);
241                    repos.push(' ');
242                }
243
244                repos
245            } else {
246                return Err(EvalexprError::ExpectedString {
247                    actual: argument.clone(),
248                });
249            };
250            let script = format!("dnf -y copr enable {repo_name}");
251
252            Command::new("fish")
253                .arg("-c")
254                .arg(script)
255                .spawn()
256                .unwrap()
257                .wait()
258                .unwrap();
259
260            Ok(Value::Empty)
261        },
262        "dnf::packages" => {
263            let tuple = argument.as_tuple()?;
264            let mut packages = String::new();
265
266            for package in tuple {
267                let package = package.as_string()?;
268
269                packages.push_str(&package);
270                packages.push(' ');
271            }
272            let script = format!("dnf -y install {packages}");
273
274            Command::new("fish")
275                .arg("-c")
276                .arg(script)
277                .spawn()
278                .unwrap()
279                .wait()
280                .unwrap();
281
282            Ok(Value::Empty)
283        },
284        "dnf::repos" => {
285            let tuple = argument.as_tuple()?;
286            let mut repos = String::new();
287
288            for repo in tuple {
289                let repo = repo.as_string()?;
290
291                repos.push_str(&repo);
292                repos.push(' ');
293            }
294            let script = format!("dnf -y config-manager --add-repo {repos}");
295
296            Command::new("fish")
297                .arg("-c")
298                .arg(script)
299                .spawn()
300                .unwrap()
301                .wait()
302                .unwrap();
303
304            Ok(Value::Empty)
305        },
306        "dnf::upgrade" => {
307            Command::new("fish")
308                .arg("-c")
309                .arg("dnf -y upgrade")
310                .spawn()
311                .unwrap()
312                .wait()
313                .unwrap();
314
315            Ok(Value::Empty)
316        },
317        "file::append" => {
318            let strings = argument.as_tuple()?;
319
320            if strings.len() < 2 {
321                return Err(EvalexprError::WrongFunctionArgumentAmount {
322                    expected: 2,
323                    actual: strings.len(),
324                });
325            }
326
327            let path = strings.first().unwrap().as_string()?;
328            let mut file = std::fs::OpenOptions::new().append(true).open(path).unwrap();
329
330            for content in &strings[1..] {
331                let content = content.as_string()?;
332
333                file.write_all(content.as_bytes()).unwrap();
334            }
335
336            Ok(Value::Empty)
337        },
338        "file::metadata" => {
339            let path = argument.as_string()?;
340            let metadata = std::fs::metadata(path).unwrap();
341
342            Ok(Value::String(format!("{:#?}", metadata)))
343        },
344        "file::move" => {
345            let mut paths = argument.as_tuple()?;
346
347            if paths.len() != 2 {
348                return Err(EvalexprError::WrongFunctionArgumentAmount {
349                    expected: 2,
350                    actual: paths.len(),
351                });
352            }
353
354            let to = paths.pop().unwrap().as_string()?;
355            let from = paths.pop().unwrap().as_string()?;
356
357            std::fs::copy(&from, to)
358                .and_then(|_| std::fs::remove_file(from))
359                .unwrap();
360
361            Ok(Value::Empty)
362        },
363        "file::read" => {
364            let path = argument.as_string()?;
365            let mut contents = String::new();
366
367            fs::OpenOptions::new()
368                .read(true)
369                .create(false)
370                .open(&path)
371                .unwrap()
372                .read_to_string(&mut contents)
373                .unwrap();
374
375            Ok(Value::String(contents))
376        },
377        "file::remove" => {
378            let path = argument.as_string()?;
379            std::fs::remove_file(path).unwrap();
380
381            Ok(Value::Empty)
382        },
383        "file::trash" => todo!(),
384        "file::write" => {
385            let strings = argument.as_tuple()?;
386
387            if strings.len() < 2 {
388                return Err(EvalexprError::WrongFunctionArgumentAmount {
389                    expected: 2,
390                    actual: strings.len(),
391                });
392            }
393
394            let path = strings.first().unwrap().as_string()?;
395            let mut file = fs::OpenOptions::new()
396                .write(true)
397                .create(true)
398                .truncate(true)
399                .open(path)
400                .unwrap();
401
402            for content in &strings[1..] {
403                let content = content.as_string()?;
404
405                file.write_all(content.as_bytes()).unwrap();
406            }
407
408            Ok(Value::Empty)
409        },
410        "gui::window" => todo!(),
411        "gui" => todo!(),
412        "map" => todo!(),
413        "network::download" => todo!(),
414        "network::hostname" => todo!(),
415        "network::scan" => todo!(),
416        "os::status" => {
417            Command::new("fish")
418                .arg("-c")
419                .arg("rpm-ostree status")
420                .spawn()
421                .unwrap()
422                .wait()
423                .unwrap();
424            Ok(Value::Empty)
425        },
426        "os::upgrade" => {
427            Command::new("fish")
428                .arg("-c")
429                .arg("rpm-ostree upgrade")
430                .spawn()
431                .unwrap()
432                .wait()
433                .unwrap();
434            Ok(Value::Empty)
435        },
436        "output" => {
437            println!("{}", argument);
438            Ok(Value::Empty)
439        },
440        "random::boolean" => todo!(),
441        "random::float" => todo!(),
442        "random::int" => todo!(),
443        "random::string" => todo!(),
444        "run::async" => todo!(),
445        "run::sync" => todo!(),
446        "run" => todo!(),
447        "rust::packages" => todo!(),
448        "rust::toolchain" => todo!(),
449        "shell::bash" => todo!(),
450        "shell::fish" => todo!(),
451        "shell::nushell" => todo!(),
452        "shell::sh" => todo!(),
453        "shell::zsh" => todo!(),
454        "system::info" => todo!(),
455        "system::monitor" => todo!(),
456        "system::processes" => todo!(),
457        "system::services" => todo!(),
458        "system::users" => todo!(),
459        "time::now" => time::now(),
460        "time::today" => time::today(),
461        "time::tomorrow" => time::tomorrow(),
462        "time" => time::now(),
463        "toolbox::create" => todo!(),
464        "toolbox::enter" => todo!(),
465        "toolbox::image::build" => todo!(),
466        "toolbox::image::list" => todo!(),
467        "toolbox::list" => todo!(),
468        "trash" => todo!(),
469        "wait" => todo!(),
470        "watch" => todo!(),
471        _ => Err(EvalexprError::FunctionIdentifierNotFound(
472            identifier.to_string(),
473        )),
474    }
475}