/*! * BoxIndex — minimal view over aliases and plugin box types * * Purpose: allow using/namespace resolver to make decisions that depend * on plugin-visible type names (e.g., enforcing strict prefix rules) and * to surface aliases defined in nyash.toml/env. */ use once_cell::sync::Lazy; use std::collections::{HashMap, HashSet}; use std::sync::Mutex; use std::sync::RwLock; use std::time::SystemTime; #[derive(Clone, Default)] pub struct BoxIndex { #[allow(dead_code)] pub aliases: HashMap, pub plugin_boxes: HashSet, #[allow(dead_code)] pub plugin_meta: HashMap, pub plugin_meta_by_box: HashMap, pub plugins_require_prefix_global: bool, } impl BoxIndex { pub fn build_current() -> Self { // aliases from nyash.toml and env let mut aliases: HashMap = HashMap::new(); if let Ok(text) = std::fs::read_to_string("nyash.toml") { if let Ok(doc) = toml::from_str::(&text) { if let Some(alias_tbl) = doc.get("aliases").and_then(|v| v.as_table()) { for (k, v) in alias_tbl.iter() { if let Some(target) = v.as_str() { aliases.insert(k.to_string(), target.to_string()); } } } } } if let Ok(raw) = std::env::var("NYASH_ALIASES") { for ent in raw.split(',') { if let Some((k, v)) = ent.split_once('=') { let k = k.trim(); let v = v.trim(); if !k.is_empty() && !v.is_empty() { aliases.insert(k.to_string(), v.to_string()); } } } } // plugin box types (best-effort; may be empty if host not initialized yet) let mut plugin_boxes: HashSet = HashSet::new(); let mut plugin_meta: HashMap = HashMap::new(); let mut plugin_meta_by_box: HashMap = HashMap::new(); let mut plugins_require_prefix_global = false; // Read per-plugin meta and global flags from nyash.toml when available if let Ok(text) = std::fs::read_to_string("nyash.toml") { if let Ok(doc) = toml::from_str::(&text) { if let Some(plugins_tbl) = doc.get("plugins").and_then(|v| v.as_table()) { // Global switch: [plugins].require_prefix = true if let Some(v) = plugins_tbl.get("require_prefix").and_then(|v| v.as_bool()) { plugins_require_prefix_global = v; } for (k, v) in plugins_tbl.iter() { // Skip non-table entries (string entries are plugin roots) if let Some(t) = v.as_table() { let prefix = t .get("prefix") .and_then(|x| x.as_str()) .map(|s| s.to_string()); let require_prefix = t .get("require_prefix") .and_then(|x| x.as_bool()) .unwrap_or(false); let expose_short_names = t .get("expose_short_names") .and_then(|x| x.as_bool()) .unwrap_or(true); let meta = PluginMeta { prefix, require_prefix, expose_short_names, }; plugin_meta.insert(k.clone(), meta.clone()); if let Some(arr) = t.get("boxes").and_then(|x| x.as_array()) { for b in arr { if let Some(name) = b.as_str() { plugin_meta_by_box.insert(name.to_string(), meta.clone()); } } } } } } } } let host = crate::runtime::get_global_plugin_host(); if let Ok(h) = host.read() { if let Some(cfg) = h.config_ref() { for (lib, def) in &cfg.libraries { for bt in &def.boxes { plugin_boxes.insert(bt.clone()); if let Some(meta) = plugin_meta.get(lib) { plugin_meta_by_box.insert(bt.clone(), meta.clone()); } } } } } Self { aliases, plugin_boxes, plugin_meta, plugin_meta_by_box, plugins_require_prefix_global, } } pub fn is_known_plugin_short(name: &str) -> bool { // Prefer global index view if GLOBAL .read() .ok() .map(|g| g.plugin_boxes.contains(name)) .unwrap_or(false) { return true; } // Env override list if let Ok(raw) = std::env::var("NYASH_KNOWN_PLUGIN_SHORTNAMES") { let set: HashSet = raw.split(',').map(|s| s.trim().to_string()).collect(); if set.contains(name) { return true; } } // Minimal fallback set const KNOWN: &[&str] = &[ "ArrayBox", "MapBox", "StringBox", "ConsoleBox", "FileBox", "PathBox", "MathBox", "IntegerBox", "TOMLBox", ]; KNOWN.iter().any(|k| *k == name) } } // Global BoxIndex view (rebuilt on-demand) static GLOBAL: Lazy> = Lazy::new(|| RwLock::new(BoxIndex::default())); // Global resolve cache (keyed by tgt|base|strict|paths) static RESOLVE_CACHE: Lazy>> = Lazy::new(|| RwLock::new(HashMap::new())); // Track env/file state to invalidate index and cache when changed #[derive(Clone, Default)] struct IndexState { aliases_env: Option, toml_mtime: Option, toml_size: Option, } static LAST_STATE: Lazy> = Lazy::new(|| Mutex::new(IndexState::default())); pub fn refresh_box_index() { let next = BoxIndex::build_current(); if let Ok(mut w) = GLOBAL.write() { *w = next; } } pub fn get_box_index() -> BoxIndex { GLOBAL.read().ok().map(|g| g.clone()).unwrap_or_default() } pub fn cache_get(key: &str) -> Option { RESOLVE_CACHE.read().ok().and_then(|m| m.get(key).cloned()) } pub fn cache_put(key: &str, value: String) { if let Ok(mut m) = RESOLVE_CACHE.write() { m.insert(key.to_string(), value); } } pub fn cache_clear() { if let Ok(mut m) = RESOLVE_CACHE.write() { m.clear(); } } /// Rebuild BoxIndex and clear resolve cache if env/toml changed pub fn rebuild_if_env_changed() { let cur_env = std::env::var("NYASH_ALIASES").ok(); let meta = std::fs::metadata("nyash.toml").ok(); let (mtime, size) = if let Some(m) = meta { (m.modified().ok(), Some(m.len())) } else { (None, None) }; let mut last = LAST_STATE.lock().expect("state"); let changed = last.aliases_env != cur_env || last.toml_mtime != mtime || last.toml_size != size; if changed { last.aliases_env = cur_env; last.toml_mtime = mtime; last.toml_size = size; refresh_box_index(); cache_clear(); } } #[derive(Clone, Debug, Default)] pub struct PluginMeta { pub prefix: Option, pub require_prefix: bool, pub expose_short_names: bool, } #[allow(dead_code)] pub fn get_plugin_meta(plugin: &str) -> Option { GLOBAL .read() .ok() .and_then(|g| g.plugin_meta.get(plugin).cloned()) }