use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Field {
Plain(String),
Secret {
key: String,
},
}
impl Field {
#[must_use]
pub fn plain(value: impl Into<String>) -> Self {
Self::Plain(value.into())
}
#[must_use]
pub fn secret(key: impl Into<String>) -> Self {
Self::Secret { key: key.into() }
}
#[must_use]
pub fn is_plain(&self) -> bool {
matches!(self, Self::Plain(_))
}
#[must_use]
pub fn plain_value(&self) -> Option<&str> {
match self {
Self::Plain(v) => Some(v.as_str()),
Self::Secret { .. } => None,
}
}
#[must_use]
pub fn secret_key(&self) -> Option<&str> {
match self {
Self::Secret { key } => Some(key.as_str()),
Self::Plain(_) => None,
}
}
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct SectionMap {
pub rev: u32,
entries: Vec<(String, Field)>,
}
impl SectionMap {
#[must_use]
pub fn new(rev: u32) -> Self {
Self {
rev,
entries: Vec::new(),
}
}
pub fn set(mut self, key: impl Into<String>, field: Field) -> Self {
let key = key.into();
match self.entries.iter_mut().find(|(k, _)| *k == key) {
Some(slot) => slot.1 = field,
None => self.entries.push((key, field)),
}
self
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&Field> {
self.entries
.iter()
.find(|(k, _)| k.as_str() == key)
.map(|(_, f)| f)
}
#[must_use]
pub fn get_plain(&self, key: &str) -> Option<&str> {
self.get(key).and_then(Field::plain_value)
}
#[must_use]
pub fn get_secret_key(&self, key: &str) -> Option<&str> {
self.get(key).and_then(Field::secret_key)
}
pub fn remove(&mut self, key: &str) -> Option<Field> {
let idx = self.entries.iter().position(|(k, _)| k.as_str() == key)?;
Some(self.entries.remove(idx).1)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Field)> {
self.entries.iter().map(|(k, f)| (k.as_str(), f))
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl fmt::Debug for SectionMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SectionMap")
.field("rev", &self.rev)
.field(
"entries",
&self
.entries
.iter()
.map(|(k, field)| match field {
Field::Plain(v) => (k.as_str(), v.as_str()),
Field::Secret { key } => (k.as_str(), key.as_str()),
})
.collect::<Vec<_>>(),
)
.finish()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ApplyReport {
pub adopted: Vec<String>,
pub deferred: Vec<String>,
}
impl ApplyReport {
#[must_use]
pub const fn all_adopted() -> Self {
Self {
adopted: Vec::new(),
deferred: Vec::new(),
}
}
#[must_use]
pub fn no_deferred(&self) -> bool {
self.deferred.is_empty()
}
}
pub trait Configurable {
fn config_ns(&self) -> &str;
fn apply_config(&self, cfg: &SectionMap) -> Result<ApplyReport, String>;
fn dump_config(&self) -> SectionMap;
}
#[must_use]
pub fn valid_config_ns(ns: &str) -> bool {
let b = ns.as_bytes();
if b.is_empty() || b.len() > 64 || !b[0].is_ascii_alphanumeric() {
return false;
}
if ns.starts_with('.') || ns.ends_with('.') || ns.contains("..") {
return false;
}
b[1..]
.iter()
.all(|c| c.is_ascii_alphanumeric() || *c == b'_' || *c == b'-' || *c == b'.')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t1_field_helpers() {
let p = Field::plain("x");
assert_eq!(p.plain_value(), Some("x"));
assert!(p.is_plain());
assert!(p.secret_key().is_none());
let s = Field::secret("k1");
assert_eq!(s.secret_key(), Some("k1"));
assert!(s.plain_value().is_none());
assert!(!s.is_plain());
}
#[test]
fn t2_set_get_remove_cover() {
let m = SectionMap::new(1)
.set("a", Field::plain("1"))
.set("b", Field::secret("bb"));
assert_eq!(m.get_plain("a"), Some("1"));
assert_eq!(m.get_secret_key("b"), Some("bb"));
assert!(m.get("c").is_none());
assert_eq!(m.len(), 2);
let m2 = m.clone().set("a", Field::plain("z"));
assert_eq!(m2.len(), 2);
assert_eq!(m2.get_plain("a"), Some("z"));
assert_eq!(m2.iter().next().map(|(k, _)| k), Some("a"));
let mut m3 = m2.clone();
assert_eq!(m3.remove("a"), Some(Field::plain("z")));
assert!(m3.get("a").is_none());
assert!(m3.remove("zz").is_none());
}
#[test]
fn t3_apply_report_default_and_helpers() {
let r = ApplyReport::all_adopted();
assert!(r.no_deferred());
let r2 = ApplyReport::default();
assert!(r2.no_deferred());
let r3 = ApplyReport {
adopted: vec!["a".into()],
deferred: vec!["b".into()],
};
assert!(!r3.no_deferred());
}
#[test]
fn t4_valid_config_ns_matrix() {
assert!(valid_config_ns("oms"));
assert!(valid_config_ns("ctp"));
assert!(valid_config_ns("ctp.ctp_i130_001"));
assert!(valid_config_ns("a1-b_c.d2"));
assert!(!valid_config_ns(""));
assert!(!valid_config_ns(&"a".repeat(65)));
assert!(!valid_config_ns("_oms"));
assert!(!valid_config_ns("-oms"));
assert!(!valid_config_ns(".oms"));
assert!(!valid_config_ns("oms."));
assert!(!valid_config_ns("a..b"));
assert!(!valid_config_ns("a/b"));
assert!(!valid_config_ns("a b"));
assert!(!valid_config_ns("a\\b"));
}
#[test]
fn t5_debug_never_leaks_secret_field_shape() {
let m = SectionMap::new(1).set("pw", Field::secret("k1"));
let d = format!("{m:?}");
assert!(d.contains("pw"));
assert!(d.contains("k1"));
let m2 = SectionMap::new(1).set("addr", Field::plain("tcp://x"));
assert!(format!("{m2:?}").contains("tcp://x"));
}
}