Skip to main content

rivescript_core/
ast.rs

1//! # RiveScript Abstract Syntax Tree
2//!
3//! The AST is a parsed object representing all of the contents of a RiveScript
4//! bot brain. It is the output object that you get when you load_file() or
5//! stream() a RiveScript document (or several). It contains all of the useful
6//! inner contents of the RiveScript brain, including global bot variables,
7//! substitutions and other configuration that was defined via RiveScript.
8
9use std::{collections::HashMap, sync::RwLock};
10
11/// Root of the "abstract syntax tree" representing a RiveScript
12/// source document and its useful contents.
13#[derive(Debug)]
14pub struct AST {
15    // Configuration fields typically found in 'begin.rive'
16    pub version: f32,                         // ! version
17    pub globals: RwLock<HashMap<String, String>>,     // ! global
18    pub vars: RwLock<HashMap<String, String>>,        // ! var
19    pub subs: HashMap<String, String>,        // ! sub stitutions
20    pub person: HashMap<String, String>,      // ! person substitutions
21    pub arrays: HashMap<String, Vec<String>>, // ! array sets
22
23    // Topics and their triggers.
24    pub topics: HashMap<String, Topic>,
25
26    // Parsed object macros.
27    pub objects: HashMap<String, Object>,
28}
29
30/// Topic is a group of triggers.
31///
32/// All triggers belong to a topic, with the default topic being
33/// a special one named "random". To move the current user into a
34/// different topic, use the `{topic}` tag, for example
35/// `{topic=random}`. A user can ONLY match triggers that are defined
36/// in their current topic, or any triggers that are 'included' or
37/// 'inherited' into their current topic.
38#[derive(Debug, Clone)]
39pub struct Topic {
40    pub name: String,
41    pub triggers: Vec<Trigger>,
42    pub includes: HashMap<String, bool>,
43    pub inherits: HashMap<String, bool>,
44}
45
46impl AST {
47    /// Initialize a new AST object ready for populating.
48    pub fn new() -> Self {
49        Self {
50            version: 0.0,
51            globals: RwLock::new(HashMap::new()),
52            vars: RwLock::new(HashMap::new()),
53            subs: HashMap::new(),
54            person: HashMap::new(),
55            arrays: HashMap::new(),
56            topics: HashMap::new(),
57            objects: HashMap::new(),
58        }
59    }
60
61    /// Merge the current AST with the contents of the other.
62    ///
63    /// This is used during parsing when e.g. loading a whole directory of files.
64    /// All parsed files add to the loaded AST for the root RiveScript instance.
65    pub fn extend(&mut self, other: AST) {
66        if other.version != 0.0 {
67            self.version = other.version;
68        }
69
70        let mut global_guard = self.globals.write().expect("RwLock poisoned");
71        let mut other_globals = other.globals.write().expect("RwLock poisoned");
72        global_guard.extend(other_globals.drain());
73
74        let mut vars_guard = self.vars.write().expect("RwLock poisoned");
75        let mut other_vars = other.vars.write().expect("RwLock poisoned");
76        vars_guard.extend(other_vars.drain());
77
78        self.subs.extend(other.subs.into_iter());
79        self.person.extend(other.person.into_iter());
80        self.arrays.extend(other.arrays.into_iter());
81        // self.topics.extend(other.topics.into_iter());
82        self.objects.extend(other.objects.into_iter());
83
84        // Merge topics more carefully.
85        for (name, topic) in other.topics {
86            match self.topics.get_mut(&name) {
87                Some(mine) => {
88                    mine.triggers.extend(topic.triggers);
89                }
90                None => {
91                    self.topics.insert(name, topic);
92                }
93            }
94        }
95    }
96
97    /// Initialize the data structure for a new topic, if it wasn't already there.
98    pub fn init_topic(&mut self, name: &String) {
99        if self.topics.contains_key(name) {
100            return;
101        }
102
103        self.topics.insert(
104            name.to_string(),
105            Topic {
106                name: name.to_string(),
107                triggers: Vec::new(),
108                includes: HashMap::new(),
109                inherits: HashMap::new(),
110            },
111        );
112    }
113
114    /// Returns true if a >begin section exists.
115    pub fn has_begin_block(&self) -> bool {
116        self.topics.contains_key(crate::BEGIN_TOPIC)
117    }
118
119    /// Returns true if the topic exists.
120    pub fn has_topic(&self, name: &str) -> bool {
121        self.topics.contains_key(name)
122    }
123
124    /// Get a global variable. Returns "undefined" if not set.
125    pub fn get_global(&self, name: &str) -> String {
126        let globals_guard = self.globals.read().expect("RwLock poisoned");
127        if let Some(value) = globals_guard.get(name) {
128            return value.to_string();
129        }
130        crate::UNDEFINED.to_string()
131    }
132
133    /// Set a global variable.
134    pub fn set_global(&self, name: &str, value: &str) {
135        let mut globals_guard = self.globals.write().expect("RwLock poisoned");
136        globals_guard.insert(name.to_string(), value.to_string());
137    }
138
139    /// Get a bot variable. Returns "undefined" if not set.
140    pub fn get_bot_var(&self, name: &str) -> String {
141        let vars_guard = self.vars.read().expect("RwLock poisoned");
142        if let Some(value) = vars_guard.get(name) {
143            return value.to_string();
144        }
145        crate::UNDEFINED.to_string()
146    }
147
148    /// Set a bot variable.
149    pub fn set_bot_var(&self, name: &str, value: &str) {
150        let mut vars_guard = self.vars.write().expect("RwLock poisoned");
151        vars_guard.insert(name.to_string(), value.to_string());
152    }
153}
154
155impl Topic {
156    pub fn set_includes(&mut self, includes: String) {
157        self.includes.insert(includes.to_string(), true);
158    }
159
160    pub fn set_inherits(&mut self, inherits: String) {
161        self.inherits.insert(inherits.to_string(), true);
162    }
163
164    pub fn add_trigger(&mut self, trigger: Trigger) {
165        self.triggers.push(trigger);
166    }
167}
168
169/// Trigger represents a pattern that matches a user's message.
170///
171/// It is the base unit of intelligence for your chatbot. A trigger
172/// of "hello bot" will match when the user says that phrase, and can
173/// pair a set of replies (multiple OK, which will be chosen at random)
174/// to be sent when that trigger is matched.
175#[derive(Debug, Clone)]
176pub struct Trigger {
177    pub trigger: String,
178    pub reply: Vec<String>,
179    pub condition: Vec<Condition>,
180    pub redirect: String,
181    pub previous: String,
182}
183
184impl Trigger {
185    pub fn new(trigger: &str) -> Self {
186        Self {
187            trigger: trigger.to_string(),
188            reply: Vec::new(),
189            condition: Vec::new(),
190            redirect: String::from(""),
191            previous: String::from(""),
192        }
193    }
194
195    pub fn is_populated(&self) -> bool {
196        self.trigger.len() > 0
197    }
198}
199
200/// Condition holds the contents of a *Condition command in RiveScript.
201#[derive(Debug, Clone)]
202pub struct Condition {
203    pub left: String,
204    pub operator: String,
205    pub right: String,
206    pub reply: String,
207}
208
209/// Object represents a parsed object macro from a RiveScript source document.
210///
211/// Object macros have a name, a programming language, and an array of their
212/// source code as defined in the RiveScript document. It is up to the
213/// interpreter program to understand how to parse an object macro and make
214/// it executable.
215#[derive(Debug, Clone)]
216pub struct Object {
217    pub name: String,
218    pub language: String,
219    pub code: Vec<String>,
220}
221
222impl Object {
223    pub fn new(name: &str, language: &str, code: Vec<String>) -> Self {
224        Self {
225            name: name.to_string(),
226            language: language.to_string(),
227            code,
228        }
229    }
230}