Skip to main content

forge_kit/
metadata.rs

1//! High-performance metadata manager for ForgeScript functions, enums, and events.
2//!
3//! This module provides:
4//! - Fast function lookup using a prefix trie
5//! - WASM-compatible by default (no filesystem dependencies)
6//! - Optional caching support for native platforms
7//! - Robust error handling with no panics
8//! - Concurrent access with DashMap
9
10use crate::types::{Event, Function};
11use dashmap::DashMap;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::Arc;
15
16// ============================================================================
17// Core Types
18// ============================================================================
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct EventField {
22    pub name: String,
23    #[serde(default)]
24    pub description: String,
25}
26
27/// Source configuration for fetching metadata
28#[derive(Debug, Clone)]
29pub struct MetadataSource {
30    pub extension: String,
31    pub functions_url: Option<String>,
32    pub enums_url: Option<String>,
33    pub events_url: Option<String>,
34}
35
36impl MetadataSource {
37    /// Create a new metadata source
38    pub fn new(extension: impl Into<String>) -> Self {
39        Self {
40            extension: extension.into(),
41            functions_url: None,
42            enums_url: None,
43            events_url: None,
44        }
45    }
46
47    /// Set functions URL
48    pub fn with_functions(mut self, url: impl Into<String>) -> Self {
49        self.functions_url = Some(url.into());
50        self
51    }
52
53    /// Set enums URL
54    pub fn with_enums(mut self, url: impl Into<String>) -> Self {
55        self.enums_url = Some(url.into());
56        self
57    }
58
59    /// Set events URL
60    pub fn with_events(mut self, url: impl Into<String>) -> Self {
61        self.events_url = Some(url.into());
62        self
63    }
64}
65
66// ============================================================================
67// Error Types
68// ============================================================================
69
70#[derive(Debug, Clone)]
71pub enum MetadataError {
72    NetworkError(String),
73    ParseError(String),
74    NotFound(String),
75    InvalidData(String),
76    CacheError(String),
77}
78
79impl std::fmt::Display for MetadataError {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        match self {
82            Self::NetworkError(e) => write!(f, "Network error: {}", e),
83            Self::ParseError(e) => write!(f, "Parse error: {}", e),
84            Self::NotFound(e) => write!(f, "Not found: {}", e),
85            Self::InvalidData(e) => write!(f, "Invalid data: {}", e),
86            Self::CacheError(e) => write!(f, "Cache error: {}", e),
87        }
88    }
89}
90
91impl std::error::Error for MetadataError {}
92
93pub type Result<T> = std::result::Result<T, MetadataError>;
94
95// ============================================================================
96// Fast Trie for Function Lookup
97// ============================================================================
98
99#[derive(Default)]
100struct TrieNode {
101    children: HashMap<char, Box<TrieNode>>,
102    value: Option<Arc<Function>>,
103}
104
105/// High-performance prefix trie for function lookup
106#[derive(Default)]
107pub struct FunctionTrie {
108    root: TrieNode,
109    count: usize,
110}
111
112impl FunctionTrie {
113    /// Create a new empty trie
114    #[inline]
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Insert a function into the trie
120    pub fn insert(&mut self, key: &str, func: Arc<Function>) {
121        let mut node = &mut self.root;
122
123        for ch in key.to_lowercase().chars() {
124            node = node
125                .children
126                .entry(ch)
127                .or_insert_with(|| Box::new(TrieNode::default()));
128        }
129
130        if node.value.is_none() {
131            self.count += 1;
132        }
133        node.value = Some(func);
134    }
135
136    /// Get exact match (case-insensitive)
137    pub fn get_exact(&self, key: &str) -> Option<Arc<Function>> {
138        let mut node = &self.root;
139
140        for ch in key.to_lowercase().chars() {
141            match node.children.get(&ch) {
142                Some(next) => node = next,
143                None => return None,
144            }
145        }
146
147        node.value.clone()
148    }
149
150    /// Get the longest registered function name that is a prefix of `text`,
151    /// matching strictly from the start of `text`.
152    ///
153    /// For example, if `$ping` is registered:
154    ///   - `get_prefix("$pingmsoko")`     → Some(("$ping", …))
155    ///   - `get_prefix("$pingsmmonwind")` → Some(("$ping", …))
156    ///   - `get_prefix("$send")`          → None  (no registered prefix)
157    ///
158    /// The search always starts at position 0 of `text`; it will never match
159    /// a function name found only in the middle of the string.
160    pub fn get_prefix(&self, text: &str) -> Option<(String, Arc<Function>)> {
161        let mut node = &self.root;
162        let mut last_match: Option<(String, Arc<Function>)> = None;
163        let mut matched = String::with_capacity(text.len());
164
165        for ch in text.to_lowercase().chars() {
166            match node.children.get(&ch) {
167                Some(next) => {
168                    matched.push(ch);
169                    node = next;
170                    if let Some(func) = &node.value {
171                        last_match = Some((matched.clone(), func.clone()));
172                    }
173                }
174                None => break,
175            }
176        }
177
178        last_match
179    }
180
181    /// Get all functions with a given prefix
182    pub fn get_completions(&self, prefix: &str) -> Vec<Arc<Function>> {
183        let mut node = &self.root;
184
185        for ch in prefix.to_lowercase().chars() {
186            match node.children.get(&ch) {
187                Some(next) => node = next,
188                None => return Vec::new(),
189            }
190        }
191
192        let mut results = Vec::new();
193        self.collect_all(node, &mut results);
194        results
195    }
196
197    fn collect_all(&self, node: &TrieNode, results: &mut Vec<Arc<Function>>) {
198        if let Some(func) = &node.value {
199            results.push(func.clone());
200        }
201
202        for child in node.children.values() {
203            self.collect_all(child, results);
204        }
205    }
206
207    /// Get all functions in the trie
208    pub fn all_functions(&self) -> Vec<Arc<Function>> {
209        let mut results = Vec::with_capacity(self.count);
210        self.collect_all(&self.root, &mut results);
211        results
212    }
213
214    /// Number of functions in trie
215    #[inline]
216    pub fn len(&self) -> usize {
217        self.count
218    }
219
220    /// Check if trie is empty
221    #[inline]
222    pub fn is_empty(&self) -> bool {
223        self.count == 0
224    }
225
226    /// Remove an exact match from the trie
227    pub fn remove(&mut self, key: &str) -> bool {
228        let mut node = &mut self.root;
229
230        for ch in key.to_lowercase().chars() {
231            match node.children.get_mut(&ch) {
232                Some(next) => node = next,
233                None => return false,
234            }
235        }
236
237        if node.value.is_some() {
238            node.value = None;
239            self.count -= 1;
240            true
241        } else {
242            false
243        }
244    }
245
246    /// Clear all functions
247    pub fn clear(&mut self) {
248        self.root = TrieNode::default();
249        self.count = 0;
250    }
251}
252
253// ============================================================================
254// HTTP Fetcher
255// ============================================================================
256
257/// HTTP fetcher for metadata
258pub struct Fetcher {
259    client: reqwest::Client,
260}
261
262impl Fetcher {
263    /// Create a new fetcher
264    pub fn new() -> Self {
265        #[cfg(not(target_arch = "wasm32"))]
266        let client = reqwest::Client::builder()
267            .timeout(std::time::Duration::from_secs(30))
268            .build()
269            .unwrap_or_else(|_| reqwest::Client::new());
270
271        #[cfg(target_arch = "wasm32")]
272        let client = reqwest::Client::builder()
273            .build()
274            .unwrap_or_else(|_| reqwest::Client::new());
275
276        Self { client }
277    }
278
279    /// Fetch JSON from a URL with proper error handling
280    pub async fn fetch_json<T: serde::de::DeserializeOwned>(&self, url: &str) -> Result<T> {
281        let response =
282            self.client.get(url).send().await.map_err(|e| {
283                MetadataError::NetworkError(format!("Failed to fetch {}: {}", url, e))
284            })?;
285
286        let status = response.status();
287        if status == reqwest::StatusCode::NOT_FOUND {
288            return Err(MetadataError::NotFound(format!("URL not found: {}", url)));
289        }
290        if !status.is_success() {
291            return Err(MetadataError::NetworkError(format!(
292                "HTTP {}: {}",
293                status, url
294            )));
295        }
296
297        let text = response.text().await.map_err(|e| {
298            MetadataError::NetworkError(format!("Failed to read response from {}: {}", url, e))
299        })?;
300
301        serde_json::from_str(&text).map_err(|e| {
302            let preview: String = text.chars().take(200).collect();
303            MetadataError::ParseError(format!(
304                "Failed to parse JSON from {}: {}\nJSON preview: {}…",
305                url, e, preview
306            ))
307        })
308    }
309
310    /// Fetch functions from URL, parsing each item individually so one bad entry
311    /// doesn't block the rest.
312    pub async fn fetch_functions(&self, url: &str, extension: String) -> Result<Vec<Function>> {
313        let response =
314            self.client.get(url).send().await.map_err(|e| {
315                MetadataError::NetworkError(format!("Failed to fetch {}: {}", url, e))
316            })?;
317
318        let status = response.status();
319        if status == reqwest::StatusCode::NOT_FOUND {
320            return Err(MetadataError::NotFound(format!("URL not found: {}", url)));
321        }
322        if !status.is_success() {
323            return Err(MetadataError::NetworkError(format!(
324                "HTTP {}: {}",
325                status, url
326            )));
327        }
328
329        let text = response.text().await.map_err(|e| {
330            MetadataError::NetworkError(format!("Failed to read response from {}: {}", url, e))
331        })?;
332
333        let raw_items: Vec<serde_json::Value> = serde_json::from_str(&text).map_err(|e| {
334            let preview: String = text.chars().take(200).collect();
335            MetadataError::ParseError(format!(
336                "Failed to parse JSON array from {}: {}\nJSON preview: {}…",
337                url, e, preview
338            ))
339        })?;
340
341        let mut functions = Vec::with_capacity(raw_items.len());
342        for (i, raw) in raw_items.into_iter().enumerate() {
343            match serde_json::from_value::<Function>(raw) {
344                Ok(mut func) => {
345                    func.extension = Some(extension.clone());
346                    func.source_url = Some(url.to_string());
347                    functions.push(func);
348                }
349                Err(e) => {
350                    eprintln!("[forge-kit] Skipping function #{} from {}: {}", i, url, e);
351                }
352            }
353        }
354
355        Ok(functions)
356    }
357
358    /// Fetch enums from URL
359    pub async fn fetch_enums(&self, url: &str) -> Result<HashMap<String, Vec<String>>> {
360        self.fetch_json(url).await
361    }
362
363    /// Fetch events from URL
364    pub async fn fetch_events(&self, url: &str) -> Result<Vec<Event>> {
365        self.fetch_json(url).await
366    }
367}
368
369impl Default for Fetcher {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375// ============================================================================
376// Metadata Manager
377// ============================================================================
378
379/// High-performance metadata manager
380pub struct MetadataManager {
381    trie: std::sync::RwLock<FunctionTrie>,
382    enums: DashMap<String, Vec<String>>,
383    events: DashMap<String, Event>,
384    sources: std::sync::RwLock<Vec<MetadataSource>>,
385    fetcher: Fetcher,
386    custom_function_names: DashMap<String, ()>,
387}
388
389impl MetadataManager {
390    /// Create a new metadata manager
391    pub fn new() -> Self {
392        Self {
393            trie: std::sync::RwLock::new(FunctionTrie::new()),
394            enums: DashMap::new(),
395            events: DashMap::new(),
396            sources: std::sync::RwLock::new(Vec::new()),
397            fetcher: Fetcher::new(),
398            custom_function_names: DashMap::new(),
399        }
400    }
401
402    /// Add a metadata source
403    pub fn add_source(&self, source: MetadataSource) {
404        self.sources.write().unwrap().push(source);
405    }
406
407    /// Fetch all metadata from configured sources
408    pub async fn fetch_all(&self) -> Result<FetchStats> {
409        let sources = self.sources.read().unwrap().clone();
410
411        let mut total_functions = 0;
412        let mut total_enums = 0;
413        let mut total_events = 0;
414        let mut errors = Vec::new();
415
416        for source in sources {
417            if let Some(url) = &source.functions_url {
418                match self
419                    .fetcher
420                    .fetch_functions(url, source.extension.clone())
421                    .await
422                {
423                    Ok(functions) => {
424                        total_functions += functions.len();
425                        self.add_functions(functions);
426                    }
427                    Err(MetadataError::NotFound(_)) => {}
428                    Err(e) => {
429                        errors.push(format!("Functions from {}: {}", source.extension, e));
430                    }
431                }
432            }
433
434            if let Some(url) = &source.enums_url {
435                match self.fetcher.fetch_enums(url).await {
436                    Ok(enums) => {
437                        total_enums += enums.len();
438                        for (name, values) in enums {
439                            self.enums.insert(name, values);
440                        }
441                    }
442                    Err(e) => {
443                        if !matches!(e, MetadataError::NotFound(_)) {
444                            errors.push(format!("Enums from {}: {}", source.extension, e));
445                        }
446                    }
447                }
448            }
449
450            if let Some(url) = &source.events_url {
451                match self.fetcher.fetch_events(url).await {
452                    Ok(events) => {
453                        total_events += events.len();
454                        for event in events {
455                            self.events.insert(event.name.clone(), event);
456                        }
457                    }
458                    Err(e) => {
459                        if !matches!(e, MetadataError::NotFound(_)) {
460                            errors.push(format!("Events from {}: {}", source.extension, e));
461                        }
462                    }
463                }
464            }
465        }
466
467        Ok(FetchStats {
468            functions: total_functions,
469            enums: total_enums,
470            events: total_events,
471            errors,
472        })
473    }
474
475    fn add_functions(&self, functions: Vec<Function>) {
476        let mut trie = self.trie.write().unwrap();
477
478        for func in functions {
479            if self.custom_function_names.contains_key(&func.name) {
480                continue;
481            }
482            let arc_func = Arc::new(func.clone());
483            trie.insert(&func.name, arc_func.clone());
484
485            if let Some(aliases) = &func.aliases {
486                for alias in aliases {
487                    let alias_name = if alias.starts_with('$') {
488                        alias.clone()
489                    } else {
490                        format!("${}", alias)
491                    };
492                    let mut alias_func = (*arc_func).clone();
493                    alias_func.name = alias_name.clone();
494                    trie.insert(&alias_name, Arc::new(alias_func));
495                }
496            }
497        }
498    }
499
500    pub fn add_custom_functions_from_json(&self, json: &str) -> Result<usize> {
501        let raw_items: Vec<serde_json::Value> = serde_json::from_str(json).map_err(|e| {
502            MetadataError::ParseError(format!("Invalid custom-functions JSON: {}", e))
503        })?;
504
505        // Remove previous custom functions first
506        self.remove_custom_functions();
507
508        let mut count = 0;
509        let mut trie = self.trie.write().unwrap();
510
511        for (i, raw) in raw_items.into_iter().enumerate() {
512            match serde_json::from_value::<Function>(raw) {
513                Ok(mut func) => {
514                    // Guarantee $ prefix
515                    if !func.name.starts_with('$') {
516                        func.name = format!("${}", func.name);
517                    }
518
519                    func.category = func.category.or(Some("custom".to_string()));
520
521                    let arc_func = Arc::new(func.clone());
522
523                    // Insert primary function
524                    trie.insert(&func.name, arc_func.clone());
525                    self.custom_function_names.insert(func.name.clone(), ());
526
527                    count += 1;
528
529                    // Register aliases
530                    if let Some(aliases) = &func.aliases {
531                        for alias in aliases {
532                            let alias_name = if alias.starts_with('$') {
533                                alias.clone()
534                            } else {
535                                format!("${}", alias)
536                            };
537
538                            let mut alias_func = (*arc_func).clone();
539                            alias_func.name = alias_name.clone();
540
541                            trie.insert(&alias_name, Arc::new(alias_func));
542                            self.custom_function_names.insert(alias_name.clone(), ());
543                        }
544                    }
545                }
546                Err(e) => {
547                    eprintln!("[forge-kit] Skipping custom function #{}: {}", i, e);
548                }
549            }
550        }
551
552        Ok(count)
553    }
554
555    pub fn remove_custom_functions(&self) {
556        let mut trie = self.trie.write().unwrap();
557
558        for entry in self.custom_function_names.iter() {
559            trie.remove(entry.key());
560        }
561
562        self.custom_function_names.clear();
563    }
564
565    /// Load custom-functions JSON from a file on disk and register every entry.
566    ///
567    /// The file must be an array of `Function` objects — the format produced by
568    /// [`generate_custom_functions_json_to_file`].
569    #[cfg(not(target_arch = "wasm32"))]
570    pub fn add_custom_functions_from_json_file(
571        &self,
572        path: impl AsRef<std::path::Path>,
573    ) -> Result<usize> {
574        let path = path.as_ref();
575        let json = std::fs::read_to_string(path).map_err(|e| {
576            MetadataError::CacheError(format!(
577                "Cannot read custom-functions file {}: {}",
578                path.display(),
579                e
580            ))
581        })?;
582        self.add_custom_functions_from_json(&json)
583    }
584
585    // ========================================================================
586    // Custom Functions: generate JSON from JS/TS source files
587    // ========================================================================
588
589    /// Scan `folder` (and all sub-folders) for `*.js` / `*.ts` files, extract
590    /// every custom function found via regex-based heuristics, and return a
591    /// pretty-printed JSON string of `Function` objects.
592    ///
593    /// **Intended as a one-time build / CLI step.**  Save the result to a file
594    /// and load it at LSP startup with [`add_custom_functions_from_json_file`]
595    /// — no source parsing is needed at runtime.
596    ///
597    /// The output is directly consumable by [`add_custom_functions_from_json`].
598    #[cfg(not(target_arch = "wasm32"))]
599    pub fn generate_custom_functions_json(
600        &self,
601        folder: impl AsRef<std::path::Path>,
602    ) -> Result<String> {
603        let folder = folder.as_ref();
604        if !folder.exists() || !folder.is_dir() {
605            return Err(MetadataError::InvalidData(format!(
606                "generate_custom_functions_json: {} is not a directory",
607                folder.display()
608            )));
609        }
610
611        let mut functions: Vec<Function> = Vec::new();
612        collect_functions_from_folder(folder, &mut functions)?;
613
614        serde_json::to_string_pretty(&functions).map_err(|e| {
615            MetadataError::ParseError(format!("Failed to serialize custom functions: {}", e))
616        })
617    }
618
619    /// Like [`generate_custom_functions_json`] but writes the output directly to
620    /// `output_path`, creating parent directories as needed.
621    ///
622    /// Returns the number of functions written.
623    #[cfg(not(target_arch = "wasm32"))]
624    pub fn generate_custom_functions_json_to_file(
625        &self,
626        folder: impl AsRef<std::path::Path>,
627        output_path: impl AsRef<std::path::Path>,
628    ) -> Result<usize> {
629        let json = self.generate_custom_functions_json(folder)?;
630
631        // Count without a second scan
632        let entries: Vec<serde_json::Value> =
633            serde_json::from_str(&json).map_err(|e| MetadataError::ParseError(e.to_string()))?;
634        let count = entries.len();
635
636        let output_path = output_path.as_ref();
637        if let Some(parent) = output_path.parent() {
638            std::fs::create_dir_all(parent).map_err(|e| {
639                MetadataError::CacheError(format!("Cannot create directories: {}", e))
640            })?;
641        }
642        std::fs::write(output_path, json).map_err(|e| {
643            MetadataError::CacheError(format!("Cannot write to {}: {}", output_path.display(), e))
644        })?;
645
646        Ok(count)
647    }
648
649    // ========================================================================
650    // Standard lookups
651    // ========================================================================
652
653    /// Get function by exact name (case-insensitive)
654    #[inline]
655    pub fn get_exact(&self, name: &str) -> Option<Arc<Function>> {
656        self.trie.read().unwrap().get_exact(name)
657    }
658
659    /// Get the longest registered function name that is a prefix of `text`,
660    /// matching strictly from the start of `text`.
661    #[inline]
662    pub fn get_prefix(&self, text: &str) -> Option<(String, Arc<Function>)> {
663        self.trie.read().unwrap().get_prefix(text)
664    }
665
666    /// Get function: tries exact match first, then prefix match from the start.
667    ///
668    /// Use `get_exact` when you need strict lookup (e.g. bracketed calls).
669    pub fn get(&self, name: &str) -> Option<Arc<Function>> {
670        let trie = self.trie.read().unwrap();
671        if let Some(func) = trie.get_exact(name) {
672            return Some(func);
673        }
674        trie.get_prefix(name).map(|(_, func)| func)
675    }
676
677    /// Get function with match info (matched key + Arc)
678    pub fn get_with_match(&self, name: &str) -> Option<(String, Arc<Function>)> {
679        let trie = self.trie.read().unwrap();
680        if let Some(func) = trie.get_exact(name) {
681            return Some((name.to_string(), func));
682        }
683        trie.get_prefix(name)
684    }
685
686    /// Get multiple functions
687    pub fn get_many(&self, names: &[&str]) -> Vec<Option<Arc<Function>>> {
688        names.iter().map(|name| self.get(name)).collect()
689    }
690
691    /// Get completions for a prefix
692    #[inline]
693    pub fn get_completions(&self, prefix: &str) -> Vec<Arc<Function>> {
694        self.trie.read().unwrap().get_completions(prefix)
695    }
696
697    /// Get all functions
698    #[inline]
699    pub fn all_functions(&self) -> Vec<Arc<Function>> {
700        self.trie.read().unwrap().all_functions()
701    }
702
703    /// Get enum values
704    #[inline]
705    pub fn get_enum(&self, name: &str) -> Option<Vec<String>> {
706        self.enums.get(name).map(|v| v.clone())
707    }
708
709    /// Get all enums
710    pub fn all_enums(&self) -> HashMap<String, Vec<String>> {
711        self.enums
712            .iter()
713            .map(|e| (e.key().clone(), e.value().clone()))
714            .collect()
715    }
716
717    /// Get event by name
718    #[inline]
719    pub fn get_event(&self, name: &str) -> Option<Event> {
720        self.events.get(name).map(|v| v.clone())
721    }
722
723    /// Get all events
724    pub fn all_events(&self) -> Vec<Event> {
725        self.events.iter().map(|e| e.value().clone()).collect()
726    }
727
728    /// Get function count
729    #[inline]
730    pub fn function_count(&self) -> usize {
731        self.trie.read().unwrap().len()
732    }
733
734    /// Get enum count
735    #[inline]
736    pub fn enum_count(&self) -> usize {
737        self.enums.len()
738    }
739
740    /// Get event count
741    #[inline]
742    pub fn event_count(&self) -> usize {
743        self.events.len()
744    }
745
746    /// Clear all metadata
747    pub fn clear(&self) {
748        self.trie.write().unwrap().clear();
749        self.enums.clear();
750        self.events.clear();
751    }
752}
753
754impl Default for MetadataManager {
755    fn default() -> Self {
756        Self::new()
757    }
758}
759
760// ============================================================================
761// JS/TS source parser  (used only by generate_custom_functions_json)
762// ============================================================================
763
764/// Recursively walk `path`, collecting `Function` values from every JS/TS file.
765/// No trie registration happens here — output is for serialization only.
766#[cfg(not(target_arch = "wasm32"))]
767fn collect_functions_from_folder(path: &std::path::Path, out: &mut Vec<Function>) -> Result<()> {
768    let entries = std::fs::read_dir(path).map_err(|e| {
769        MetadataError::InvalidData(format!("Cannot read dir {}: {}", path.display(), e))
770    })?;
771
772    for entry in entries {
773        let entry_path = entry
774            .map_err(|e| MetadataError::InvalidData(e.to_string()))?
775            .path();
776
777        if entry_path.is_dir() {
778            collect_functions_from_folder(&entry_path, out)?;
779        } else if entry_path.is_file() {
780            let is_js_ts = entry_path
781                .extension()
782                .and_then(|e| e.to_str())
783                .map(|e| e == "js" || e == "ts")
784                .unwrap_or(false);
785
786            if is_js_ts {
787                let content = std::fs::read_to_string(&entry_path).map_err(|e| {
788                    MetadataError::InvalidData(format!(
789                        "Cannot read {}: {}",
790                        entry_path.display(),
791                        e
792                    ))
793                })?;
794                out.extend(parse_functions_from_js_ts(
795                    &content,
796                    entry_path.to_str().unwrap_or_default(),
797                ));
798            }
799        }
800    }
801
802    Ok(())
803}
804
805/// Extract `Function` metadata from a single JS/TS source file using regex
806/// heuristics.  Mirrors the logic from `parse_custom_functions_from_js` in the
807/// older metadata implementation but produces `Function` values directly so they
808/// can be round-tripped through JSON without a lossy intermediate type.
809#[cfg(not(target_arch = "wasm32"))]
810fn parse_functions_from_js_ts(content: &str, file_path: &str) -> Vec<Function> {
811    use regex::Regex;
812    use serde_json::Value as JsonValue;
813
814    // ── Regexes ──────────────────────────────────────────────────────────────
815    let name_re = Regex::new(r#"name:\s*['"]([^'"]+)['"]"#).expect("regex");
816    let params_re = Regex::new(r#"(?:params|args):\s*\["#).expect("regex");
817    let desc_re = Regex::new(
818        r#"(?s)description:\s*(?:'((?:[^'\\]|\\.)*?)'|"((?:[^"\\]|\\.)*?)"|`((?:[^`\\]|\\.)*?)`)"#,
819    )
820    .expect("regex");
821    let brackets_re = Regex::new(r"brackets:\s*(true|false)").expect("regex");
822    let p_name_re = Regex::new(r#"name:\s*['"]([^'"]+)['"]"#).expect("regex");
823    let required_re = Regex::new(r"(?i)required:\s*(true|false)").expect("regex");
824    let rest_re = Regex::new(r"(?i)rest:\s*(true|false)").expect("regex");
825    let type_re = Regex::new(r"type:\s*([^,}\n\s]+)").expect("regex");
826    let output_re = Regex::new(r"output:\s*([^,}\n\s]+)").expect("regex");
827
828    // ── Collect all name: positions with line numbers ─────────────────────────
829    let name_matches: Vec<(usize, usize, String, u32)> = name_re
830        .captures_iter(content)
831        .map(|c: regex::Captures| {
832            let m = c.get(0).unwrap();
833            let start = m.start();
834            let line = content[..start].chars().filter(|&c| c == '\n').count() as u32;
835            (start, m.end(), c[1].to_string(), line)
836        })
837        .collect();
838
839    // ── Collect params/args array ranges ─────────────────────────────────────
840    let mut params_ranges: Vec<std::ops::Range<usize>> = Vec::new();
841    for m in params_re.find_iter(content) {
842        let start = m.start();
843        let mut depth = 0i32;
844        for (i, c) in content[start..].char_indices() {
845            if c == '[' {
846                depth += 1;
847            } else if c == ']' {
848                depth -= 1;
849                if depth == 0 {
850                    params_ranges.push(start..start + i);
851                    break;
852                }
853            }
854        }
855    }
856
857    // ── Filter: keep only top-level function name: declarations ──────────────
858    let func_names: Vec<_> = name_matches
859        .into_iter()
860        .filter(|m| !params_ranges.iter().any(|r| r.contains(&m.0)))
861        .collect();
862
863    // ── Build Function values ─────────────────────────────────────────────────
864    let mut functions = Vec::new();
865
866    for i in 0..func_names.len() {
867        let (_, end_pos, raw_name, line) = &func_names[i];
868        let chunk_end = if i + 1 < func_names.len() {
869            func_names[i + 1].0
870        } else {
871            content.len()
872        };
873        let chunk = &content[*end_pos..chunk_end];
874
875        // Ensure $ prefix
876        let name = if raw_name.starts_with('$') {
877            raw_name.clone()
878        } else {
879            format!("${}", raw_name)
880        };
881
882        let description = desc_re
883            .captures(chunk)
884            .and_then(|c: regex::Captures| c.get(1).or(c.get(2)).or(c.get(3)))
885            .map(|m: regex::Match| m.as_str().to_string())
886            .unwrap_or_else(|| "Custom function".to_string());
887
888        let brackets = brackets_re
889            .captures(chunk)
890            .map(|c: regex::Captures| &c[1] == "true");
891
892        let output: Option<Vec<String>> = output_re.captures(chunk).map(|c: regex::Captures| {
893            c[1].split(',')
894                .map(|s: &str| {
895                    s.trim()
896                        .trim_matches(|c: char| c == '\'' || c == '"')
897                        .to_string()
898                })
899                .filter(|s: &String| !s.is_empty())
900                .collect()
901        });
902
903        // Parse args from the params block that belongs to this function chunk
904        let args: Option<Vec<crate::types::Arg>> = params_ranges
905            .iter()
906            .find(|r| r.start >= *end_pos && r.start < chunk_end)
907            .and_then(|p_range| {
908                let p_content = &content[p_range.clone()];
909                let mut parsed_args: Vec<crate::types::Arg> = Vec::new();
910                let mut search = 0;
911
912                while let Some(bstart) = p_content[search..].find('{') {
913                    let abs = search + bstart;
914                    let mut depth = 0i32;
915                    for (j, c) in p_content[abs..].char_indices() {
916                        if c == '{' {
917                            depth += 1;
918                        } else if c == '}' {
919                            depth -= 1;
920                            if depth == 0 {
921                                let body = &p_content[abs + 1..abs + j];
922                                if let Some(n_cap) = p_name_re.captures(body) {
923                                    let raw_type = type_re
924                                        .captures(body)
925                                        .map(|c: regex::Captures| {
926                                            let t = c[1]
927                                                .trim()
928                                                .trim_matches(|c: char| c == '\'' || c == '"');
929                                            t.strip_prefix("ArgType.").unwrap_or(t).to_string()
930                                        })
931                                        .unwrap_or_else(|| "String".to_string());
932
933                                    parsed_args.push(crate::types::Arg {
934                                        name: n_cap[1].to_string(),
935                                        description: desc_re
936                                            .captures(body)
937                                            .and_then(|c: regex::Captures| {
938                                                c.get(1).or(c.get(2)).or(c.get(3))
939                                            })
940                                            .map(|m: regex::Match| m.as_str().to_string())
941                                            .unwrap_or_default(),
942                                        rest: rest_re
943                                            .captures(body)
944                                            .map(|c: regex::Captures| &c[1] == "true")
945                                            .unwrap_or(false),
946                                        required: required_re
947                                            .captures(body)
948                                            .map(|c: regex::Captures| &c[1] == "true"),
949                                        arg_type: JsonValue::String(raw_type),
950                                        ..Default::default()
951                                    });
952                                }
953                                search = abs + j + 1;
954                                break;
955                            }
956                        }
957                    }
958                }
959
960                if parsed_args.is_empty() {
961                    None
962                } else {
963                    Some(parsed_args)
964                }
965            });
966
967        functions.push(Function {
968            name,
969            version: Some(JsonValue::String("1.0.0".to_string())),
970            description,
971            brackets: brackets.or(if args.is_some() { Some(true) } else { None }),
972            unwrap: false,
973            args,
974            output,
975            category: Some("custom".to_string()),
976            local_path: Some(std::path::PathBuf::from(file_path)),
977            line: Some(*line),
978            ..Default::default()
979        });
980    }
981
982    functions
983}
984
985// ============================================================================
986// FetchStats
987// ============================================================================
988
989/// Statistics from a fetch operation
990#[derive(Debug, Clone)]
991pub struct FetchStats {
992    pub functions: usize,
993    pub enums: usize,
994    pub events: usize,
995    pub errors: Vec<String>,
996}
997
998impl std::fmt::Display for FetchStats {
999    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1000        write!(
1001            f,
1002            "Fetched {} functions, {} enums, {} events",
1003            self.functions, self.enums, self.events
1004        )?;
1005        if !self.errors.is_empty() {
1006            write!(f, " ({} errors)", self.errors.len())?;
1007        }
1008        Ok(())
1009    }
1010}
1011
1012// ============================================================================
1013// Caching Support
1014// ============================================================================
1015
1016/// Serializable cache format
1017#[derive(Debug, Serialize, Deserialize)]
1018pub struct MetadataCache {
1019    pub functions: Vec<Function>,
1020    pub enums: HashMap<String, Vec<String>>,
1021    pub events: Vec<Event>,
1022    pub version: u32,
1023}
1024
1025impl MetadataCache {
1026    const VERSION: u32 = 1;
1027
1028    pub fn new(
1029        functions: Vec<Function>,
1030        enums: HashMap<String, Vec<String>>,
1031        events: Vec<Event>,
1032    ) -> Self {
1033        Self {
1034            functions,
1035            enums,
1036            events,
1037            version: Self::VERSION,
1038        }
1039    }
1040}
1041
1042impl MetadataManager {
1043    pub fn export_cache(&self) -> MetadataCache {
1044        let functions = self
1045            .all_functions()
1046            .into_iter()
1047            .filter(|f| !self.custom_function_names.contains_key(&f.name))
1048            .map(|f| (*f).clone())
1049            .collect();
1050
1051        MetadataCache::new(functions, self.all_enums(), self.all_events())
1052    }
1053
1054    pub fn import_cache(&self, cache: MetadataCache) -> Result<()> {
1055        if cache.version != MetadataCache::VERSION {
1056            return Err(MetadataError::CacheError(format!(
1057                "Incompatible cache version: expected {}, got {}",
1058                MetadataCache::VERSION,
1059                cache.version
1060            )));
1061        }
1062        self.clear();
1063        self.add_functions(cache.functions);
1064        for (name, values) in cache.enums {
1065            self.enums.insert(name, values);
1066        }
1067        for event in cache.events {
1068            self.events.insert(event.name.clone(), event);
1069        }
1070        Ok(())
1071    }
1072
1073    pub fn cache_to_json(&self) -> Result<String> {
1074        serde_json::to_string(&self.export_cache())
1075            .map_err(|e| MetadataError::CacheError(format!("Serialization failed: {}", e)))
1076    }
1077
1078    pub fn cache_from_json(&self, json: &str) -> Result<()> {
1079        let cache: MetadataCache = serde_json::from_str(json)
1080            .map_err(|e| MetadataError::CacheError(format!("Deserialization failed: {}", e)))?;
1081        self.import_cache(cache)
1082    }
1083}
1084
1085#[cfg(not(target_arch = "wasm32"))]
1086impl MetadataManager {
1087    pub fn save_cache_to_file(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
1088        use std::io::Write;
1089        let json = self.cache_to_json()?;
1090        let mut file = std::fs::File::create(path)
1091            .map_err(|e| MetadataError::CacheError(format!("Failed to create file: {}", e)))?;
1092        file.write_all(json.as_bytes())
1093            .map_err(|e| MetadataError::CacheError(format!("Failed to write file: {}", e)))?;
1094        Ok(())
1095    }
1096
1097    pub fn load_cache_from_file(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
1098        let json = std::fs::read_to_string(path)
1099            .map_err(|e| MetadataError::CacheError(format!("Failed to read file: {}", e)))?;
1100        self.cache_from_json(&json)
1101    }
1102}
1103
1104// ============================================================================
1105// Utility Functions
1106// ============================================================================
1107
1108/// Create a metadata source from a GitHub repository
1109pub fn github_source(extension: impl Into<String>, repo: &str, branch: &str) -> MetadataSource {
1110    let base = format!("https://raw.githubusercontent.com/{}/{}/", repo, branch);
1111    MetadataSource::new(extension)
1112        .with_functions(format!("{}functions.json", base))
1113        .with_enums(format!("{}enums.json", base))
1114        .with_events(format!("{}events.json", base))
1115}
1116
1117/// Create a metadata source from custom URLs
1118pub fn custom_source(extension: impl Into<String>) -> MetadataSource {
1119    MetadataSource::new(extension)
1120}