1use include_dir::{include_dir, Dir};
9use crate::search::{SearchEngine, SearchResult};
10
11use crate::metadata::{self, ModuleMetadata};
12#[cfg(not(feature = "db"))]
13use crate::search::BM25Index;
14#[cfg(feature = "db")]
15use crate::search_fts5::FTS5Index;
16
17static STDLIB_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/stdlib");
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum ModuleSource {
22 BuiltIn,
24 Project,
26 Global,
28}
29
30#[derive(Debug, Clone)]
32pub struct DiscoveredModule {
33 pub module_name: String,
34 pub source: ModuleSource,
35 pub metadata: ModuleMetadata,
36 pub lua_source: String,
37}
38
39const BUILTINS: &[(&str, &str)] = &[
41 (
42 "http",
43 "HTTP client and server: get, post, put, patch, delete, serve",
44 ),
45 ("json", "JSON serialization: parse and encode"),
46 ("yaml", "YAML serialization: parse and encode"),
47 ("toml", "TOML serialization: parse and encode"),
48 ("fs", "Filesystem: read and write files"),
49 ("crypto", "Cryptography: jwt_sign, hash, hmac, random"),
50 ("base64", "Base64 encoding and decoding"),
51 (
52 "regex",
53 "Regular expressions: match, find, find_all, replace",
54 ),
55 (
56 "db",
57 "Database: connect, query, execute, close (Postgres, MySQL, SQLite)",
58 ),
59 ("ws", "WebSocket: connect, send, recv, close"),
60 (
61 "template",
62 "Jinja2-compatible templates: render file or string",
63 ),
64 ("async", "Async tasks: spawn, spawn_interval, await, cancel"),
65 (
66 "assert",
67 "Assertions: eq, gt, lt, contains, not_nil, matches",
68 ),
69 ("log", "Logging: info, warn, error"),
70 ("env", "Environment variables: get"),
71 ("sleep", "Sleep for N seconds"),
72 ("time", "Unix timestamp in seconds"),
73];
74
75pub fn discover_modules() -> Vec<DiscoveredModule> {
80 let mut modules = Vec::new();
81
82 discover_filesystem_modules(
84 std::path::Path::new("./modules"),
85 ModuleSource::Project,
86 &mut modules,
87 );
88
89 let global_path = resolve_global_modules_path();
91 if let Some(path) = global_path {
92 discover_filesystem_modules(&path, ModuleSource::Global, &mut modules);
93 }
94
95 discover_embedded_stdlib(&mut modules);
97
98 discover_rust_builtins(&mut modules);
100
101 modules
102}
103
104pub fn build_index(modules: &[DiscoveredModule]) -> Box<dyn SearchEngine> {
109 #[cfg(feature = "db")]
110 {
111 let mut idx = FTS5Index::new();
112 for m in modules {
113 idx.add_document(
114 &m.module_name,
115 &[
116 ("keywords", &m.metadata.keywords.join(" "), 3.0),
117 ("module_name", &m.module_name, 2.0),
118 ("description", &m.metadata.description, 1.0),
119 ("functions", &m.metadata.auto_functions.join(" "), 1.0),
120 ],
121 );
122 }
123 Box::new(idx)
124 }
125 #[cfg(not(feature = "db"))]
126 {
127 let mut idx = BM25Index::new();
128 for m in modules {
129 idx.add_document(
130 &m.module_name,
131 &[
132 ("keywords", &m.metadata.keywords.join(" "), 3.0),
133 ("module_name", &m.module_name, 2.0),
134 ("description", &m.metadata.description, 1.0),
135 ("functions", &m.metadata.auto_functions.join(" "), 1.0),
136 ],
137 );
138 }
139 Box::new(idx)
140 }
141}
142
143pub fn search_modules(query: &str, limit: usize) -> Vec<SearchResult> {
145 let modules = discover_modules();
146 let index = build_index(&modules);
147 index.search(query, limit)
148}
149
150fn resolve_global_modules_path() -> Option<std::path::PathBuf> {
155 if let Ok(custom) = std::env::var(crate::lua::MODULES_PATH_ENV) {
156 return Some(std::path::PathBuf::from(custom));
157 }
158 if let Ok(home) = std::env::var("HOME") {
159 return Some(std::path::Path::new(&home).join(".assay/modules"));
160 }
161 None
162}
163
164fn discover_filesystem_modules(
168 dir: &std::path::Path,
169 source: ModuleSource,
170 modules: &mut Vec<DiscoveredModule>,
171) {
172 let entries = match std::fs::read_dir(dir) {
173 Ok(entries) => entries,
174 Err(_) => return, };
176
177 for entry in entries.flatten() {
178 let path = entry.path();
179 if path.extension().and_then(|e| e.to_str()) != Some("lua") {
180 continue;
181 }
182
183 let lua_source = match std::fs::read_to_string(&path) {
184 Ok(s) => s,
185 Err(_) => continue,
186 };
187
188 let stem = path
189 .file_stem()
190 .and_then(|s| s.to_str())
191 .unwrap_or_default();
192 let module_name = format!("assay.{stem}");
193 let meta = metadata::parse_metadata(&lua_source);
194
195 modules.push(DiscoveredModule {
196 module_name,
197 source: source.clone(),
198 metadata: meta,
199 lua_source,
200 });
201 }
202}
203
204fn discover_embedded_stdlib(modules: &mut Vec<DiscoveredModule>) {
206 for file in STDLIB_DIR.files() {
207 let path = file.path();
208 if path.extension().and_then(|e| e.to_str()) != Some("lua") {
209 continue;
210 }
211
212 let lua_source = match file.contents_utf8() {
213 Some(s) => s,
214 None => continue,
215 };
216
217 let stem = path
218 .file_stem()
219 .and_then(|s| s.to_str())
220 .unwrap_or_default();
221 let module_name = format!("assay.{stem}");
222 let meta = metadata::parse_metadata(lua_source);
223
224 modules.push(DiscoveredModule {
225 module_name,
226 source: ModuleSource::BuiltIn,
227 metadata: meta,
228 lua_source: lua_source.to_string(),
229 });
230 }
231}
232
233fn discover_rust_builtins(modules: &mut Vec<DiscoveredModule>) {
235 for &(name, description) in BUILTINS {
236 modules.push(DiscoveredModule {
237 module_name: name.to_string(),
238 source: ModuleSource::BuiltIn,
239 lua_source: String::new(),
240 metadata: ModuleMetadata {
241 module_name: name.to_string(),
242 description: description.to_string(),
243 keywords: vec![name.to_string()],
244 ..Default::default()
245 },
246 });
247 }
248}