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