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        // Normalize to lowercase for case-insensitive lookup
124        for ch in key.to_lowercase().chars() {
125            node = node
126                .children
127                .entry(ch)
128                .or_insert_with(|| Box::new(TrieNode::default()));
129        }
130
131        if node.value.is_none() {
132            self.count += 1;
133        }
134        node.value = Some(func);
135    }
136
137    /// Get exact match (case-insensitive)
138    pub fn get_exact(&self, key: &str) -> Option<Arc<Function>> {
139        let mut node = &self.root;
140
141        for ch in key.to_lowercase().chars() {
142            match node.children.get(&ch) {
143                Some(next) => node = next,
144                None => return None,
145            }
146        }
147
148        node.value.clone()
149    }
150
151    /// Get longest prefix match
152    pub fn get_prefix(&self, text: &str) -> Option<(String, Arc<Function>)> {
153        let chars: Vec<char> = text.to_lowercase().chars().collect();
154        let mut best_match: Option<(String, Arc<Function>)> = None;
155
156        for start in 0..chars.len() {
157            let mut node = &self.root;
158            let mut matched = String::with_capacity(chars.len() - start);
159
160            for &ch in &chars[start..] {
161                match node.children.get(&ch) {
162                    Some(next) => {
163                        matched.push(ch);
164                        node = next;
165
166                        if let Some(func) = &node.value {
167                            best_match = Some((matched.clone(), func.clone()));
168                        }
169                    }
170                    None => break,
171                }
172            }
173        }
174
175        best_match
176    }
177
178    /// Get all functions with a given prefix
179    pub fn get_completions(&self, prefix: &str) -> Vec<Arc<Function>> {
180        let mut node = &self.root;
181
182        // Navigate to prefix
183        for ch in prefix.to_lowercase().chars() {
184            match node.children.get(&ch) {
185                Some(next) => node = next,
186                None => return Vec::new(),
187            }
188        }
189
190        // Collect all functions under this prefix
191        let mut results = Vec::new();
192        self.collect_all(node, &mut results);
193        results
194    }
195
196    fn collect_all(&self, node: &TrieNode, results: &mut Vec<Arc<Function>>) {
197        if let Some(func) = &node.value {
198            results.push(func.clone());
199        }
200
201        for child in node.children.values() {
202            self.collect_all(child, results);
203        }
204    }
205
206    /// Get all functions in the trie
207    pub fn all_functions(&self) -> Vec<Arc<Function>> {
208        let mut results = Vec::with_capacity(self.count);
209        self.collect_all(&self.root, &mut results);
210        results
211    }
212
213    /// Number of functions in trie
214    #[inline]
215    pub fn len(&self) -> usize {
216        self.count
217    }
218
219    /// Check if trie is empty
220    #[inline]
221    pub fn is_empty(&self) -> bool {
222        self.count == 0
223    }
224
225    /// Clear all functions
226    pub fn clear(&mut self) {
227        self.root = TrieNode::default();
228        self.count = 0;
229    }
230}
231
232// ============================================================================
233// HTTP Fetcher
234// ============================================================================
235
236/// HTTP fetcher for metadata
237pub struct Fetcher {
238    client: reqwest::Client,
239}
240
241impl Fetcher {
242    /// Create a new fetcher
243    pub fn new() -> Self {
244        #[cfg(not(target_arch = "wasm32"))]
245        let client = reqwest::Client::builder()
246            .timeout(std::time::Duration::from_secs(30))
247            .build()
248            .unwrap_or_else(|_| reqwest::Client::new());
249
250        #[cfg(target_arch = "wasm32")]
251        let client = reqwest::Client::builder()
252            .build()
253            .unwrap_or_else(|_| reqwest::Client::new());
254
255        Self { client }
256    }
257
258    /// Fetch JSON from a URL with proper error handling
259    pub async fn fetch_json<T: serde::de::DeserializeOwned>(&self, url: &str) -> Result<T> {
260        // Make request
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        // Check status
267        let status = response.status();
268        if status == reqwest::StatusCode::NOT_FOUND {
269            return Err(MetadataError::NotFound(format!("URL not found: {}", url)));
270        }
271
272        if !status.is_success() {
273            return Err(MetadataError::NetworkError(format!(
274                "HTTP {}: {}",
275                status, url
276            )));
277        }
278
279        // Parse JSON
280        let text = response.text().await.map_err(|e| {
281            MetadataError::NetworkError(format!("Failed to read response from {}: {}", url, e))
282        })?;
283
284        serde_json::from_str(&text).map_err(|e| {
285            // Include a preview of the raw JSON to help debug which field is malformed
286            let preview: String = text.chars().take(200).collect();
287            MetadataError::ParseError(format!(
288                "Failed to parse JSON from {}: {}\nJSON preview: {}…",
289                url, e, preview
290            ))
291        })
292    }
293
294    /// Fetch functions from URL, parsing each item individually so one bad entry
295    /// doesn't block the rest.
296    pub async fn fetch_functions(&self, url: &str, extension: String) -> Result<Vec<Function>> {
297        let response =
298            self.client.get(url).send().await.map_err(|e| {
299                MetadataError::NetworkError(format!("Failed to fetch {}: {}", url, e))
300            })?;
301
302        let status = response.status();
303        if status == reqwest::StatusCode::NOT_FOUND {
304            return Err(MetadataError::NotFound(format!("URL not found: {}", url)));
305        }
306        if !status.is_success() {
307            return Err(MetadataError::NetworkError(format!(
308                "HTTP {}: {}",
309                status, url
310            )));
311        }
312
313        let text = response.text().await.map_err(|e| {
314            MetadataError::NetworkError(format!("Failed to read response from {}: {}", url, e))
315        })?;
316
317        // Parse the outer array as raw values first
318        let raw_items: Vec<serde_json::Value> = serde_json::from_str(&text).map_err(|e| {
319            let preview: String = text.chars().take(200).collect();
320            MetadataError::ParseError(format!(
321                "Failed to parse JSON array from {}: {}\nJSON preview: {}…",
322                url, e, preview
323            ))
324        })?;
325
326        let mut functions = Vec::with_capacity(raw_items.len());
327        for (i, raw) in raw_items.into_iter().enumerate() {
328            match serde_json::from_value::<Function>(raw) {
329                Ok(mut func) => {
330                    func.extension = Some(extension.clone());
331                    func.source_url = Some(url.to_string());
332                    functions.push(func);
333                }
334                Err(e) => {
335                    // Log and skip the bad entry — don't abort the whole file
336                    eprintln!("[forge-kit] Skipping function #{} from {}: {}", i, url, e);
337                }
338            }
339        }
340
341        Ok(functions)
342    }
343
344    /// Fetch enums from URL
345    pub async fn fetch_enums(&self, url: &str) -> Result<HashMap<String, Vec<String>>> {
346        self.fetch_json(url).await
347    }
348
349    /// Fetch events from URL
350    pub async fn fetch_events(&self, url: &str) -> Result<Vec<Event>> {
351        self.fetch_json(url).await
352    }
353}
354
355impl Default for Fetcher {
356    fn default() -> Self {
357        Self::new()
358    }
359}
360
361// ============================================================================
362// Metadata Manager
363// ============================================================================
364
365/// High-performance metadata manager
366pub struct MetadataManager {
367    trie: std::sync::RwLock<FunctionTrie>,
368    enums: DashMap<String, Vec<String>>,
369    events: DashMap<String, Event>,
370    sources: std::sync::RwLock<Vec<MetadataSource>>,
371    fetcher: Fetcher,
372}
373
374impl MetadataManager {
375    /// Create a new metadata manager
376    pub fn new() -> Self {
377        Self {
378            trie: std::sync::RwLock::new(FunctionTrie::new()),
379            enums: DashMap::new(),
380            events: DashMap::new(),
381            sources: std::sync::RwLock::new(Vec::new()),
382            fetcher: Fetcher::new(),
383        }
384    }
385
386    /// Add a metadata source
387    pub fn add_source(&self, source: MetadataSource) {
388        self.sources.write().unwrap().push(source);
389    }
390
391    /// Fetch all metadata from configured sources
392    pub async fn fetch_all(&self) -> Result<FetchStats> {
393        let sources = self.sources.read().unwrap().clone();
394
395        let mut total_functions = 0;
396        let mut total_enums = 0;
397        let mut total_events = 0;
398        let mut errors = Vec::new();
399
400        for source in sources {
401            // Fetch functions — don't abort on error; continue to enums/events
402            if let Some(url) = &source.functions_url {
403                match self
404                    .fetcher
405                    .fetch_functions(url, source.extension.clone())
406                    .await
407                {
408                    Ok(functions) => {
409                        total_functions += functions.len();
410                        self.add_functions(functions);
411                    }
412                    Err(MetadataError::NotFound(_)) => {
413                        // 404 is fine — optional
414                    }
415                    Err(e) => {
416                        errors.push(format!("Functions from {}: {}", source.extension, e));
417                    }
418                }
419            }
420
421            // Fetch enums — always continue regardless of functions result
422            if let Some(url) = &source.enums_url {
423                match self.fetcher.fetch_enums(url).await {
424                    Ok(enums) => {
425                        total_enums += enums.len();
426                        for (name, values) in enums {
427                            self.enums.insert(name, values);
428                        }
429                    }
430                    Err(e) => {
431                        if !matches!(e, MetadataError::NotFound(_)) {
432                            errors.push(format!("Enums from {}: {}", source.extension, e));
433                        }
434                    }
435                }
436            }
437
438            // Fetch events — always continue regardless of functions/enums result
439            if let Some(url) = &source.events_url {
440                match self.fetcher.fetch_events(url).await {
441                    Ok(events) => {
442                        total_events += events.len();
443                        for event in events {
444                            self.events.insert(event.name.clone(), event);
445                        }
446                    }
447                    Err(e) => {
448                        if !matches!(e, MetadataError::NotFound(_)) {
449                            errors.push(format!("Events from {}: {}", source.extension, e));
450                        }
451                    }
452                }
453            }
454        }
455
456        Ok(FetchStats {
457            functions: total_functions,
458            enums: total_enums,
459            events: total_events,
460            errors,
461        })
462    }
463
464    /// Add functions to the manager
465    fn add_functions(&self, functions: Vec<Function>) {
466        let mut trie = self.trie.write().unwrap();
467
468        for func in functions {
469            let arc_func = Arc::new(func.clone());
470
471            // Insert main name
472            trie.insert(&func.name, arc_func.clone());
473
474            // Insert aliases
475            if let Some(aliases) = &func.aliases {
476                for alias in aliases {
477                    let alias_name = if alias.starts_with('$') {
478                        alias.clone()
479                    } else {
480                        format!("${}", alias)
481                    };
482
483                    // Create alias function
484                    let mut alias_func = (*arc_func).clone();
485                    alias_func.name = alias_name.clone();
486                    trie.insert(&alias_name, Arc::new(alias_func));
487                }
488            }
489        }
490    }
491
492    /// Get function by exact name (case-insensitive)
493    #[inline]
494    pub fn get_exact(&self, name: &str) -> Option<Arc<Function>> {
495        self.trie.read().unwrap().get_exact(name)
496    }
497
498    /// Get function by prefix match
499    #[inline]
500    pub fn get_prefix(&self, text: &str) -> Option<(String, Arc<Function>)> {
501        self.trie.read().unwrap().get_prefix(text)
502    }
503
504    /// Get function (tries exact first, then prefix)
505    pub fn get(&self, name: &str) -> Option<Arc<Function>> {
506        let trie = self.trie.read().unwrap();
507
508        // Try exact match first
509        if let Some(func) = trie.get_exact(name) {
510            return Some(func);
511        }
512
513        // Try prefix match
514        trie.get_prefix(name).map(|(_, func)| func)
515    }
516
517    /// Get function with match info (for compatibility)
518    pub fn get_with_match(&self, name: &str) -> Option<(String, Arc<Function>)> {
519        let trie = self.trie.read().unwrap();
520
521        // Try exact match first
522        if let Some(func) = trie.get_exact(name) {
523            return Some((name.to_string(), func));
524        }
525
526        // Try prefix match
527        trie.get_prefix(name)
528    }
529
530    /// Get multiple functions
531    pub fn get_many(&self, names: &[&str]) -> Vec<Option<Arc<Function>>> {
532        names.iter().map(|name| self.get(name)).collect()
533    }
534
535    /// Get completions for a prefix
536    #[inline]
537    pub fn get_completions(&self, prefix: &str) -> Vec<Arc<Function>> {
538        self.trie.read().unwrap().get_completions(prefix)
539    }
540
541    /// Get all functions
542    #[inline]
543    pub fn all_functions(&self) -> Vec<Arc<Function>> {
544        self.trie.read().unwrap().all_functions()
545    }
546
547    /// Get enum values
548    #[inline]
549    pub fn get_enum(&self, name: &str) -> Option<Vec<String>> {
550        self.enums.get(name).map(|v| v.clone())
551    }
552
553    /// Get all enums
554    pub fn all_enums(&self) -> HashMap<String, Vec<String>> {
555        self.enums
556            .iter()
557            .map(|e| (e.key().clone(), e.value().clone()))
558            .collect()
559    }
560
561    /// Get event by name
562    #[inline]
563    pub fn get_event(&self, name: &str) -> Option<Event> {
564        self.events.get(name).map(|v| v.clone())
565    }
566
567    /// Get all events
568    pub fn all_events(&self) -> Vec<Event> {
569        self.events.iter().map(|e| e.value().clone()).collect()
570    }
571
572    /// Get function count
573    #[inline]
574    pub fn function_count(&self) -> usize {
575        self.trie.read().unwrap().len()
576    }
577
578    /// Get enum count
579    #[inline]
580    pub fn enum_count(&self) -> usize {
581        self.enums.len()
582    }
583
584    /// Get event count
585    #[inline]
586    pub fn event_count(&self) -> usize {
587        self.events.len()
588    }
589
590    /// Clear all metadata
591    pub fn clear(&self) {
592        self.trie.write().unwrap().clear();
593        self.enums.clear();
594        self.events.clear();
595    }
596}
597
598impl Default for MetadataManager {
599    fn default() -> Self {
600        Self::new()
601    }
602}
603
604/// Statistics from a fetch operation
605#[derive(Debug, Clone)]
606pub struct FetchStats {
607    pub functions: usize,
608    pub enums: usize,
609    pub events: usize,
610    pub errors: Vec<String>,
611}
612
613impl std::fmt::Display for FetchStats {
614    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615        write!(
616            f,
617            "Fetched {} functions, {} enums, {} events",
618            self.functions, self.enums, self.events
619        )?;
620
621        if !self.errors.is_empty() {
622            write!(f, " ({} errors)", self.errors.len())?;
623        }
624
625        Ok(())
626    }
627}
628
629// ============================================================================
630// Caching Support (Optional)
631// ============================================================================
632
633/// Serializable cache format
634#[derive(Debug, Serialize, Deserialize)]
635pub struct MetadataCache {
636    pub functions: Vec<Function>,
637    pub enums: HashMap<String, Vec<String>>,
638    pub events: Vec<Event>,
639    pub version: u32,
640}
641
642impl MetadataCache {
643    const VERSION: u32 = 1;
644
645    /// Create a new cache
646    pub fn new(
647        functions: Vec<Function>,
648        enums: HashMap<String, Vec<String>>,
649        events: Vec<Event>,
650    ) -> Self {
651        Self {
652            functions,
653            enums,
654            events,
655            version: Self::VERSION,
656        }
657    }
658}
659
660impl MetadataManager {
661    /// Export metadata to cache
662    pub fn export_cache(&self) -> MetadataCache {
663        MetadataCache::new(
664            self.all_functions().iter().map(|f| (**f).clone()).collect(),
665            self.all_enums(),
666            self.all_events(),
667        )
668    }
669
670    /// Import metadata from cache
671    pub fn import_cache(&self, cache: MetadataCache) -> Result<()> {
672        if cache.version != MetadataCache::VERSION {
673            return Err(MetadataError::CacheError(format!(
674                "Incompatible cache version: expected {}, got {}",
675                MetadataCache::VERSION,
676                cache.version
677            )));
678        }
679
680        // Clear existing data
681        self.clear();
682
683        // Add functions
684        self.add_functions(cache.functions);
685
686        // Add enums
687        for (name, values) in cache.enums {
688            self.enums.insert(name, values);
689        }
690
691        // Add events
692        for event in cache.events {
693            self.events.insert(event.name.clone(), event);
694        }
695
696        Ok(())
697    }
698
699    /// Serialize cache to JSON
700    pub fn cache_to_json(&self) -> Result<String> {
701        let cache = self.export_cache();
702        serde_json::to_string(&cache)
703            .map_err(|e| MetadataError::CacheError(format!("Serialization failed: {}", e)))
704    }
705
706    /// Deserialize cache from JSON
707    pub fn cache_from_json(&self, json: &str) -> Result<()> {
708        let cache: MetadataCache = serde_json::from_str(json)
709            .map_err(|e| MetadataError::CacheError(format!("Deserialization failed: {}", e)))?;
710        self.import_cache(cache)
711    }
712}
713
714// ============================================================================
715// Native Filesystem Caching
716// ============================================================================
717
718#[cfg(not(target_arch = "wasm32"))]
719impl MetadataManager {
720    /// Save cache to file
721    pub fn save_cache_to_file(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
722        use std::io::Write;
723
724        let json = self.cache_to_json()?;
725        let mut file = std::fs::File::create(path)
726            .map_err(|e| MetadataError::CacheError(format!("Failed to create file: {}", e)))?;
727
728        file.write_all(json.as_bytes())
729            .map_err(|e| MetadataError::CacheError(format!("Failed to write file: {}", e)))?;
730
731        Ok(())
732    }
733
734    /// Load cache from file
735    pub fn load_cache_from_file(&self, path: impl AsRef<std::path::Path>) -> Result<()> {
736        let json = std::fs::read_to_string(path)
737            .map_err(|e| MetadataError::CacheError(format!("Failed to read file: {}", e)))?;
738
739        self.cache_from_json(&json)
740    }
741}
742
743// ============================================================================
744// Utility Functions
745// ============================================================================
746
747/// Create a metadata source from a GitHub repository
748pub fn github_source(extension: impl Into<String>, repo: &str, branch: &str) -> MetadataSource {
749    let base = format!("https://raw.githubusercontent.com/{}/{}/", repo, branch);
750
751    MetadataSource::new(extension)
752        .with_functions(format!("{}functions.json", base))
753        .with_enums(format!("{}enums.json", base))
754        .with_events(format!("{}events.json", base))
755}
756
757/// Create a metadata source from custom URLs
758pub fn custom_source(extension: impl Into<String>) -> MetadataSource {
759    MetadataSource::new(extension)
760}