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