1mod context;
10pub mod json;
11pub mod text;
12
13use crate::brain::{Brain, NewEntry};
14use crate::model::{Kind, RepoLink, Status, Task, now, one_line};
15use anyhow::Result;
16use std::fmt;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum ApiError {
22 NotFound(u32),
24 NotATask(u32, Kind),
26 NotADecision(u32, Kind),
28 Empty(&'static str),
30 SelfLink(u32),
32 NoComment(u32, usize),
34 NoRepo(String),
36 NotFoundIn(RepoLink),
38 DoingFull(u32),
40 NoTag(String),
42}
43
44impl fmt::Display for ApiError {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 ApiError::NotFound(id) => write!(f, "no entry #{id}"),
48 ApiError::NotATask(id, kind) => write!(f, "#{id} is a {}: only tasks have a status", kind.as_str()),
49 ApiError::NotADecision(id, kind) => {
50 write!(f, "#{id} is a {}: only decisions are accepted or rejected", kind.as_str())
51 }
52 ApiError::Empty(what) => write!(f, "the {what} is empty"),
53 ApiError::SelfLink(id) => write!(f, "#{id} can't link to itself"),
54 ApiError::NoComment(id, index) => write!(f, "#{id} has no comment {}", index + 1),
55 ApiError::NoRepo(repo) => write!(f, "`{repo}` isn't another repository of this project"),
56 ApiError::NotFoundIn(link) => write!(f, "no entry {link}"),
57 ApiError::DoingFull(id) => {
58 write!(f, "#{id} is in doing, and a person works on one task at a time: park or finish it first")
59 }
60 ApiError::NoTag(tag) => write!(f, "no entry is tagged #{tag}"),
61 }
62 }
63}
64
65impl std::error::Error for ApiError {}
66
67impl Brain {
68 pub fn entry(&self, id: u32) -> Result<&Task, ApiError> {
70 self.task(id).ok_or(ApiError::NotFound(id))
71 }
72
73 pub fn add(&mut self, new: NewEntry) -> Result<u32> {
75 if new.title.trim().is_empty() {
76 return Err(ApiError::Empty("title").into());
77 }
78 for &link in &new.links {
79 self.entry(link)?;
80 }
81 self.check_repo_links(&new.repo_links)?;
82 let new = NewEntry { title: one_line(&new.title), ..new };
83 self.create(new)
84 }
85
86 #[deprecated(since = "0.6.3", note = "use `Brain::set_status`, which also says whether the status changed")]
88 pub fn change_status(&mut self, id: u32, status: Status) -> Result<()> {
89 self.set_status(id, status).map(drop)
90 }
91
92 #[deprecated(since = "0.6.3", note = "use `Brain::add_comment`")]
94 pub fn comment(&mut self, id: u32, text: &str) -> Result<()> {
95 self.add_comment(id, text).map(drop)
96 }
97
98 #[deprecated(since = "0.6.3", note = "use `Brain::set_description`")]
100 pub fn describe(&mut self, id: u32, text: &str) -> Result<()> {
101 self.set_description(id, text).map(drop)
102 }
103
104 pub fn link(&mut self, id: u32, others: &[u32], repo_links: &[RepoLink]) -> Result<()> {
108 for &other in others {
109 if other == id {
110 return Err(ApiError::SelfLink(id).into());
111 }
112 self.entry(other)?;
113 }
114 self.check_repo_links(repo_links)?;
115 let index = self.index_of(id)?;
116 let mut new: Vec<u32> = Vec::new();
117 for &other in others {
118 if !self.are_related(id, other) && !new.contains(&other) {
119 new.push(other);
120 }
121 }
122 let now = now();
123 let task = &mut self.tasks[index];
124 let mut added: Vec<String> = new.iter().map(u32::to_string).collect();
125 for &other in others {
126 if !task.links.contains(&other) {
127 task.links.push(other);
128 }
129 }
130 for link in repo_links {
131 if !task.repo_links.contains(link) {
132 task.repo_links.push(link.clone());
133 added.push(link.to_string());
134 }
135 }
136 task.relate(now, self.author, &added, &[]);
137 self.store.save(task)?;
138 for &other in &new {
140 let back = self.index_of(other)?;
141 self.tasks[back].relate(now, self.author, &[id.to_string()], &[]);
142 self.store.save(&mut self.tasks[back])?;
143 }
144 Ok(())
145 }
146
147 fn check_repo_links(&self, links: &[RepoLink]) -> Result<(), ApiError> {
149 for link in links {
150 let other = self.sibling(&link.repo).ok_or_else(|| ApiError::NoRepo(link.repo.clone()))?;
151 other.task(link.id).ok_or_else(|| ApiError::NotFoundIn(link.clone()))?;
152 }
153 Ok(())
154 }
155
156 pub fn search(&self, query: &str) -> Vec<&Task> {
159 let terms: Vec<String> = query.split_whitespace().map(str::to_lowercase).collect();
160 let mut found: Vec<&Task> =
161 self.tasks.iter().filter(|t| terms.iter().all(|term| t.has_term(term, true))).collect();
162 found.sort_by_key(|t| (t.is_task() && t.status == Status::Done, std::cmp::Reverse(t.id)));
163 found
164 }
165}
166
167#[cfg(test)]
168#[path = "../../tests/api.rs"]
169mod tests;