Files
chongming-worker-rs/src/descriptor.rs
T
2026-08-31 17:18:03 +08:00

158 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 路由描述符(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())
}