1use serde::{Deserialize, Serialize};
14
15#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct Task {
19 pub id: u32,
21 pub title: String,
23 #[serde(default)]
25 pub estimate_minutes: Option<u32>,
26 #[serde(default)]
28 pub done: bool,
29}
30
31impl Task {
32 pub fn line(&self) -> String {
34 let mark = if self.done { "x" } else { " " };
35 let estimate = match self.estimate_minutes {
36 Some(minutes) => format!(" · {minutes}m"),
37 None => String::new(),
38 };
39 format!("[{mark}] #{} {}{estimate}", self.id, self.title)
40 }
41}
42
43#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct Board {
47 #[serde(default)]
49 pub tasks: Vec<Task>,
50}
51
52impl Board {
53 pub fn open(&self) -> usize {
55 self.tasks.iter().filter(|task| !task.done).count()
56 }
57
58 pub fn done(&self) -> usize {
60 self.tasks.iter().filter(|task| task.done).count()
61 }
62
63 pub fn summary(&self) -> String {
65 if self.tasks.is_empty() {
66 return "empty".to_owned();
67 }
68 format!("{} open · {} done", self.open(), self.done())
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use serde_json::json;
76
77 #[test]
79 fn state_the_agent_publishes_deserializes_into_the_view_model() {
80 let published = json!({
81 "tasks": [
82 {"id": 1, "title": "draft the agenda", "done": false},
83 {"id": 2, "title": "book the room", "estimateMinutes": 45, "done": true},
84 ],
85 "nextId": 2,
88 });
89
90 let board: Board = serde_json::from_value(published).expect("the published shape");
91 assert_eq!(board.summary(), "1 open · 1 done");
92 assert_eq!(board.tasks[1].line(), "[x] #2 book the room · 45m");
93 }
94
95 #[test]
96 fn an_empty_state_object_is_an_empty_board() {
97 let board: Board = serde_json::from_value(json!({})).expect("the empty shape");
98 assert_eq!(board, Board::default());
99 assert_eq!(board.summary(), "empty");
100 }
101}