1use std::collections::HashMap;
16use std::sync::Mutex;
17use std::time::{Duration, Instant};
18
19#[derive(Debug, Clone, Default)]
21pub struct ResolvedContext {
22 values: HashMap<String, String>,
23}
24
25impl ResolvedContext {
26 pub fn get(&self, key: &str) -> Option<&str> {
27 self.values.get(key).map(String::as_str)
28 }
29 pub fn contains(&self, key: &str) -> bool {
30 self.values.contains_key(key)
31 }
32}
33
34struct Overlay {
35 values: HashMap<String, String>,
36 expires_at: Option<Instant>,
37}
38
39pub struct ContextStore {
41 base: HashMap<String, String>,
42 overlay: Mutex<Option<Overlay>>,
43}
44
45impl ContextStore {
46 pub fn new(base: HashMap<String, String>) -> Self {
48 Self {
49 base,
50 overlay: Mutex::new(None),
51 }
52 }
53
54 pub fn push(&self, values: HashMap<String, String>, ttl: Option<Duration>) {
56 let expires_at = ttl.map(|d| Instant::now() + d);
57 *self.overlay.lock().unwrap() = Some(Overlay { values, expires_at });
58 }
59
60 pub fn clear(&self) {
62 *self.overlay.lock().unwrap() = None;
63 }
64
65 pub fn resolve(&self) -> ResolvedContext {
66 self.resolve_at(Instant::now())
67 }
68
69 pub fn resolve_at(&self, now: Instant) -> ResolvedContext {
71 let mut values = self.base.clone();
72 let mut guard = self.overlay.lock().unwrap();
73 if let Some(o) = guard.as_ref() {
74 if o.expires_at.map(|e| now >= e).unwrap_or(false) {
75 *guard = None; } else {
77 for (k, v) in &o.values {
78 values.insert(k.clone(), v.clone());
79 }
80 }
81 }
82 ResolvedContext { values }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn base() -> HashMap<String, String> {
91 HashMap::from([("org".to_string(), "base-org".to_string())])
92 }
93
94 #[test]
95 fn resolves_base_when_no_overlay() {
96 let s = ContextStore::new(base());
97 let c = s.resolve();
98 assert_eq!(c.get("org"), Some("base-org"));
99 assert_eq!(c.get("missing"), None);
100 }
101
102 #[test]
103 fn live_overlay_overrides_base() {
104 let s = ContextStore::new(base());
105 s.push(
106 HashMap::from([
107 ("org".into(), "pushed".into()),
108 ("workspace".into(), "ws".into()),
109 ]),
110 Some(Duration::from_secs(3600)),
111 );
112 let c = s.resolve();
113 assert_eq!(c.get("org"), Some("pushed")); assert_eq!(c.get("workspace"), Some("ws"));
115 }
116
117 #[test]
118 fn expired_overlay_reverts_to_base() {
119 let s = ContextStore::new(base());
120 let now = Instant::now();
121 s.push(
122 HashMap::from([("org".into(), "pushed".into())]),
123 Some(Duration::from_secs(10)),
124 );
125 let c = s.resolve_at(now + Duration::from_secs(20));
127 assert_eq!(c.get("org"), Some("base-org"));
128 }
129
130 #[test]
131 fn clear_drops_overlay() {
132 let s = ContextStore::new(base());
133 s.push(HashMap::from([("org".into(), "pushed".into())]), None);
134 s.clear();
135 assert_eq!(s.resolve().get("org"), Some("base-org"));
136 }
137}