Skip to main content

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
329#[allow(dead_code)]
330const fn default_jit_enabled() -> bool {
331    true
332}
333
334#[cfg(feature = "std")]
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn default_config_has_jit_enabled() {
341        let cfg = LustConfig::default();
342        assert!(cfg.jit_enabled());
343        assert!(cfg.enabled_modules().next().is_none());
344    }
345
346    #[test]
347    fn parse_config_with_modules_and_jit() {
348        let toml = r#"
349            [settings]
350            stdlib_modules = ["io", "os"]
351            jit = false
352        "#;
353        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
354        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
355        assert!(!cfg.jit_enabled());
356        assert!(cfg.is_module_enabled("io"));
357        assert!(cfg.is_module_enabled("os"));
358    }
359
360    #[test]
361    fn dependencies_parse_version() {
362        let toml = r#"
363            [dependencies]
364            foo = "1.2.3"
365        "#;
366        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
367        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
368        let deps = cfg.dependencies();
369        assert_eq!(deps.len(), 1);
370        assert_eq!(deps[0].name(), "foo");
371        assert_eq!(deps[0].version(), Some("1.2.3"));
372        assert!(deps[0].path().is_none());
373    }
374
375    #[test]
376    fn settings_dependencies_still_supported() {
377        let toml = r#"
378            [settings]
379            [settings.dependencies]
380            bar = { path = "ext/bar", kind = "rust" }
381        "#;
382        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
383        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
384        let deps = cfg.dependencies();
385        assert_eq!(deps.len(), 1);
386        assert_eq!(deps[0].name(), "bar");
387        assert_eq!(deps[0].path(), Some("ext/bar"));
388        assert_eq!(deps[0].kind(), Some(DependencyKind::Rust));
389    }
390
391    #[test]
392    fn settings_dependencies_override_top_level() {
393        let toml = r#"
394            [dependencies]
395            baz = { path = "ext/baz" }
396
397            [settings]
398            [settings.dependencies]
399            baz = { version = "1.2.3" }
400        "#;
401        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
402        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
403        let deps = cfg.dependencies();
404        assert_eq!(deps.len(), 1);
405        assert_eq!(deps[0].name(), "baz");
406        assert_eq!(deps[0].version(), Some("1.2.3"));
407        assert!(deps[0].path().is_none());
408    }
409
410    #[test]
411    fn legacy_rust_modules_are_mapped_to_dependencies() {
412        let toml = r#"
413            [settings]
414            rust_modules = [
415                { path = "ext/foo", externs = "externs" },
416                { path = "/absolute/bar" }
417            ]
418        "#;
419        let parsed: LustConfigToml = toml::from_str(toml).unwrap();
420        let cfg = LustConfig::from_parsed(parsed, None).unwrap();
421        let deps = cfg.dependencies();
422        assert_eq!(deps.len(), 2);
423        assert_eq!(deps[0].path(), Some("ext/foo"));
424        assert_eq!(deps[0].externs(), Some("externs"));
425        assert_eq!(deps[0].kind(), Some(DependencyKind::Rust));
426        assert!(deps[0].is_legacy());
427        assert_eq!(deps[1].path(), Some("/absolute/bar"));
428        assert!(deps[1].externs().is_none());
429    }
430}