Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 98b1aa6e0672b8f30542ebd24c68f22c2d8ea056
Timestamp: Tue, 09 May 2023 12:26:02 +0000 (1 year ago)

+163 -4 +/-2 browse
web: add unit tests to utils functions
1diff --git a/web/src/minijinja_utils.rs b/web/src/minijinja_utils.rs
2index 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
62index 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")]