1mod context;
10pub mod json;
11pub mod text;
12
13use crate::brain::{Brain, NewEntry};
14use crate::model::{Kind, RepoLink, Status, Task, one_line};
15use anyhow::Result;
16use std::fmt;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum ApiError {
21 NotFound(u32),
23 NotATask(u32, Kind),
25 NotADecision(u32, Kind),
27 Empty(&'static str),
29 SelfLink(u32),
31 NoComment(u32, usize),
33 NoRepo(String),
35 NotFoundIn(RepoLink),
37 DoingFull(u32),
39}
40
41impl fmt::Display for ApiError {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 ApiError::NotFound(id) => write!(f, "no entry #{id}"),
45 ApiError::NotATask(id, kind) => write!(f, "#{id} is a {}: only tasks have a status", kind.as_str()),
46 ApiError::NotADecision(id, kind) => {
47 write!(f, "#{id} is a {}: only decisions are accepted or rejected", kind.as_str())
48 }
49 ApiError::Empty(what) => write!(f, "the {what} is empty"),
50 ApiError::SelfLink(id) => write!(f, "#{id} can't link to itself"),
51 ApiError::NoComment(id, index) => write!(f, "#{id} has no comment {}", index + 1),
52 ApiError::NoRepo(repo) => write!(f, "`{repo}` isn't another repository of this project"),
53 ApiError::NotFoundIn(link) => write!(f, "no entry {link}"),
54 ApiError::DoingFull(id) => {
55 write!(f, "#{id} is in doing, and a person works on one task at a time: park or finish it first")
56 }
57 }
58 }
59}
60
61impl std::error::Error for ApiError {}
62
63impl Brain {
64 pub fn entry(&self, id: u32) -> Result<&Task, ApiError> {
66 self.task(id).ok_or(ApiError::NotFound(id))
67 }
68
69 pub fn add(&mut self, new: NewEntry) -> Result<u32> {
71 if new.title.trim().is_empty() {
72 return Err(ApiError::Empty("title").into());
73 }
74 for &link in &new.links {
75 self.entry(link)?;
76 }
77 self.check_repo_links(&new.repo_links)?;
78 let new = NewEntry { title: one_line(&new.title), ..new };
79 self.create(new)
80 }
81
82 pub fn change_status(&mut self, id: u32, status: Status) -> Result<()> {
84 self.set_status(id, status).map(drop)
85 }
86
87 pub fn comment(&mut self, id: u32, text: &str) -> Result<()> {
89 self.add_comment(id, text).map(drop)
90 }
91
92 pub fn describe(&mut self, id: u32, text: &str) -> Result<()> {
94 self.set_description(id, text).map(drop)
95 }
96
97 pub fn link(&mut self, id: u32, others: &[u32], repo_links: &[RepoLink]) -> Result<()> {
100 for &other in others {
101 if other == id {
102 return Err(ApiError::SelfLink(id).into());
103 }
104 self.entry(other)?;
105 }
106 self.check_repo_links(repo_links)?;
107 let index = self.index_of(id)?;
108 let task = &mut self.tasks[index];
109 for &other in others {
110 if !task.links.contains(&other) {
111 task.links.push(other);
112 }
113 }
114 for link in repo_links {
115 if !task.repo_links.contains(link) {
116 task.repo_links.push(link.clone());
117 }
118 }
119 self.store.save(task)
120 }
121
122 fn check_repo_links(&self, links: &[RepoLink]) -> Result<(), ApiError> {
124 for link in links {
125 let other = self.sibling(&link.repo).ok_or_else(|| ApiError::NoRepo(link.repo.clone()))?;
126 other.task(link.id).ok_or_else(|| ApiError::NotFoundIn(link.clone()))?;
127 }
128 Ok(())
129 }
130
131 pub fn search(&self, query: &str) -> Vec<&Task> {
134 let terms: Vec<String> = query.split_whitespace().map(str::to_lowercase).collect();
135 let mut found: Vec<&Task> =
136 self.tasks.iter().filter(|t| terms.iter().all(|term| t.has_term(term, true))).collect();
137 found.sort_by_key(|t| (t.is_task() && t.status == Status::Done, std::cmp::Reverse(t.id)));
138 found
139 }
140}
141
142#[cfg(test)]
143#[path = "../../tests/api.rs"]
144mod tests;