1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#![doc = include_str!("../README.md")]

mod adapters;
mod driver;
mod list;
mod methods;
mod set;

pub use driver::Driver;
pub use methods::RawMethod;
pub use set::*;

use anyhow::Result;
use fancy_regex::Regex;
use rhai::{Dynamic, Engine, EvalAltResult};
use std::time::SystemTime;

/// A Forne engine, which can act as the backend for learn operations. An instance of this `struct` should be
/// instantiated with a [`Set`] to operate on and an operation to perform.
///
/// The engine has the same lifetime as the reference it is given to its interface for communicating with the host
/// environment.
pub struct Forne {
    /// The set being operated on.
    set: Set,
    /// A Rhai scripting engine used to compile and execute the scripts that drive adapters and learning methods.
    rhai_engine: Engine,
}
impl Forne {
    /// Creates a new set from the given source file text and adapter script. This is a thin wrapper over the `Set::new_with_adapter`
    /// method, abstracting away the internal use of a Rhai engine. In general, you should prefer this method, as there is no additional
    /// overhead to using it.
    pub fn new_set(src: String, adapter_script: &str, raw_method: RawMethod) -> Result<Self> {
        let engine = Self::create_engine();
        let set = Set::new_with_adapter(src, adapter_script, raw_method, &engine)?;

        Ok(Self {
            set,
            rhai_engine: engine,
        })
    }
    /// Updates the given set from a source. See [`Set::update_with_adapter`] for the exact behaviour of this method.
    pub fn update(
        &mut self,
        src: String,
        adapter_script: &str,
        raw_method: RawMethod,
    ) -> Result<()> {
        self.set
            .update_with_adapter(adapter_script, src, raw_method, &self.rhai_engine)
    }
    /// Creates a new Forne engine. While not inherently expensive, this should generally only be called once, or when
    /// the system needs to restart.
    pub fn from_set(set: Set) -> Self {
        Self {
            set,
            rhai_engine: Self::create_engine(),
        }
    }
    /// Start a new learning session with this instance and the given method (see [`RawMethod`]), creating a [`Driver`]
    /// to run it.
    ///
    /// # Errors
    ///
    /// This will return an error if the given method has not previously been used with this set, and a reset must be performed in that case,
    /// which will lead to the loss of previous progress, unless a transformer is used.
    pub fn learn(&mut self, raw_method: RawMethod) -> Result<Driver<'_, '_>> {
        let driver = Driver::new_learn(&mut self.set, raw_method, &self.rhai_engine)?;
        Ok(driver)
    }
    /// Start a new test with this instance, creating a [`Driver`] to run it.
    pub fn test(&mut self) -> Driver<'_, '_> {
        Driver::new_test(&mut self.set)
    }
    /// Saves this set to JSON.
    ///
    /// # Errors
    ///
    /// This can only possible fail if the learning method produces metadata that cannot be serialized into JSON.
    // TODO Is that even possible with Rhai objects?
    pub fn save_set(&self) -> Result<String> {
        self.set.save()
    }
    /// Resets all cards in a learn session back to the default metadata values prescribed by the learning method.
    pub fn reset_learn(&mut self, method: RawMethod) -> Result<()> {
        let method = method.into_method(&self.rhai_engine)?;
        self.set.reset_learn((method.get_default_metadata)()?);

        Ok(())
    }
    /// Resets all test progress for this set. This is irreversible!
    ///
    /// This will not change whether or not cards are starred.
    pub fn reset_test(&mut self) {
        self.set.reset_test();
    }

    /// Creates a Rhai engine with the utilities Forne provides all pre-registered.
    fn create_engine() -> Engine {
        let mut engine = Engine::new();
        // Regex utilities (with support for backreferences etc.)
        engine.register_fn("is_match", |regex: String, text: String| {
            let re = Regex::new(&regex).map_err(|e| e.to_string())?;
            let is_match = re.is_match(&text).map_err(|e| e.to_string())?;
            Ok::<_, Box<EvalAltResult>>(Dynamic::from_bool(is_match))
        });
        engine.register_fn("matches", |regex: &str, text: &str| {
            let re = Regex::new(regex).map_err(|e| e.to_string())?;
            let mut matches = Vec::new();
            for m in re.find_iter(text) {
                let m = m.map_err(|e| e.to_string())?.as_str();
                matches.push(Dynamic::from(m.to_string()));
            }
            Ok::<_, Box<EvalAltResult>>(Dynamic::from_array(matches))
        });
        engine.register_fn("captures", |regex: &str, text: &str| {
            let re = Regex::new(regex).map_err(|e| e.to_string())?;
            let mut capture_groups = Vec::new();
            for raw_caps in re.captures_iter(text) {
                let raw_caps = raw_caps.map_err(|e| e.to_string())?;
                let mut caps = Vec::new();
                for cap in raw_caps.iter() {
                    let cap = cap.ok_or("invalid capture found")?.as_str();
                    caps.push(Dynamic::from(cap.to_string()));
                }
                capture_groups.push(Dynamic::from_array(caps));
            }

            Ok::<_, Box<EvalAltResult>>(Dynamic::from_array(capture_groups))
        });
        engine.register_fn(
            "replace_one",
            |regex: &str, replacement: &str, text: &str| {
                let re = Regex::new(regex).map_err(|e| e.to_string())?;
                let result = re.replace(text, replacement).into_owned();
                Ok::<_, Box<EvalAltResult>>(Dynamic::from(result))
            },
        );
        engine.register_fn(
            "replace_all",
            |regex: &str, replacement: &str, text: &str| {
                let re = Regex::new(regex).map_err(|e| e.to_string())?;
                let result = re.replace_all(text, replacement).into_owned();
                Ok::<_, Box<EvalAltResult>>(Dynamic::from(result))
            },
        );
        engine.register_fn(
            "regexp_to_pairs",
            |regex: &str, question_idx: i64, answer_idx: i64, text: &str| {
                let re = Regex::new(regex).map_err(|e| e.to_string())?;
                let mut pairs = Vec::new();
                for raw_caps in re.captures_iter(text) {
                    let raw_caps = raw_caps.map_err(|e| e.to_string())?;
                    let question = raw_caps
                        .get(question_idx as usize)
                        .ok_or("question index did not exist (did you start from 1?)")?
                        .as_str();
                    let answer = raw_caps
                        .get(answer_idx as usize)
                        .ok_or("answer index did not exist (did you start from 1?)")?
                        .as_str();

                    pairs.push(Dynamic::from_array(vec![question.into(), answer.into()]));
                }

                Ok::<_, Box<EvalAltResult>>(Dynamic::from_array(pairs))
            },
        );
        // Support for working with timestamps
        engine.register_fn(
            "get_seconds_since_epoch", // Gets the number of *seconds* since Unix epoch
            || {
                match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
                    Ok(duration) => duration.as_secs() as i64,
                    // If we're before 01/01/1970...well ok then!
                    Err(err) => -(err.duration().as_secs() as i64),
                }
            },
        );

        engine
    }
}