1use crate::brain::Brain;
5use crate::model::{Activity, Asked, Author, Changed, Comment, Kind, Task, fmt_ts, now};
6use serde::Serialize;
7
8#[derive(Debug, Serialize)]
10#[non_exhaustive]
11pub struct Entry<'a> {
12 pub id: u32,
14 pub kind: &'static str,
16 pub title: &'a str,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub status: Option<&'static str>,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub decision: Option<&'static str>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub asked: Option<&'static str>,
27 pub tags: &'a [String],
29 #[serde(skip_serializing_if = "<[u32]>::is_empty")]
31 pub links: &'a [u32],
32 #[serde(skip_serializing_if = "Vec::is_empty")]
34 pub repo_links: Vec<String>,
35 #[serde(skip_serializing_if = "Vec::is_empty")]
37 pub linked_from: Vec<u32>,
38 pub created: String,
40 #[serde(skip_serializing_if = "str::is_empty")]
42 pub description: &'a str,
43 #[serde(skip_serializing_if = "Vec::is_empty")]
45 pub comments: Vec<EntryComment<'a>>,
46 #[serde(skip_serializing_if = "Vec::is_empty")]
48 pub activity: Vec<EntryChange<'a>>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub tracked_minutes: Option<i64>,
52 #[serde(skip_serializing_if = "std::ops::Not::not")]
54 pub running: bool,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub path: Option<String>,
58}
59
60#[derive(Debug, Serialize)]
62#[non_exhaustive]
63pub struct EntryComment<'a> {
64 pub created: String,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub edited: Option<String>,
69 pub body: &'a str,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub author: Option<&'static str>,
74 #[serde(skip_serializing_if = "std::ops::Not::not")]
76 pub question: bool,
77}
78
79#[derive(Debug, Serialize)]
81#[non_exhaustive]
82pub struct EntryChange<'a> {
83 pub at: String,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub author: Option<&'static str>,
88 pub changed: &'static str,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub from: Option<&'a str>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub to: Option<&'a str>,
96 #[serde(skip_serializing_if = "<[String]>::is_empty")]
98 pub added: &'a [String],
99 #[serde(skip_serializing_if = "<[String]>::is_empty")]
101 pub removed: &'a [String],
102}
103
104#[derive(Debug, Serialize)]
106#[non_exhaustive]
107pub struct Summary<'a> {
108 pub id: u32,
110 pub kind: &'static str,
112 pub title: &'a str,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub status: Option<&'static str>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub decision: Option<&'static str>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub asked: Option<&'static str>,
123 pub tags: &'a [String],
125}
126
127impl<'a> Entry<'a> {
128 pub fn new(brain: &Brain, task: &'a Task) -> Self {
130 let now = now();
131 Self {
132 id: task.id,
133 kind: task.kind.as_str(),
134 title: &task.title,
135 status: task.is_task().then(|| task.status.as_str()),
136 decision: decision(task),
137 asked: asked(task),
138 tags: &task.tags,
139 links: &task.links,
140 repo_links: task.repo_links.iter().map(ToString::to_string).collect(),
141 linked_from: brain.backlinks(task.id).iter().map(|t| t.id).collect(),
142 created: fmt_ts(task.created),
143 description: &task.description,
144 comments: task.comments.iter().map(EntryComment::new).collect(),
145 activity: task.activity.iter().map(EntryChange::new).collect(),
146 tracked_minutes: (!task.sessions.is_empty()).then(|| task.total(now).num_minutes()),
147 running: task.is_running(),
148 path: task.path.as_ref().map(|p| p.display().to_string()),
149 }
150 }
151}
152
153impl<'a> EntryComment<'a> {
154 fn new(comment: &'a Comment) -> Self {
156 Self {
157 created: fmt_ts(comment.created),
158 edited: comment.edited.map(fmt_ts),
159 body: &comment.body,
160 author: (comment.author != Author::Human).then(|| comment.author.as_str()),
161 question: comment.question,
162 }
163 }
164}
165
166impl<'a> EntryChange<'a> {
167 fn new(activity: &'a Activity) -> Self {
169 let none: &[String] = &[];
170 let (changed, from, to, added, removed) = match &activity.change {
171 Changed::Title { from, to } => ("title", Some(from.as_str()), Some(to.as_str()), none, none),
172 Changed::Tags { added, removed } => ("tags", None, None, added.as_slice(), removed.as_slice()),
173 Changed::Related { added, removed } => ("related", None, None, added.as_slice(), removed.as_slice()),
174 Changed::Description => ("description", None, None, none, none),
175 };
176 Self {
177 at: fmt_ts(activity.at),
178 author: (activity.by != Author::Human).then(|| activity.by.as_str()),
179 changed,
180 from,
181 to,
182 added,
183 removed,
184 }
185 }
186}
187
188impl<'a> Summary<'a> {
189 pub fn new(task: &'a Task) -> Self {
191 Self {
192 id: task.id,
193 kind: task.kind.as_str(),
194 title: &task.title,
195 status: task.is_task().then(|| task.status.as_str()),
196 decision: decision(task),
197 asked: asked(task),
198 tags: &task.tags,
199 }
200 }
201}
202
203fn decision(task: &Task) -> Option<&'static str> {
205 (task.kind == Kind::Decision).then(|| task.decision.as_str())
206}
207
208fn asked(task: &Task) -> Option<&'static str> {
210 task.asked().map(Asked::label)
211}