1use crate::search::{SearchEngine, SearchResult};
9use include_dir::{Dir, include_dir};
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
30impl ModuleSource {
31 pub fn label(&self) -> &'static str {
32 match self {
33 ModuleSource::BuiltIn => "builtin",
34 ModuleSource::Project => "project",
35 ModuleSource::Global => "global",
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42#[non_exhaustive]
43pub struct DiscoveredModule {
44 pub module_name: String,
45 pub source: ModuleSource,
46 pub metadata: ModuleMetadata,
47 pub lua_source: String,
48}
49
50const BUILTINS: &[(&str, &str, &[&str], &str)] = &[
52 (
53 "http",
54 "HTTP client and server: get, post, put, patch, delete, serve",
55 &[
56 "http", "client", "server", "request", "response", "headers", "endpoint", "api",
57 "webhook", "rest",
58 ],
59 "core",
60 ),
61 (
62 "json",
63 "JSON serialization: parse and encode",
64 &[
65 "json",
66 "serialization",
67 "deserialize",
68 "stringify",
69 "parse",
70 "encode",
71 "format",
72 ],
73 "core",
74 ),
75 (
76 "yaml",
77 "YAML serialization: parse and encode",
78 &[
79 "yaml",
80 "serialization",
81 "deserialize",
82 "parse",
83 "encode",
84 "format",
85 ],
86 "core",
87 ),
88 (
89 "toml",
90 "TOML serialization: parse and encode",
91 &[
92 "toml",
93 "serialization",
94 "deserialize",
95 "parse",
96 "encode",
97 "configuration",
98 ],
99 "core",
100 ),
101 (
102 "fs",
103 "Filesystem: read and write files",
104 &["fs", "filesystem", "file", "read", "write", "io", "path"],
105 "core",
106 ),
107 (
108 "crypto",
109 "Cryptography: jwt_sign, hash, hmac, random",
110 &[
111 "crypto",
112 "jwt",
113 "signature",
114 "hash",
115 "hmac",
116 "encryption",
117 "random",
118 "security",
119 "password",
120 "signing",
121 "rsa",
122 "sha256",
123 ],
124 "core",
125 ),
126 (
127 "base64",
128 "Base64 encoding and decoding",
129 &["base64", "encoding", "decode", "encode", "binary"],
130 "core",
131 ),
132 (
133 "regex",
134 "Regular expressions: match, find, find_all, replace",
135 &[
136 "regex",
137 "pattern",
138 "match",
139 "find",
140 "replace",
141 "regular-expression",
142 "regexp",
143 ],
144 "core",
145 ),
146 (
147 "db",
148 "Database: connect, query, execute, close (Postgres, MySQL, SQLite)",
149 &[
150 "db",
151 "database",
152 "sql",
153 "postgres",
154 "mysql",
155 "sqlite",
156 "connection",
157 "query",
158 "execute",
159 ],
160 "data",
161 ),
162 (
163 "ws",
164 "WebSocket: connect, send, recv, close",
165 &[
166 "ws",
167 "websocket",
168 "connection",
169 "message",
170 "streaming",
171 "realtime",
172 "socket",
173 ],
174 "core",
175 ),
176 (
177 "dns",
178 "DNS lookups: A, AAAA, CNAME, MX, NS, TXT, plus DNSBL blacklist checks",
179 &[
180 "dns",
181 "resolve",
182 "resolver",
183 "lookup",
184 "nameserver",
185 "mx",
186 "txt",
187 "spf",
188 "dkim",
189 "dmarc",
190 "dnsbl",
191 "blacklist",
192 "blocklist",
193 "domain",
194 "deliverability",
195 ],
196 "core",
197 ),
198 (
199 "template",
200 "Jinja2-compatible templates: render file or string",
201 &[
202 "template",
203 "jinja2",
204 "rendering",
205 "string-template",
206 "mustache",
207 "render",
208 ],
209 "core",
210 ),
211 (
212 "async",
213 "Async tasks: spawn, spawn_interval, await, cancel",
214 &[
215 "async",
216 "asynchronous",
217 "task",
218 "coroutine",
219 "concurrent",
220 "spawn",
221 "interval",
222 ],
223 "core",
224 ),
225 (
226 "assert",
227 "Assertions: eq, gt, lt, contains, not_nil, matches",
228 &[
229 "assert",
230 "assertion",
231 "test",
232 "validation",
233 "comparison",
234 "check",
235 "verify",
236 ],
237 "core",
238 ),
239 (
240 "log",
241 "Logging: info, warn, error",
242 &[
243 "log", "logging", "output", "debug", "error", "warning", "info", "trace",
244 ],
245 "core",
246 ),
247 (
248 "env",
249 "Environment variables: get",
250 &["env", "environment", "variable", "configuration", "config"],
251 "core",
252 ),
253 (
254 "sleep",
255 "Sleep for N seconds",
256 &["sleep", "delay", "pause", "wait", "time"],
257 "core",
258 ),
259 (
260 "time",
261 "Unix timestamp in seconds",
262 &["time", "timestamp", "unix", "epoch", "clock", "datetime"],
263 "core",
264 ),
265 (
266 "compress",
267 "Decompression: gunzip, unxz, unzstd. Pure binary in/out.",
268 &[
269 "compress",
270 "decompress",
271 "gunzip",
272 "gzip",
273 "xz",
274 "lzma",
275 "zstd",
276 ],
277 "core",
278 ),
279];
280
281pub fn discover_modules() -> Vec<DiscoveredModule> {
286 let mut modules = Vec::new();
287
288 discover_filesystem_modules(
290 std::path::Path::new("./modules"),
291 ModuleSource::Project,
292 &mut modules,
293 );
294
295 let global_path = resolve_global_modules_path();
297 if let Some(path) = global_path {
298 discover_filesystem_modules(&path, ModuleSource::Global, &mut modules);
299 }
300
301 discover_embedded_stdlib(&mut modules);
303
304 discover_rust_builtins(&mut modules);
306
307 modules
308}
309
310pub fn build_index(modules: &[DiscoveredModule]) -> Box<dyn SearchEngine> {
315 #[cfg(feature = "db")]
316 {
317 let mut idx = FTS5Index::new();
318 for m in modules {
319 idx.add_document(
320 &m.module_name,
321 &[
322 ("keywords", &m.metadata.keywords.join(" "), 3.0),
323 ("module_name", &m.module_name, 2.0),
324 ("description", &m.metadata.description, 1.0),
325 ("functions", &m.metadata.auto_functions.join(" "), 1.0),
326 ],
327 );
328 }
329 Box::new(idx)
330 }
331 #[cfg(not(feature = "db"))]
332 {
333 let mut idx = BM25Index::new();
334 for m in modules {
335 idx.add_document(
336 &m.module_name,
337 &[
338 ("keywords", &m.metadata.keywords.join(" "), 3.0),
339 ("module_name", &m.module_name, 2.0),
340 ("description", &m.metadata.description, 1.0),
341 ("functions", &m.metadata.auto_functions.join(" "), 1.0),
342 ],
343 );
344 }
345 Box::new(idx)
346 }
347}
348
349pub fn search_modules(query: &str, limit: usize) -> Vec<SearchResult> {
351 let modules = discover_modules();
352 let index = build_index(&modules);
353 index.search(query, limit)
354}
355
356fn resolve_global_modules_path() -> Option<std::path::PathBuf> {
361 if let Ok(custom) = std::env::var(crate::lua::MODULES_PATH_ENV) {
362 return Some(std::path::PathBuf::from(custom));
363 }
364 if let Ok(home) = std::env::var("HOME") {
365 return Some(std::path::Path::new(&home).join(".assay/modules"));
366 }
367 None
368}
369
370fn discover_filesystem_modules(
374 dir: &std::path::Path,
375 source: ModuleSource,
376 modules: &mut Vec<DiscoveredModule>,
377) {
378 let entries = match std::fs::read_dir(dir) {
379 Ok(entries) => entries,
380 Err(_) => return, };
382
383 for entry in entries.flatten() {
384 let path = entry.path();
385 if path.extension().and_then(|e| e.to_str()) != Some("lua") {
386 continue;
387 }
388
389 let lua_source = match std::fs::read_to_string(&path) {
390 Ok(s) => s,
391 Err(_) => continue,
392 };
393
394 let stem = path
395 .file_stem()
396 .and_then(|s| s.to_str())
397 .unwrap_or_default();
398 let module_name = format!("assay.{stem}");
399 let meta = metadata::parse_metadata(&lua_source);
400
401 modules.push(DiscoveredModule {
402 module_name,
403 source: source.clone(),
404 metadata: meta,
405 lua_source,
406 });
407 }
408}
409
410fn discover_embedded_stdlib(modules: &mut Vec<DiscoveredModule>) {
419 fn walk(dir: &include_dir::Dir<'_>, modules: &mut Vec<DiscoveredModule>) {
420 for file in dir.files() {
421 let path = file.path();
422 if path.extension().and_then(|e| e.to_str()) != Some("lua") {
423 continue;
424 }
425 let Some(lua_source) = file.contents_utf8() else {
426 continue;
427 };
428 let segments: Vec<&str> = path.iter().filter_map(|c| c.to_str()).collect();
431 if segments.is_empty() {
432 continue;
433 }
434 let mut joined = segments.join(".");
435 if joined.ends_with(".lua") {
436 joined.truncate(joined.len() - 4);
437 }
438 let module_name = format!("assay.{joined}");
439 let meta = metadata::parse_metadata(lua_source);
440 modules.push(DiscoveredModule {
441 module_name,
442 source: ModuleSource::BuiltIn,
443 metadata: meta,
444 lua_source: lua_source.to_string(),
445 });
446 }
447 for sub in dir.dirs() {
448 walk(sub, modules);
449 }
450 }
451 walk(&STDLIB_DIR, modules);
452}
453
454fn discover_rust_builtins(modules: &mut Vec<DiscoveredModule>) {
456 for &(name, description, kw, category) in BUILTINS {
457 modules.push(DiscoveredModule {
458 module_name: name.to_string(),
459 source: ModuleSource::BuiltIn,
460 lua_source: String::new(),
461 metadata: ModuleMetadata {
462 module_name: name.to_string(),
463 description: description.to_string(),
464 keywords: kw.iter().map(|k| k.to_string()).collect(),
465 category: Some(category.to_string()),
466 ..Default::default()
467 },
468 });
469 }
470}