lust/
config.rs

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