lust/
config.rs

1use alloc::string::String;
2#[cfg(feature = "std")]
3use alloc::vec::Vec;
4use hashbrown::HashSet;
5#[cfg(feature = "std")]
6use serde::Deserialize;
7#[cfg(feature = "std")]
8use std::{
9    fs,
10    path::{Path, PathBuf},
11};
12use thiserror::Error;
13#[derive(Debug, Error)]
14pub enum ConfigError {
15    #[cfg(feature = "std")]
16    #[error("failed to read configuration: {0}")]
17    Io(#[from] std::io::Error),
18    #[cfg(feature = "std")]
19    #[error("failed to parse configuration: {0}")]
20    Parse(#[from] toml::de::Error),
21    #[error("{0}")]
22    Unsupported(String),
23}
24
25#[derive(Debug, Clone)]
26pub struct LustConfig {
27    enabled_modules: HashSet<String>,
28    jit_enabled: bool,
29    #[cfg(feature = "std")]
30    rust_modules: Vec<RustModule>,
31}
32
33#[cfg(feature = "std")]
34#[derive(Debug, Clone)]
35pub struct RustModule {
36    path: PathBuf,
37    externs: Option<PathBuf>,
38}
39
40#[cfg(feature = "std")]
41#[derive(Debug, Deserialize)]
42struct LustConfigToml {
43    settings: Settings,
44}
45
46#[cfg(feature = "std")]
47#[derive(Debug, Deserialize)]
48struct Settings {
49    #[serde(default)]
50    stdlib_modules: Vec<String>,
51    #[serde(default = "default_jit_enabled")]
52    jit: bool,
53    #[serde(default)]
54    rust_modules: Vec<RustModuleEntry>,
55}
56
57#[cfg(feature = "std")]
58#[derive(Debug, Deserialize)]
59struct RustModuleEntry {
60    path: String,
61    #[serde(default)]
62    externs: Option<String>,
63}
64
65const fn default_jit_enabled() -> bool {
66    true
67}
68
69impl Default for LustConfig {
70    fn default() -> Self {
71        Self {
72            enabled_modules: HashSet::new(),
73            jit_enabled: true,
74            #[cfg(feature = "std")]
75            rust_modules: Vec::new(),
76        }
77    }
78}
79
80impl LustConfig {
81    #[cfg(feature = "std")]
82    pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
83        let path_ref = path.as_ref();
84        let content = fs::read_to_string(path_ref)?;
85        let parsed: LustConfigToml = toml::from_str(&content)?;
86        Ok(Self::from_parsed(parsed, path_ref.parent()))
87    }
88
89    #[cfg(feature = "std")]
90    pub fn from_toml_str(source: &str) -> Result<Self, ConfigError> {
91        let parsed: LustConfigToml = toml::from_str(source)?;
92        Ok(Self::from_parsed(parsed, None))
93    }
94
95    #[cfg(feature = "std")]
96    pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self, ConfigError> {
97        let mut path = PathBuf::from(dir.as_ref());
98        path.push("lust-config.toml");
99        if !path.exists() {
100            return Ok(Self::default());
101        }
102
103        Self::load_from_path(path)
104    }
105
106    #[cfg(feature = "std")]
107    pub fn load_for_entry<P: AsRef<Path>>(entry_file: P) -> Result<Self, ConfigError> {
108        let entry_path = entry_file.as_ref();
109        let dir = entry_path.parent().unwrap_or_else(|| Path::new("."));
110        Self::load_from_dir(dir)
111    }
112
113    pub fn jit_enabled(&self) -> bool {
114        self.jit_enabled
115    }
116
117    pub fn is_module_enabled(&self, module: &str) -> bool {
118        let key = module.to_ascii_lowercase();
119        self.enabled_modules.contains(&key)
120    }
121
122    pub fn enabled_modules(&self) -> impl Iterator<Item = &str> {
123        self.enabled_modules.iter().map(|s| s.as_str())
124    }
125
126    pub fn enable_module<S: AsRef<str>>(&mut self, module: S) {
127        let key = module.as_ref().trim().to_ascii_lowercase();
128        if !key.is_empty() {
129            self.enabled_modules.insert(key);
130        }
131    }
132
133    pub fn set_jit_enabled(&mut self, enabled: bool) {
134        self.jit_enabled = enabled;
135    }
136
137    pub fn with_enabled_modules<I, S>(modules: I) -> Self
138    where
139        I: IntoIterator<Item = S>,
140        S: AsRef<str>,
141    {
142        let mut config = Self::default();
143        for module in modules {
144            config.enable_module(module);
145        }
146
147        config
148    }
149
150    #[cfg(feature = "std")]
151    pub fn rust_modules(&self) -> impl Iterator<Item = &RustModule> {
152        self.rust_modules.iter()
153    }
154
155    #[cfg(feature = "std")]
156    fn from_parsed(parsed: LustConfigToml, base_dir: Option<&Path>) -> Self {
157        let modules = parsed
158            .settings
159            .stdlib_modules
160            .into_iter()
161            .map(|m| m.trim().to_ascii_lowercase())
162            .filter(|m| !m.is_empty())
163            .collect::<HashSet<_>>();
164        let rust_modules = parsed
165            .settings
166            .rust_modules
167            .into_iter()
168            .map(|entry| {
169                let path = match base_dir {
170                    Some(root) => root.join(&entry.path),
171                    None => PathBuf::from(&entry.path),
172                };
173                let externs = entry.externs.map(PathBuf::from);
174                RustModule { path, externs }
175            })
176            .collect();
177        Self {
178            enabled_modules: modules,
179            jit_enabled: parsed.settings.jit,
180            rust_modules,
181        }
182    }
183}
184
185#[cfg(feature = "std")]
186impl RustModule {
187    pub fn path(&self) -> &Path {
188        &self.path
189    }
190
191    pub fn externs(&self) -> Option<&Path> {
192        self.externs.as_deref()
193    }
194
195    pub fn externs_dir(&self) -> Option<PathBuf> {
196        self.externs.as_ref().map(|path| {
197            if path.is_absolute() {
198                path.clone()
199            } else {
200                self.path.join(path)
201            }
202        })
203    }
204}
205
206#[cfg(feature = "std")]
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use std::path::Path;
211    #[test]
212    fn default_config_has_jit_enabled() {
213        let cfg = LustConfig::default();
214        assert!(cfg.jit_enabled());
215        assert!(cfg.enabled_modules().next().is_none());
216    }
217
218    #[test]
219    fn parse_config_with_modules_and_jit() {
220        let toml = r#"
221            [settings]
222            stdlib_modules = ["io", "os"]
223            jit = false
224        "#;
225        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
226        let cfg = LustConfig::from_parsed(parsed, None);
227        assert!(!cfg.jit_enabled());
228        assert!(cfg.is_module_enabled("io"));
229        assert!(cfg.is_module_enabled("os"));
230    }
231
232    #[test]
233    fn rust_modules_are_resolved_relative_to_config() {
234        let toml = r#"
235            [settings]
236            rust_modules = [
237                { path = "ext/foo", externs = "externs" },
238                { path = "/absolute/bar" }
239            ]
240        "#;
241        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
242        let base = PathBuf::from("/var/project");
243        let cfg = LustConfig::from_parsed(parsed, Some(base.as_path()));
244        let modules: Vec<&RustModule> = cfg.rust_modules().collect();
245        assert_eq!(modules.len(), 2);
246        assert_eq!(modules[0].path(), Path::new("/var/project/ext/foo"));
247        assert_eq!(
248            modules[0].externs_dir(),
249            Some(PathBuf::from("/var/project/ext/foo/externs"))
250        );
251        assert_eq!(modules[1].path(), Path::new("/absolute/bar"));
252        assert!(modules[1].externs().is_none());
253    }
254}