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}