use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Default)]
pub struct ResolvedContext {
values: HashMap<String, String>,
}
impl ResolvedContext {
pub fn get(&self, key: &str) -> Option<&str> {
self.values.get(key).map(String::as_str)
}
pub fn contains(&self, key: &str) -> bool {
self.values.contains_key(key)
}
}
struct Overlay {
values: HashMap<String, String>,
expires_at: Option<Instant>,
}
pub struct ContextStore {
base: HashMap<String, String>,
overlay: Mutex<Option<Overlay>>,
}
impl ContextStore {
pub fn new(base: HashMap<String, String>) -> Self {
Self {
base,
overlay: Mutex::new(None),
}
}
pub fn push(&self, values: HashMap<String, String>, ttl: Option<Duration>) {
let expires_at = ttl.map(|d| Instant::now() + d);
*self.overlay.lock().unwrap() = Some(Overlay { values, expires_at });
}
pub fn clear(&self) {
*self.overlay.lock().unwrap() = None;
}
pub fn resolve(&self) -> ResolvedContext {
self.resolve_at(Instant::now())
}
pub fn resolve_at(&self, now: Instant) -> ResolvedContext {
let mut values = self.base.clone();
let mut guard = self.overlay.lock().unwrap();
if let Some(o) = guard.as_ref() {
if o.expires_at.map(|e| now >= e).unwrap_or(false) {
*guard = None; } else {
for (k, v) in &o.values {
values.insert(k.clone(), v.clone());
}
}
}
ResolvedContext { values }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> HashMap<String, String> {
HashMap::from([("org".to_string(), "base-org".to_string())])
}
#[test]
fn resolves_base_when_no_overlay() {
let s = ContextStore::new(base());
let c = s.resolve();
assert_eq!(c.get("org"), Some("base-org"));
assert_eq!(c.get("missing"), None);
}
#[test]
fn live_overlay_overrides_base() {
let s = ContextStore::new(base());
s.push(
HashMap::from([
("org".into(), "pushed".into()),
("workspace".into(), "ws".into()),
]),
Some(Duration::from_secs(3600)),
);
let c = s.resolve();
assert_eq!(c.get("org"), Some("pushed")); assert_eq!(c.get("workspace"), Some("ws"));
}
#[test]
fn expired_overlay_reverts_to_base() {
let s = ContextStore::new(base());
let now = Instant::now();
s.push(
HashMap::from([("org".into(), "pushed".into())]),
Some(Duration::from_secs(10)),
);
let c = s.resolve_at(now + Duration::from_secs(20));
assert_eq!(c.get("org"), Some("base-org"));
}
#[test]
fn clear_drops_overlay() {
let s = ContextStore::new(base());
s.push(HashMap::from([("org".into(), "pushed".into())]), None);
s.clear();
assert_eq!(s.resolve().get("org"), Some("base-org"));
}
}