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 "template",
178 "Jinja2-compatible templates: render file or string",
179 &[
180 "template",
181 "jinja2",
182 "rendering",
183 "string-template",
184 "mustache",
185 "render",
186 ],
187 "core",
188 ),
189 (
190 "async",
191 "Async tasks: spawn, spawn_interval, await, cancel",
192 &[
193 "async",
194 "asynchronous",
195 "task",
196 "coroutine",
197 "concurrent",
198 "spawn",
199 "interval",
200 ],
201 "core",
202 ),
203 (
204 "assert",
205 "Assertions: eq, gt, lt, contains, not_nil, matches",
206 &[
207 "assert",
208 "assertion",
209 "test",
210 "validation",
211 "comparison",
212 "check",
213 "verify",
214 ],
215 "core",
216 ),
217 (
218 "log",
219 "Logging: info, warn, error",
220 &[
221 "log", "logging", "output", "debug", "error", "warning", "info", "trace",
222 ],
223 "core",
224 ),
225 (
226 "env",
227 "Environment variables: get",
228 &["env", "environment", "variable", "configuration", "config"],
229 "core",
230 ),
231 (
232 "sleep",
233 "Sleep for N seconds",
234 &["sleep", "delay", "pause", "wait", "time"],
235 "core",
236 ),
237 (
238 "time",
239 "Unix timestamp in seconds",
240 &["time", "timestamp", "unix", "epoch", "clock", "datetime"],
241 "core",
242 ),
243 (
244 "compress",
245 "Decompression: gunzip, unxz, unzstd. Pure binary in/out.",
246 &[
247 "compress",
248 "decompress",
249 "gunzip",
250 "gzip",
251 "xz",
252 "lzma",
253 "zstd",
254 ],
255 "core",
256 ),
257];
258
259pub fn discover_modules() -> Vec<DiscoveredModule> {
264 let mut modules = Vec::new();
265
266 discover_filesystem_modules(
268 std::path::Path::new("./modules"),
269 ModuleSource::Project,
270 &mut modules,
271 );
272
273 let global_path = resolve_global_modules_path();
275 if let Some(path) = global_path {
276 discover_filesystem_modules(&path, ModuleSource::Global, &mut modules);
277 }
278
279 discover_embedded_stdlib(&mut modules);
281
282 discover_rust_builtins(&mut modules);
284
285 modules
286}
287
288pub fn build_index(modules: &[DiscoveredModule]) -> Box<dyn SearchEngine> {
293 #[cfg(feature = "db")]
294 {
295 let mut idx = FTS5Index::new();
296 for m in modules {
297 idx.add_document(
298 &m.module_name,
299 &[
300 ("keywords", &m.metadata.keywords.join(" "), 3.0),
301 ("module_name", &m.module_name, 2.0),
302 ("description", &m.metadata.description, 1.0),
303 ("functions", &m.metadata.auto_functions.join(" "), 1.0),
304 ],
305 );
306 }
307 Box::new(idx)
308 }
309 #[cfg(not(feature = "db"))]
310 {
311 let mut idx = BM25Index::new();
312 for m in modules {
313 idx.add_document(
314 &m.module_name,
315 &[
316 ("keywords", &m.metadata.keywords.join(" "), 3.0),
317 ("module_name", &m.module_name, 2.0),
318 ("description", &m.metadata.description, 1.0),
319 ("functions", &m.metadata.auto_functions.join(" "), 1.0),
320 ],
321 );
322 }
323 Box::new(idx)
324 }
325}
326
327pub fn search_modules(query: &str, limit: usize) -> Vec<SearchResult> {
329 let modules = discover_modules();
330 let index = build_index(&modules);
331 index.search(query, limit)
332}
333
334fn resolve_global_modules_path() -> Option<std::path::PathBuf> {
339 if let Ok(custom) = std::env::var(crate::lua::MODULES_PATH_ENV) {
340 return Some(std::path::PathBuf::from(custom));
341 }
342 if let Ok(home) = std::env::var("HOME") {
343 return Some(std::path::Path::new(&home).join(".assay/modules"));
344 }
345 None
346}
347
348fn discover_filesystem_modules(
352 dir: &std::path::Path,
353 source: ModuleSource,
354 modules: &mut Vec<DiscoveredModule>,
355) {
356 let entries = match std::fs::read_dir(dir) {
357 Ok(entries) => entries,
358 Err(_) => return, };
360
361 for entry in entries.flatten() {
362 let path = entry.path();
363 if path.extension().and_then(|e| e.to_str()) != Some("lua") {
364 continue;
365 }
366
367 let lua_source = match std::fs::read_to_string(&path) {
368 Ok(s) => s,
369 Err(_) => continue,
370 };
371
372 let stem = path
373 .file_stem()
374 .and_then(|s| s.to_str())
375 .unwrap_or_default();
376 let module_name = format!("assay.{stem}");
377 let meta = metadata::parse_metadata(&lua_source);
378
379 modules.push(DiscoveredModule {
380 module_name,
381 source: source.clone(),
382 metadata: meta,
383 lua_source,
384 });
385 }
386}
387
388fn discover_embedded_stdlib(modules: &mut Vec<DiscoveredModule>) {
397 fn walk(dir: &include_dir::Dir<'_>, modules: &mut Vec<DiscoveredModule>) {
398 for file in dir.files() {
399 let path = file.path();
400 if path.extension().and_then(|e| e.to_str()) != Some("lua") {
401 continue;
402 }
403 let Some(lua_source) = file.contents_utf8() else {
404 continue;
405 };
406 let segments: Vec<&str> = path.iter().filter_map(|c| c.to_str()).collect();
409 if segments.is_empty() {
410 continue;
411 }
412 let mut joined = segments.join(".");
413 if joined.ends_with(".lua") {
414 joined.truncate(joined.len() - 4);
415 }
416 let module_name = format!("assay.{joined}");
417 let meta = metadata::parse_metadata(lua_source);
418 modules.push(DiscoveredModule {
419 module_name,
420 source: ModuleSource::BuiltIn,
421 metadata: meta,
422 lua_source: lua_source.to_string(),
423 });
424 }
425 for sub in dir.dirs() {
426 walk(sub, modules);
427 }
428 }
429 walk(&STDLIB_DIR, modules);
430}
431
432fn discover_rust_builtins(modules: &mut Vec<DiscoveredModule>) {
434 for &(name, description, kw, category) in BUILTINS {
435 modules.push(DiscoveredModule {
436 module_name: name.to_string(),
437 source: ModuleSource::BuiltIn,
438 lua_source: String::new(),
439 metadata: ModuleMetadata {
440 module_name: name.to_string(),
441 description: description.to_string(),
442 keywords: kw.iter().map(|k| k.to_string()).collect(),
443 category: Some(category.to_string()),
444 ..Default::default()
445 },
446 });
447 }
448}