rskit_config/sink/
memory.rs1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use parking_lot::Mutex;
5use rskit_errors::AppResult;
6use rskit_util::SecretString;
7
8use super::ConfigSink;
9
10#[derive(Debug, Default)]
12struct State {
13 values: BTreeMap<String, SecretString>,
14}
15
16#[cfg_attr(
24 feature = "watch",
25 doc = " [`ConfigWatch`](crate::ConfigWatch): every [`set`](ConfigSink::set) and"
26)]
27#[cfg_attr(
28 not(feature = "watch"),
29 doc = " `ConfigWatch`: every [`set`](ConfigSink::set) and"
30)]
31#[derive(Debug, Clone, Default)]
33pub struct InMemoryConfigSink {
34 state: Arc<Mutex<State>>,
35 #[cfg(feature = "watch")]
36 broadcaster: rskit_stream::Broadcaster<crate::watch::ConfigChange>,
37}
38
39impl InMemoryConfigSink {
40 #[must_use]
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 #[must_use]
51 pub fn get(&self, key: &str) -> Option<SecretString> {
52 self.state.lock().values.get(key).cloned()
53 }
54
55 #[must_use]
57 pub fn len(&self) -> usize {
58 self.state.lock().values.len()
59 }
60
61 #[must_use]
63 pub fn is_empty(&self) -> bool {
64 self.state.lock().values.is_empty()
65 }
66}
67
68impl ConfigSink for InMemoryConfigSink {
69 fn set(&self, key: &str, value: SecretString) -> AppResult<()> {
70 self.state.lock().values.insert(key.to_string(), value);
71 #[cfg(feature = "watch")]
72 self.broadcaster
73 .broadcast(&crate::watch::ConfigChange::Set {
74 key: key.to_string(),
75 });
76 Ok(())
77 }
78
79 fn remove(&self, key: &str) -> AppResult<()> {
80 let removed = self.state.lock().values.remove(key).is_some();
81 #[cfg(feature = "watch")]
84 if removed {
85 self.broadcaster
86 .broadcast(&crate::watch::ConfigChange::Removed {
87 key: key.to_string(),
88 });
89 }
90 #[cfg(not(feature = "watch"))]
91 let _ = removed;
92 Ok(())
93 }
94}
95
96#[cfg(feature = "watch")]
97impl crate::watch::ConfigWatch for InMemoryConfigSink {
98 fn watch(
99 &self,
100 cancel: tokio_util::sync::CancellationToken,
101 ) -> AppResult<crate::watch::ConfigChangeStream> {
102 Ok(self.broadcaster.subscribe(cancel))
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn set_then_get_round_trips_plaintext() {
112 let sink = InMemoryConfigSink::new();
113 sink.set("api_token", SecretString::new("s3cret")).unwrap();
114 assert_eq!(sink.get("api_token").unwrap().expose(), "s3cret");
115 assert_eq!(sink.len(), 1);
116 }
117
118 #[test]
119 fn set_replaces_existing_value() {
120 let sink = InMemoryConfigSink::new();
121 sink.set("k", SecretString::new("old")).unwrap();
122 sink.set("k", SecretString::new("new")).unwrap();
123 assert_eq!(sink.get("k").unwrap().expose(), "new");
124 assert_eq!(sink.len(), 1);
125 }
126
127 #[test]
128 fn remove_deletes_key_and_is_idempotent() {
129 let sink = InMemoryConfigSink::new();
130 sink.set("k", SecretString::new("v")).unwrap();
131 sink.remove("k").unwrap();
132 assert!(sink.get("k").is_none());
133 sink.remove("k").unwrap();
134 assert!(sink.is_empty());
135 }
136
137 #[test]
138 fn set_many_applies_all_entries() {
139 let sink = InMemoryConfigSink::new();
140 sink.set_many(vec![
141 ("a".to_string(), SecretString::new("1")),
142 ("b".to_string(), SecretString::new("2")),
143 ])
144 .unwrap();
145 assert_eq!(sink.get("a").unwrap().expose(), "1");
146 assert_eq!(sink.get("b").unwrap().expose(), "2");
147 }
148
149 #[test]
150 fn debug_does_not_leak_secret() {
151 let sink = InMemoryConfigSink::new();
152 sink.set("k", SecretString::new("topsecret")).unwrap();
153 let rendered = format!("{sink:?}");
154 assert!(!rendered.contains("topsecret"));
155 }
156
157 #[test]
158 fn clones_share_state() {
159 let sink = InMemoryConfigSink::new();
160 let clone = sink.clone();
161 sink.set("k", SecretString::new("v")).unwrap();
162 assert_eq!(clone.get("k").unwrap().expose(), "v");
163 }
164}