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