Skip to main content

cardinal_core/
reducer.rs

1use crate::command::{AgendaRange, CalendarView, CardinalCommand, ListTarget, SyncTarget};
2use crate::workspace::{View, WorkspaceState};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum DomainEffectRequest {
6    ReadMaildir,
7    MoveMessage,
8    SendMail,
9    ReadCalendar,
10    WriteEvent,
11    RunSync(SyncTarget),
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct CommandOutcome {
16    pub state: WorkspaceState,
17    pub effect: Option<DomainEffectRequest>,
18    pub status_message: Option<String>,
19}
20
21pub fn dispatch_command(state: &WorkspaceState, command: &CardinalCommand) -> CommandOutcome {
22    let mut next_state = state.clone();
23    let mut effect = None;
24    let mut status_message = None;
25
26    match command {
27        CardinalCommand::List(target) => match target {
28            ListTarget::Inboxes | ListTarget::Folders => next_state.set_view(View::Inboxes),
29            ListTarget::Mail => {
30                next_state.set_view(View::MailList {
31                    mailbox: "mail".to_owned(),
32                });
33                effect = Some(DomainEffectRequest::ReadMaildir);
34            }
35            ListTarget::Unread => {
36                next_state.set_view(View::MailList {
37                    mailbox: "unread".to_owned(),
38                });
39                effect = Some(DomainEffectRequest::ReadMaildir);
40            }
41            ListTarget::Flagged => {
42                next_state.set_view(View::MailList {
43                    mailbox: "flagged".to_owned(),
44                });
45                effect = Some(DomainEffectRequest::ReadMaildir);
46            }
47            ListTarget::Calendars => {
48                next_state.set_view(View::Calendars);
49                effect = Some(DomainEffectRequest::ReadCalendar);
50            }
51            ListTarget::Invites => {
52                next_state.set_view(View::MailList {
53                    mailbox: "invites".to_owned(),
54                });
55                effect = Some(DomainEffectRequest::ReadMaildir);
56            }
57        },
58        CardinalCommand::Calendar(view) => {
59            let range = match view {
60                CalendarView::Today => AgendaRange::Today,
61                CalendarView::Tomorrow => AgendaRange::Tomorrow,
62                CalendarView::Week => AgendaRange::Week,
63                CalendarView::Month => AgendaRange::Default,
64            };
65            next_state.set_view(View::Agenda { range });
66            effect = Some(DomainEffectRequest::ReadCalendar);
67        }
68        CardinalCommand::Agenda(range) => {
69            next_state.set_view(View::Agenda {
70                range: range.clone(),
71            });
72            effect = Some(DomainEffectRequest::ReadCalendar);
73        }
74        CardinalCommand::Compose
75        | CardinalCommand::Reply { .. }
76        | CardinalCommand::Forward { .. } => {
77            next_state.set_view(View::Compose);
78        }
79        CardinalCommand::Send { .. } => {
80            effect = Some(DomainEffectRequest::SendMail);
81        }
82        CardinalCommand::Search { query } => {
83            next_state.set_view(View::SearchResults {
84                query: query.clone(),
85            });
86        }
87        CardinalCommand::Archive
88        | CardinalCommand::Delete
89        | CardinalCommand::Spam
90        | CardinalCommand::Mark(_)
91        | CardinalCommand::Move { .. }
92        | CardinalCommand::Undo => {
93            effect = Some(DomainEffectRequest::MoveMessage);
94        }
95        CardinalCommand::Event(_) | CardinalCommand::Invite(_) => {
96            effect = Some(DomainEffectRequest::WriteEvent);
97        }
98        CardinalCommand::Sync(target) => {
99            effect = Some(DomainEffectRequest::RunSync(target.clone()));
100            status_message = Some("sync queued".to_owned());
101        }
102        CardinalCommand::Open(_)
103        | CardinalCommand::Help
104        | CardinalCommand::Bindings
105        | CardinalCommand::Config
106        | CardinalCommand::Reload
107        | CardinalCommand::Quit => {}
108    }
109
110    CommandOutcome {
111        state: next_state,
112        effect,
113        status_message,
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::command::{CardinalCommand, EventCommand, InviteCommand, MarkState, Selector};
121
122    #[test]
123    fn dispatches_agenda_command_to_typed_view() {
124        let state = WorkspaceState::default();
125        let outcome = dispatch_command(&state, &CardinalCommand::Agenda(AgendaRange::Week));
126
127        assert_eq!(
128            outcome.state.view,
129            View::Agenda {
130                range: AgendaRange::Week
131            }
132        );
133        assert_eq!(outcome.effect, Some(DomainEffectRequest::ReadCalendar));
134    }
135
136    #[test]
137    fn dispatches_sync_command_to_effect() {
138        let state = WorkspaceState::default();
139        let outcome = dispatch_command(&state, &CardinalCommand::Sync(SyncTarget::Mail));
140
141        assert_eq!(
142            outcome.effect,
143            Some(DomainEffectRequest::RunSync(SyncTarget::Mail))
144        );
145        assert_eq!(outcome.status_message, Some("sync queued".to_owned()));
146    }
147
148    #[test]
149    fn dispatches_list_calendars_to_calendar_view() {
150        let state = WorkspaceState::default();
151        let outcome = dispatch_command(&state, &CardinalCommand::List(ListTarget::Calendars));
152
153        assert_eq!(outcome.state.view, View::Calendars);
154        assert_eq!(outcome.effect, Some(DomainEffectRequest::ReadCalendar));
155    }
156
157    #[test]
158    fn dispatches_archive_to_mail_move_effect() {
159        let state = WorkspaceState::default();
160        let outcome = dispatch_command(&state, &CardinalCommand::Archive);
161
162        assert_eq!(outcome.effect, Some(DomainEffectRequest::MoveMessage));
163    }
164
165    #[test]
166    fn dispatches_mail_list_targets_to_maildir_read() {
167        let state = WorkspaceState::default();
168        let list_mail = dispatch_command(&state, &CardinalCommand::List(ListTarget::Mail));
169        let list_unread = dispatch_command(&state, &CardinalCommand::List(ListTarget::Unread));
170        let list_flagged = dispatch_command(&state, &CardinalCommand::List(ListTarget::Flagged));
171        let list_invites = dispatch_command(&state, &CardinalCommand::List(ListTarget::Invites));
172
173        assert_eq!(
174            list_mail.state.view,
175            View::MailList {
176                mailbox: "mail".to_owned()
177            }
178        );
179        assert_eq!(
180            list_unread.state.view,
181            View::MailList {
182                mailbox: "unread".to_owned()
183            }
184        );
185        assert_eq!(
186            list_flagged.state.view,
187            View::MailList {
188                mailbox: "flagged".to_owned()
189            }
190        );
191        assert_eq!(
192            list_invites.state.view,
193            View::MailList {
194                mailbox: "invites".to_owned()
195            }
196        );
197        assert_eq!(list_mail.effect, Some(DomainEffectRequest::ReadMaildir));
198        assert_eq!(list_unread.effect, Some(DomainEffectRequest::ReadMaildir));
199        assert_eq!(list_flagged.effect, Some(DomainEffectRequest::ReadMaildir));
200        assert_eq!(list_invites.effect, Some(DomainEffectRequest::ReadMaildir));
201    }
202
203    #[test]
204    fn dispatches_non_mail_list_targets_without_maildir_effect() {
205        let state = WorkspaceState::default();
206        let list_inboxes = dispatch_command(&state, &CardinalCommand::List(ListTarget::Inboxes));
207        let list_folders = dispatch_command(&state, &CardinalCommand::List(ListTarget::Folders));
208
209        assert_eq!(list_inboxes.state.view, View::Inboxes);
210        assert_eq!(list_folders.state.view, View::Inboxes);
211        assert_eq!(list_inboxes.effect, None);
212        assert_eq!(list_folders.effect, None);
213    }
214
215    #[test]
216    fn dispatches_calendar_views_to_agenda_ranges() {
217        let state = WorkspaceState::default();
218        let today = dispatch_command(&state, &CardinalCommand::Calendar(CalendarView::Today));
219        let tomorrow = dispatch_command(&state, &CardinalCommand::Calendar(CalendarView::Tomorrow));
220        let week = dispatch_command(&state, &CardinalCommand::Calendar(CalendarView::Week));
221        let month = dispatch_command(&state, &CardinalCommand::Calendar(CalendarView::Month));
222
223        assert_eq!(
224            today.state.view,
225            View::Agenda {
226                range: AgendaRange::Today
227            }
228        );
229        assert_eq!(
230            tomorrow.state.view,
231            View::Agenda {
232                range: AgendaRange::Tomorrow
233            }
234        );
235        assert_eq!(
236            week.state.view,
237            View::Agenda {
238                range: AgendaRange::Week
239            }
240        );
241        assert_eq!(
242            month.state.view,
243            View::Agenda {
244                range: AgendaRange::Default
245            }
246        );
247        assert_eq!(today.effect, Some(DomainEffectRequest::ReadCalendar));
248        assert_eq!(tomorrow.effect, Some(DomainEffectRequest::ReadCalendar));
249        assert_eq!(week.effect, Some(DomainEffectRequest::ReadCalendar));
250        assert_eq!(month.effect, Some(DomainEffectRequest::ReadCalendar));
251    }
252
253    #[test]
254    fn dispatches_compose_reply_and_forward_to_compose_view() {
255        let state = WorkspaceState::default();
256        let compose = dispatch_command(&state, &CardinalCommand::Compose);
257        let reply = dispatch_command(&state, &CardinalCommand::Reply { all: true });
258        let forward = dispatch_command(
259            &state,
260            &CardinalCommand::Forward {
261                recipient: "a@example.com".to_owned(),
262            },
263        );
264
265        assert_eq!(compose.state.view, View::Compose);
266        assert_eq!(reply.state.view, View::Compose);
267        assert_eq!(forward.state.view, View::Compose);
268    }
269
270    #[test]
271    fn dispatches_send_to_sendmail_effect() {
272        let state = WorkspaceState::default();
273        let send = dispatch_command(&state, &CardinalCommand::Send { confirm: false });
274        let confirm = dispatch_command(&state, &CardinalCommand::Send { confirm: true });
275
276        assert_eq!(send.effect, Some(DomainEffectRequest::SendMail));
277        assert_eq!(confirm.effect, Some(DomainEffectRequest::SendMail));
278        assert_eq!(send.state, state);
279    }
280
281    #[test]
282    fn dispatches_search_to_search_results_view() {
283        let state = WorkspaceState::default();
284        let outcome = dispatch_command(
285            &state,
286            &CardinalCommand::Search {
287                query: "from:alice".to_owned(),
288            },
289        );
290
291        assert_eq!(
292            outcome.state.view,
293            View::SearchResults {
294                query: "from:alice".to_owned()
295            }
296        );
297    }
298
299    #[test]
300    fn dispatches_all_mail_mutations_to_move_message_effect() {
301        let state = WorkspaceState::default();
302        let delete = dispatch_command(&state, &CardinalCommand::Delete);
303        let spam = dispatch_command(&state, &CardinalCommand::Spam);
304        let mark = dispatch_command(&state, &CardinalCommand::Mark(MarkState::Read));
305        let mv = dispatch_command(
306            &state,
307            &CardinalCommand::Move {
308                target: "archive".to_owned(),
309            },
310        );
311        let undo = dispatch_command(&state, &CardinalCommand::Undo);
312
313        assert_eq!(delete.effect, Some(DomainEffectRequest::MoveMessage));
314        assert_eq!(spam.effect, Some(DomainEffectRequest::MoveMessage));
315        assert_eq!(mark.effect, Some(DomainEffectRequest::MoveMessage));
316        assert_eq!(mv.effect, Some(DomainEffectRequest::MoveMessage));
317        assert_eq!(undo.effect, Some(DomainEffectRequest::MoveMessage));
318    }
319
320    #[test]
321    fn dispatches_noop_commands_without_side_effects() {
322        let state = WorkspaceState::default();
323        let open = dispatch_command(&state, &CardinalCommand::Open(Selector::Index(1)));
324        let help = dispatch_command(&state, &CardinalCommand::Help);
325        let bindings = dispatch_command(&state, &CardinalCommand::Bindings);
326        let config = dispatch_command(&state, &CardinalCommand::Config);
327        let reload = dispatch_command(&state, &CardinalCommand::Reload);
328        let quit = dispatch_command(&state, &CardinalCommand::Quit);
329
330        assert_eq!(open.effect, None);
331        assert_eq!(help.effect, None);
332        assert_eq!(bindings.effect, None);
333        assert_eq!(config.effect, None);
334        assert_eq!(reload.effect, None);
335        assert_eq!(quit.effect, None);
336        assert_eq!(open.state, state);
337        assert_eq!(quit.state, state);
338    }
339
340    #[test]
341    fn dispatches_event_and_invite_to_calendar_write_effect() {
342        let state = WorkspaceState::default();
343        let event_outcome = dispatch_command(&state, &CardinalCommand::Event(EventCommand::New));
344        let invite_outcome =
345            dispatch_command(&state, &CardinalCommand::Invite(InviteCommand::Accept));
346
347        assert_eq!(event_outcome.effect, Some(DomainEffectRequest::WriteEvent));
348        assert_eq!(invite_outcome.effect, Some(DomainEffectRequest::WriteEvent));
349    }
350}