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