lust/
config.rs

1use alloc::{string::String, vec::Vec};
2use hashbrown::HashSet;
3#[cfg(feature = "std")]
4use serde::Deserialize;
5#[cfg(feature = "std")]
6use std::{
7    collections::BTreeMap,
8    fs,
9    path::{Path, PathBuf},
10};
11use thiserror::Error;
12
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    #[cfg(feature = "std")]
22    #[error("dependency '{0}' must specify either a version or a path")]
23    MissingDependencySource(String),
24    #[cfg(feature = "std")]
25    #[error("dependency '{0}' has unknown kind '{1}'")]
26    UnknownDependencyKind(String, String),
27    #[error("{0}")]
28    Unsupported(String),
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum DependencyKind {
33    Lust,
34    Rust,
35}
36
37#[derive(Debug, Clone)]
38pub struct DependencySpec {
39    name: String,
40    version: Option<String>,
41    path: Option<String>,
42    kind: Option<DependencyKind>,
43    features: Vec<String>,
44    default_features: Option<bool>,
45    externs: Option<String>,
46    legacy: bool,
47}
48
49impl DependencySpec {
50    pub fn name(&self) -> &str {
51        &self.name
52    }
53
54    pub fn version(&self) -> Option<&str> {
55        self.version.as_deref()
56    }
57
58    pub fn path(&self) -> Option<&str> {
59        self.path.as_deref()
60    }
61
62    pub fn kind(&self) -> Option<DependencyKind> {
63        self.kind
64    }
65
66    pub fn features(&self) -> &[String] {
67        &self.features
68    }
69
70    pub fn default_features(&self) -> Option<bool> {
71        self.default_features
72    }
73
74    pub fn externs(&self) -> Option<&str> {
75        self.externs.as_deref()
76    }
77
78    pub fn is_legacy(&self) -> bool {
79        self.legacy
80    }
81}
82
83#[derive(Debug, Clone)]
84pub struct LustConfig {
85    enabled_modules: HashSet<String>,
86    jit_enabled: bool,
87    #[cfg(feature = "std")]
88    dependencies: Vec<DependencySpec>,
89}
90
91impl Default for LustConfig {
92    fn default() -> Self {
93        Self {
94            enabled_modules: HashSet::new(),
95            jit_enabled: true,
96            #[cfg(feature = "std")]
97            dependencies: Vec::new(),
98        }
99    }
100}
101
102impl LustConfig {
103    #[cfg(feature = "std")]
104    pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
105        let path_ref = path.as_ref();
106        let content = fs::read_to_string(path_ref)?;
107        let parsed: LustConfigToml = toml::from_str(&content)?;
108        Self::from_parsed(parsed, path_ref.parent())
109    }
110
111    #[cfg(feature = "std")]
112    pub fn from_toml_str(source: &str) -> Result<Self, ConfigError> {
113        let parsed: LustConfigToml = toml::from_str(source)?;
114        Self::from_parsed(parsed, None)
115    }
116
117    #[cfg(feature = "std")]
118    pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self, ConfigError> {
119        let mut path = PathBuf::from(dir.as_ref());
120        path.push("lust-config.toml");
121        if !path.exists() {
122            return Ok(Self::default());
123        }
124
125        Self::load_from_path(path)
126    }
127
128    #[cfg(feature = "std")]
129    pub fn load_for_entry<P: AsRef<Path>>(entry_file: P) -> Result<Self, ConfigError> {
130        let entry_path = entry_file.as_ref();
131        let dir = entry_path.parent().unwrap_or_else(|| Path::new("."));
132        Self::load_from_dir(dir)
133    }
134
135    pub fn jit_enabled(&self) -> bool {
136        self.jit_enabled
137    }
138
139    pub fn is_module_enabled(&self, module: &str) -> bool {
140        let key = module.to_ascii_lowercase();
141        self.enabled_modules.contains(&key)
142    }
143
144    pub fn enabled_modules(&self) -> impl Iterator<Item = &str> {
145        self.enabled_modules.iter().map(|s| s.as_str())
146    }
147
148    pub fn enable_module<S: AsRef<str>>(&mut self, module: S) {
149        let key = module.as_ref().trim().to_ascii_lowercase();
150        if !key.is_empty() {
151            self.enabled_modules.insert(key);
152        }
153    }
154
155    pub fn set_jit_enabled(&mut self, enabled: bool) {
156        self.jit_enabled = enabled;
157    }
158
159    pub fn with_enabled_modules<I, S>(modules: I) -> Self
160    where
161        I: IntoIterator<Item = S>,
162        S: AsRef<str>,
163    {
164        let mut config = Self::default();
165        for module in modules {
166            config.enable_module(module);
167        }
168
169        config
170    }
171
172    #[cfg(feature = "std")]
173    pub fn dependencies(&self) -> &[DependencySpec] {
174        &self.dependencies
175    }
176
177    #[cfg(feature = "std")]
178    fn from_parsed(parsed: LustConfigToml, _base_dir: Option<&Path>) -> Result<Self, ConfigError> {
179        let LustConfigToml {
180            settings,
181            dependencies: mut root_dependencies,
182        } = parsed;
183        let Settings {
184            stdlib_modules,
185            jit,
186            rust_modules,
187            dependencies: nested_dependencies,
188        } = settings;
189
190        let modules = stdlib_modules
191            .into_iter()
192            .map(|m| m.trim().to_ascii_lowercase())
193            .filter(|m| !m.is_empty())
194            .collect::<HashSet<_>>();
195
196        for (name, entry) in nested_dependencies {
197            root_dependencies.insert(name, entry);
198        }
199
200        let mut dependencies = Vec::new();
201        for (name, entry) in root_dependencies {
202            let (version, path, kind, features, default_features, externs) = match entry {
203                DependencyToml::Version(version) => {
204                    (Some(version), None, None, Vec::new(), None, None)
205                }
206                DependencyToml::Detailed(table) => {
207                    let kind = match table.kind {
208                        Some(raw) => match raw.trim().to_ascii_lowercase().as_str() {
209                            "lust" => Some(DependencyKind::Lust),
210                            "rust" => Some(DependencyKind::Rust),
211                            other => {
212                                return Err(ConfigError::UnknownDependencyKind(
213                                    name.clone(),
214                                    other.to_string(),
215                                ))
216                            }
217                        },
218                        None => None,
219                    };
220                    (
221                        table.version,
222                        table.path,
223                        kind,
224                        table.features,
225                        table.default_features,
226                        table.externs,
227                    )
228                }
229            };
230            let has_path = path.as_ref().map(|p| !p.trim().is_empty()).unwrap_or(false);
231            if version.is_none() && !has_path {
232                return Err(ConfigError::MissingDependencySource(name));
233            }
234            dependencies.push(DependencySpec {
235                name,
236                version,
237                path,
238                kind,
239                features,
240                default_features,
241                externs,
242                legacy: false,
243            });
244        }
245
246        for legacy in rust_modules {
247            let inferred_name = Path::new(&legacy.path)
248                .file_name()
249                .and_then(|s| s.to_str())
250                .unwrap_or(&legacy.path)
251                .to_string();
252            dependencies.push(DependencySpec {
253                name: inferred_name,
254                version: None,
255                path: Some(legacy.path),
256                kind: Some(DependencyKind::Rust),
257                features: Vec::new(),
258                default_features: None,
259                externs: legacy.externs,
260                legacy: true,
261            });
262        }
263
264        Ok(Self {
265            enabled_modules: modules,
266            jit_enabled: jit,
267            dependencies,
268        })
269    }
270}
271
272#[cfg(feature = "std")]
273#[derive(Debug, Deserialize)]
274struct LustConfigToml {
275    #[serde(default)]
276    settings: Settings,
277    #[serde(default)]
278    dependencies: BTreeMap<String, DependencyToml>,
279}
280
281#[cfg(feature = "std")]
282#[derive(Debug, Default, Deserialize)]
283struct Settings {
284    #[serde(default)]
285    stdlib_modules: Vec<String>,
286    #[serde(default = "default_jit_enabled")]
287    jit: bool,
288    #[serde(default)]
289    rust_modules: Vec<RustModuleEntry>,
290    #[serde(default)]
291    dependencies: BTreeMap<String, DependencyToml>,
292}
293
294#[cfg(feature = "std")]
295#[derive(Debug, Deserialize)]
296struct RustModuleEntry {
297    path: String,
298    #[serde(default)]
299    externs: Option<String>,
300}
301
302#[cfg(feature = "std")]
303#[derive(Debug, Deserialize)]
304#[serde(untagged)]
305enum DependencyToml {
306    Version(String),
307    Detailed(DependencyTomlTable),
308}
309
310#[cfg(feature = "std")]
311#[derive(Debug, Default, Deserialize)]
312struct DependencyTomlTable {
313    #[serde(default)]
314    version: Option<String>,
315    #[serde(default)]
316    path: Option<String>,
317    #[serde(default)]
318    kind: Option<String>,
319    #[serde(default)]
320    features: Vec<String>,
321    #[serde(default)]
322    default_features: Option<bool>,
323    #[serde(default)]
324    externs: Option<String>,
325}
326
327const fn default_jit_enabled() -> bool {
328    true
329}
330
331#[cfg(feature = "std")]
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn default_config_has_jit_enabled() {
338        let cfg = LustConfig::default();
339        assert!(cfg.jit_enabled());
340        assert!(cfg.enabled_modules().next().is_none());
341    }
342
343    #[test]
344    fn parse_config_with_modules_and_jit() {
345        let toml = r#"
346            [settings]
347            stdlib_modules = ["io", "os"]
348            jit = false
349        "#;
350        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
351        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
352        assert!(!cfg.jit_enabled());
353        assert!(cfg.is_module_enabled("io"));
354        assert!(cfg.is_module_enabled("os"));
355    }
356
357    #[test]
358    fn dependencies_parse_version() {
359        let toml = r#"
360            [dependencies]
361            foo = "1.2.3"
362        "#;
363        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
364        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
365        let deps = cfg.dependencies();
366        assert_eq!(deps.len(), 1);
367        assert_eq!(deps[0].name(), "foo");
368        assert_eq!(deps[0].version(), Some("1.2.3"));
369        assert!(deps[0].path().is_none());
370    }
371
372    #[test]
373    fn settings_dependencies_still_supported() {
374        let toml = r#"
375            [settings]
376            [settings.dependencies]
377            bar = { path = "ext/bar", kind = "rust" }
378        "#;
379        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
380        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
381        let deps = cfg.dependencies();
382        assert_eq!(deps.len(), 1);
383        assert_eq!(deps[0].name(), "bar");
384        assert_eq!(deps[0].path(), Some("ext/bar"));
385        assert_eq!(deps[0].kind(), Some(DependencyKind::Rust));
386    }
387
388    #[test]
389    fn settings_dependencies_override_top_level() {
390        let toml = r#"
391            [dependencies]
392            baz = { path = "ext/baz" }
393
394            [settings]
395            [settings.dependencies]
396            baz = { version = "1.2.3" }
397        "#;
398        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
399        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
400        let deps = cfg.dependencies();
401        assert_eq!(deps.len(), 1);
402        assert_eq!(deps[0].name(), "baz");
403        assert_eq!(deps[0].version(), Some("1.2.3"));
404        assert!(deps[0].path().is_none());
405    }
406
407    #[test]
408    fn legacy_rust_modules_are_mapped_to_dependencies() {
409        let toml = r#"
410            [settings]
411            rust_modules = [
412                { path = "ext/foo", externs = "externs" },
413                { path = "/absolute/bar" }
414            ]
415        "#;
416        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
417        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
418        let deps = cfg.dependencies();
419        assert_eq!(deps.len(), 2);
420        assert_eq!(deps[0].path(), Some("ext/foo"));
421        assert_eq!(deps[0].externs(), Some("externs"));
422        assert_eq!(deps[0].kind(), Some(DependencyKind::Rust));
423        assert!(deps[0].is_legacy());
424        assert_eq!(deps[1].path(), Some("/absolute/bar"));
425        assert!(deps[1].externs().is_none());
426    }
427}