Skip to main content

ek_core/
section.rs

1//! section —— 配置托底契约(BD-CFG-1..6;2026-10-25 定盘,用户裁定「软集中」)。
2//!
3//! 定位纪律(BD-CFG-2):**纯数据 + trait,零格式解析**。行式键值的解析/序列化
4//! 在 ek-cfg(BD-CFG-3);本 crate 只保证「全链一份类型」。两宿主(ek-station /
5//! ek-ui)的 boot 载入与 svctl.start 重注路径经 `Plugin::as_configurable`
6//! 探查后直调 [`Configurable::apply_config`](Configurable::apply_config)
7//! 回灌(BD-CFG-1:宿主两步直调,不解释不转发)。
8//!
9//! 生效三档(BD-CFG-6):`apply_config` 回 [`ApplyReport`],`deferred` 非空 =
10//! 「已采纳+需重启插件才生效」。配置面修改**永不**要求重启整个应用;插件级
11//! 重启 = svctl.remove→start(五步闭停,秒级)。
12
13use std::fmt;
14
15/// 字段形态(BD-CFG-12/15)。
16///
17/// - [`Field::Plain`] = 明面值(进文件);
18/// - [`Field::Secret`] = 秘面引用(**只有键名,无值**)——真值在系统凭据库或
19///   env(BD-CFG-12),文件里落的是 `secret://ek/<ns>/<key>` 引用串
20///   (由 ek-cfg 的 `secret::ref_of` 单源生成,BD-CFG-13)。
21///
22/// 本枚举**不含秘值本体**(key 只是键名),`derive(Debug)` 合法;
23/// 含真值的类型(ek-cfg `SecretValue`)按 BD-CFG-18 走手工 Debug。
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Field {
26    /// 明面值(单值或逗号并列的列表值;分隔细节归格式层 ek-cfg)。
27    Plain(String),
28    /// 秘面引用;`key` = 秘库键名(不含值)。
29    Secret {
30        /// 秘库键名(与 ns 组合出唯一条目 `ek/<ns>/<key>`)。
31        key: String,
32    },
33}
34
35impl Field {
36    /// 明面值便捷构造。
37    #[must_use]
38    pub fn plain(value: impl Into<String>) -> Self {
39        Self::Plain(value.into())
40    }
41
42    /// 秘面引用便捷构造。
43    #[must_use]
44    pub fn secret(key: impl Into<String>) -> Self {
45        Self::Secret { key: key.into() }
46    }
47
48    /// 是明面吗。秘面(false)时取引用键走 [`Self::secret_key`]。
49    #[must_use]
50    pub fn is_plain(&self) -> bool {
51        matches!(self, Self::Plain(_))
52    }
53
54    /// 明面值借用(`None` = 秘面)。
55    #[must_use]
56    pub fn plain_value(&self) -> Option<&str> {
57        match self {
58            Self::Plain(v) => Some(v.as_str()),
59            Self::Secret { .. } => None,
60        }
61    }
62
63    /// 秘面引用键借用(`None` = 明面)。
64    #[must_use]
65    pub fn secret_key(&self) -> Option<&str> {
66        match self {
67            Self::Secret { key } => Some(key.as_str()),
68            Self::Plain(_) => None,
69        }
70    }
71}
72
73/// 一段配置(一个 ns 一段;磁盘上 = `${config_dir}/ek/cfg/<ns>.ini`)。
74///
75/// 纯数据容器(BD-CFG-2):键不重复(`set` 覆盖同键);`rev` 对应文件首行
76/// `rev = 1`(语义迁移由插件按 rev 自裁,BD-CFG-3);列表值 = 逗号并列字符串,
77/// 分列/归一归格式层。**不含解析**——从文件到本类型只发生在 ek-cfg。
78#[derive(Clone, Default, PartialEq, Eq)]
79pub struct SectionMap {
80    /// 文件首行 `rev`;无文件回灌时 = 0(插件自判首启形态)。
81    pub rev: u32,
82    entries: Vec<(String, Field)>,
83}
84
85impl SectionMap {
86    /// 空段(rev 显式给——0 = 「无文件」,插件侧自判首启)。
87    #[must_use]
88    pub fn new(rev: u32) -> Self {
89        Self { rev, entries: Vec::new() }
90    }
91
92    /// 设键(存在即覆盖,保位次序)。链式可用。
93    pub fn set(mut self, key: impl Into<String>, field: Field) -> Self {
94        let key = key.into();
95        match self.entries.iter_mut().find(|(k, _)| *k == key) {
96            Some(slot) => slot.1 = field,
97            None => self.entries.push((key, field)),
98        }
99        self
100    }
101
102    /// 取字段借查。
103    #[must_use]
104    pub fn get(&self, key: &str) -> Option<&Field> {
105        self.entries.iter().find(|(k, _)| k.as_str() == key).map(|(_, f)| f)
106    }
107
108    /// 明面值借查(存在且为明面才 `Some`)。
109    #[must_use]
110    pub fn get_plain(&self, key: &str) -> Option<&str> {
111        self.get(key).and_then(Field::plain_value)
112    }
113
114    /// 秘面引用键借查(存在且为秘面才 `Some`)。
115    #[must_use]
116    pub fn get_secret_key(&self, key: &str) -> Option<&str> {
117        self.get(key).and_then(Field::secret_key)
118    }
119
120    /// 摘键(不存在 = `None`)。
121    pub fn remove(&mut self, key: &str) -> Option<Field> {
122        let idx = self.entries.iter().position(|(k, _)| k.as_str() == key)?;
123        Some(self.entries.remove(idx).1)
124    }
125
126    /// 键序遍历(载体顺序 = set 序,覆盖保位)。
127    pub fn iter(&self) -> impl Iterator<Item = (&str, &Field)> {
128        self.entries.iter().map(|(k, f)| (k.as_str(), f))
129    }
130
131    /// 键数。
132    #[must_use]
133    pub fn len(&self) -> usize {
134        self.entries.len()
135    }
136
137    /// 空段吗。
138    #[must_use]
139    pub fn is_empty(&self) -> bool {
140        self.entries.is_empty()
141    }
142}
143
144impl fmt::Debug for SectionMap {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        // 明面值可直读;秘面只打键名,不打任何"值"形态(BD-CFG-18 兜底),
147        // 与 Field 自身的派生 Debug 语义一致。
148        f.debug_struct("SectionMap")
149            .field("rev", &self.rev)
150            .field(
151                "entries",
152                &self
153                    .entries
154                    .iter()
155                    .map(|(k, field)| match field {
156                        Field::Plain(v) => (k.as_str(), v.as_str()),
157                        Field::Secret { key } => (k.as_str(), key.as_str()),
158                    })
159                    .collect::<Vec<_>>(),
160            )
161            .finish()
162    }
163}
164
165/// `apply_config` 的回执(BD-CFG-6):两段键清单,缺省 = 全采纳。
166#[derive(Debug, Clone, Default, PartialEq, Eq)]
167pub struct ApplyReport {
168    /// 本回已采纳(即 Live/NextUse 两档的字段;具体档位是插件内部细节,不外漏)。
169    pub adopted: Vec<String>,
170    /// 「已采纳但需重启插件才生效」的键(NextStart 档)。
171    pub deferred: Vec<String>,
172}
173
174impl ApplyReport {
175    /// 全采纳报告。
176    #[must_use]
177    pub const fn all_adopted() -> Self {
178        Self { adopted: Vec::new(), deferred: Vec::new() }
179    }
180
181    /// 无 deferred 吗(boot 载入恒真——插件未启动,一切即 Live)。
182    #[must_use]
183    pub fn no_deferred(&self) -> bool {
184        self.deferred.is_empty()
185    }
186}
187
188/// 配置托底契约(**可选能力**:有配置的插件实现它,没配置的插件零负担)。
189///
190/// 实现面(目标:
191/// - [`Configurable::config_ns`] 全链唯一(命名实例 = `<工厂>.<注册名>`,
192///   存构造期字符串回 `&str`;字符纪律及校验见 [`valid_config_ns`]);
193/// - [`Configurable::apply_config`] **只回灌引用**:秘面字段在本方法内**禁止**
194///   调 secret 取值(宿主管理线程,ek-cfg 秘取可秒级阻塞,BD-CFG-16);
195///   真实取值由插件在自己的时机(典型 = on_start 后自发连接前)做;
196/// - [`Configurable::dump_config`] 只输出要持久的字段——不输出 = 不持久
197///   (BD-CFG-15);秘面字段只出引用形态。
198///
199/// 内部状态修改用自锁(与 Plugin 纪律同式:`Mutex/RwLock`)——本 trait 是
200/// `&self`,可重入(boot 载入与运行期回灌同一条路径)。
201pub trait Configurable {
202    /// 命名空间(全链唯一;add_plugin 期撞名校验)。
203    fn config_ns(&self) -> &str;
204
205    /// 托底载入(宿主 boot / svctl.start 时直调;失败 = 上抛给单一硬报错点)。
206    fn apply_config(&self, cfg: &SectionMap) -> Result<ApplyReport, String>;
207
208    /// 托底导出(插件受理成功后由插件自己 dump → ask `cfg.save`;见 Ek-14)。
209    fn dump_config(&self) -> SectionMap;
210}
211
212/// ns 字符纪律(BD-CFG-3/6):首字符字母或数字,其余 alnum/`_`/`-`/`.`;
213/// 长度 1..=64;禁 `..`(防路径穿越)、禁首尾点。命名实例形态
214/// `<工厂名>.<注册名>` 两段均过 [`crate::plugin::valid_plugin_name`]。
215#[must_use]
216pub fn valid_config_ns(ns: &str) -> bool {
217    let b = ns.as_bytes();
218    if b.is_empty() || b.len() > 64 || b[0].is_ascii_alphanumeric() == false {
219        return false;
220    }
221    if ns.starts_with('.') || ns.ends_with('.') || ns.contains("..") {
222        return false;
223    }
224    b[1..]
225        .iter()
226        .all(|c| c.is_ascii_alphanumeric() || *c == b'_' || *c == b'-' || *c == b'.')
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn t1_field_helpers() {
235        let p = Field::plain("x");
236        assert_eq!(p.plain_value(), Some("x"));
237        assert!(p.is_plain());
238        assert!(p.secret_key().is_none());
239
240        let s = Field::secret("k1");
241        assert_eq!(s.secret_key(), Some("k1"));
242        assert!(s.plain_value().is_none());
243        assert!(!s.is_plain());
244    }
245
246    #[test]
247    fn t2_set_get_remove_cover() {
248        let m = SectionMap::new(1).set("a", Field::plain("1")).set("b", Field::secret("bb"));
249        assert_eq!(m.get_plain("a"), Some("1"));
250        assert_eq!(m.get_secret_key("b"), Some("bb"));
251        assert!(m.get("c").is_none());
252        assert_eq!(m.len(), 2);
253
254        // 覆盖 = 同键覆盖,保位次序,不增行。
255        let m2 = m.clone().set("a", Field::plain("z"));
256        assert_eq!(m2.len(), 2);
257        assert_eq!(m2.get_plain("a"), Some("z"));
258        assert_eq!(m2.iter().next().map(|(k, _)| k), Some("a"));
259
260        let mut m3 = m2.clone();
261        assert_eq!(m3.remove("a"), Some(Field::plain("z")));
262        assert!(m3.get("a").is_none());
263        assert!(m3.remove("zz").is_none());
264    }
265
266    #[test]
267    fn t3_apply_report_default_and_helpers() {
268        let r = ApplyReport::all_adopted();
269        assert!(r.no_deferred());
270        let r2 = ApplyReport::default();
271        assert!(r2.no_deferred());
272        let r3 = ApplyReport { adopted: vec!["a".into()], deferred: vec!["b".into()] };
273        assert!(!r3.no_deferred());
274    }
275
276    #[test]
277    fn t4_valid_config_ns_matrix() {
278        // 正样(含命名实例形态)。
279        assert!(valid_config_ns("oms"));
280        assert!(valid_config_ns("ctp"));
281        assert!(valid_config_ns("ctp.ctp_i130_001"));
282        assert!(valid_config_ns("a1-b_c.d2"));
283        // 拒样:空/超长/非首字母开/首尾点/双点/非法字符。
284        assert!(!valid_config_ns(""));
285        assert!(!valid_config_ns(&"a".repeat(65)));
286        assert!(!valid_config_ns("_oms"));
287        assert!(!valid_config_ns("-oms"));
288        assert!(!valid_config_ns(".oms"));
289        assert!(!valid_config_ns("oms."));
290        assert!(!valid_config_ns("a..b"));
291        assert!(!valid_config_ns("a/b"));
292        assert!(!valid_config_ns("a b"));
293        assert!(!valid_config_ns("a\\b"));
294    }
295
296    #[test]
297    fn t5_debug_never_leaks_secret_field_shape() {
298        // 秘面在 Debug 里只出键名(这本身就是键名,非值)——形状钉防将来
299        // 有人把 Field::Secret 改回携带值(值一进来就会随这行测试的形状断言爆)。
300        let m = SectionMap::new(1).set("pw", Field::secret("k1"));
301        let d = format!("{m:?}");
302        assert!(d.contains("pw"));
303        assert!(d.contains("k1"));
304        // 明面照读。
305        let m2 = SectionMap::new(1).set("addr", Field::plain("tcp://x"));
306        assert!(format!("{m2:?}").contains("tcp://x"));
307    }
308}