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}