Skip to main content

spo_rhai/module/resolvers/
file.rs

1#![cfg(not(feature = "no_std"))]
2#![cfg(any(not(target_family = "wasm"), not(target_os = "unknown")))]
3
4use crate::eval::GlobalRuntimeState;
5use crate::func::{locked_read, locked_write};
6use crate::{
7    Engine, Identifier, Locked, Module, ModuleResolver, Position, RhaiResultOf, Scope, Shared,
8    SharedModule, ERR,
9};
10
11use std::{
12    collections::BTreeMap,
13    io::Error as IoError,
14    path::{Path, PathBuf},
15};
16
17pub const RHAI_SCRIPT_EXTENSION: &str = "rhai";
18
19/// A [module][Module] resolution service that loads [module][Module] script files from the file system.
20///
21/// ## Caching
22///
23/// Resolved [Modules][Module] are cached internally so script files are not reloaded and recompiled
24/// for subsequent requests.
25///
26/// Use [`clear_cache`][FileModuleResolver::clear_cache] or
27/// [`clear_cache_for_path`][FileModuleResolver::clear_cache_for_path] to clear the internal cache.
28///
29/// ## Namespace
30///
31/// When a function within a script file module is called, all functions defined within the same
32/// script are available, evan `private` ones.  In other words, functions defined in a module script
33/// can always cross-call each other.
34///
35/// # Example
36///
37/// ```
38/// use rhai::Engine;
39/// use rhai::module_resolvers::FileModuleResolver;
40///
41/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
42/// // with file extension '.x'.
43/// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x");
44///
45/// let mut engine = Engine::new();
46///
47/// engine.set_module_resolver(resolver);
48/// ```
49#[derive(Debug)]
50pub struct FileModuleResolver {
51    base_path: Option<PathBuf>,
52    extension: Identifier,
53    cache_enabled: bool,
54    scope: Scope<'static>,
55    cache: Locked<BTreeMap<PathBuf, SharedModule>>,
56}
57
58impl Default for FileModuleResolver {
59    #[inline(always)]
60    #[must_use]
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl FileModuleResolver {
67    /// Create a new [`FileModuleResolver`] with the current directory as base path.
68    ///
69    /// The default extension is `.rhai`.
70    ///
71    /// # Example
72    ///
73    /// ```
74    /// use rhai::Engine;
75    /// use rhai::module_resolvers::FileModuleResolver;
76    ///
77    /// // Create a new 'FileModuleResolver' loading scripts from the current directory
78    /// // with file extension '.rhai' (the default).
79    /// let resolver = FileModuleResolver::new();
80    ///
81    /// let mut engine = Engine::new();
82    /// engine.set_module_resolver(resolver);
83    /// ```
84    #[inline(always)]
85    #[must_use]
86    pub fn new() -> Self {
87        Self::new_with_extension(RHAI_SCRIPT_EXTENSION)
88    }
89
90    /// Create a new [`FileModuleResolver`] with a specific base path.
91    ///
92    /// The default extension is `.rhai`.
93    ///
94    /// # Example
95    ///
96    /// ```
97    /// use rhai::Engine;
98    /// use rhai::module_resolvers::FileModuleResolver;
99    ///
100    /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
101    /// // with file extension '.rhai' (the default).
102    /// let resolver = FileModuleResolver::new_with_path("./scripts");
103    ///
104    /// let mut engine = Engine::new();
105    /// engine.set_module_resolver(resolver);
106    /// ```
107    #[inline(always)]
108    #[must_use]
109    pub fn new_with_path(path: impl Into<PathBuf>) -> Self {
110        Self::new_with_path_and_extension(path, RHAI_SCRIPT_EXTENSION)
111    }
112
113    /// Create a new [`FileModuleResolver`] with a file extension.
114    ///
115    /// # Example
116    ///
117    /// ```
118    /// use rhai::Engine;
119    /// use rhai::module_resolvers::FileModuleResolver;
120    ///
121    /// // Create a new 'FileModuleResolver' loading scripts with file extension '.rhai' (the default).
122    /// let resolver = FileModuleResolver::new_with_extension("rhai");
123    ///
124    /// let mut engine = Engine::new();
125    /// engine.set_module_resolver(resolver);
126    /// ```
127    #[inline(always)]
128    #[must_use]
129    pub fn new_with_extension(extension: impl Into<Identifier>) -> Self {
130        Self {
131            base_path: None,
132            extension: extension.into(),
133            cache_enabled: true,
134            cache: BTreeMap::new().into(),
135            scope: Scope::new(),
136        }
137    }
138
139    /// Create a new [`FileModuleResolver`] with a specific base path and file extension.
140    ///
141    /// # Example
142    ///
143    /// ```
144    /// use rhai::Engine;
145    /// use rhai::module_resolvers::FileModuleResolver;
146    ///
147    /// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
148    /// // with file extension '.x'.
149    /// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x");
150    ///
151    /// let mut engine = Engine::new();
152    /// engine.set_module_resolver(resolver);
153    /// ```
154    #[inline(always)]
155    #[must_use]
156    pub fn new_with_path_and_extension(
157        path: impl Into<PathBuf>,
158        extension: impl Into<Identifier>,
159    ) -> Self {
160        Self {
161            base_path: Some(path.into()),
162            extension: extension.into(),
163            cache_enabled: true,
164            cache: BTreeMap::new().into(),
165            scope: Scope::new(),
166        }
167    }
168
169    /// Get the base path for script files.
170    #[inline(always)]
171    #[must_use]
172    pub fn base_path(&self) -> Option<&Path> {
173        self.base_path.as_deref()
174    }
175    /// Set the base path for script files.
176    #[inline(always)]
177    pub fn set_base_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
178        self.base_path = Some(path.into());
179        self
180    }
181
182    /// Get the script file extension.
183    #[inline(always)]
184    #[must_use]
185    pub fn extension(&self) -> &str {
186        &self.extension
187    }
188
189    /// Set the script file extension.
190    #[inline(always)]
191    pub fn set_extension(&mut self, extension: impl Into<Identifier>) -> &mut Self {
192        self.extension = extension.into();
193        self
194    }
195
196    /// Get a reference to the file module resolver's [scope][Scope].
197    ///
198    /// The [scope][Scope] is used for compiling module scripts.
199    #[inline(always)]
200    #[must_use]
201    pub const fn scope(&self) -> &Scope {
202        &self.scope
203    }
204
205    /// Set the file module resolver's [scope][Scope].
206    ///
207    /// The [scope][Scope] is used for compiling module scripts.
208    #[inline(always)]
209    pub fn set_scope(&mut self, scope: Scope<'static>) {
210        self.scope = scope;
211    }
212
213    /// Get a mutable reference to the file module resolver's [scope][Scope].
214    ///
215    /// The [scope][Scope] is used for compiling module scripts.
216    #[inline(always)]
217    #[must_use]
218    pub fn scope_mut(&mut self) -> &mut Scope<'static> {
219        &mut self.scope
220    }
221
222    /// Enable/disable the cache.
223    #[inline(always)]
224    pub fn enable_cache(&mut self, enable: bool) -> &mut Self {
225        self.cache_enabled = enable;
226        self
227    }
228    /// Is the cache enabled?
229    #[inline(always)]
230    #[must_use]
231    pub const fn is_cache_enabled(&self) -> bool {
232        self.cache_enabled
233    }
234
235    /// Is a particular path cached?
236    #[inline]
237    #[must_use]
238    pub fn is_cached(&self, path: impl AsRef<Path>) -> bool {
239        if !self.cache_enabled {
240            return false;
241        }
242        locked_read(&self.cache).contains_key(path.as_ref())
243    }
244    /// Empty the internal cache.
245    #[inline]
246    pub fn clear_cache(&mut self) -> &mut Self {
247        locked_write(&self.cache).clear();
248        self
249    }
250    /// Remove the specified path from internal cache.
251    ///
252    /// The next time this path is resolved, the script file will be loaded once again.
253    #[inline]
254    #[must_use]
255    pub fn clear_cache_for_path(&mut self, path: impl AsRef<Path>) -> Option<SharedModule> {
256        locked_write(&self.cache)
257            .remove_entry(path.as_ref())
258            .map(|(.., v)| v)
259    }
260    /// Construct a full file path.
261    #[must_use]
262    pub fn get_file_path(&self, path: &str, source_path: Option<&Path>) -> PathBuf {
263        let path = Path::new(path);
264
265        let mut file_path;
266
267        if path.is_relative() {
268            file_path = self
269                .base_path
270                .clone()
271                .or_else(|| source_path.map(Into::into))
272                .unwrap_or_default();
273            file_path.push(path);
274        } else {
275            file_path = path.into();
276        }
277
278        file_path.set_extension(self.extension.as_str()); // Force extension
279        file_path
280    }
281
282    /// Resolve a module based on a path.
283    fn impl_resolve(
284        &self,
285        engine: &Engine,
286        global: &mut GlobalRuntimeState,
287        scope: &mut Scope,
288        source: Option<&str>,
289        path: &str,
290        pos: Position,
291    ) -> Result<SharedModule, Box<crate::EvalAltResult>> {
292        // Load relative paths from source if there is no base path specified
293        let source_path = global
294            .source()
295            .or(source)
296            .and_then(|p| Path::new(p).parent());
297
298        let file_path = self.get_file_path(path, source_path);
299
300        if self.is_cache_enabled() {
301            if let Some(module) = locked_read(&self.cache).get(&file_path) {
302                return Ok(module.clone());
303            }
304        }
305
306        let mut ast = engine
307            .compile_file_with_scope(&self.scope, file_path.clone())
308            .map_err(|err| match *err {
309                ERR::ErrorSystem(.., err) if err.is::<IoError>() => {
310                    Box::new(ERR::ErrorModuleNotFound(path.to_string(), pos))
311                }
312                _ => Box::new(ERR::ErrorInModule(path.to_string(), err, pos)),
313            })?;
314
315        ast.set_source(path);
316
317        let m: Shared<_> = Module::eval_ast_as_new_raw(engine, scope, global, &ast)
318            .map_err(|err| Box::new(ERR::ErrorInModule(path.to_string(), err, pos)))?
319            .into();
320
321        if self.is_cache_enabled() {
322            locked_write(&self.cache).insert(file_path, m.clone());
323        }
324
325        Ok(m)
326    }
327}
328
329impl ModuleResolver for FileModuleResolver {
330    fn resolve_raw(
331        &self,
332        engine: &Engine,
333        global: &mut GlobalRuntimeState,
334        scope: &mut Scope,
335        path: &str,
336        pos: Position,
337    ) -> RhaiResultOf<SharedModule> {
338        self.impl_resolve(engine, global, scope, None, path, pos)
339    }
340
341    #[inline(always)]
342    fn resolve(
343        &self,
344        engine: &Engine,
345        source: Option<&str>,
346        path: &str,
347        pos: Position,
348    ) -> RhaiResultOf<SharedModule> {
349        let global = &mut GlobalRuntimeState::new(engine);
350        let scope = &mut Scope::new();
351        self.impl_resolve(engine, global, scope, source, path, pos)
352    }
353
354    /// Resolve an `AST` based on a path string.
355    ///
356    /// The file system is accessed during each call; the internal cache is by-passed.
357    fn resolve_ast(
358        &self,
359        engine: &Engine,
360        source_path: Option<&str>,
361        path: &str,
362        pos: Position,
363    ) -> Option<RhaiResultOf<crate::AST>> {
364        // Construct the script file path
365        let file_path = self.get_file_path(path, source_path.map(Path::new));
366
367        // Load the script file and compile it
368        Some(
369            engine
370                .compile_file(file_path)
371                .map(|mut ast| {
372                    ast.set_source(path);
373                    ast
374                })
375                .map_err(|err| match *err {
376                    ERR::ErrorSystem(.., err) if err.is::<IoError>() => {
377                        ERR::ErrorModuleNotFound(path.to_string(), pos).into()
378                    }
379                    _ => ERR::ErrorInModule(path.to_string(), err, pos).into(),
380                }),
381        )
382    }
383}