From 9ba820a43c20dcf4e92ab29c7fecb45338955a1a Mon Sep 17 00:00:00 2001 From: itsscb Date: Sun, 11 Aug 2024 20:33:58 +0200 Subject: [PATCH] feat: adds JiraDatabase with CRUD --- src/db.rs | 409 +++++++++++++++++++++++++++++++++++++++++++++++++- src/models.rs | 8 +- 2 files changed, 411 insertions(+), 6 deletions(-) diff --git a/src/db.rs b/src/db.rs index 142c8cc..fb17429 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,9 +1,111 @@ use std::fs; -use anyhow::Result; +use anyhow::{anyhow, Result}; use crate::models::{DBState, Epic, Status, Story}; +pub struct JiraDatabase { + database: Box, +} + +impl JiraDatabase { + pub fn new(file_path: String) -> Self { + Self { + database: Box::new(JSONFileDatabase { file_path }), + } + } + + pub fn read_db(&self) -> Result { + self.database.read_db() + } + + pub fn create_epic(&self, epic: Epic) -> Result { + let mut db = self.read_db()?; + + let id = db.last_item_id + 1; + db.last_item_id = id; + db.epics.insert(id, epic); + self.database.write_db(&db)?; + Ok(id) + } + + pub fn create_story(&self, story: Story, epic_id: u32) -> Result { + let mut db = self.read_db()?; + + let id = db.last_item_id + 1; + db.last_item_id = id; + db.stories.insert(id, story); + db.epics + .get_mut(&epic_id) + .ok_or_else(|| anyhow!(format!("epic not found: {epic_id}")))? + .stories + .push(id); + self.database.write_db(&db)?; + Ok(id) + } + + pub fn delete_epic(&self, epic_id: u32) -> Result<()> { + let mut db = self.read_db()?; + + let epic = db + .epics + .get(&epic_id) + .ok_or_else(|| anyhow!(format!("epic not found: {epic_id}")))?; + db.stories.retain(|k, _| !epic.stories.contains(k)); + + db.epics.retain(|k, _| k != &epic_id); + + self.database.write_db(&db)?; + Ok(()) + } + + pub fn delete_story(&self, epic_id: u32, story_id: u32) -> Result<()> { + let mut db = self.read_db()?; + + let epic = db + .epics + .get_mut(&epic_id) + .ok_or_else(|| anyhow!(format!("epic not found: {epic_id}")))?; + + if !epic.stories.contains(&story_id) { + return Err(anyhow!(format!( + "story {story_id} not found in epic {epic_id}" + ))); + } + + epic.stories.retain(|k| k != &story_id); + + db.stories.retain(|k, _| k != &story_id); + + self.database.write_db(&db)?; + Ok(()) + } + + pub fn update_epic_status(&self, epic_id: u32, status: Status) -> Result<()> { + let mut db = self.read_db()?; + + db.epics + .get_mut(&epic_id) + .ok_or_else(|| anyhow!(format!("epic not found: {epic_id}")))? + .status = status; + + self.database.write_db(&db)?; + Ok(()) + } + + pub fn update_story_status(&self, story_id: u32, status: Status) -> Result<()> { + let mut db = self.read_db()?; + + db.stories + .get_mut(&story_id) + .ok_or_else(|| anyhow!(format!("story not found: {story_id}")))? + .status = status; + + self.database.write_db(&db)?; + Ok(()) + } +} + trait Database { fn read_db(&self) -> Result; fn write_db(&self, db_state: &DBState) -> Result<()>; @@ -26,10 +128,314 @@ impl Database for JSONFileDatabase { } } +pub mod test_utils { + use std::{cell::RefCell, collections::HashMap}; + + use super::*; + + pub struct MockDB { + last_written_state: RefCell, + } + + impl MockDB { + pub fn new() -> Self { + Self { + last_written_state: RefCell::new(DBState { + last_item_id: 0, + epics: HashMap::new(), + stories: HashMap::new(), + }), + } + } + } + + impl Database for MockDB { + fn read_db(&self) -> Result { + // TODO: fix this error by deriving the appropriate traits for Story + let state = self.last_written_state.borrow().clone(); + Ok(state) + } + + fn write_db(&self, db_state: &DBState) -> Result<()> { + let latest_state = &self.last_written_state; + // TODO: fix this error by deriving the appropriate traits for DBState + *latest_state.borrow_mut() = db_state.clone(); + Ok(()) + } + } +} + #[cfg(test)] mod tests { + use super::test_utils::MockDB; use super::*; + #[test] + fn create_epic_should_work() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let epic = Epic::new("".to_owned(), "".to_owned()); + + // TODO: fix this error by deriving the appropriate traits for Epic + let result = db.create_epic(epic.clone()); + + assert_eq!(result.is_ok(), true); + + let id = result.unwrap(); + let db_state = db.read_db().unwrap(); + + let expected_id = 1; + + assert_eq!(id, expected_id); + assert_eq!(db_state.last_item_id, expected_id); + assert_eq!(db_state.epics.get(&id), Some(&epic)); + } + + #[test] + fn create_story_should_error_if_invalid_epic_id() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let story = Story::new("".to_owned(), "".to_owned()); + + let non_existent_epic_id = 999; + + let result = db.create_story(story, non_existent_epic_id); + assert_eq!(result.is_err(), true); + } + + #[test] + fn create_story_should_work() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let epic = Epic::new("".to_owned(), "".to_owned()); + let story = Story::new("".to_owned(), "".to_owned()); + + let result = db.create_epic(epic); + assert_eq!(result.is_ok(), true); + + let epic_id = result.unwrap(); + + // TODO: fix this error by deriving the appropriate traits for Story + let result = db.create_story(story.clone(), epic_id); + assert_eq!(result.is_ok(), true); + + let id = result.unwrap(); + let db_state = db.read_db().unwrap(); + + let expected_id = 2; + + assert_eq!(id, expected_id); + assert_eq!(db_state.last_item_id, expected_id); + assert_eq!( + db_state.epics.get(&epic_id).unwrap().stories.contains(&id), + true + ); + assert_eq!(db_state.stories.get(&id), Some(&story)); + } + + #[test] + fn delete_epic_should_error_if_invalid_epic_id() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + + let non_existent_epic_id = 999; + + let result = db.delete_epic(non_existent_epic_id); + assert_eq!(result.is_err(), true); + } + + #[test] + fn delete_epic_should_work() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let epic = Epic::new("".to_owned(), "".to_owned()); + let story = Story::new("".to_owned(), "".to_owned()); + + let result = db.create_epic(epic); + assert_eq!(result.is_ok(), true); + + let epic_id = result.unwrap(); + + let result = db.create_story(story, epic_id); + assert_eq!(result.is_ok(), true); + + let story_id = result.unwrap(); + + let result = db.delete_epic(epic_id); + assert_eq!(result.is_ok(), true); + + let db_state = db.read_db().unwrap(); + + let expected_last_id = 2; + + assert_eq!(db_state.last_item_id, expected_last_id); + assert_eq!(db_state.epics.get(&epic_id), None); + assert_eq!(db_state.stories.get(&story_id), None); + } + + #[test] + fn delete_story_should_error_if_invalid_epic_id() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let epic = Epic::new("".to_owned(), "".to_owned()); + let story = Story::new("".to_owned(), "".to_owned()); + + let result = db.create_epic(epic); + assert_eq!(result.is_ok(), true); + + let epic_id = result.unwrap(); + + let result = db.create_story(story, epic_id); + assert_eq!(result.is_ok(), true); + + let story_id = result.unwrap(); + + let non_existent_epic_id = 999; + + let result = db.delete_story(non_existent_epic_id, story_id); + assert_eq!(result.is_err(), true); + } + + #[test] + fn delete_story_should_error_if_story_not_found_in_epic() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let epic = Epic::new("".to_owned(), "".to_owned()); + let story = Story::new("".to_owned(), "".to_owned()); + + let result = db.create_epic(epic); + assert_eq!(result.is_ok(), true); + + let epic_id = result.unwrap(); + + let result = db.create_story(story, epic_id); + assert_eq!(result.is_ok(), true); + + let non_existent_story_id = 999; + + let result = db.delete_story(epic_id, non_existent_story_id); + assert_eq!(result.is_err(), true); + } + + #[test] + fn delete_story_should_work() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let epic = Epic::new("".to_owned(), "".to_owned()); + let story = Story::new("".to_owned(), "".to_owned()); + + let result = db.create_epic(epic); + assert_eq!(result.is_ok(), true); + + let epic_id = result.unwrap(); + + let result = db.create_story(story, epic_id); + assert_eq!(result.is_ok(), true); + + let story_id = result.unwrap(); + + let result = db.delete_story(epic_id, story_id); + assert_eq!(result.is_ok(), true); + + let db_state = db.read_db().unwrap(); + + let expected_last_id = 2; + + assert_eq!(db_state.last_item_id, expected_last_id); + assert_eq!( + db_state + .epics + .get(&epic_id) + .unwrap() + .stories + .contains(&story_id), + false + ); + assert_eq!(db_state.stories.get(&story_id), None); + } + + #[test] + fn update_epic_status_should_error_if_invalid_epic_id() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + + let non_existent_epic_id = 999; + + let result = db.update_epic_status(non_existent_epic_id, Status::Closed); + assert_eq!(result.is_err(), true); + } + + #[test] + fn update_epic_status_should_work() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let epic = Epic::new("".to_owned(), "".to_owned()); + + let result = db.create_epic(epic); + + assert_eq!(result.is_ok(), true); + + let epic_id = result.unwrap(); + + let result = db.update_epic_status(epic_id, Status::Closed); + + assert_eq!(result.is_ok(), true); + + let db_state = db.read_db().unwrap(); + + assert_eq!(db_state.epics.get(&epic_id).unwrap().status, Status::Closed); + } + + #[test] + fn update_story_status_should_error_if_invalid_story_id() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + + let non_existent_story_id = 999; + + let result = db.update_story_status(non_existent_story_id, Status::Closed); + assert_eq!(result.is_err(), true); + } + + #[test] + fn update_story_status_should_work() { + let db = JiraDatabase { + database: Box::new(MockDB::new()), + }; + let epic = Epic::new("".to_owned(), "".to_owned()); + let story = Story::new("".to_owned(), "".to_owned()); + + let result = db.create_epic(epic); + + let epic_id = result.unwrap(); + + let result = db.create_story(story, epic_id); + + let story_id = result.unwrap(); + + let result = db.update_story_status(story_id, Status::Closed); + + assert_eq!(result.is_ok(), true); + + let db_state = db.read_db().unwrap(); + + assert_eq!( + db_state.stories.get(&story_id).unwrap().status, + Status::Closed + ); + } + mod database { use std::collections::HashMap; use std::io::Write; @@ -127,7 +533,6 @@ mod tests { let read_result = db.read_db().unwrap(); assert_eq!(write_result.is_ok(), true); - // TODO: fix this error by deriving the appropriate traits for DBState assert_eq!(read_result, state); } } diff --git a/src/models.rs b/src/models.rs index 0f5a471..ab871cc 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Status { Open, InProgress, @@ -9,7 +9,7 @@ pub enum Status { Closed, } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Epic { pub name: String, pub description: String, @@ -28,7 +28,7 @@ impl Epic { } } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Story { pub name: String, pub description: String, @@ -45,7 +45,7 @@ impl Story { } } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DBState { pub last_item_id: u32, pub epics: HashMap,