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