irc_bot/core/
modl_sys.rs

1use super::trigger::TriggerPriority;
2use super::BotCmdAttr;
3use super::BotCmdAuthLvl;
4use super::BotCmdHandler;
5use super::BotCommand;
6use super::Error;
7use super::ErrorKind;
8use super::GetDebugInfo;
9use super::Result;
10use super::State;
11use super::Trigger;
12use super::TriggerAttr;
13use super::TriggerHandler;
14use itertools;
15use regex::Regex;
16use std;
17use std::borrow::Cow;
18use std::sync::Arc;
19use std::sync::RwLock;
20use util;
21use uuid::Uuid;
22use yaml_rust::Yaml;
23
24pub struct Module {
25    pub name: Cow<'static, str>,
26    uuid: Uuid,
27    features: Vec<ModuleFeature>,
28}
29
30impl PartialEq for Module {
31    fn eq(&self, other: &Self) -> bool {
32        if self.uuid == other.uuid {
33            debug_assert_eq!(self.name, other.name);
34            true
35        } else {
36            false
37        }
38    }
39}
40
41impl Eq for Module {}
42
43impl GetDebugInfo for Module {
44    type Output = ModuleInfo;
45
46    fn dbg_info(&self) -> ModuleInfo {
47        ModuleInfo {
48            name: self.name.to_string(),
49        }
50    }
51}
52
53pub struct ModuleBuilder {
54    name: Cow<'static, str>,
55    features: Vec<ModuleFeature>,
56}
57
58pub fn mk_module<'modl, S>(name: S) -> ModuleBuilder
59where
60    S: Into<Cow<'static, str>>,
61{
62    ModuleBuilder {
63        name: name.into(),
64        features: Default::default(),
65    }
66}
67
68impl ModuleBuilder {
69    pub fn command<'attr, Attrs, S1, S2, S3>(
70        mut self,
71        name: S1,
72        syntax: S2,
73        help_msg: S3,
74        auth_lvl: BotCmdAuthLvl,
75        handler: Box<BotCmdHandler>,
76        attrs: Attrs,
77    ) -> Self
78    where
79        S1: Into<Cow<'static, str>>,
80        S2: Into<Cow<'static, str>>,
81        S3: Into<Cow<'static, str>>,
82        Attrs: IntoIterator<Item = &'attr BotCmdAttr>,
83    {
84        let name = name.into();
85
86        assert!(
87            !name.as_ref().contains(char::is_whitespace),
88            "The name of the bot command {:?} contains whitespace, which is not allowed.",
89            name.as_ref()
90        );
91
92        let syntax = syntax.into();
93        let usage_yaml = util::yaml::parse_node(&syntax)
94            .unwrap()
95            .unwrap_or(Yaml::Hash(Default::default()));
96
97        let cmd = ModuleFeature::Command {
98            name: name,
99            usage_str: syntax,
100            usage_yaml,
101            help_msg: help_msg.into(),
102            auth_lvl: auth_lvl,
103            handler: handler.into(),
104        };
105
106        for attr in attrs {
107            match *attr {
108                // ...
109            }
110        }
111
112        self.features.push(cmd);
113
114        self
115    }
116
117    pub fn trigger<'attr, Attrs, Rx1, S1, S2>(
118        mut self,
119        name: S1,
120        regex_str: Rx1,
121        help_msg: S2,
122        priority: TriggerPriority,
123        handler: Box<TriggerHandler>,
124        attrs: Attrs,
125    ) -> Self
126    where
127        Rx1: util::regex::IntoRegexCI,
128        S1: Into<Cow<'static, str>>,
129        S2: Into<Cow<'static, str>>,
130        Attrs: IntoIterator<Item = &'attr TriggerAttr>,
131    {
132        for attr in attrs {
133            match attr {
134                &TriggerAttr::AlwaysWatching => unimplemented!(),
135            }
136        }
137
138        let trigger = ModuleFeature::Trigger {
139            name: name.into(),
140            regex: Arc::new(RwLock::new(regex_str.into_regex_ci().expect(
141                "Your regex was erroneous, it \
142                 seems.",
143            ))),
144            help_msg: help_msg.into(),
145            handler: handler.into(),
146            priority,
147            uuid: Uuid::new_v4(),
148        };
149
150        self.features.push(trigger);
151
152        self
153    }
154
155    pub fn end(self) -> Module {
156        let ModuleBuilder { name, mut features } = self;
157
158        features.shrink_to_fit();
159
160        Module {
161            name: name,
162            uuid: Uuid::new_v4(),
163            features: features,
164        }
165    }
166}
167
168/// Information about a `Module` that can be gathered without needing any lifetime annotation.
169#[derive(Clone, Debug, Eq, PartialEq)]
170pub struct ModuleInfo {
171    name: String,
172}
173
174enum ModuleFeature {
175    Command {
176        name: Cow<'static, str>,
177        usage_str: Cow<'static, str>,
178        usage_yaml: Yaml,
179        help_msg: Cow<'static, str>,
180        auth_lvl: BotCmdAuthLvl,
181        handler: Arc<BotCmdHandler>,
182    },
183    Trigger {
184        name: Cow<'static, str>,
185        help_msg: Cow<'static, str>,
186        regex: Arc<RwLock<Regex>>,
187        handler: Arc<TriggerHandler>,
188        priority: TriggerPriority,
189        uuid: Uuid,
190    },
191}
192
193impl GetDebugInfo for ModuleFeature {
194    type Output = ModuleFeatureInfo;
195
196    fn dbg_info(&self) -> ModuleFeatureInfo {
197        ModuleFeatureInfo {
198            name: self.name().to_string(),
199            kind: match self {
200                &ModuleFeature::Command { .. } => ModuleFeatureKind::Command,
201                &ModuleFeature::Trigger { .. } => ModuleFeatureKind::Trigger,
202            },
203        }
204    }
205}
206
207/// Information about a `ModuleFeature` that can be gathered without needing any lifetime
208/// annotation.
209#[derive(Clone, Debug, Eq, PartialEq)]
210pub struct ModuleFeatureInfo {
211    name: String,
212    kind: ModuleFeatureKind,
213}
214
215#[derive(Clone, Debug, Eq, PartialEq)]
216pub enum ModuleFeatureKind {
217    Command,
218    Trigger,
219}
220
221impl ModuleFeature {
222    pub fn name(&self) -> &str {
223        match self {
224            &ModuleFeature::Command { ref name, .. } => name.as_ref(),
225            &ModuleFeature::Trigger { ref name, .. } => name.as_ref(),
226        }
227    }
228
229    // fn provider(&self) -> &Module {
230    //     match self {
231    //         &ModuleFeature::Command { provider, .. } => provider,
232    //         &ModuleFeature::Trigger => unimplemented!(),
233    //     }
234    // }
235}
236
237impl GetDebugInfo for BotCommand {
238    type Output = ModuleFeatureInfo;
239
240    fn dbg_info(&self) -> ModuleFeatureInfo {
241        ModuleFeatureInfo {
242            name: self.name.to_string(),
243            kind: ModuleFeatureKind::Command,
244        }
245    }
246}
247
248impl GetDebugInfo for Trigger {
249    type Output = ModuleFeatureInfo;
250
251    fn dbg_info(&self) -> ModuleFeatureInfo {
252        ModuleFeatureInfo {
253            name: self.name.to_string(),
254            kind: ModuleFeatureKind::Trigger,
255        }
256    }
257}
258
259#[derive(Clone, Copy, Debug, Eq, PartialEq)]
260pub enum ModuleLoadMode {
261    /// Emit an error if any of the new module's features conflict with already present modules'
262    /// features.
263    Add,
264    /// Overwrite any already loaded features that conflict with the new module's features, if the
265    /// old features were provided by a module with the same name as the new module.
266    Replace,
267    /// Overwrite old modules' features unconditionally.
268    Force,
269}
270
271impl State {
272    pub fn load_modules<Modls>(
273        &mut self,
274        modules: Modls,
275        mode: ModuleLoadMode,
276    ) -> std::result::Result<(), Vec<Error>>
277    where
278        Modls: IntoIterator<Item = Module>,
279    {
280        let errs = itertools::flatten(modules.into_iter().filter_map(|module| {
281            match self.load_module(module, mode) {
282                Ok(()) => None,
283                Err(e) => Some(e),
284            }
285        })).collect::<Vec<Error>>();
286
287        if errs.is_empty() {
288            Ok(())
289        } else {
290            Err(errs)
291        }
292    }
293
294    pub fn load_module(
295        &mut self,
296        module: Module,
297        mode: ModuleLoadMode,
298    ) -> std::result::Result<(), Vec<Error>> {
299        debug!(
300            "Loading module {:?}, mode {:?}, providing {:?}",
301            module.name,
302            mode,
303            module
304                .features
305                .iter()
306                .map(GetDebugInfo::dbg_info)
307                .collect::<Vec<_>>()
308        );
309
310        if let Some(existing_module) = match (mode, self.modules.get(module.name.as_ref())) {
311            (_, None) | (ModuleLoadMode::Replace, _) | (ModuleLoadMode::Force, _) => None,
312            (ModuleLoadMode::Add, Some(old)) => Some(old),
313        } {
314            return Err(vec![
315                ErrorKind::ModuleRegistryClash(existing_module.dbg_info(), module.dbg_info())
316                    .into(),
317            ]);
318        }
319
320        let module = Arc::new(module);
321
322        self.modules.insert(module.name.clone(), module.clone());
323
324        let errs = module
325            .features
326            .iter()
327            .filter_map(
328                |feature| match self.load_module_feature(module.clone(), feature, mode) {
329                    Ok(()) => None,
330                    Err(e) => Some(e),
331                },
332            )
333            .collect::<Vec<Error>>();
334
335        if errs.is_empty() {
336            Ok(())
337        } else {
338            Err(errs)
339        }
340    }
341
342    fn load_module_feature<'modl>(
343        &mut self,
344        provider: Arc<Module>,
345        feature: &'modl ModuleFeature,
346        mode: ModuleLoadMode,
347    ) -> Result<()> {
348        debug!("Loading module feature (f1): {:?}", feature.dbg_info());
349
350        if let Some(existing_feature) = match feature {
351            &ModuleFeature::Command { .. } => match (mode, self.commands.get(feature.name())) {
352                (_, None) | (ModuleLoadMode::Force, _) => None,
353                (ModuleLoadMode::Replace, Some(old)) if old.provider.name == provider.name => None,
354                (ModuleLoadMode::Replace, Some(old)) => Some(old.dbg_info()),
355                (ModuleLoadMode::Add, Some(old)) => Some(old.dbg_info()),
356            },
357            &ModuleFeature::Trigger { .. } => None,
358        } {
359            bail!(ErrorKind::ModuleFeatureRegistryClash(
360                existing_feature,
361                feature.dbg_info(),
362            ))
363        }
364
365        self.force_load_module_feature(provider, feature);
366
367        Ok(())
368    }
369
370    fn force_load_module_feature<'modl>(
371        &mut self,
372        provider: Arc<Module>,
373        feature: &'modl ModuleFeature,
374    ) {
375        debug!("Loading module feature (f2): {:?}", feature.dbg_info());
376
377        match feature {
378            &ModuleFeature::Command {
379                ref name,
380                ref handler,
381                ref auth_lvl,
382                ref usage_str,
383                ref usage_yaml,
384                ref help_msg,
385            } => {
386                self.commands.insert(
387                    name.clone(),
388                    BotCommand {
389                        provider: provider,
390                        name: name.clone(),
391                        auth_lvl: auth_lvl.clone(),
392                        handler: handler.clone(),
393                        usage_str: usage_str.clone(),
394                        usage_yaml: usage_yaml.clone(),
395                        help_msg: help_msg.clone(),
396                    },
397                );
398            }
399            &ModuleFeature::Trigger {
400                ref name,
401                ref regex,
402                ref handler,
403                ref help_msg,
404                priority,
405                uuid,
406            } => {
407                self.triggers
408                    .entry(priority)
409                    .or_insert_with(Default::default)
410                    .push(Trigger {
411                        provider,
412                        name: name.clone(),
413                        regex: regex.clone(),
414                        handler: handler.clone(),
415                        priority,
416                        help_msg: help_msg.clone(),
417                        uuid,
418                    });
419            }
420        };
421    }
422}