1use std::collections::BTreeMap;
2use std::sync::Mutex;
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct FlashEntry {
12 pub level: String,
14 pub message: String,
16}
17
18pub(crate) struct FlashState {
19 pub(crate) incoming: Vec<FlashEntry>,
20 pub(crate) outgoing: Mutex<Vec<FlashEntry>>,
21 pub(crate) read: AtomicBool,
22}
23
24impl FlashState {
25 pub(crate) fn new(incoming: Vec<FlashEntry>) -> Self {
26 Self {
27 incoming,
28 outgoing: Mutex::new(Vec::new()),
29 read: AtomicBool::new(false),
30 }
31 }
32
33 pub(crate) fn push(&self, level: &str, message: &str) {
34 let mut outgoing = self.outgoing.lock().expect("flash mutex poisoned");
35 outgoing.push(FlashEntry {
36 level: level.to_string(),
37 message: message.to_string(),
38 });
39 }
40
41 pub(crate) fn drain_outgoing(&self) -> Vec<FlashEntry> {
42 let mut outgoing = self.outgoing.lock().expect("flash mutex poisoned");
43 std::mem::take(&mut *outgoing)
44 }
45
46 pub(crate) fn was_read(&self) -> bool {
47 self.read.load(Ordering::Acquire)
48 }
49
50 pub(crate) fn mark_read(&self) {
51 self.read.store(true, Ordering::Release);
52 }
53
54 #[cfg_attr(not(feature = "templates"), allow(dead_code))]
55 pub(crate) fn incoming_as_template_value(&self) -> Vec<BTreeMap<String, String>> {
56 self.incoming
57 .iter()
58 .map(|entry| {
59 let mut map = BTreeMap::new();
60 map.insert(entry.level.clone(), entry.message.clone());
61 map
62 })
63 .collect()
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn new_with_empty_incoming() {
73 let state = FlashState::new(vec![]);
74 assert!(state.incoming.is_empty());
75 assert!(!state.was_read());
76 }
77
78 #[test]
79 fn new_with_incoming_entries() {
80 let entries = vec![
81 FlashEntry {
82 level: "success".into(),
83 message: "Done".into(),
84 },
85 FlashEntry {
86 level: "error".into(),
87 message: "Oops".into(),
88 },
89 ];
90 let state = FlashState::new(entries.clone());
91 assert_eq!(state.incoming, entries);
92 }
93
94 #[test]
95 fn push_adds_to_outgoing() {
96 let state = FlashState::new(vec![]);
97 state.push("info", "hello");
98 state.push("error", "fail");
99 let outgoing = state.drain_outgoing();
100 assert_eq!(outgoing.len(), 2);
101 assert_eq!(
102 outgoing[0],
103 FlashEntry {
104 level: "info".into(),
105 message: "hello".into()
106 }
107 );
108 assert_eq!(
109 outgoing[1],
110 FlashEntry {
111 level: "error".into(),
112 message: "fail".into()
113 }
114 );
115 }
116
117 #[test]
118 fn drain_outgoing_clears_vec() {
119 let state = FlashState::new(vec![]);
120 state.push("info", "msg");
121 let first = state.drain_outgoing();
122 assert_eq!(first.len(), 1);
123 let second = state.drain_outgoing();
124 assert!(second.is_empty());
125 }
126
127 #[test]
128 fn read_flag_default_false() {
129 let state = FlashState::new(vec![]);
130 assert!(!state.was_read());
131 }
132
133 #[test]
134 fn mark_read_sets_flag() {
135 let state = FlashState::new(vec![]);
136 state.mark_read();
137 assert!(state.was_read());
138 }
139
140 #[test]
141 fn multiple_same_level_preserved_in_order() {
142 let state = FlashState::new(vec![]);
143 state.push("error", "first");
144 state.push("error", "second");
145 state.push("info", "third");
146 let outgoing = state.drain_outgoing();
147 assert_eq!(outgoing.len(), 3);
148 assert_eq!(outgoing[0].level, "error");
149 assert_eq!(outgoing[0].message, "first");
150 assert_eq!(outgoing[1].level, "error");
151 assert_eq!(outgoing[1].message, "second");
152 assert_eq!(outgoing[2].level, "info");
153 }
154
155 #[test]
156 fn incoming_as_template_value_formats_correctly() {
157 let entries = vec![
158 FlashEntry {
159 level: "error".into(),
160 message: "bad".into(),
161 },
162 FlashEntry {
163 level: "info".into(),
164 message: "ok".into(),
165 },
166 ];
167 let state = FlashState::new(entries);
168 let result = state.incoming_as_template_value();
169 assert_eq!(result.len(), 2);
170 assert_eq!(result[0].get("error").unwrap(), "bad");
171 assert_eq!(result[1].get("info").unwrap(), "ok");
172 }
173
174 #[test]
175 fn flash_entry_serialization_roundtrip() {
176 let entry = FlashEntry {
177 level: "success".into(),
178 message: "Item saved".into(),
179 };
180 let json = serde_json::to_string(&entry).unwrap();
181 let parsed: FlashEntry = serde_json::from_str(&json).unwrap();
182 assert_eq!(entry, parsed);
183 }
184
185 #[test]
186 fn flash_entry_vec_serialization() {
187 let entries = vec![
188 FlashEntry {
189 level: "error".into(),
190 message: "fail".into(),
191 },
192 FlashEntry {
193 level: "success".into(),
194 message: "ok".into(),
195 },
196 ];
197 let json = serde_json::to_string(&entries).unwrap();
198 let parsed: Vec<FlashEntry> = serde_json::from_str(&json).unwrap();
199 assert_eq!(entries, parsed);
200 }
201}