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}