234 lines
8.0 KiB
Rust
234 lines
8.0 KiB
Rust
/*!
|
|
* 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<String, String>,
|
|
pub plugin_boxes: HashSet<String>,
|
|
#[allow(dead_code)]
|
|
pub plugin_meta: HashMap<String, PluginMeta>,
|
|
pub plugin_meta_by_box: HashMap<String, PluginMeta>,
|
|
pub plugins_require_prefix_global: bool,
|
|
}
|
|
|
|
impl BoxIndex {
|
|
pub fn build_current() -> Self {
|
|
// aliases from nyash.toml and env
|
|
let mut aliases: HashMap<String, String> = HashMap::new();
|
|
if let Ok(text) = std::fs::read_to_string("nyash.toml") {
|
|
if let Ok(doc) = toml::from_str::<toml::Value>(&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<String> = HashSet::new();
|
|
let mut plugin_meta: HashMap<String, PluginMeta> = HashMap::new();
|
|
let mut plugin_meta_by_box: HashMap<String, PluginMeta> = 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::<toml::Value>(&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<String> = 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<RwLock<BoxIndex>> = Lazy::new(|| RwLock::new(BoxIndex::default()));
|
|
|
|
// Global resolve cache (keyed by tgt|base|strict|paths)
|
|
static RESOLVE_CACHE: Lazy<RwLock<HashMap<String, String>>> =
|
|
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<String>,
|
|
toml_mtime: Option<SystemTime>,
|
|
toml_size: Option<u64>,
|
|
}
|
|
|
|
static LAST_STATE: Lazy<Mutex<IndexState>> = 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<String> {
|
|
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<String>,
|
|
pub require_prefix: bool,
|
|
pub expose_short_names: bool,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn get_plugin_meta(plugin: &str) -> Option<PluginMeta> {
|
|
GLOBAL
|
|
.read()
|
|
.ok()
|
|
.and_then(|g| g.plugin_meta.get(plugin).cloned())
|
|
}
|