1use crate::error::{Result, RhaiError};
4use dashmap::DashMap;
5use rhai::AST;
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::SystemTime;
12
13#[derive(Debug)]
15pub struct CompiledScript {
16 pub path: PathBuf,
18 ast: AST,
20 compiled_at: SystemTime,
22}
23
24impl CompiledScript {
25 pub fn new(path: PathBuf, ast: AST) -> Self {
27 Self {
28 path,
29 ast,
30 compiled_at: SystemTime::now(),
31 }
32 }
33
34 pub fn ast(&self) -> &AST {
36 &self.ast
37 }
38
39 pub fn compiled_at(&self) -> SystemTime {
41 self.compiled_at
42 }
43
44 pub fn is_stale(&self) -> bool {
46 if let Ok(metadata) = fs::metadata(&self.path) {
47 if let Ok(modified) = metadata.modified() {
48 return modified > self.compiled_at;
49 }
50 }
51 false
52 }
53}
54
55pub struct ScriptCache {
57 scripts: HashMap<PathBuf, Arc<CompiledScript>>,
58 hits: AtomicU64,
59 misses: AtomicU64,
60}
61
62impl Default for ScriptCache {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl ScriptCache {
69 pub fn new() -> Self {
71 Self {
72 scripts: HashMap::new(),
73 hits: AtomicU64::new(0),
74 misses: AtomicU64::new(0),
75 }
76 }
77
78 pub fn get(&self, path: &Path) -> Option<Arc<CompiledScript>> {
80 if let Some(script) = self.scripts.get(path) {
81 self.hits.fetch_add(1, Ordering::Relaxed);
82 Some(script.clone())
83 } else {
84 self.misses.fetch_add(1, Ordering::Relaxed);
85 None
86 }
87 }
88
89 pub fn insert(&mut self, path: PathBuf, script: Arc<CompiledScript>) {
91 self.scripts.insert(path, script);
92 }
93
94 pub fn remove(&mut self, path: &Path) -> Option<Arc<CompiledScript>> {
96 self.scripts.remove(path)
97 }
98
99 pub fn clear(&mut self) {
101 self.scripts.clear();
102 }
103
104 pub fn stats(&self) -> crate::engine::CacheStats {
106 crate::engine::CacheStats {
107 cached_scripts: self.scripts.len(),
108 hits: self.hits.load(Ordering::Relaxed),
109 misses: self.misses.load(Ordering::Relaxed),
110 }
111 }
112
113 pub fn evict_stale(&mut self) -> Vec<PathBuf> {
115 let stale: Vec<PathBuf> = self
116 .scripts
117 .iter()
118 .filter(|(_, script)| script.is_stale())
119 .map(|(path, _)| path.clone())
120 .collect();
121
122 for path in &stale {
123 self.scripts.remove(path);
124 }
125
126 stale
127 }
128}
129
130pub struct ConcurrentScriptCache {
132 scripts: DashMap<PathBuf, Arc<CompiledScript>>,
133 hits: AtomicU64,
134 misses: AtomicU64,
135}
136
137impl Default for ConcurrentScriptCache {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl ConcurrentScriptCache {
144 pub fn new() -> Self {
146 Self {
147 scripts: DashMap::new(),
148 hits: AtomicU64::new(0),
149 misses: AtomicU64::new(0),
150 }
151 }
152
153 pub fn get(&self, path: &Path) -> Option<Arc<CompiledScript>> {
155 if let Some(script) = self.scripts.get(path) {
156 self.hits.fetch_add(1, Ordering::Relaxed);
157 Some(script.clone())
158 } else {
159 self.misses.fetch_add(1, Ordering::Relaxed);
160 None
161 }
162 }
163
164 pub fn insert(&self, path: PathBuf, script: Arc<CompiledScript>) {
166 self.scripts.insert(path, script);
167 }
168
169 pub fn remove(&self, path: &Path) -> Option<Arc<CompiledScript>> {
171 self.scripts.remove(path).map(|(_, v)| v)
172 }
173
174 pub fn clear(&self) {
176 self.scripts.clear();
177 }
178}
179
180pub struct ScriptLoader {
182 base_dir: PathBuf,
184}
185
186impl ScriptLoader {
187 pub fn new(base_dir: impl Into<PathBuf>) -> Self {
189 Self {
190 base_dir: base_dir.into(),
191 }
192 }
193
194 pub fn resolve_path(&self, path: &Path) -> PathBuf {
196 if path.is_absolute() {
197 path.to_path_buf()
198 } else {
199 self.base_dir.join(path)
200 }
201 }
202
203 pub fn load(&self, path: &Path) -> Result<String> {
205 let full_path = self.resolve_path(path);
206
207 if !full_path.exists() {
208 return Err(RhaiError::ScriptNotFound { path: full_path });
209 }
210
211 fs::read_to_string(&full_path).map_err(RhaiError::from)
212 }
213
214 pub fn exists(&self, path: &Path) -> bool {
216 self.resolve_path(path).exists()
217 }
218
219 pub fn list_scripts(&self, dir: &Path, extension: &str) -> Result<Vec<PathBuf>> {
221 let full_path = self.resolve_path(dir);
222 let mut scripts = Vec::new();
223
224 if !full_path.exists() {
225 return Ok(scripts);
226 }
227
228 self.collect_scripts(&full_path, extension, &mut scripts)?;
229 Ok(scripts)
230 }
231
232 fn collect_scripts(
233 &self,
234 dir: &Path,
235 extension: &str,
236 scripts: &mut Vec<PathBuf>,
237 ) -> Result<()> {
238 for entry in fs::read_dir(dir)? {
239 let entry = entry?;
240 let path = entry.path();
241
242 if path.is_dir() {
243 self.collect_scripts(&path, extension, scripts)?;
244 } else if path.extension().map(|e| e == extension).unwrap_or(false) {
245 scripts.push(path);
246 }
247 }
248 Ok(())
249 }
250
251 pub fn base_dir(&self) -> &Path {
253 &self.base_dir
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use std::io::Write;
261 use tempfile::TempDir;
262
263 #[test]
264 fn test_script_loader() {
265 let temp = TempDir::new().unwrap();
266 let mut file = fs::File::create(temp.path().join("test.rhai")).unwrap();
267 writeln!(file, "let x = 42;").unwrap();
268
269 let loader = ScriptLoader::new(temp.path());
270 let content = loader.load(Path::new("test.rhai")).unwrap();
271 assert!(content.contains("42"));
272 }
273
274 #[test]
275 fn test_script_cache() {
276 use rhai::Engine;
277
278 let engine = Engine::new();
279 let ast = engine.compile("let x = 1;").unwrap();
280 let script = Arc::new(CompiledScript::new(PathBuf::from("test.rhai"), ast));
281
282 let mut cache = ScriptCache::new();
283 cache.insert(PathBuf::from("test.rhai"), script.clone());
284
285 assert!(cache.get(Path::new("test.rhai")).is_some());
286 assert!(cache.get(Path::new("other.rhai")).is_none());
287
288 let stats = cache.stats();
289 assert_eq!(stats.cached_scripts, 1);
290 assert_eq!(stats.hits, 1);
291 assert_eq!(stats.misses, 1);
292 }
293}
294