//! nyash.toml v2 configuration parser //! //! Ultimate simple design: nyash.toml-centric architecture + minimal FFI //! No Host VTable, single entry point (nyash_plugin_invoke) use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Root configuration structure #[derive(Debug, Deserialize, Serialize)] pub struct NyashConfigV2 { /// Library definitions (multi-box capable) #[serde(default)] pub libraries: HashMap, /// Plugin search paths #[serde(default)] pub plugin_paths: PluginPaths, /// New: Plugins registry (name -> plugin root directory) #[serde(default)] pub plugins: HashMap, /// Optional central type_id mapping (box name -> type_id) #[serde(default)] pub box_types: HashMap, } /// Library definition (simplified) #[derive(Debug, Deserialize, Serialize)] pub struct LibraryDefinition { /// Box types provided by this library pub boxes: Vec, /// Path to the shared library pub path: String, } /// Plugin search paths #[derive(Debug, Default, Deserialize, Serialize)] pub struct PluginPaths { #[serde(default)] pub search_paths: Vec, } /// Box type configuration (nested under library) #[derive(Debug, Deserialize, Serialize)] pub struct BoxTypeConfig { /// Box type ID pub type_id: u32, /// ABI version (default: 1) #[serde(default = "default_abi_version")] pub abi_version: u32, /// Method definitions pub methods: HashMap, /// Singleton service flag (keep one shared instance alive in loader) #[serde(default)] pub singleton: bool, } /// Method definition (simplified - no argument info needed) #[derive(Debug, Deserialize, Serialize, Clone)] pub struct MethodDefinition { /// Method ID for FFI pub method_id: u32, /// Optional argument declarations (v2.1+) #[serde(default)] pub args: Option>, /// Optional: wrap return and errors into ResultBox when true #[serde(default)] pub returns_result: bool, } /// Method argument declaration (v2.1+) #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(untagged)] pub enum ArgDecl { /// Backward-compatible string form (treated as kind="string") Name(String), /// Typed declaration Typed { kind: String, #[serde(default)] category: Option, }, } impl ArgDecl { /// Returns a canonical kind string ("string", "box", etc.) pub fn kind_str(&self) -> &str { match self { ArgDecl::Name(_) => "string", ArgDecl::Typed { kind, .. } => kind.as_str(), } } } fn default_abi_version() -> u32 { 1 } impl NyashConfigV2 { /// Parse nyash.toml file pub fn from_file(path: &str) -> Result> { let content = std::fs::read_to_string(path)?; Self::from_str(&content) } /// Parse nyash.toml content from string (test/helper) pub fn from_str(content: &str) -> Result> { // Parse as raw TOML first to handle nested box configs let mut config: toml::Value = toml::from_str(content)?; // Extract library definitions let libraries = Self::parse_libraries(&mut config)?; // Extract plugin paths let plugin_paths = if let Some(paths) = config.get("plugin_paths") { paths.clone().try_into::()? } else { PluginPaths::default() }; // Extract plugins map let plugins = if let Some(tbl) = config.get("plugins").and_then(|v| v.as_table()) { let mut m = HashMap::new(); for (k, v) in tbl.iter() { if let Some(s) = v.as_str() { m.insert(k.clone(), s.to_string()); } } m } else { HashMap::new() }; // Extract optional box_types map let box_types = if let Some(tbl) = config.get("box_types").and_then(|v| v.as_table()) { let mut m = HashMap::new(); for (k, v) in tbl.iter() { if let Some(id) = v.as_integer() { m.insert(k.clone(), id as u32); } } m } else { HashMap::new() }; Ok(NyashConfigV2 { libraries, plugin_paths, plugins, box_types }) } /// Parse library definitions with nested box configs fn parse_libraries(config: &mut toml::Value) -> Result, Box> { let mut libraries = HashMap::new(); if let Some(libs_section) = config.get("libraries").and_then(|v| v.as_table()) { for (lib_name, lib_value) in libs_section { if let Some(lib_table) = lib_value.as_table() { let boxes = lib_table.get("boxes") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .map(|s| s.to_string()) .collect() }) .unwrap_or_default(); let path = lib_table.get("path") .and_then(|v| v.as_str()) .unwrap_or(lib_name) .to_string(); libraries.insert(lib_name.clone(), LibraryDefinition { boxes, path, }); } } } Ok(libraries) } /// Get box configuration from nested structure /// e.g., [libraries."libnyash_filebox_plugin.so".FileBox] pub fn get_box_config(&self, lib_name: &str, box_name: &str, config_value: &toml::Value) -> Option { config_value .get("libraries") .and_then(|v| v.get(lib_name)) .and_then(|v| v.get(box_name)) .and_then(|v| v.clone().try_into::().ok()) } /// Find library that provides a specific box type pub fn find_library_for_box(&self, box_type: &str) -> Option<(&str, &LibraryDefinition)> { self.libraries.iter() .find(|(_, lib)| lib.boxes.contains(&box_type.to_string())) .map(|(name, lib)| (name.as_str(), lib)) } /// Resolve plugin path from search paths pub fn resolve_plugin_path(&self, plugin_name: &str) -> Option { // Try exact path first if std::path::Path::new(plugin_name).exists() { return Some(plugin_name.to_string()); } // Build effective search paths: config + ENV:NYASH_PLUGIN_PATHS (sep=';' or ':') let mut paths: Vec = Vec::new(); paths.extend(self.plugin_paths.search_paths.iter().cloned()); if let Ok(envp) = std::env::var("NYASH_PLUGIN_PATHS") { let sep = if cfg!(target_os = "windows") { ';' } else { ':' }; for p in envp.split(sep).filter(|s| !s.is_empty()) { paths.push(p.to_string()); } } // Search in effective paths for search_path in &paths { let path = std::path::Path::new(search_path).join(plugin_name); if path.exists() { return Some(path.to_string_lossy().to_string()); } } None } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_v2_config() { let toml_str = r#" [libraries] [libraries."libnyash_filebox_plugin.so"] boxes = ["FileBox"] path = "./target/release/libnyash_filebox_plugin.so" [libraries."libnyash_filebox_plugin.so".FileBox] type_id = 6 abi_version = 1 [libraries."libnyash_filebox_plugin.so".FileBox.methods] birth = { method_id = 0 } open = { method_id = 1 } close = { method_id = 4 } "#; let nyash_config = NyashConfigV2::from_str(toml_str).unwrap(); assert!(nyash_config.libraries.contains_key("libnyash_filebox_plugin.so")); let (lib_name, lib_def) = nyash_config.find_library_for_box("FileBox").unwrap(); assert_eq!(lib_name, "libnyash_filebox_plugin.so"); assert_eq!(lib_def.boxes, vec!["FileBox".to_string()]); // Nested box config should be retrievable let raw: toml::Value = toml::from_str(toml_str).unwrap(); let box_conf = nyash_config.get_box_config("libnyash_filebox_plugin.so", "FileBox", &raw).unwrap(); assert_eq!(box_conf.type_id, 6); } }