//! 路由描述符(docs/plugin-wire-protocol.md §2) //! //! 描述符来源为 `config/workers//*.toml` 端点定义。解析规则与 Python SDK //! route_config.py 完全对位(TOML 兼容 dict),保证与 Python 插件发布的内容 //! 逐字段一致 —— 网关按描述符内容 hash 做多副本幂等去重与冲突判定, //! 混布部署时内容不一致会触发告警。 use std::collections::BTreeMap; use std::path::Path; use serde_json::{Map, Value}; pub const ROUTES_PREFIX: &str = "chongming/routes"; pub const META_KEY: &str = "__meta__"; pub const SCHEMA_VERSION: u8 = 1; /// route_id = subject 的 "." 替换为 "_" + "__" + METHOD 大写 /// (与 Python routes_publisher.route_id 一致) pub fn route_id(subject: &str, method: &str) -> String { format!("{}__{}", subject.replace('.', "_"), method.to_uppercase()) } /// 从 `config/workers//` 目录解析全部路由描述符(TOML 兼容 dict)。 /// /// 规则(对齐 Python load_worker_routes_from_dir): /// - 每个端点 TOML 的每个方法节(GET/POST/MQTT...)生成一个独立描述符 /// - `[worker.defaults]` 作为字段缺省值合并(显式字段优先) /// - method 恒取节键大写;path 缺省为 `router_prefix + "/" + 端点名`; /// subject 缺省为 `.<端点名>`;route_name 记录端点文件名 pub fn load_routes_from_dir( worker_type: &str, config_dir: &str, ) -> Result, String> { let base = Path::new(config_dir).join(worker_type); if !base.is_dir() { return Err(format!("Worker 配置目录不存在: {}", base.display())); } // 1. 分组配置:[worker].defaults + [worker].registration.router_prefix let group_path = base.join("config.toml"); let mut group_defaults: Map = Map::new(); let mut router_prefix = String::new(); if group_path.exists() { let text = std::fs::read_to_string(&group_path) .map_err(|e| format!("读取 {} 失败: {e}", group_path.display()))?; let cfg: toml::Value = toml::from_str(&text) .map_err(|e| format!("解析 {} 失败: {e}", group_path.display()))?; if let Some(defaults) = cfg .get("worker") .and_then(|w| w.get("defaults")) .and_then(toml::Value::as_table) { group_defaults = defaults .iter() .map(|(k, v)| (k.clone(), crate::config::toml_to_json(v))) .collect(); } if let Some(prefix) = cfg .get("worker") .and_then(|w| w.get("registration")) .and_then(|r| r.get("router_prefix")) .and_then(toml::Value::as_str) { router_prefix = prefix.to_string(); } } // 2. 遍历所有端点 TOML(config.toml 除外) let mut routes: Vec = Vec::new(); let mut files: Vec = std::fs::read_dir(&base) .map_err(|e| format!("读取目录 {} 失败: {e}", base.display()))? .filter_map(|e| e.ok()) .filter(|e| { e.file_name() .to_string_lossy() .ends_with(".toml") }) .map(|e| e.file_name().to_string_lossy().to_string()) .collect(); files.sort(); for file in files { if file == "config.toml" { continue; } // 端点名 = 文件名(不含扩展名) let route_name = file.trim_end_matches(".toml").to_string(); let base_path = format!("{}/{}", router_prefix, route_name); let text = std::fs::read_to_string(base.join(&file)) .map_err(|e| format!("读取 {} 失败: {e}", file))?; let route_data: toml::Value = toml::from_str(&text) .map_err(|e| format!("解析 {} 失败: {e}", file))?; let route_table = match route_data.as_table() { Some(t) => t, None => continue, }; // 3. 每个方法节生成一个独立路由 for (method, spec) in route_table { let spec_table = match spec.as_table() { Some(t) => t, None => continue, }; let mut route: Map = Map::new(); // 显式字段优先,defaults 兜底 for (k, v) in spec_table { route.insert(k.clone(), crate::config::toml_to_json(v)); } for (k, v) in &group_defaults { if !route.contains_key(k) { route.insert(k.clone(), v.clone()); } } // 覆盖关键字段(与 Python 一致) route.insert("method".into(), Value::String(method.to_uppercase())); if !route.contains_key("path") { route.insert("path".into(), Value::String(base_path.clone())); } if !route.contains_key("subject") { route.insert( "subject".into(), Value::String(format!("{worker_type}.{route_name}")), ); } route.insert("route_name".into(), Value::String(route_name.clone())); routes.push(Value::Object(route)); } } Ok(routes) } /// 构造发布用描述符:附加 schema_version 与 type(对齐 Python publish_routes) pub fn finalize_descriptor(route: &Value, worker_type: &str) -> Value { let mut map = match route.as_object() { Some(m) => m.clone(), None => return route.clone(), }; map.insert("schema_version".into(), Value::from(SCHEMA_VERSION)); map.insert("type".into(), Value::String(worker_type.to_string())); Value::Object(map) } /// 元信息描述符([[workers.mqtt_topics]] 时发布) pub fn meta_descriptor(mqtt_topics: &[Value]) -> Value { let mut map = BTreeMap::new(); map.insert( "schema_version".to_string(), Value::from(SCHEMA_VERSION), ); map.insert( "mqtt_topics".to_string(), Value::Array(mqtt_topics.to_vec()), ); Value::Object(map.into_iter().collect()) }