use std::collections::BTreeMap;
use std::sync::Arc;
use parking_lot::Mutex;
use rskit_errors::AppResult;
use rskit_util::SecretString;
use super::ConfigSink;
#[derive(Debug, Default)]
struct State {
values: BTreeMap<String, SecretString>,
}
#[cfg_attr(
feature = "watch",
doc = " [`ConfigWatch`](crate::ConfigWatch): every [`set`](ConfigSink::set) and"
)]
#[cfg_attr(
not(feature = "watch"),
doc = " `ConfigWatch`: every [`set`](ConfigSink::set) and"
)]
#[derive(Debug, Clone, Default)]
pub struct InMemoryConfigSink {
state: Arc<Mutex<State>>,
#[cfg(feature = "watch")]
broadcaster: rskit_stream::Broadcaster<crate::watch::ConfigChange>,
}
impl InMemoryConfigSink {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get(&self, key: &str) -> Option<SecretString> {
self.state.lock().values.get(key).cloned()
}
#[must_use]
pub fn len(&self) -> usize {
self.state.lock().values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.state.lock().values.is_empty()
}
}
impl ConfigSink for InMemoryConfigSink {
fn set(&self, key: &str, value: SecretString) -> AppResult<()> {
self.state.lock().values.insert(key.to_string(), value);
#[cfg(feature = "watch")]
self.broadcaster
.broadcast(&crate::watch::ConfigChange::Set {
key: key.to_string(),
});
Ok(())
}
fn remove(&self, key: &str) -> AppResult<()> {
let removed = self.state.lock().values.remove(key).is_some();
#[cfg(feature = "watch")]
if removed {
self.broadcaster
.broadcast(&crate::watch::ConfigChange::Removed {
key: key.to_string(),
});
}
#[cfg(not(feature = "watch"))]
let _ = removed;
Ok(())
}
}
#[cfg(feature = "watch")]
impl crate::watch::ConfigWatch for InMemoryConfigSink {
fn watch(
&self,
cancel: tokio_util::sync::CancellationToken,
) -> AppResult<crate::watch::ConfigChangeStream> {
Ok(self.broadcaster.subscribe(cancel))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_then_get_round_trips_plaintext() {
let sink = InMemoryConfigSink::new();
sink.set("api_token", SecretString::new("s3cret")).unwrap();
assert_eq!(sink.get("api_token").unwrap().expose(), "s3cret");
assert_eq!(sink.len(), 1);
}
#[test]
fn set_replaces_existing_value() {
let sink = InMemoryConfigSink::new();
sink.set("k", SecretString::new("old")).unwrap();
sink.set("k", SecretString::new("new")).unwrap();
assert_eq!(sink.get("k").unwrap().expose(), "new");
assert_eq!(sink.len(), 1);
}
#[test]
fn remove_deletes_key_and_is_idempotent() {
let sink = InMemoryConfigSink::new();
sink.set("k", SecretString::new("v")).unwrap();
sink.remove("k").unwrap();
assert!(sink.get("k").is_none());
sink.remove("k").unwrap();
assert!(sink.is_empty());
}
#[test]
fn set_many_applies_all_entries() {
let sink = InMemoryConfigSink::new();
sink.set_many(vec![
("a".to_string(), SecretString::new("1")),
("b".to_string(), SecretString::new("2")),
])
.unwrap();
assert_eq!(sink.get("a").unwrap().expose(), "1");
assert_eq!(sink.get("b").unwrap().expose(), "2");
}
#[test]
fn debug_does_not_leak_secret() {
let sink = InMemoryConfigSink::new();
sink.set("k", SecretString::new("topsecret")).unwrap();
let rendered = format!("{sink:?}");
assert!(!rendered.contains("topsecret"));
}
#[test]
fn clones_share_state() {
let sink = InMemoryConfigSink::new();
let clone = sink.clone();
sink.set("k", SecretString::new("v")).unwrap();
assert_eq!(clone.get("k").unwrap().expose(), "v");
}
}