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 { rev, entries: Vec::new() }
90 }
91
92 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 #[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 #[must_use]
110 pub fn get_plain(&self, key: &str) -> Option<&str> {
111 self.get(key).and_then(Field::plain_value)
112 }
113
114 #[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 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 pub fn iter(&self) -> impl Iterator<Item = (&str, &Field)> {
128 self.entries.iter().map(|(k, f)| (k.as_str(), f))
129 }
130
131 #[must_use]
133 pub fn len(&self) -> usize {
134 self.entries.len()
135 }
136
137 #[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 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
167pub struct ApplyReport {
168 pub adopted: Vec<String>,
170 pub deferred: Vec<String>,
172}
173
174impl ApplyReport {
175 #[must_use]
177 pub const fn all_adopted() -> Self {
178 Self { adopted: Vec::new(), deferred: Vec::new() }
179 }
180
181 #[must_use]
183 pub fn no_deferred(&self) -> bool {
184 self.deferred.is_empty()
185 }
186}
187
188pub trait Configurable {
202 fn config_ns(&self) -> &str;
204
205 fn apply_config(&self, cfg: &SectionMap) -> Result<ApplyReport, String>;
207
208 fn dump_config(&self) -> SectionMap;
210}
211
212#[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 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 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 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 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 let m2 = SectionMap::new(1).set("addr", Field::plain("tcp://x"));
306 assert!(format!("{m2:?}").contains("tcp://x"));
307 }
308}