158 lines
6.0 KiB
Rust
158 lines
6.0 KiB
Rust
//! 路由描述符(docs/plugin-wire-protocol.md §2)
|
||||
|
|
//!
|
|||
|
|
//! 描述符来源为 `config/workers/<type>/*.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/<type>/` 目录解析全部路由描述符(TOML 兼容 dict)。
|
|||
|
|
///
|
|||
|
|
/// 规则(对齐 Python load_worker_routes_from_dir):
|
|||
|
|
/// - 每个端点 TOML 的每个方法节(GET/POST/MQTT...)生成一个独立描述符
|
|||
|
|
/// - `[worker.defaults]` 作为字段缺省值合并(显式字段优先)
|
|||
|
|
/// - method 恒取节键大写;path 缺省为 `router_prefix + "/" + 端点名`;
|
|||
|
|
/// subject 缺省为 `<type>.<端点名>`;route_name 记录端点文件名
|
|||
|
|
pub fn load_routes_from_dir(
|
|||
|
|
worker_type: &str,
|
|||
|
|
config_dir: &str,
|
|||
|
|
) -> Result<Vec<Value>, 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<String, Value> = 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<Value> = Vec::new();
|
|||
|
|
let mut files: Vec<String> = 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<String, Value> = 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())
|
|||
|
|
}
|