persistant_core/
config.rs1use std::collections::BTreeMap;
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum ConfigError {
9 BadPair(String),
11 DuplicateKey(String),
13}
14
15impl fmt::Display for ConfigError {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 ConfigError::BadPair(p) => write!(f, "config piece {p:?} is not key=value"),
19 ConfigError::DuplicateKey(k) => write!(f, "config key {k:?} appears twice"),
20 }
21 }
22}
23
24impl std::error::Error for ConfigError {}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct Config(BTreeMap<String, String>);
32
33impl Config {
34 pub fn new() -> Self {
36 Config::default()
37 }
38
39 pub fn parse(s: &str) -> Result<Self, ConfigError> {
41 let mut map = BTreeMap::new();
42 if s.is_empty() {
43 return Ok(Config(map));
44 }
45 for piece in s.split(';') {
46 match piece.split_once('=') {
47 Some((k, v)) if !k.is_empty() => {
48 if map.insert(k.to_string(), v.to_string()).is_some() {
49 return Err(ConfigError::DuplicateKey(k.to_string()));
50 }
51 }
52 _ => return Err(ConfigError::BadPair(piece.to_string())),
53 }
54 }
55 Ok(Config(map))
56 }
57
58 pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
60 self.0.insert(key.into(), value.into());
61 self
62 }
63
64 pub fn holds_in(&self, actual: &Config) -> bool {
68 self.0.iter().all(|(k, v)| actual.0.get(k) == Some(v))
69 }
70
71 pub fn get(&self, key: &str) -> Option<&str> {
73 self.0.get(key).map(String::as_str)
74 }
75}
76
77impl fmt::Display for Config {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 for (i, (k, v)) in self.0.iter().enumerate() {
80 if i > 0 {
81 f.write_str(";")?;
82 }
83 write!(f, "{k}={v}")?;
84 }
85 Ok(())
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn round_trips_and_sorts() {
95 let c = Config::parse("schema=none;collation=utf8mb4_bin").unwrap();
96 assert_eq!(c.to_string(), "collation=utf8mb4_bin;schema=none");
97 assert_eq!(Config::parse(&c.to_string()).unwrap(), c);
98 assert_eq!(Config::parse("").unwrap(), Config::new());
99 }
100
101 #[test]
102 fn rejects_malformed_pieces() {
103 assert_eq!(Config::parse("a"), Err(ConfigError::BadPair("a".into())));
104 assert_eq!(Config::parse("=v"), Err(ConfigError::BadPair("=v".into())));
105 assert_eq!(
106 Config::parse("a=1;a=2"),
107 Err(ConfigError::DuplicateKey("a".into()))
108 );
109 }
110
111 #[test]
112 fn holds_in_is_a_subset_test_with_negative_controls() {
113 let row = Config::parse("collation=utf8mb4_bin").unwrap();
114 let actual = Config::parse("collation=utf8mb4_bin;schema=none").unwrap();
115 assert!(row.holds_in(&actual));
116 assert!(Config::new().holds_in(&actual));
117 let other = Config::parse("collation=utf8mb4_0900_ai_ci").unwrap();
118 assert!(
119 !row.holds_in(&other),
120 "control: a different value must not match"
121 );
122 assert!(
123 !row.holds_in(&Config::new()),
124 "control: an absent fact must not match"
125 );
126 }
127}