1use crate::state::{PatchSink, State};
10use crate::{get_at_path, parse_path, DocCell, Path};
11use serde_json::Value;
12use std::collections::HashSet;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
17pub enum SealedStateError {
18 #[error("sealed state key already set: {0}")]
20 AlreadySet(String),
21 #[error("sealed state serialization error: {0}")]
23 SerializationError(String),
24}
25
26#[derive(Clone)]
44pub struct SealedState {
45 doc: Value,
46 doc_cell: DocCell,
47 sensitive_keys: HashSet<String>,
48}
49
50impl SealedState {
51 pub fn new() -> Self {
53 let doc = Value::Object(Default::default());
54 Self {
55 doc_cell: DocCell::new(doc.clone()),
56 doc,
57 sensitive_keys: HashSet::new(),
58 }
59 }
60
61 pub fn put_once(
63 &mut self,
64 key: impl Into<String>,
65 value: impl serde::Serialize,
66 ) -> Result<(), SealedStateError> {
67 let key = key.into();
68 let obj = self
69 .doc
70 .as_object_mut()
71 .expect("sealed state doc is object");
72 if obj.contains_key(&key) {
73 return Err(SealedStateError::AlreadySet(key));
74 }
75 let v = serde_json::to_value(value)
76 .map_err(|e| SealedStateError::SerializationError(e.to_string()))?;
77 obj.insert(key, v);
78 *self.doc_cell.get() = self.doc.clone();
80 Ok(())
81 }
82
83 pub fn put_sensitive_once(
85 &mut self,
86 key: impl Into<String>,
87 value: impl serde::Serialize,
88 ) -> Result<(), SealedStateError> {
89 let key = key.into();
90 self.put_once(key.clone(), value)?;
91 self.sensitive_keys.insert(key);
92 Ok(())
93 }
94
95 pub fn set(
97 &mut self,
98 key: impl Into<String>,
99 value: impl serde::Serialize,
100 ) -> Result<(), SealedStateError> {
101 self.put_once(key, value)
102 }
103
104 pub fn set_sensitive(
106 &mut self,
107 key: impl Into<String>,
108 value: impl serde::Serialize,
109 ) -> Result<(), SealedStateError> {
110 self.put_sensitive_once(key, value)
111 }
112
113 pub fn get<T: State>(&self) -> T::Ref<'_> {
118 T::state_ref(&self.doc_cell, Path::root(), PatchSink::read_only())
119 }
120
121 pub fn get_at<T: State>(&self, path: &str) -> T::Ref<'_> {
123 let base = parse_path(path);
124 T::state_ref(&self.doc_cell, base, PatchSink::read_only())
125 }
126
127 pub fn value(&self, key: &str) -> Option<&Value> {
129 self.doc.as_object().and_then(|obj| obj.get(key))
130 }
131
132 pub fn value_at(&self, path: &str) -> Option<&Value> {
134 if path.is_empty() {
135 return Some(&self.doc);
136 }
137 let p = parse_path(path);
138 get_at_path(&self.doc, &p)
139 }
140
141 pub fn is_sensitive(&self, key: &str) -> bool {
143 self.sensitive_keys.contains(key)
144 }
145
146 pub fn contains_key(&self, key: &str) -> bool {
148 self.doc
149 .as_object()
150 .is_some_and(|obj| obj.contains_key(key))
151 }
152}
153
154impl Default for SealedState {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl std::fmt::Debug for SealedState {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 let mut map = f.debug_map();
163 if let Some(obj) = self.doc.as_object() {
164 for (k, v) in obj {
165 if self.sensitive_keys.contains(k) {
166 map.entry(k, &"[REDACTED]");
167 } else {
168 map.entry(k, v);
169 }
170 }
171 }
172 map.finish()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use serde_json::json;
180
181 #[test]
182 fn test_scope_new_is_empty() {
183 let rt = SealedState::new();
184 assert!(!rt.contains_key("anything"));
185 assert!(rt.value("anything").is_none());
186 }
187
188 #[test]
189 fn test_scope_default() {
190 let rt = SealedState::default();
191 assert!(!rt.contains_key("x"));
192 }
193
194 #[test]
195 fn test_set_once_success() {
196 let mut rt = SealedState::new();
197 rt.set("user_id", "u123").unwrap();
198 assert_eq!(rt.value("user_id"), Some(&json!("u123")));
199 }
200
201 #[test]
202 fn test_set_once_duplicate_fails() {
203 let mut rt = SealedState::new();
204 rt.set("key", "first").unwrap();
205 let err = rt.set("key", "second").unwrap_err();
206 assert!(matches!(err, SealedStateError::AlreadySet(k) if k == "key"));
207 assert_eq!(rt.value("key"), Some(&json!("first")));
209 }
210
211 #[test]
212 fn test_set_sensitive() {
213 let mut rt = SealedState::new();
214 rt.set_sensitive("token", "secret-abc").unwrap();
215 assert!(rt.is_sensitive("token"));
216 assert_eq!(rt.value("token"), Some(&json!("secret-abc")));
217 }
218
219 #[test]
220 fn test_set_sensitive_duplicate_fails() {
221 let mut rt = SealedState::new();
222 rt.set_sensitive("token", "first").unwrap();
223 let err = rt.set_sensitive("token", "second").unwrap_err();
224 assert!(matches!(err, SealedStateError::AlreadySet(_)));
225 }
226
227 #[test]
228 fn test_non_sensitive_key() {
229 let rt = SealedState::new();
230 assert!(!rt.is_sensitive("anything"));
231 }
232
233 #[test]
234 fn test_debug_redacts_sensitive() {
235 let mut rt = SealedState::new();
236 rt.set("user_id", "u123").unwrap();
237 rt.set_sensitive("token", "secret").unwrap();
238
239 let debug = format!("{:?}", rt);
240 assert!(debug.contains("u123"));
241 assert!(debug.contains("[REDACTED]"));
242 assert!(!debug.contains("secret"));
243 }
244
245 #[test]
246 fn test_clone() {
247 let mut rt = SealedState::new();
248 rt.set("a", 1).unwrap();
249 rt.set_sensitive("b", "secret").unwrap();
250
251 let rt2 = rt.clone();
252 assert_eq!(rt2.value("a"), Some(&json!(1)));
253 assert!(rt2.is_sensitive("b"));
254 }
255
256 #[test]
257 fn test_value_at_path() {
258 let mut rt = SealedState::new();
259 rt.set("config", json!({"nested": {"value": 42}})).unwrap();
260 assert_eq!(rt.value_at("config.nested.value"), Some(&json!(42)));
261 assert_eq!(rt.value_at("config.missing"), None);
262 }
263
264 #[test]
265 fn test_value_at_empty_path() {
266 let mut rt = SealedState::new();
267 rt.set("key", "val").unwrap();
268 let root = rt.value_at("").unwrap();
270 assert!(root.is_object());
271 }
272
273 #[test]
274 fn test_set_various_types() {
275 let mut rt = SealedState::new();
276 rt.set("string", "hello").unwrap();
277 rt.set("number", 42).unwrap();
278 rt.set("bool", true).unwrap();
279 rt.set("array", vec![1, 2, 3]).unwrap();
280
281 assert_eq!(rt.value("string"), Some(&json!("hello")));
282 assert_eq!(rt.value("number"), Some(&json!(42)));
283 assert_eq!(rt.value("bool"), Some(&json!(true)));
284 assert_eq!(rt.value("array"), Some(&json!([1, 2, 3])));
285 }
286
287 #[test]
288 fn test_contains_key() {
289 let mut rt = SealedState::new();
290 assert!(!rt.contains_key("x"));
291 rt.set("x", 1).unwrap();
292 assert!(rt.contains_key("x"));
293 }
294}