1use alloc::string::String;
2#[cfg(feature = "std")]
3use alloc::vec::Vec;
4use hashbrown::HashSet;
5#[cfg(feature = "std")]
6use serde::Deserialize;
7#[cfg(feature = "std")]
8use std::{
9 fs,
10 path::{Path, PathBuf},
11};
12use thiserror::Error;
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 #[error("{0}")]
22 Unsupported(String),
23}
24
25#[derive(Debug, Clone)]
26pub struct LustConfig {
27 enabled_modules: HashSet<String>,
28 jit_enabled: bool,
29 #[cfg(feature = "std")]
30 rust_modules: Vec<RustModule>,
31}
32
33#[cfg(feature = "std")]
34#[derive(Debug, Clone)]
35pub struct RustModule {
36 path: PathBuf,
37 externs: Option<PathBuf>,
38}
39
40#[cfg(feature = "std")]
41#[derive(Debug, Deserialize)]
42struct LustConfigToml {
43 settings: Settings,
44}
45
46#[cfg(feature = "std")]
47#[derive(Debug, Deserialize)]
48struct Settings {
49 #[serde(default)]
50 stdlib_modules: Vec<String>,
51 #[serde(default = "default_jit_enabled")]
52 jit: bool,
53 #[serde(default)]
54 rust_modules: Vec<RustModuleEntry>,
55}
56
57#[cfg(feature = "std")]
58#[derive(Debug, Deserialize)]
59struct RustModuleEntry {
60 path: String,
61 #[serde(default)]
62 externs: Option<String>,
63}
64
65const fn default_jit_enabled() -> bool {
66 true
67}
68
69impl Default for LustConfig {
70 fn default() -> Self {
71 Self {
72 enabled_modules: HashSet::new(),
73 jit_enabled: true,
74 #[cfg(feature = "std")]
75 rust_modules: Vec::new(),
76 }
77 }
78}
79
80impl LustConfig {
81 #[cfg(feature = "std")]
82 pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
83 let path_ref = path.as_ref();
84 let content = fs::read_to_string(path_ref)?;
85 let parsed: LustConfigToml = toml::from_str(&content)?;
86 Ok(Self::from_parsed(parsed, path_ref.parent()))
87 }
88
89 #[cfg(feature = "std")]
90 pub fn from_toml_str(source: &str) -> Result<Self, ConfigError> {
91 let parsed: LustConfigToml = toml::from_str(source)?;
92 Ok(Self::from_parsed(parsed, None))
93 }
94
95 #[cfg(feature = "std")]
96 pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self, ConfigError> {
97 let mut path = PathBuf::from(dir.as_ref());
98 path.push("lust-config.toml");
99 if !path.exists() {
100 return Ok(Self::default());
101 }
102
103 Self::load_from_path(path)
104 }
105
106 #[cfg(feature = "std")]
107 pub fn load_for_entry<P: AsRef<Path>>(entry_file: P) -> Result<Self, ConfigError> {
108 let entry_path = entry_file.as_ref();
109 let dir = entry_path.parent().unwrap_or_else(|| Path::new("."));
110 Self::load_from_dir(dir)
111 }
112
113 pub fn jit_enabled(&self) -> bool {
114 self.jit_enabled
115 }
116
117 pub fn is_module_enabled(&self, module: &str) -> bool {
118 let key = module.to_ascii_lowercase();
119 self.enabled_modules.contains(&key)
120 }
121
122 pub fn enabled_modules(&self) -> impl Iterator<Item = &str> {
123 self.enabled_modules.iter().map(|s| s.as_str())
124 }
125
126 pub fn enable_module<S: AsRef<str>>(&mut self, module: S) {
127 let key = module.as_ref().trim().to_ascii_lowercase();
128 if !key.is_empty() {
129 self.enabled_modules.insert(key);
130 }
131 }
132
133 pub fn set_jit_enabled(&mut self, enabled: bool) {
134 self.jit_enabled = enabled;
135 }
136
137 pub fn with_enabled_modules<I, S>(modules: I) -> Self
138 where
139 I: IntoIterator<Item = S>,
140 S: AsRef<str>,
141 {
142 let mut config = Self::default();
143 for module in modules {
144 config.enable_module(module);
145 }
146
147 config
148 }
149
150 #[cfg(feature = "std")]
151 pub fn rust_modules(&self) -> impl Iterator<Item = &RustModule> {
152 self.rust_modules.iter()
153 }
154
155 #[cfg(feature = "std")]
156 fn from_parsed(parsed: LustConfigToml, base_dir: Option<&Path>) -> Self {
157 let modules = parsed
158 .settings
159 .stdlib_modules
160 .into_iter()
161 .map(|m| m.trim().to_ascii_lowercase())
162 .filter(|m| !m.is_empty())
163 .collect::<HashSet<_>>();
164 let rust_modules = parsed
165 .settings
166 .rust_modules
167 .into_iter()
168 .map(|entry| {
169 let path = match base_dir {
170 Some(root) => root.join(&entry.path),
171 None => PathBuf::from(&entry.path),
172 };
173 let externs = entry.externs.map(PathBuf::from);
174 RustModule { path, externs }
175 })
176 .collect();
177 Self {
178 enabled_modules: modules,
179 jit_enabled: parsed.settings.jit,
180 rust_modules,
181 }
182 }
183}
184
185#[cfg(feature = "std")]
186impl RustModule {
187 pub fn path(&self) -> &Path {
188 &self.path
189 }
190
191 pub fn externs(&self) -> Option<&Path> {
192 self.externs.as_deref()
193 }
194
195 pub fn externs_dir(&self) -> Option<PathBuf> {
196 self.externs.as_ref().map(|path| {
197 if path.is_absolute() {
198 path.clone()
199 } else {
200 self.path.join(path)
201 }
202 })
203 }
204}
205
206#[cfg(feature = "std")]
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use std::path::Path;
211 #[test]
212 fn default_config_has_jit_enabled() {
213 let cfg = LustConfig::default();
214 assert!(cfg.jit_enabled());
215 assert!(cfg.enabled_modules().next().is_none());
216 }
217
218 #[test]
219 fn parse_config_with_modules_and_jit() {
220 let toml = r#"
221 [settings]
222 stdlib_modules = ["io", "os"]
223 jit = false
224 "#;
225 let parsed: LustConfigToml = toml::from_str(toml).unwrap();
226 let cfg = LustConfig::from_parsed(parsed, None);
227 assert!(!cfg.jit_enabled());
228 assert!(cfg.is_module_enabled("io"));
229 assert!(cfg.is_module_enabled("os"));
230 }
231
232 #[test]
233 fn rust_modules_are_resolved_relative_to_config() {
234 let toml = r#"
235 [settings]
236 rust_modules = [
237 { path = "ext/foo", externs = "externs" },
238 { path = "/absolute/bar" }
239 ]
240 "#;
241 let parsed: LustConfigToml = toml::from_str(toml).unwrap();
242 let base = PathBuf::from("/var/project");
243 let cfg = LustConfig::from_parsed(parsed, Some(base.as_path()));
244 let modules: Vec<&RustModule> = cfg.rust_modules().collect();
245 assert_eq!(modules.len(), 2);
246 assert_eq!(modules[0].path(), Path::new("/var/project/ext/foo"));
247 assert_eq!(
248 modules[0].externs_dir(),
249 Some(PathBuf::from("/var/project/ext/foo/externs"))
250 );
251 assert_eq!(modules[1].path(), Path::new("/absolute/bar"));
252 assert!(modules[1].externs().is_none());
253 }
254}