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