rant/
modres.rs

1use std::{env, path::PathBuf, io::ErrorKind, fmt::Debug};
2use super::*;
3
4/// Result type used by the module loader.
5pub type ModuleResolveResult = Result<RantProgram, ModuleResolveError>;
6
7/// Represents the features required for a module resolver.
8/// 
9/// A module resolver only resolves the `RantProgram` that the final module object is loaded from.
10/// This is designed as such to ensure that module loading is limited to the maximum call stack size of the requesting program.
11pub trait ModuleResolver: Debug {
12  fn try_resolve(&self, context: &mut Rant, module_path: &str, dependant: Option<&RantProgramInfo>) -> ModuleResolveResult;
13}
14
15/// The default filesystem-based module resolver.
16/// 
17/// ### Resolution strategy
18/// This resolver uses the following strategy to locate module files:
19/// 1. If triggered by a program, the program's containing directory is searched first.
20/// 1. The directory specified in `local_modules_path` is searched next. If not specified, uses the host application's current working directory.
21/// 1. If `enable_global_modules` is set to `true`, the global modules path is searched.
22#[derive(Debug)]
23pub struct DefaultModuleResolver {  
24  /// Enables loading modules from RANT_MODULES_PATH.
25  pub enable_global_modules: bool,
26  /// Specifies a preferred module loading path with higher precedence than the global module path.
27  /// If not specified, looks in the current working directory.
28  pub local_modules_path: Option<String>,
29}
30
31impl DefaultModuleResolver {
32  /// The name of the environment variable that used to provide the global modules path.
33  pub const ENV_MODULES_PATH_KEY: &'static str = "RANT_MODULES_PATH";
34}
35
36impl Default for DefaultModuleResolver {
37  fn default() -> Self {
38    Self {
39      enable_global_modules: true,
40      local_modules_path: None,
41    }
42  }
43}
44
45impl ModuleResolver for DefaultModuleResolver {
46  fn try_resolve(&self, context: &mut Rant, module_path: &str, dependant: Option<&RantProgramInfo>) -> ModuleResolveResult {
47    
48    // Try to find module path that exists
49    if let Some(full_module_path) = self.find_module_path(module_path, dependant) {
50      let mut errors = vec![];
51      let compile_result = context.compile_file(full_module_path, &mut errors);
52      match compile_result {
53        Ok(module) => Ok(module),
54        Err(err) => {
55          Err(ModuleResolveError {
56            name: module_path.to_owned(),
57            reason: match err{
58              CompilerError::SyntaxError => {
59                ModuleResolveErrorReason::CompileFailed(errors)
60              },
61              CompilerError::IOError(ioerr) => {
62                match ioerr {
63                  IOErrorKind::NotFound => {
64                    ModuleResolveErrorReason::NotFound
65                  },
66                  _ => ModuleResolveErrorReason::FileIOError(ioerr)
67                }
68              }
69            }
70          })
71        }
72      }
73    } else {
74      Err(ModuleResolveError {
75        name: module_path.to_owned(),
76        reason: ModuleResolveErrorReason::NotFound,
77      })
78    }
79  }
80}
81
82impl DefaultModuleResolver {
83  #[inline]
84  fn find_module_path(&self, module_path: &str, dependant: Option<&RantProgramInfo>) -> Option<PathBuf> {
85
86    let module_path = PathBuf::from(
87        module_path.replace("/", &String::from(std::path::MAIN_SEPARATOR))
88      )
89      .with_extension(RANT_FILE_EXTENSION);
90
91    macro_rules! search_for_module {
92      ($path:expr) => {
93        let path = $path;
94        // Construct full path to module
95        if let Ok(full_module_path) = path
96          .join(&module_path)
97          .canonicalize() 
98        {
99          // Verify file is still in modules directory and it exists
100          if full_module_path.starts_with(path) 
101          && full_module_path.exists() 
102          {
103            return Some(full_module_path)
104          }
105        }
106      }
107    }
108
109    // Search path of dependant running program
110    if let Some(dependant_path) = dependant.map(|d| d.path.as_deref()) {
111      if let Some(program_path) = 
112        dependant_path
113        .map(PathBuf::from)
114        .as_deref()
115        .and_then(|p| p.parent())
116      {
117        search_for_module!(program_path);
118      }
119    }
120
121    // Search local modules path
122    if let Some(local_modules_path) = 
123      self.local_modules_path
124      .as_ref()
125      .map(PathBuf::from)
126      .or_else(||
127        env::current_dir()
128        .ok()
129      )
130      .and_then(|p| p.canonicalize().ok())
131    {
132      search_for_module!(local_modules_path);
133    }
134
135    // Check global modules, if enabled
136    if self.enable_global_modules {
137      if let Some(global_modules_path) = 
138        env::var_os(Self::ENV_MODULES_PATH_KEY)
139        .map(PathBuf::from)
140        .and_then(|p| p.canonicalize().ok())
141      {
142        search_for_module!(global_modules_path);
143      }
144    }
145    
146    None
147  }
148}
149
150/// Stub module resolver that completely disables modules.
151/// 
152/// All calls to `try_resolve` on this resolver will return a "not found" error.
153#[derive(Debug)]
154pub struct NoModuleResolver;
155
156impl ModuleResolver for NoModuleResolver {
157  fn try_resolve(&self, _context: &mut Rant, module_path: &str, _dependant: Option<&RantProgramInfo>) -> ModuleResolveResult {
158    Err(ModuleResolveError {
159      name: module_path.to_owned(),
160      reason: ModuleResolveErrorReason::NotFound,
161    })
162  }
163}
164
165/// Represents an error that occurred when attempting to load a Rant module.
166#[derive(Debug)]
167pub struct ModuleResolveError {
168  pub name: String,
169  pub reason: ModuleResolveErrorReason,
170}
171
172impl Error for ModuleResolveError {}
173
174impl ModuleResolveError {
175  /// Gets the name of the module that failed to load.
176  #[inline]
177  pub fn name(&self) -> &str {
178    &self.name
179  }
180
181  /// Gets the reason for the module load failure.
182  #[inline]
183  pub fn reason(&self) -> &ModuleResolveErrorReason {
184    &self.reason
185  }
186}
187
188/// Represents the reason for which a Rant module failed to load.
189#[derive(Debug)]
190pub enum ModuleResolveErrorReason {
191  /// The module was not found.
192  NotFound,
193  /// The module could not be compiled.
194  CompileFailed(Vec<CompilerMessage>),
195  /// The module could not load due to a file I/O error.
196  FileIOError(ErrorKind),
197}
198
199impl Display for ModuleResolveError {
200  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201    match self.reason() {
202      ModuleResolveErrorReason::NotFound => write!(f, "module '{}' not found", self.name()),
203      ModuleResolveErrorReason::CompileFailed(msgs) => write!(f, "module '{}' failed to compile: {}",
204      self.name(),
205      msgs.iter().fold(String::new(), |mut acc, msg| {
206        acc.push_str(&format!("[{}] {}\n", msg.severity(), msg.message())); 
207        acc
208      })),
209      ModuleResolveErrorReason::FileIOError(ioerr) => write!(f, "file I/O error ({:?})", ioerr),
210    }
211  }
212}
213
214impl IntoRuntimeResult<RantProgram> for ModuleResolveResult {
215  fn into_runtime_result(self) -> RuntimeResult<RantProgram> {
216    self.map_err(|err| RuntimeError {
217      error_type: RuntimeErrorType::ModuleError(err),
218      description: None,
219      stack_trace: None,
220    })
221  }
222}