Files
scrumtask-cli/src/navigator.rs
itsscb 6f8b865e33 feat: adds main loop
fix: some logic bugs
2024-08-12 22:26:24 +02:00

361 lines
12 KiB
Rust

#[allow(unused_imports)]
use anyhow::{anyhow, Context, Ok, Result};
use std::rc::Rc;
use crate::{
db::JiraDatabase,
models::Action,
ui::{EpicDetail, HomePage, Page, Prompts, StoryDetail},
};
pub struct Navigator {
pages: Vec<Box<dyn Page>>,
prompts: Prompts,
db: Rc<JiraDatabase>,
}
impl Navigator {
pub fn new(db: Rc<JiraDatabase>) -> Self {
Self {
pages: vec![Box::new(HomePage::new(Rc::clone(&db)))],
prompts: Prompts::new(),
db,
}
}
pub fn get_current_page(&self) -> Option<&Box<dyn Page>> {
self.pages.last()
}
pub fn handle_action(&mut self, action: Action) -> Result<()> {
match action {
Action::NavigateToEpicDetail { epic_id } => {
// create a new EpicDetail instance and add it to the pages vector
self.pages.push(Box::new(EpicDetail {
epic_id,
db: Rc::clone(&self.db),
}));
}
Action::NavigateToStoryDetail { epic_id, story_id } => {
// create a new StoryDetail instance and add it to the pages vector
self.pages.push(Box::new(StoryDetail {
epic_id,
story_id,
db: Rc::clone(&self.db),
}));
}
Action::NavigateToPreviousPage => {
// remove the last page from the pages vector
if !self.pages.is_empty() {
self.pages.pop();
}
}
Action::CreateEpic => {
// prompt the user to create a new epic and persist it in the database
self.db
.create_epic((self.prompts.create_epic)())
.with_context(|| format!("failed to create epic"))?;
}
Action::UpdateEpicStatus { epic_id } => {
// prompt the user to update status and persist it in the database
let status = (self.prompts.update_status)()
.with_context(|| format!("invalid status: {epic_id}"))?;
self.db.update_epic_status(epic_id, status)?;
}
Action::DeleteEpic { epic_id } => {
// prompt the user to delete the epic and persist it in the database
if (self.prompts.delete_epic)() {
self.db
.delete_epic(epic_id)
.with_context(|| format!("failed to delete epic: {epic_id}"))?;
self.pages.pop();
}
}
Action::CreateStory { epic_id } => {
// prompt the user to create a new story and persist it in the database
self.db
.create_story((self.prompts.create_story)(), epic_id)
.with_context(|| format!("failed to create story: {epic_id}"))?;
}
Action::UpdateStoryStatus { story_id } => {
// prompt the user to update status and persist it in the database
if let Some(status) = (self.prompts.update_status)() {
let s = status.clone();
self.db
.update_story_status(story_id, status)
.with_context(|| format!("invalid status: {s}"))?;
}
}
Action::DeleteStory { epic_id, story_id } => {
// prompt the user to delete the story and persist it in the database
if (self.prompts.delete_story)() {
self.db
.delete_story(epic_id, story_id)
.with_context(|| format!("failed to delete story: {story_id}"))?;
self.pages.pop();
}
}
Action::Exit => {
// remove all pages from the pages vector
self.pages.clear();
}
}
Ok(())
}
// Private functions used for testing
#[allow(dead_code)]
fn get_page_count(&self) -> usize {
self.pages.len()
}
#[allow(dead_code)]
fn set_prompts(&mut self, prompts: Prompts) {
self.prompts = prompts;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
db::test_utils::MockDB,
models::{Epic, Status, Story},
};
#[test]
fn should_start_on_home_page() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let nav = Navigator::new(db);
assert_eq!(nav.get_page_count(), 1);
let current_page = nav.get_current_page().unwrap();
let home_page = current_page.as_any().downcast_ref::<HomePage>();
assert_eq!(home_page.is_some(), true);
}
#[test]
fn handle_action_should_navigate_pages() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let mut nav = Navigator::new(db);
nav.handle_action(Action::NavigateToEpicDetail { epic_id: 1 })
.unwrap();
assert_eq!(nav.get_page_count(), 2);
let current_page = nav.get_current_page().unwrap();
let epic_detail_page = current_page.as_any().downcast_ref::<EpicDetail>();
assert_eq!(epic_detail_page.is_some(), true);
nav.handle_action(Action::NavigateToStoryDetail {
epic_id: 1,
story_id: 2,
})
.unwrap();
assert_eq!(nav.get_page_count(), 3);
let current_page = nav.get_current_page().unwrap();
let story_detail_page = current_page.as_any().downcast_ref::<StoryDetail>();
assert_eq!(story_detail_page.is_some(), true);
nav.handle_action(Action::NavigateToPreviousPage).unwrap();
assert_eq!(nav.get_page_count(), 2);
let current_page = nav.get_current_page().unwrap();
let epic_detail_page = current_page.as_any().downcast_ref::<EpicDetail>();
assert_eq!(epic_detail_page.is_some(), true);
nav.handle_action(Action::NavigateToPreviousPage).unwrap();
assert_eq!(nav.get_page_count(), 1);
let current_page = nav.get_current_page().unwrap();
let home_page = current_page.as_any().downcast_ref::<HomePage>();
assert_eq!(home_page.is_some(), true);
nav.handle_action(Action::NavigateToPreviousPage).unwrap();
assert_eq!(nav.get_page_count(), 0);
nav.handle_action(Action::NavigateToPreviousPage).unwrap();
assert_eq!(nav.get_page_count(), 0);
}
#[test]
fn handle_action_should_clear_pages_on_exit() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let mut nav = Navigator::new(db);
nav.handle_action(Action::NavigateToEpicDetail { epic_id: 1 })
.unwrap();
nav.handle_action(Action::NavigateToStoryDetail {
epic_id: 1,
story_id: 2,
})
.unwrap();
nav.handle_action(Action::Exit).unwrap();
assert_eq!(nav.get_page_count(), 0);
}
#[test]
fn handle_action_should_handle_create_epic() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let mut nav = Navigator::new(Rc::clone(&db));
let mut prompts = Prompts::new();
prompts.create_epic = Box::new(|| Epic::new("name".to_owned(), "description".to_owned()));
nav.set_prompts(prompts);
nav.handle_action(Action::CreateEpic).unwrap();
let db_state = db.read_db().unwrap();
assert_eq!(db_state.epics.len(), 1);
let epic = db_state.epics.into_iter().next().unwrap().1;
assert_eq!(epic.name, "name".to_owned());
assert_eq!(epic.description, "description".to_owned());
}
#[test]
fn handle_action_should_handle_update_epic() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let epic_id = db
.create_epic(Epic::new("".to_owned(), "".to_owned()))
.unwrap();
let mut nav = Navigator::new(Rc::clone(&db));
let mut prompts = Prompts::new();
prompts.update_status = Box::new(|| Some(Status::InProgress));
nav.set_prompts(prompts);
nav.handle_action(Action::UpdateEpicStatus { epic_id })
.unwrap();
let db_state = db.read_db().unwrap();
assert_eq!(
db_state.epics.get(&epic_id).unwrap().status,
Status::InProgress
);
}
#[test]
fn handle_action_should_handle_delete_epic() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let epic_id = db
.create_epic(Epic::new("".to_owned(), "".to_owned()))
.unwrap();
let mut nav = Navigator::new(Rc::clone(&db));
let mut prompts = Prompts::new();
prompts.delete_epic = Box::new(|| true);
nav.set_prompts(prompts);
nav.handle_action(Action::DeleteEpic { epic_id }).unwrap();
let db_state = db.read_db().unwrap();
assert_eq!(db_state.epics.len(), 0);
}
#[test]
fn handle_action_should_handle_create_story() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let epic_id = db
.create_epic(Epic::new("".to_owned(), "".to_owned()))
.unwrap();
let mut nav = Navigator::new(Rc::clone(&db));
let mut prompts = Prompts::new();
prompts.create_story = Box::new(|| Story::new("name".to_owned(), "description".to_owned()));
nav.set_prompts(prompts);
nav.handle_action(Action::CreateStory { epic_id }).unwrap();
let db_state = db.read_db().unwrap();
assert_eq!(db_state.stories.len(), 1);
let story = db_state.stories.into_iter().next().unwrap().1;
assert_eq!(story.name, "name".to_owned());
assert_eq!(story.description, "description".to_owned());
}
#[test]
fn handle_action_should_handle_update_story() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let epic_id = db
.create_epic(Epic::new("".to_owned(), "".to_owned()))
.unwrap();
let story_id = db
.create_story(Story::new("".to_owned(), "".to_owned()), epic_id)
.unwrap();
let mut nav = Navigator::new(Rc::clone(&db));
let mut prompts = Prompts::new();
prompts.update_status = Box::new(|| Some(Status::InProgress));
nav.set_prompts(prompts);
nav.handle_action(Action::UpdateStoryStatus { story_id })
.unwrap();
let db_state = db.read_db().unwrap();
assert_eq!(
db_state.stories.get(&story_id).unwrap().status,
Status::InProgress
);
}
#[test]
fn handle_action_should_handle_delete_story() {
let db = Rc::new(JiraDatabase {
database: Box::new(MockDB::new()),
});
let epic_id = db
.create_epic(Epic::new("".to_owned(), "".to_owned()))
.unwrap();
let story_id = db
.create_story(Story::new("".to_owned(), "".to_owned()), epic_id)
.unwrap();
let mut nav = Navigator::new(Rc::clone(&db));
let mut prompts = Prompts::new();
prompts.delete_story = Box::new(|| true);
nav.set_prompts(prompts);
nav.handle_action(Action::DeleteStory { epic_id, story_id })
.unwrap();
let db_state = db.read_db().unwrap();
assert_eq!(db_state.stories.len(), 0);
}
}