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 pub(crate) fn incoming_as_template_value(&self) -> Vec<BTreeMap<String, String>> {
55 self.incoming
56 .iter()
57 .map(|entry| {
58 let mut map = BTreeMap::new();
59 map.insert(entry.level.clone(), entry.message.clone());
60 map
61 })
62 .collect()
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn new_with_empty_incoming() {
72 let state = FlashState::new(vec![]);
73 assert!(state.incoming.is_empty());
74 assert!(!state.was_read());
75 }
76
77 #[test]
78 fn new_with_incoming_entries() {
79 let entries = vec![
80 FlashEntry {
81 level: "success".into(),
82 message: "Done".into(),
83 },
84 FlashEntry {
85 level: "error".into(),
86 message: "Oops".into(),
87 },
88 ];
89 let state = FlashState::new(entries.clone());
90 assert_eq!(state.incoming, entries);
91 }
92
93 #[test]
94 fn push_adds_to_outgoing() {
95 let state = FlashState::new(vec![]);
96 state.push("info", "hello");
97 state.push("error", "fail");
98 let outgoing = state.drain_outgoing();
99 assert_eq!(outgoing.len(), 2);
100 assert_eq!(
101 outgoing[0],
102 FlashEntry {
103 level: "info".into(),
104 message: "hello".into()
105 }
106 );
107 assert_eq!(
108 outgoing[1],
109 FlashEntry {
110 level: "error".into(),
111 message: "fail".into()
112 }
113 );
114 }
115
116 #[test]
117 fn drain_outgoing_clears_vec() {
118 let state = FlashState::new(vec![]);
119 state.push("info", "msg");
120 let first = state.drain_outgoing();
121 assert_eq!(first.len(), 1);
122 let second = state.drain_outgoing();
123 assert!(second.is_empty());
124 }
125
126 #[test]
127 fn read_flag_default_false() {
128 let state = FlashState::new(vec![]);
129 assert!(!state.was_read());
130 }
131
132 #[test]
133 fn mark_read_sets_flag() {
134 let state = FlashState::new(vec![]);
135 state.mark_read();
136 assert!(state.was_read());
137 }
138
139 #[test]
140 fn multiple_same_level_preserved_in_order() {
141 let state = FlashState::new(vec![]);
142 state.push("error", "first");
143 state.push("error", "second");
144 state.push("info", "third");
145 let outgoing = state.drain_outgoing();
146 assert_eq!(outgoing.len(), 3);
147 assert_eq!(outgoing[0].level, "error");
148 assert_eq!(outgoing[0].message, "first");
149 assert_eq!(outgoing[1].level, "error");
150 assert_eq!(outgoing[1].message, "second");
151 assert_eq!(outgoing[2].level, "info");
152 }
153
154 #[test]
155 fn incoming_as_template_value_formats_correctly() {
156 let entries = vec![
157 FlashEntry {
158 level: "error".into(),
159 message: "bad".into(),
160 },
161 FlashEntry {
162 level: "info".into(),
163 message: "ok".into(),
164 },
165 ];
166 let state = FlashState::new(entries);
167 let result = state.incoming_as_template_value();
168 assert_eq!(result.len(), 2);
169 assert_eq!(result[0].get("error").unwrap(), "bad");
170 assert_eq!(result[1].get("info").unwrap(), "ok");
171 }
172
173 #[test]
174 fn flash_entry_serialization_roundtrip() {
175 let entry = FlashEntry {
176 level: "success".into(),
177 message: "Item saved".into(),
178 };
179 let json = serde_json::to_string(&entry).unwrap();
180 let parsed: FlashEntry = serde_json::from_str(&json).unwrap();
181 assert_eq!(entry, parsed);
182 }
183
184 #[test]
185 fn flash_entry_vec_serialization() {
186 let entries = vec![
187 FlashEntry {
188 level: "error".into(),
189 message: "fail".into(),
190 },
191 FlashEntry {
192 level: "success".into(),
193 message: "ok".into(),
194 },
195 ];
196 let json = serde_json::to_string(&entries).unwrap();
197 let parsed: Vec<FlashEntry> = serde_json::from_str(&json).unwrap();
198 assert_eq!(entries, parsed);
199 }
200}