#![cfg(not(feature = "no_std"))]
#![cfg(any(not(target_family = "wasm"), not(target_os = "unknown")))]
use crate::eval::GlobalRuntimeState;
use crate::func::{locked_read, locked_write};
use crate::{
Engine, Identifier, Locked, Module, ModuleResolver, Position, RhaiResultOf, Scope,
SharedModule, ERR,
};
use std::{
collections::BTreeMap,
io::Error as IoError,
path::{Path, PathBuf},
};
#[cfg(not(feature = "no_ast"))]
pub const RHAI_SCRIPT_EXTENSION: &str = "rhai";
#[cfg(feature = "grain")]
pub const RHAI_GRAIN_EXTENSION: &str = "rgrn";
#[derive(Debug)]
pub struct FileModuleResolver {
base_path: Option<PathBuf>,
extension: Identifier,
cache_enabled: bool,
scope: Scope<'static>,
cache: Locked<BTreeMap<PathBuf, SharedModule>>,
}
impl Default for FileModuleResolver {
#[inline(always)]
fn default() -> Self {
Self::new()
}
}
impl FileModuleResolver {
#[inline(always)]
#[must_use]
pub fn new() -> Self {
#[cfg(not(feature = "no_ast"))]
return Self::new_with_extension(RHAI_SCRIPT_EXTENSION);
#[cfg(feature = "grain")]
#[cfg(feature = "no_ast")]
return Self::new_with_extension(RHAI_GRAIN_EXTENSION);
#[cfg(not(feature = "grain"))]
#[cfg(feature = "no_ast")]
unreachable!();
}
#[inline(always)]
#[must_use]
pub fn new_with_path(path: impl Into<PathBuf>) -> Self {
#[cfg(not(feature = "no_ast"))]
return Self::new_with_path_and_extension(path, RHAI_SCRIPT_EXTENSION);
#[cfg(feature = "grain")]
#[cfg(feature = "no_ast")]
return Self::new_with_path_and_extension(path, RHAI_GRAIN_EXTENSION);
#[cfg(not(feature = "grain"))]
#[cfg(feature = "no_ast")]
unreachable!();
}
#[inline(always)]
#[must_use]
pub fn new_with_extension(extension: impl Into<Identifier>) -> Self {
Self {
base_path: None,
extension: extension.into(),
cache_enabled: true,
cache: BTreeMap::new().into(),
scope: Scope::new(),
}
}
#[inline(always)]
#[must_use]
pub fn new_with_path_and_extension(
path: impl Into<PathBuf>,
extension: impl Into<Identifier>,
) -> Self {
Self {
base_path: Some(path.into()),
extension: extension.into(),
cache_enabled: true,
cache: BTreeMap::new().into(),
scope: Scope::new(),
}
}
#[inline(always)]
#[must_use]
pub fn base_path(&self) -> Option<&Path> {
self.base_path.as_deref()
}
#[inline(always)]
pub fn set_base_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.base_path = Some(path.into());
self
}
#[inline(always)]
#[must_use]
pub fn extension(&self) -> &str {
&self.extension
}
#[inline(always)]
pub fn set_extension(&mut self, extension: impl Into<Identifier>) -> &mut Self {
self.extension = extension.into();
self
}
#[inline(always)]
#[must_use]
pub const fn scope(&self) -> &Scope<'_> {
&self.scope
}
#[inline(always)]
pub fn set_scope(&mut self, scope: Scope<'static>) {
self.scope = scope;
}
#[inline(always)]
#[must_use]
pub fn scope_mut(&mut self) -> &mut Scope<'static> {
&mut self.scope
}
#[inline(always)]
pub fn enable_cache(&mut self, enable: bool) -> &mut Self {
self.cache_enabled = enable;
self
}
#[inline(always)]
#[must_use]
pub const fn is_cache_enabled(&self) -> bool {
self.cache_enabled
}
#[inline]
#[must_use]
pub fn is_cached(&self, path: impl AsRef<Path>) -> bool {
if !self.cache_enabled {
return false;
}
locked_read(&self.cache)
.unwrap()
.contains_key(path.as_ref())
}
#[inline]
pub fn clear_cache(&mut self) -> &mut Self {
locked_write(&self.cache).unwrap().clear();
self
}
#[inline]
#[must_use]
pub fn clear_cache_for_path(&mut self, path: impl AsRef<Path>) -> Option<SharedModule> {
locked_write(&self.cache)
.unwrap()
.remove_entry(path.as_ref())
.map(|(.., v)| v)
}
#[must_use]
pub fn get_file_path(&self, path: &str, source_path: Option<&Path>) -> PathBuf {
let path = Path::new(path);
let mut file_path;
if path.is_relative() {
file_path = self
.base_path
.clone()
.or_else(|| source_path.map(Into::into))
.unwrap_or_default();
file_path.push(path);
} else {
file_path = path.into();
}
#[cfg(feature = "grain")]
if path
.extension()
.and_then(|ext| ext.to_str())
.map_or(false, |ext| ext.eq_ignore_ascii_case(RHAI_GRAIN_EXTENSION))
{
return file_path;
}
file_path.set_extension(self.extension.as_str()); file_path
}
fn impl_resolve(
&self,
engine: &Engine,
global: &mut GlobalRuntimeState,
scope: &mut Scope,
source: Option<&str>,
path: &str,
pos: Position,
) -> Result<SharedModule, Box<crate::EvalAltResult>> {
let source_path = global
.source()
.or(source)
.and_then(|p| Path::new(p).parent());
#[cfg(feature = "grain")]
let mut is_grain_file = Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.map_or(false, |ext| ext.eq_ignore_ascii_case(RHAI_GRAIN_EXTENSION));
let mut _file_path = self.get_file_path(path, source_path);
#[cfg(feature = "grain")]
if !is_grain_file {
let mut grain_path = _file_path.clone();
grain_path.set_extension(RHAI_GRAIN_EXTENSION);
if cfg!(feature = "no_ast") || grain_path.is_file() {
is_grain_file = true;
_file_path = grain_path;
}
}
if self.is_cache_enabled() {
if let Some(module) = locked_read(&self.cache).unwrap().get(&_file_path) {
return Ok(module.clone());
}
}
let mut module: Option<SharedModule> = None;
#[cfg(feature = "grain")]
if is_grain_file {
let buf = std::fs::read(&_file_path).map_err(|err| {
let path = path.to_string();
Box::new(match err.kind() {
std::io::ErrorKind::NotFound => ERR::ErrorModuleNotFound(path, pos),
_ => {
let msg = format!(
"cannot read Rhai Grain file '{}'",
_file_path.to_string_lossy()
);
ERR::ErrorInModule(path, ERR::ErrorSystem(msg, err.into()).into(), pos)
}
})
})?;
let mut program = crate::grain::Program::read(&buf).map_err(|err| {
Box::new(ERR::ErrorInModule(
path.to_string(),
ERR::ErrorRuntime(format!("failed to load Rhai Grain file: {err}").into(), pos)
.into(),
pos,
))
})?;
program.set_source(path);
module = Some(
Module::eval_grain_as_new_raw(engine, scope, global, program.into_shared())
.map_err(|err| Box::new(ERR::ErrorInModule(path.to_string(), err, pos)))?
.into(),
);
}
#[cfg(not(feature = "no_ast"))]
if module.is_none() {
let mut ast = engine
.compile_file_with_scope(&self.scope, _file_path.clone())
.map_err(|err| match *err {
ERR::ErrorSystem(.., err) if err.is::<IoError>() => {
Box::new(ERR::ErrorModuleNotFound(path.to_string(), pos))
}
_ => Box::new(ERR::ErrorInModule(path.to_string(), err, pos)),
})?;
ast.set_source(path);
module = Some(
Module::eval_ast_as_new_raw(engine, scope, global, &ast)
.map_err(|err| Box::new(ERR::ErrorInModule(path.to_string(), err, pos)))?
.into(),
);
}
let Some(module) = module else {
return Err(Box::new(ERR::ErrorModuleNotFound(path.to_string(), pos)));
};
if self.is_cache_enabled() {
locked_write(&self.cache)
.unwrap()
.insert(_file_path, module.clone());
}
Ok(module)
}
}
impl ModuleResolver for FileModuleResolver {
fn resolve_raw(
&self,
engine: &Engine,
global: &mut GlobalRuntimeState,
scope: &mut Scope,
path: &str,
pos: Position,
) -> RhaiResultOf<SharedModule> {
self.impl_resolve(engine, global, scope, None, path, pos)
}
#[inline(always)]
fn resolve(
&self,
engine: &Engine,
source: Option<&str>,
path: &str,
pos: Position,
) -> RhaiResultOf<SharedModule> {
let global = &mut engine.new_global_runtime_state();
let scope = &mut Scope::new();
self.impl_resolve(engine, global, scope, source, path, pos)
}
#[cfg(not(feature = "no_ast"))]
fn resolve_ast(
&self,
engine: &Engine,
source_path: Option<&str>,
path: &str,
pos: Position,
) -> Option<RhaiResultOf<crate::AST>> {
#[cfg(feature = "grain")]
if Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.map_or(false, |ext| ext.eq_ignore_ascii_case(RHAI_GRAIN_EXTENSION))
{
return None;
}
let file_path = self.get_file_path(path, source_path.map(Path::new));
#[cfg(feature = "grain")]
if !file_path.is_file() {
let mut grain_path = file_path.clone();
grain_path.set_extension(RHAI_GRAIN_EXTENSION);
if grain_path.is_file() {
return None;
}
}
Some(
engine
.compile_file(file_path)
.map(|mut ast| {
ast.set_source(path);
ast
})
.map_err(|err| match *err {
ERR::ErrorSystem(.., err) if err.is::<IoError>() => {
ERR::ErrorModuleNotFound(path.to_string(), pos).into()
}
_ => ERR::ErrorInModule(path.to_string(), err, pos).into(),
}),
)
}
}