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(crate) fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
73 self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
74 }
75
76 pub fn get(&self, key: &str) -> Option<&str> {
78 self.0.get(key).map(String::as_str)
79 }
80}
81
82impl fmt::Display for Config {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 for (i, (k, v)) in self.0.iter().enumerate() {
85 if i > 0 {
86 f.write_str(";")?;
87 }
88 write!(f, "{k}={v}")?;
89 }
90 Ok(())
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn round_trips_and_sorts() {
100 let c = Config::parse("schema=none;collation=utf8mb4_bin").unwrap();
101 assert_eq!(c.to_string(), "collation=utf8mb4_bin;schema=none");
102 assert_eq!(Config::parse(&c.to_string()).unwrap(), c);
103 assert_eq!(Config::parse("").unwrap(), Config::new());
104 }
105
106 #[test]
107 fn rejects_malformed_pieces() {
108 assert_eq!(Config::parse("a"), Err(ConfigError::BadPair("a".into())));
109 assert_eq!(Config::parse("=v"), Err(ConfigError::BadPair("=v".into())));
110 assert_eq!(
111 Config::parse("a=1;a=2"),
112 Err(ConfigError::DuplicateKey("a".into()))
113 );
114 }
115
116 #[test]
117 fn holds_in_is_a_subset_test_with_negative_controls() {
118 let row = Config::parse("collation=utf8mb4_bin").unwrap();
119 let actual = Config::parse("collation=utf8mb4_bin;schema=none").unwrap();
120 assert!(row.holds_in(&actual));
121 assert!(Config::new().holds_in(&actual));
122 let other = Config::parse("collation=utf8mb4_0900_ai_ci").unwrap();
123 assert!(
124 !row.holds_in(&other),
125 "control: a different value must not match"
126 );
127 assert!(
128 !row.holds_in(&Config::new()),
129 "control: an absent fact must not match"
130 );
131 }
132}