1use std::sync::{Arc, Mutex};
4
5use rainier_support::Result;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9
10pub const TOKEN_KEY: &str = "_token";
12
13pub fn generate_session_id() -> String {
19 use rand::RngCore;
20
21 let mut bytes = [0u8; 32];
22 rand::rngs::OsRng.fill_bytes(&mut bytes);
23 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
28pub struct SessionData {
29 #[serde(default)]
31 pub values: Map<String, Value>,
32
33 #[serde(default)]
36 pub flash: Vec<String>,
37}
38
39#[derive(Debug)]
40struct State {
41 id: String,
42 values: Map<String, Value>,
43 flash_old: Vec<String>,
46 flash_new: Vec<String>,
48 dirty: bool,
49 superseded: Vec<String>,
52}
53
54#[derive(Clone)]
63pub struct Session {
64 state: Arc<Mutex<State>>,
65}
66
67impl Session {
68 pub fn new() -> Self {
70 Self::restore(generate_session_id(), SessionData::default())
71 }
72
73 pub fn restore(id: impl Into<String>, data: SessionData) -> Self {
75 Self {
76 state: Arc::new(Mutex::new(State {
77 id: id.into(),
78 values: data.values,
79 flash_old: data.flash,
80 flash_new: Vec::new(),
81 dirty: false,
82 superseded: Vec::new(),
83 })),
84 }
85 }
86
87 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
88 self.state.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
92 }
93
94 pub fn id(&self) -> String {
96 self.lock().id.clone()
97 }
98
99 pub fn is_dirty(&self) -> bool {
101 self.lock().dirty
102 }
103
104 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
108 serde_json::from_value(self.lock().values.get(key)?.clone()).ok()
109 }
110
111 pub fn string(&self, key: &str) -> Option<String> {
113 match self.lock().values.get(key)? {
114 Value::String(value) => Some(value.clone()),
115 other => Some(other.to_string()),
116 }
117 }
118
119 pub fn value(&self, key: &str) -> Option<Value> {
121 self.lock().values.get(key).cloned()
122 }
123
124 pub fn has(&self, key: &str) -> bool {
126 matches!(self.lock().values.get(key), Some(value) if !value.is_null())
127 }
128
129 pub fn all(&self) -> Map<String, Value> {
131 self.lock().values.clone()
132 }
133
134 pub fn len(&self) -> usize {
136 self.lock().values.len()
137 }
138
139 pub fn is_empty(&self) -> bool {
141 self.len() == 0
142 }
143
144 pub fn put(&self, key: impl Into<String>, value: impl Serialize) -> Result<()> {
148 let value = serde_json::to_value(value)?;
149 let mut state = self.lock();
150 state.values.insert(key.into(), value);
151 state.dirty = true;
152 Ok(())
153 }
154
155 pub fn pull<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
157 let mut state = self.lock();
158 let value = state.values.remove(key)?;
159 state.dirty = true;
160 serde_json::from_value(value).ok()
161 }
162
163 pub fn forget(&self, key: &str) {
165 let mut state = self.lock();
166 if state.values.remove(key).is_some() {
167 state.dirty = true;
168 }
169 }
170
171 pub fn flush(&self) {
173 let mut state = self.lock();
174 state.values.clear();
175 state.flash_new.clear();
176 state.flash_old.clear();
177 state.dirty = true;
178 }
179
180 pub fn flash(&self, key: impl Into<String>, value: impl Serialize) -> Result<()> {
188 let key = key.into();
189 let value = serde_json::to_value(value)?;
190
191 let mut state = self.lock();
192 state.values.insert(key.clone(), value);
193 if !state.flash_new.contains(&key) {
194 state.flash_new.push(key);
195 }
196 state.dirty = true;
197 Ok(())
198 }
199
200 pub fn keep(&self, keys: &[&str]) {
205 let mut state = self.lock();
206 for key in keys {
207 let key = (*key).to_string();
208 if state.flash_old.contains(&key) && !state.flash_new.contains(&key) {
209 state.flash_new.push(key);
210 state.dirty = true;
211 }
212 }
213 }
214
215 pub fn reflash(&self) {
217 let mut state = self.lock();
218 let carried: Vec<String> =
219 state.flash_old.iter().filter(|key| !state.flash_new.contains(key)).cloned().collect();
220
221 if !carried.is_empty() {
222 state.flash_new.extend(carried);
223 state.dirty = true;
224 }
225 }
226
227 pub fn token(&self) -> String {
231 if let Some(Value::String(token)) = self.lock().values.get(TOKEN_KEY) {
232 return token.clone();
233 }
234
235 let token = generate_session_id();
236 let mut state = self.lock();
237 state.values.insert(TOKEN_KEY.to_string(), Value::String(token.clone()));
238 state.dirty = true;
239 token
240 }
241
242 pub fn regenerate(&self) {
249 let mut state = self.lock();
250 let previous = std::mem::replace(&mut state.id, generate_session_id());
251 state.superseded.push(previous);
252 state.dirty = true;
253 }
254
255 pub fn invalidate(&self) {
257 self.flush();
258 self.regenerate();
259 }
260
261 pub fn superseded_ids(&self) -> Vec<String> {
265 self.lock().superseded.clone()
266 }
267
268 pub fn age_and_take(&self) -> SessionData {
274 let mut state = self.lock();
275
276 let expired: Vec<String> =
277 state.flash_old.iter().filter(|key| !state.flash_new.contains(key)).cloned().collect();
278
279 for key in expired {
280 state.values.remove(&key);
281 }
282
283 let flash = std::mem::take(&mut state.flash_new);
284 state.flash_old = flash.clone();
285
286 SessionData { values: state.values.clone(), flash }
287 }
288}
289
290impl Default for Session {
291 fn default() -> Self {
292 Self::new()
293 }
294}
295
296impl std::fmt::Debug for Session {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 let state = self.lock();
301 f.debug_struct("Session")
302 .field("id", &"<redacted>")
303 .field("values", &state.values.len())
304 .field("dirty", &state.dirty)
305 .finish()
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn ids_are_long_and_unpredictable() {
315 let a = generate_session_id();
316 let b = generate_session_id();
317
318 assert_eq!(a.len(), 64, "256 bits, hex-encoded");
319 assert_ne!(a, b);
320 assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
321 }
322
323 #[test]
324 fn values_round_trip() {
325 let session = Session::new();
326 session.put("user_id", 42u64).unwrap();
327 session.put("name", "Ada").unwrap();
328
329 assert_eq!(session.get::<u64>("user_id"), Some(42));
330 assert_eq!(session.string("name").as_deref(), Some("Ada"));
331 assert!(session.has("user_id"));
332 assert_eq!(session.len(), 2);
333 }
334
335 #[test]
336 fn a_fresh_session_is_not_dirty() {
337 assert!(!Session::new().is_dirty(), "nothing to write back yet");
338 }
339
340 #[test]
341 fn writing_marks_it_dirty_and_reading_does_not() {
342 let session = Session::restore("id", SessionData::default());
343 assert!(!session.is_dirty());
344
345 session.get::<u64>("absent");
346 session.has("absent");
347 assert!(!session.is_dirty(), "reads must not force a write");
348
349 session.put("x", 1).unwrap();
350 assert!(session.is_dirty());
351 }
352
353 #[test]
354 fn forgetting_an_absent_key_is_not_a_change() {
355 let session = Session::restore("id", SessionData::default());
356 session.forget("absent");
357
358 assert!(!session.is_dirty());
359 }
360
361 #[test]
362 fn pull_reads_and_removes() {
363 let session = Session::new();
364 session.put("once", "value").unwrap();
365
366 assert_eq!(session.pull::<String>("once").as_deref(), Some("value"));
367 assert!(!session.has("once"));
368 }
369
370 #[test]
371 fn flushing_empties_it_but_keeps_the_id() {
372 let session = Session::new();
373 let id = session.id();
374 session.put("a", 1).unwrap();
375
376 session.flush();
377
378 assert!(session.is_empty());
379 assert_eq!(session.id(), id);
380 }
381
382 #[test]
383 fn a_flashed_value_survives_exactly_one_more_request() {
384 let session = Session::new();
386 session.flash("status", "Saved.").unwrap();
387 assert_eq!(session.string("status").as_deref(), Some("Saved."), "readable immediately");
388 let after_one = session.age_and_take();
389
390 let session = Session::restore("id", after_one);
392 assert_eq!(session.string("status").as_deref(), Some("Saved."));
393 let after_two = session.age_and_take();
394
395 let session = Session::restore("id", after_two);
397 assert!(!session.has("status"), "flash data must not linger");
398 }
399
400 #[test]
401 fn keep_rescues_a_flashed_value_for_one_more_request() {
402 let session = Session::new();
403 session.flash("status", "Saved.").unwrap();
404 let data = session.age_and_take();
405
406 let session = Session::restore("id", data);
408 session.keep(&["status"]);
409 let data = session.age_and_take();
410
411 let session = Session::restore("id", data);
412 assert_eq!(session.string("status").as_deref(), Some("Saved."));
413 }
414
415 #[test]
416 fn reflash_keeps_everything() {
417 let session = Session::new();
418 session.flash("a", 1).unwrap();
419 session.flash("b", 2).unwrap();
420 let data = session.age_and_take();
421
422 let session = Session::restore("id", data);
423 session.reflash();
424 let data = session.age_and_take();
425
426 let session = Session::restore("id", data);
427 assert!(session.has("a") && session.has("b"));
428 }
429
430 #[test]
431 fn keeping_an_unflashed_key_does_nothing() {
432 let session = Session::new();
433 session.put("permanent", 1).unwrap();
434 session.keep(&["permanent"]);
435
436 let data = session.age_and_take();
437 assert!(data.values.contains_key("permanent"));
438 assert!(data.flash.is_empty(), "a normal value is not flash data");
439 }
440
441 #[test]
442 fn a_permanent_value_is_not_aged_out() {
443 let session = Session::new();
444 session.put("user_id", 42u64).unwrap();
445 session.flash("status", "hi").unwrap();
446
447 let data = session.age_and_take();
448 let session = Session::restore("id", data);
449 let data = session.age_and_take();
450
451 assert!(data.values.contains_key("user_id"));
452 assert!(!data.values.contains_key("status"));
453 }
454
455 #[test]
456 fn the_token_is_minted_once_and_then_stable() {
457 let session = Session::new();
458 let first = session.token();
459
460 assert_eq!(session.token(), first, "a second read must not rotate it");
461 assert_eq!(first.len(), 64);
462 assert!(session.is_dirty(), "minting it needs writing back");
463 }
464
465 #[test]
466 fn regenerating_changes_the_id_and_keeps_the_data() {
467 let session = Session::new();
468 let before = session.id();
469 session.put("user_id", 42u64).unwrap();
470
471 session.regenerate();
472
473 assert_ne!(session.id(), before);
474 assert_eq!(session.get::<u64>("user_id"), Some(42), "login must not lose the session");
475 assert_eq!(session.superseded_ids(), vec![before], "the old row should be deleted");
476 }
477
478 #[test]
479 fn invalidating_changes_the_id_and_drops_the_data() {
480 let session = Session::new();
481 let before = session.id();
482 session.put("user_id", 42u64).unwrap();
483
484 session.invalidate();
485
486 assert_ne!(session.id(), before);
487 assert!(session.is_empty());
488 assert_eq!(session.superseded_ids(), vec![before]);
489 }
490
491 #[test]
492 fn clones_share_one_bag() {
493 let session = Session::new();
494 let handle = session.clone();
495
496 handle.put("written-through-the-clone", true).unwrap();
497
498 assert!(session.has("written-through-the-clone"));
499 }
500
501 #[test]
502 fn debug_does_not_disclose_the_id_or_the_values() {
503 let session = Session::new();
504 session.put("secret", "hunter2").unwrap();
505
506 let rendered = format!("{session:?}");
507 assert!(!rendered.contains("hunter2"), "{rendered}");
508 assert!(!rendered.contains(&session.id()), "{rendered}");
509 }
510
511 #[test]
512 fn data_round_trips_through_json() {
513 let session = Session::new();
514 session.put("user_id", 42u64).unwrap();
515 session.flash("status", "Saved.").unwrap();
516
517 let data = session.age_and_take();
518 let encoded = serde_json::to_string(&data).unwrap();
519 let decoded: SessionData = serde_json::from_str(&encoded).unwrap();
520
521 assert_eq!(decoded, data);
522 }
523}