mod context;
pub mod json;
pub mod text;
use crate::brain::{Brain, NewEntry};
use crate::model::{Kind, RepoLink, Status, Task, one_line};
use anyhow::Result;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiError {
NotFound(u32),
NotATask(u32, Kind),
NotADecision(u32, Kind),
Empty(&'static str),
SelfLink(u32),
NoComment(u32, usize),
NoRepo(String),
NotFoundIn(RepoLink),
DoingFull(u32),
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApiError::NotFound(id) => write!(f, "no entry #{id}"),
ApiError::NotATask(id, kind) => write!(f, "#{id} is a {}: only tasks have a status", kind.as_str()),
ApiError::NotADecision(id, kind) => {
write!(f, "#{id} is a {}: only decisions are accepted or rejected", kind.as_str())
}
ApiError::Empty(what) => write!(f, "the {what} is empty"),
ApiError::SelfLink(id) => write!(f, "#{id} can't link to itself"),
ApiError::NoComment(id, index) => write!(f, "#{id} has no comment {}", index + 1),
ApiError::NoRepo(repo) => write!(f, "`{repo}` isn't another repository of this project"),
ApiError::NotFoundIn(link) => write!(f, "no entry {link}"),
ApiError::DoingFull(id) => {
write!(f, "#{id} is in doing, and a person works on one task at a time: park or finish it first")
}
}
}
}
impl std::error::Error for ApiError {}
impl Brain {
pub fn entry(&self, id: u32) -> Result<&Task, ApiError> {
self.task(id).ok_or(ApiError::NotFound(id))
}
pub fn add(&mut self, new: NewEntry) -> Result<u32> {
if new.title.trim().is_empty() {
return Err(ApiError::Empty("title").into());
}
for &link in &new.links {
self.entry(link)?;
}
self.check_repo_links(&new.repo_links)?;
let new = NewEntry { title: one_line(&new.title), ..new };
self.create(new)
}
pub fn change_status(&mut self, id: u32, status: Status) -> Result<()> {
self.set_status(id, status).map(drop)
}
pub fn comment(&mut self, id: u32, text: &str) -> Result<()> {
self.add_comment(id, text).map(drop)
}
pub fn describe(&mut self, id: u32, text: &str) -> Result<()> {
self.set_description(id, text).map(drop)
}
pub fn link(&mut self, id: u32, others: &[u32], repo_links: &[RepoLink]) -> Result<()> {
for &other in others {
if other == id {
return Err(ApiError::SelfLink(id).into());
}
self.entry(other)?;
}
self.check_repo_links(repo_links)?;
let index = self.index_of(id)?;
let task = &mut self.tasks[index];
for &other in others {
if !task.links.contains(&other) {
task.links.push(other);
}
}
for link in repo_links {
if !task.repo_links.contains(link) {
task.repo_links.push(link.clone());
}
}
self.store.save(task)
}
fn check_repo_links(&self, links: &[RepoLink]) -> Result<(), ApiError> {
for link in links {
let other = self.sibling(&link.repo).ok_or_else(|| ApiError::NoRepo(link.repo.clone()))?;
other.task(link.id).ok_or_else(|| ApiError::NotFoundIn(link.clone()))?;
}
Ok(())
}
pub fn search(&self, query: &str) -> Vec<&Task> {
let terms: Vec<String> = query.split_whitespace().map(str::to_lowercase).collect();
let mut found: Vec<&Task> =
self.tasks.iter().filter(|t| terms.iter().all(|term| t.has_term(term, true))).collect();
found.sort_by_key(|t| (t.is_task() && t.status == Status::Done, std::cmp::Reverse(t.id)));
found
}
}
#[cfg(test)]
#[path = "../../tests/api.rs"]
mod tests;