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