//! 配置读取(与 Python SDK process.py 完全对位) //! //! - 全局配置 `config.toml`:仅读取 `[zenoh].json5_path`(默认 `config/zenoh_config.json5`) //! - 插件配置 `config/workers//config.toml` 的 `[workers]` 节:展平 + mqtt_topics use std::path::Path; use serde_json::Value; /// 全局配置(仅 zenoh 会话配置路径) pub struct GlobalConfig { pub zenoh_json5_path: String, } /// 插件配置([workers] 节,语义对齐 Python load_plugin_config) pub struct PluginConfig { pub worker_type: String, pub module: String, pub factory: String, pub cleanup: String, pub count: usize, pub load_mode: String, pub mqtt_topics: Vec, /// 展平后的运行时配置(`[workers.config]` 等,供插件自用) pub runtime: Value, } /// 读取全局 config.toml 的 `[zenoh].json5_path` /// /// `json5_path` 若为相对路径,则基于 config.toml 所在目录解析为绝对路径——这样 /// worker 从任何 CWD 启动(含容器部署)都能定位到与之同目录的 zenoh 会话配置, /// 与 Python 进程插件"配置自包含在插件根"的约定一致。 pub fn load_global_config(path: &str) -> Result { let cfg_path = std::path::Path::new(path); let text = std::fs::read_to_string(cfg_path).map_err(|e| format!("读取全局配置 {path} 失败: {e}"))?; let cfg: toml::Value = toml::from_str(&text).map_err(|e| format!("解析全局配置 {path} 失败: {e}"))?; let json5_path = cfg .get("zenoh") .and_then(|z| z.get("json5_path")) .and_then(toml::Value::as_str) .unwrap_or("config/zenoh_config.json5"); let base_dir = cfg_path .parent() .unwrap_or_else(|| std::path::Path::new(".")); let resolved = std::path::Path::new(json5_path); let zenoh_json5_path = if resolved.is_absolute() { resolved.to_path_buf() } else { base_dir.join(resolved) }; Ok(GlobalConfig { zenoh_json5_path: zenoh_json5_path.to_string_lossy().into_owned(), }) } /// 读取 `config/workers//config.toml` 的 `[workers]` 节并展平 /// (展平规则与 Python SDK load_plugin_config 一致,worker 侧独立实现) pub fn load_plugin_config(worker_type: &str, config_dir: &str) -> Result { let path = Path::new(config_dir).join(worker_type).join("config.toml"); let text = std::fs::read_to_string(&path) .map_err(|e| format!("未找到插件配置 {}: {e}", path.display()))?; let plugin_conf: toml::Value = toml::from_str(&text).map_err(|e| format!("解析插件配置 {} 失败: {e}", path.display()))?; let workers = plugin_conf .get("workers") .and_then(toml::Value::as_table) .ok_or_else(|| format!("{} 缺少 [workers] 节", path.display()))?; let plugin_type = workers .get("type") .and_then(toml::Value::as_str) .unwrap_or(worker_type) .to_string(); let load_mode = workers .get("load_mode") .and_then(toml::Value::as_str) .unwrap_or("inline") .to_string(); // mqtt_topics:仅保留带 topic 的 dict,并补 worker 字段(与 Python 一致) let mut mqtt_topics: Vec = Vec::new(); if let Some(topics) = workers.get("mqtt_topics").and_then(toml::Value::as_array) { for t in topics { let has_topic = t.get("topic").map(toml::Value::is_str).unwrap_or(false); if !has_topic { continue; } let mut t = t.clone(); let has_worker = t.get("worker").map(toml::Value::is_str).unwrap_or(false); if !has_worker { if let Some(table) = t.as_table_mut() { table.insert( "worker".to_string(), toml::Value::String(plugin_type.clone()), ); } } mqtt_topics.push(toml_to_json(&t)); } } // 展平:插件全量配置 + [workers] 节(除 config/mqtt_topics/load_mode)+ [workers.config] let mut runtime: toml::Value = plugin_conf.clone(); if let Some(flat) = runtime.as_table_mut() { for (k, v) in workers { if k == "config" || k == "mqtt_topics" || k == "load_mode" { continue; } flat.insert(k.clone(), v.clone()); } } if let Some(cfg) = workers.get("config") { if let Some(table) = runtime.as_table_mut() { table.insert("config".to_string(), cfg.clone()); } } // 顶层保留完整 [workers](含 config),与 Python runtime_config 结构一致 let runtime_json = toml_to_json(&runtime); Ok(PluginConfig { worker_type: plugin_type, module: workers .get("module") .and_then(toml::Value::as_str) .unwrap_or(&format!("plugins.{worker_type}")) .to_string(), factory: workers .get("factory") .and_then(toml::Value::as_str) .unwrap_or("create_workers") .to_string(), cleanup: workers .get("cleanup") .and_then(toml::Value::as_str) .unwrap_or("cleanup") .to_string(), count: workers .get("count") .and_then(toml::Value::as_integer) .map(|c| c.max(1) as usize) .unwrap_or(1), load_mode, mqtt_topics, runtime: runtime_json, }) } /// toml::Value → serde_json::Value(插件运行时配置直接以 JSON 暴露) pub fn toml_to_json(v: &toml::Value) -> Value { match v { toml::Value::String(s) => Value::String(s.clone()), toml::Value::Integer(i) => Value::from(*i), toml::Value::Float(f) => Value::from(*f), toml::Value::Boolean(b) => Value::Bool(*b), toml::Value::Datetime(d) => Value::String(d.to_string()), toml::Value::Array(a) => Value::Array(a.iter().map(toml_to_json).collect()), toml::Value::Table(t) => Value::Object( t.iter() .map(|(k, v)| (k.clone(), toml_to_json(v))) .collect(), ), } } // ============================================================ // [workers.config] 运行时读写(对齐 Python runtime.py 的 config 管理) // ============================================================ /// 禁止通过 config_set 写入的保留键(破坏插件加载/路由语义) pub const CONFIG_FORBIDDEN_KEYS: &[&str] = &[ "type", "module", "factory", "cleanup", "count", "load_mode", "mqtt_topics", "workers", "worker", "defaults", "registration", ]; /// 插件 config.toml 路径(config/workers//config.toml) pub fn plugin_config_path(worker_type: &str, config_dir: &str) -> std::path::PathBuf { std::path::Path::new(config_dir) .join(worker_type) .join("config.toml") } /// 读该类型 `[workers].config` 段(对象),缺失/损坏返回空对象 pub fn read_worker_config_section(worker_type: &str, config_dir: &str) -> Value { let path = plugin_config_path(worker_type, config_dir); let text = match std::fs::read_to_string(&path) { Ok(t) => t, Err(_) => return Value::Object(Default::default()), }; let data: toml::Value = toml::from_str(&text).unwrap_or(toml::Value::Table(Default::default())); match data.get("workers").and_then(toml::Value::as_table) { Some(w) => match w.get("config") { Some(cfg) => toml_to_json(cfg), None => Value::Object(Default::default()), }, None => Value::Object(Default::default()), } } /// 把 updates 合并写回该类型 `[workers].config`(仅 index0 落盘)。 /// 返回合并后的段;校验保留键失败抛 Err。 pub fn persist_worker_config_section( worker_type: &str, config_dir: &str, updates: &Value, ) -> Result { if !updates.is_object() { return Err("config_set 需要 data.config 字段(非空对象)".into()); } let bad: Vec = updates .as_object() .unwrap() .keys() .filter(|k| CONFIG_FORBIDDEN_KEYS.contains(&k.as_str())) .cloned() .collect(); if !bad.is_empty() { return Err(format!("禁止修改保留配置键: {bad:?}")); } let path = plugin_config_path(worker_type, config_dir); let current = merge_confirmed(read_worker_config_section(worker_type, config_dir), updates); // 写回 toml 文件:[workers].config = merged(缺 [workers] 节则创建) let text = std::fs::read_to_string(&path).unwrap_or_default(); let parsed: toml::Value = toml::from_str(&text).unwrap_or_else(|_| toml::Value::Table(toml::value::Table::new())); let mut data: toml::Value = if parsed.as_table().is_some() { parsed } else { toml::Value::Table(toml::value::Table::new()) }; // 定位/创建 [workers] 表 if data .get("workers") .and_then(toml::Value::as_table) .is_none() { let t = data.as_table_mut().unwrap(); t.insert( "workers".into(), toml::Value::Table(toml::value::Table::new()), ); } let workers_tbl = data.get_mut("workers").unwrap().as_table_mut().unwrap(); workers_tbl.insert("config".into(), json_to_toml(¤t)); let out = toml::to_string(&data).map_err(|e| format!("序列化 config.toml 失败: {e}"))?; // 原子写:临时文件 + rename let tmp = path.with_extension("toml.tmp"); std::fs::write(&tmp, out).map_err(|e| format!("写入配置失败: {e}"))?; std::fs::rename(&tmp, &path).map_err(|e| format!("原子替换配置失败: {e}"))?; Ok(current) } /// json::Value → toml::Value pub fn json_to_toml(v: &Value) -> toml::Value { match v { Value::Null => toml::Value::String(String::new()), Value::Bool(b) => toml::Value::Boolean(*b), Value::Number(n) => { if let Some(i) = n.as_i64() { toml::Value::Integer(i) } else if let Some(f) = n.as_f64() { toml::Value::Float(f) } else { toml::Value::String(n.to_string()) } } Value::String(s) => toml::Value::String(s.clone()), Value::Array(a) => toml::Value::Array(a.iter().map(json_to_toml).collect()), Value::Object(m) => toml::Value::Table( m.iter() .map(|(k, v)| (k.clone(), json_to_toml(v))) .collect(), ), } } /// 基于 current 合并 updates(过滤保留键) pub fn merge_confirmed(current: Value, updates: &Value) -> Value { let mut obj = current.as_object().cloned().unwrap_or_default(); for (k, v) in updates.as_object().unwrap_or(&Default::default()) { if !CONFIG_FORBIDDEN_KEYS.contains(&k.as_str()) { obj.insert(k.clone(), v.clone()); } } Value::Object(obj) } /// 计算 updates 中被允许(非保留键)的键个数 pub fn count_allowed(updates: &Value) -> usize { updates .as_object() .map(|m| { m.keys() .filter(|k| !CONFIG_FORBIDDEN_KEYS.contains(&k.as_str())) .count() }) .unwrap_or(0) }