1use std::fmt;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Field {
26 Plain(String),
28 Secret {
30 key: String,
32 },
33}
34
35impl Field {
36 #[must_use]
38 pub fn plain(value: impl Into<String>) -> Self {
39 Self::Plain(value.into())
40 }
41
42 #[must_use]
44 pub fn secret(key: impl Into<String>) -> Self {
45 Self::Secret { key: key.into() }
46 }
47
48 #[must_use]
50 pub fn is_plain(&self) -> bool {
51 matches!(self, Self::Plain(_))
52 }
53
54 #[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 #[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#[derive(Clone, Default, PartialEq, Eq)]
79pub struct SectionMap {
80 pub rev: u32,
82 entries: Vec<(String, Field)>,
83}
84
85impl SectionMap {
86 #[must_use]
88 pub fn new(rev: u32) -> Self {
89 Self {
90 rev,
91 entries: Vec::new(),
92 }
93 }
94
95 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 #[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 #[must_use]
116 pub fn get_plain(&self, key: &str) -> Option<&str> {
117 self.get(key).and_then(Field::plain_value)
118 }
119
120 #[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 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 pub fn iter(&self) -> impl Iterator<Item = (&str, &Field)> {
134 self.entries.iter().map(|(k, f)| (k.as_str(), f))
135 }
136
137 #[must_use]
139 pub fn len(&self) -> usize {
140 self.entries.len()
141 }
142
143 #[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 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
173pub struct ApplyReport {
174 pub adopted: Vec<String>,
176 pub deferred: Vec<String>,
178}
179
180impl ApplyReport {
181 #[must_use]
183 pub const fn all_adopted() -> Self {
184 Self {
185 adopted: Vec::new(),
186 deferred: Vec::new(),
187 }
188 }
189
190 #[must_use]
192 pub fn no_deferred(&self) -> bool {
193 self.deferred.is_empty()
194 }
195}
196
197pub trait Configurable {
211 fn config_ns(&self) -> &str;
213
214 fn apply_config(&self, cfg: &SectionMap) -> Result<ApplyReport, String>;
216
217 fn dump_config(&self) -> SectionMap;
219}
220
221#[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 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 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 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 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 let m2 = SectionMap::new(1).set("addr", Field::plain("tcp://x"));
320 assert!(format!("{m2:?}").contains("tcp://x"));
321 }
322}