+163 -4 +/-2 browse
1 | diff --git a/web/src/minijinja_utils.rs b/web/src/minijinja_utils.rs |
2 | index 338a627..5a1bdd0 100644 |
3 | --- a/web/src/minijinja_utils.rs |
4 | +++ b/web/src/minijinja_utils.rs |
5 | @@ -187,6 +187,46 @@ impl minijinja::value::StructObject for MailingList { |
6 | } |
7 | } |
8 | |
9 | + /// Return a vector of weeks, with each week being a vector of 7 days and |
10 | + /// corresponding sum of posts per day. |
11 | + /// |
12 | + /// |
13 | + /// # Example |
14 | + /// |
15 | + /// ```rust |
16 | + /// # use mailpot_web::minijinja_utils::calendarize; |
17 | + /// # use minijinja::Environment; |
18 | + /// # use minijinja::value::Value; |
19 | + /// # use std::collections::HashMap; |
20 | + /// |
21 | + /// let mut env = Environment::new(); |
22 | + /// env.add_function("calendarize", calendarize); |
23 | + /// |
24 | + /// let month = "2001-09"; |
25 | + /// let mut hist = [0usize; 31]; |
26 | + /// hist[15] = 5; |
27 | + /// hist[1] = 1; |
28 | + /// hist[0] = 512; |
29 | + /// hist[30] = 30; |
30 | + /// assert_eq!( |
31 | + /// &env.render_str( |
32 | + /// "{% set c=calendarize(month, hists) %}Month: {{ c.month }} Month Name: {{ \ |
33 | + /// c.month_name }} Month Int: {{ c.month_int }} Year: {{ c.year }} Sum: {{ c.sum }} {% \ |
34 | + /// for week in c.weeks %}{% for day in week %}{% set num = c.hist[day-1] %}({{ day }}, \ |
35 | + /// {{ num }}){% endfor %}{% endfor %}", |
36 | + /// minijinja::context! { |
37 | + /// month, |
38 | + /// hists => vec![(month.to_string(), hist)].into_iter().collect::<HashMap<String, [usize; |
39 | + /// 31]>>(), |
40 | + /// } |
41 | + /// ) |
42 | + /// .unwrap(), |
43 | + /// "Month: 2001-09 Month Name: September Month Int: 9 Year: 2001 Sum: 548 (0, 30)(0, 30)(0, \ |
44 | + /// 30)(0, 30)(0, 30)(1, 512)(2, 1)(3, 0)(4, 0)(5, 0)(6, 0)(7, 0)(8, 0)(9, 0)(10, 0)(11, \ |
45 | + /// 0)(12, 0)(13, 0)(14, 0)(15, 0)(16, 5)(17, 0)(18, 0)(19, 0)(20, 0)(21, 0)(22, 0)(23, \ |
46 | + /// 0)(24, 0)(25, 0)(26, 0)(27, 0)(28, 0)(29, 0)(30, 0)" |
47 | + /// ); |
48 | + /// ``` |
49 | pub fn calendarize( |
50 | _state: &minijinja::State, |
51 | args: Value, |
52 | @@ -431,7 +471,7 @@ pub fn strip_carets(_state: &minijinja::State, arg: Value) -> std::result::Resul |
53 | |
54 | /// `urlize` filter for [`minijinja`]. |
55 | /// |
56 | - /// Returns a safe string for use in <a> href= attributes. |
57 | + /// Returns a safe string for use in `<a href=..` attributes. |
58 | /// |
59 | /// # Examples |
60 | /// |
61 | diff --git a/web/src/utils.rs b/web/src/utils.rs |
62 | index 35daa40..b99b866 100644 |
63 | --- a/web/src/utils.rs |
64 | +++ b/web/src/utils.rs |
65 | @@ -19,6 +19,18 @@ |
66 | |
67 | use super::*; |
68 | |
69 | + /// Navigation crumbs, e.g.: Home > Page > Subpage |
70 | + /// |
71 | + /// # Example |
72 | + /// |
73 | + /// ```rust |
74 | + /// # use mailpot_web::utils::Crumb; |
75 | + /// let crumbs = vec![Crumb { |
76 | + /// label: "Home".into(), |
77 | + /// url: "/".into(), |
78 | + /// }]; |
79 | + /// println!("{} {}", crumbs[0].label, crumbs[0].url); |
80 | + /// ``` |
81 | #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)] |
82 | pub struct Crumb { |
83 | pub label: Cow<'static, str>, |
84 | @@ -26,7 +38,10 @@ pub struct Crumb { |
85 | pub url: Cow<'static, str>, |
86 | } |
87 | |
88 | - #[derive(Debug, Default, Hash, Copy, Clone, serde::Deserialize, serde::Serialize)] |
89 | + /// Message urgency level or info. |
90 | + #[derive( |
91 | + Debug, Default, Hash, Copy, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq, |
92 | + )] |
93 | pub enum Level { |
94 | Success, |
95 | #[default] |
96 | @@ -35,7 +50,8 @@ pub enum Level { |
97 | Error, |
98 | } |
99 | |
100 | - #[derive(Debug, Hash, Clone, serde::Deserialize, serde::Serialize)] |
101 | + /// UI message notifications. |
102 | + #[derive(Debug, Hash, Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] |
103 | pub struct Message { |
104 | pub message: Cow<'static, str>, |
105 | #[serde(default)] |
106 | @@ -46,12 +62,62 @@ impl Message { |
107 | const MESSAGE_KEY: &str = "session-message"; |
108 | } |
109 | |
110 | + /// Drain messages from session. |
111 | + /// |
112 | + /// # Example |
113 | + /// |
114 | + /// ```rust |
115 | + /// # use mailpot_web::utils::{Message, Level, SessionMessages}; |
116 | + /// struct Session(Vec<Message>); |
117 | + /// |
118 | + /// impl SessionMessages for Session { |
119 | + /// type Error = std::convert::Infallible; |
120 | + /// fn drain_messages(&mut self) -> Vec<Message> { |
121 | + /// std::mem::take(&mut self.0) |
122 | + /// } |
123 | + /// |
124 | + /// fn add_message(&mut self, m: Message) -> Result<(), std::convert::Infallible> { |
125 | + /// self.0.push(m); |
126 | + /// Ok(()) |
127 | + /// } |
128 | + /// } |
129 | + /// let mut s = Session(vec![]); |
130 | + /// s.add_message(Message { |
131 | + /// message: "foo".into(), |
132 | + /// level: Level::default(), |
133 | + /// }) |
134 | + /// .unwrap(); |
135 | + /// s.add_message(Message { |
136 | + /// message: "bar".into(), |
137 | + /// level: Level::Error, |
138 | + /// }) |
139 | + /// .unwrap(); |
140 | + /// assert_eq!( |
141 | + /// s.drain_messages().as_slice(), |
142 | + /// [ |
143 | + /// Message { |
144 | + /// message: "foo".into(), |
145 | + /// level: Level::default(), |
146 | + /// }, |
147 | + /// Message { |
148 | + /// message: "bar".into(), |
149 | + /// level: Level::Error |
150 | + /// } |
151 | + /// ] |
152 | + /// .as_slice() |
153 | + /// ); |
154 | + /// assert!(s.0.is_empty()); |
155 | + /// ``` |
156 | pub trait SessionMessages { |
157 | + type Error; |
158 | + |
159 | fn drain_messages(&mut self) -> Vec<Message>; |
160 | - fn add_message(&mut self, _: Message) -> Result<(), ResponseError>; |
161 | + fn add_message(&mut self, _: Message) -> Result<(), Self::Error>; |
162 | } |
163 | |
164 | impl SessionMessages for WritableSession { |
165 | + type Error = ResponseError; |
166 | + |
167 | fn drain_messages(&mut self) -> Vec<Message> { |
168 | let ret = self.get(Message::MESSAGE_KEY).unwrap_or_default(); |
169 | self.remove(Message::MESSAGE_KEY); |
170 | @@ -67,6 +133,19 @@ impl SessionMessages for WritableSession { |
171 | } |
172 | } |
173 | |
174 | + /// Deserialize a string integer into `i64`, because POST parameters are |
175 | + /// strings. |
176 | + /// |
177 | + /// ``` |
178 | + /// # use mailpot_web::utils::IntPOST; |
179 | + /// # use mailpot::serde_json::{self, json}; |
180 | + /// assert_eq!( |
181 | + /// IntPOST(5), |
182 | + /// serde_json::from_str::<IntPOST>("\"5\"").unwrap() |
183 | + /// ); |
184 | + /// assert_eq!(IntPOST(5), serde_json::from_str::<IntPOST>("5").unwrap()); |
185 | + /// assert_eq!(&json! { IntPOST(5) }.to_string(), "5"); |
186 | + /// ``` |
187 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] |
188 | #[repr(transparent)] |
189 | pub struct IntPOST(pub i64); |
190 | @@ -101,6 +180,13 @@ impl<'de> serde::Deserialize<'de> for IntPOST { |
191 | Ok(IntPOST(int)) |
192 | } |
193 | |
194 | + fn visit_u64<E>(self, int: u64) -> Result<Self::Value, E> |
195 | + where |
196 | + E: serde::de::Error, |
197 | + { |
198 | + Ok(IntPOST(int.try_into().unwrap())) |
199 | + } |
200 | + |
201 | fn visit_str<E>(self, int: &str) -> Result<Self::Value, E> |
202 | where |
203 | E: serde::de::Error, |
204 | @@ -113,6 +199,22 @@ impl<'de> serde::Deserialize<'de> for IntPOST { |
205 | } |
206 | } |
207 | |
208 | + /// Deserialize a string integer into `bool`, because POST parameters are |
209 | + /// strings. |
210 | + /// |
211 | + /// ``` |
212 | + /// # use mailpot_web::utils::BoolPOST; |
213 | + /// # use mailpot::serde_json::{self, json}; |
214 | + /// assert_eq!( |
215 | + /// BoolPOST(true), |
216 | + /// serde_json::from_str::<BoolPOST>("true").unwrap() |
217 | + /// ); |
218 | + /// assert_eq!( |
219 | + /// BoolPOST(true), |
220 | + /// serde_json::from_str::<BoolPOST>("\"true\"").unwrap() |
221 | + /// ); |
222 | + /// assert_eq!(&json! { BoolPOST(false) }.to_string(), "false"); |
223 | + /// ``` |
224 | #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd, Hash)] |
225 | #[repr(transparent)] |
226 | pub struct BoolPOST(pub bool); |
227 | @@ -159,6 +261,23 @@ impl<'de> serde::Deserialize<'de> for BoolPOST { |
228 | } |
229 | } |
230 | |
231 | + /// ``` |
232 | + /// use axum::response::Redirect; |
233 | + /// use mailpot_web::Next; |
234 | + /// |
235 | + /// let next = Next { |
236 | + /// next: Some("foo".to_string()), |
237 | + /// }; |
238 | + /// assert_eq!( |
239 | + /// format!("{:?}", Redirect::to("foo")), |
240 | + /// format!("{:?}", next.or_else(|| "bar".to_string())) |
241 | + /// ); |
242 | + /// let next = Next { next: None }; |
243 | + /// assert_eq!( |
244 | + /// format!("{:?}", Redirect::to("bar")), |
245 | + /// format!("{:?}", next.or_else(|| "bar".to_string())) |
246 | + /// ); |
247 | + /// ``` |
248 | #[derive(Debug, Clone, serde::Deserialize)] |
249 | pub struct Next { |
250 | #[serde(default, deserialize_with = "empty_string_as_none")] |