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 {
90            rev,
91            entries: Vec::new(),
92        }
93    }
94
95    /// 设键(存在即覆盖,保位次序)。链式可用。
96    pub fn set(mut self, key: impl Into<String>, field: Field) -> Self {
97        let key = key.into();
98        match self.entries.iter_mut().find(|(k, _)| *k == key) {
99            Some(slot) => slot.1 = field,
100            None => self.entries.push((key, field)),
101        }
102        self
103    }
104
105    /// 取字段借查。
106    #[must_use]
107    pub fn get(&self, key: &str) -> Option<&Field> {
108        self.entries
109            .iter()
110            .find(|(k, _)| k.as_str() == key)
111            .map(|(_, f)| f)
112    }
113
114    /// 明面值借查(存在且为明面才 `Some`)。
115    #[must_use]
116    pub fn get_plain(&self, key: &str) -> Option<&str> {
117        self.get(key).and_then(Field::plain_value)
118    }
119
120    /// 秘面引用键借查(存在且为秘面才 `Some`)。
121    #[must_use]
122    pub fn get_secret_key(&self, key: &str) -> Option<&str> {
123        self.get(key).and_then(Field::secret_key)
124    }
125
126    /// 摘键(不存在 = `None`)。
127    pub fn remove(&mut self, key: &str) -> Option<Field> {
128        let idx = self.entries.iter().position(|(k, _)| k.as_str() == key)?;
129        Some(self.entries.remove(idx).1)
130    }
131
132    /// 键序遍历(载体顺序 = set 序,覆盖保位)。
133    pub fn iter(&self) -> impl Iterator<Item = (&str, &Field)> {
134        self.entries.iter().map(|(k, f)| (k.as_str(), f))
135    }
136
137    /// 键数。
138    #[must_use]
139    pub fn len(&self) -> usize {
140        self.entries.len()
141    }
142
143    /// 空段吗。
144    #[must_use]
145    pub fn is_empty(&self) -> bool {
146        self.entries.is_empty()
147    }
148}
149
150impl fmt::Debug for SectionMap {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        // 明面值可直读;秘面只打键名,不打任何"值"形态(BD-CFG-18 兜底),
153        // 与 Field 自身的派生 Debug 语义一致。
154        f.debug_struct("SectionMap")
155            .field("rev", &self.rev)
156            .field(
157                "entries",
158                &self
159                    .entries
160                    .iter()
161                    .map(|(k, field)| match field {
162                        Field::Plain(v) => (k.as_str(), v.as_str()),
163                        Field::Secret { key } => (k.as_str(), key.as_str()),
164                    })
165                    .collect::<Vec<_>>(),
166            )
167            .finish()
168    }
169}
170
171/// `apply_config` 的回执(BD-CFG-6):两段键清单,缺省 = 全采纳。
172#[derive(Debug, Clone, Default, PartialEq, Eq)]
173pub struct ApplyReport {
174    /// 本回已采纳(即 Live/NextUse 两档的字段;具体档位是插件内部细节,不外漏)。
175    pub adopted: Vec<String>,
176    /// 「已采纳但需重启插件才生效」的键(NextStart 档)。
177    pub deferred: Vec<String>,
178}
179
180impl ApplyReport {
181    /// 全采纳报告。
182    #[must_use]
183    pub const fn all_adopted() -> Self {
184        Self {
185            adopted: Vec::new(),
186            deferred: Vec::new(),
187        }
188    }
189
190    /// 无 deferred 吗(boot 载入恒真——插件未启动,一切即 Live)。
191    #[must_use]
192    pub fn no_deferred(&self) -> bool {
193        self.deferred.is_empty()
194    }
195}
196
197/// 配置托底契约(**可选能力**:有配置的插件实现它,没配置的插件零负担)。
198///
199/// 实现面(目标:
200/// - [`Configurable::config_ns`] 全链唯一(命名实例 = `<工厂>.<注册名>`,
201///   存构造期字符串回 `&str`;字符纪律及校验见 [`valid_config_ns`]);
202/// - [`Configurable::apply_config`] **只回灌引用**:秘面字段在本方法内**禁止**
203///   调 secret 取值(宿主管理线程,ek-cfg 秘取可秒级阻塞,BD-CFG-16);
204///   真实取值由插件在自己的时机(典型 = on_start 后自发连接前)做;
205/// - [`Configurable::dump_config`] 只输出要持久的字段——不输出 = 不持久
206///   (BD-CFG-15);秘面字段只出引用形态。
207///
208/// 内部状态修改用自锁(与 Plugin 纪律同式:`Mutex/RwLock`)——本 trait 是
209/// `&self`,可重入(boot 载入与运行期回灌同一条路径)。
210pub trait Configurable {
211    /// 命名空间(全链唯一;add_plugin 期撞名校验)。
212    fn config_ns(&self) -> &str;
213
214    /// 托底载入(宿主 boot / svctl.start 时直调;失败 = 上抛给单一硬报错点)。
215    fn apply_config(&self, cfg: &SectionMap) -> Result<ApplyReport, String>;
216
217    /// 托底导出(插件受理成功后由插件自己 dump → ask `cfg.save`;见 Ek-14)。
218    fn dump_config(&self) -> SectionMap;
219}
220
221/// ns 字符纪律(BD-CFG-3/6):首字符字母或数字,其余 alnum/`_`/`-`/`.`;
222/// 长度 1..=64;禁 `..`(防路径穿越)、禁首尾点。命名实例形态
223/// `<工厂名>.<注册名>` 两段均过 [`crate::plugin::valid_plugin_name`]。
224#[must_use]
225pub fn valid_config_ns(ns: &str) -> bool {
226    let b = ns.as_bytes();
227    if b.is_empty() || b.len() > 64 || !b[0].is_ascii_alphanumeric() {
228        return false;
229    }
230    if ns.starts_with('.') || ns.ends_with('.') || ns.contains("..") {
231        return false;
232    }
233    b[1..]
234        .iter()
235        .all(|c| c.is_ascii_alphanumeric() || *c == b'_' || *c == b'-' || *c == b'.')
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn t1_field_helpers() {
244        let p = Field::plain("x");
245        assert_eq!(p.plain_value(), Some("x"));
246        assert!(p.is_plain());
247        assert!(p.secret_key().is_none());
248
249        let s = Field::secret("k1");
250        assert_eq!(s.secret_key(), Some("k1"));
251        assert!(s.plain_value().is_none());
252        assert!(!s.is_plain());
253    }
254
255    #[test]
256    fn t2_set_get_remove_cover() {
257        let m = SectionMap::new(1)
258            .set("a", Field::plain("1"))
259            .set("b", Field::secret("bb"));
260        assert_eq!(m.get_plain("a"), Some("1"));
261        assert_eq!(m.get_secret_key("b"), Some("bb"));
262        assert!(m.get("c").is_none());
263        assert_eq!(m.len(), 2);
264
265        // 覆盖 = 同键覆盖,保位次序,不增行。
266        let m2 = m.clone().set("a", Field::plain("z"));
267        assert_eq!(m2.len(), 2);
268        assert_eq!(m2.get_plain("a"), Some("z"));
269        assert_eq!(m2.iter().next().map(|(k, _)| k), Some("a"));
270
271        let mut m3 = m2.clone();
272        assert_eq!(m3.remove("a"), Some(Field::plain("z")));
273        assert!(m3.get("a").is_none());
274        assert!(m3.remove("zz").is_none());
275    }
276
277    #[test]
278    fn t3_apply_report_default_and_helpers() {
279        let r = ApplyReport::all_adopted();
280        assert!(r.no_deferred());
281        let r2 = ApplyReport::default();
282        assert!(r2.no_deferred());
283        let r3 = ApplyReport {
284            adopted: vec!["a".into()],
285            deferred: vec!["b".into()],
286        };
287        assert!(!r3.no_deferred());
288    }
289
290    #[test]
291    fn t4_valid_config_ns_matrix() {
292        // 正样(含命名实例形态)。
293        assert!(valid_config_ns("oms"));
294        assert!(valid_config_ns("ctp"));
295        assert!(valid_config_ns("ctp.ctp_i130_001"));
296        assert!(valid_config_ns("a1-b_c.d2"));
297        // 拒样:空/超长/非首字母开/首尾点/双点/非法字符。
298        assert!(!valid_config_ns(""));
299        assert!(!valid_config_ns(&"a".repeat(65)));
300        assert!(!valid_config_ns("_oms"));
301        assert!(!valid_config_ns("-oms"));
302        assert!(!valid_config_ns(".oms"));
303        assert!(!valid_config_ns("oms."));
304        assert!(!valid_config_ns("a..b"));
305        assert!(!valid_config_ns("a/b"));
306        assert!(!valid_config_ns("a b"));
307        assert!(!valid_config_ns("a\\b"));
308    }
309
310    #[test]
311    fn t5_debug_never_leaks_secret_field_shape() {
312        // 秘面在 Debug 里只出键名(这本身就是键名,非值)——形状钉防将来
313        // 有人把 Field::Secret 改回携带值(值一进来就会随这行测试的形状断言爆)。
314        let m = SectionMap::new(1).set("pw", Field::secret("k1"));
315        let d = format!("{m:?}");
316        assert!(d.contains("pw"));
317        assert!(d.contains("k1"));
318        // 明面照读。
319        let m2 = SectionMap::new(1).set("addr", Field::plain("tcp://x"));
320        assert!(format!("{m2:?}").contains("tcp://x"));
321    }
322}