diff options
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/mod.rs | 224 | ||||
| -rw-r--r-- | src/app/store/mod.rs | 58 | ||||
| -rw-r--r-- | src/app/store/sqlite.rs | 199 |
3 files changed, 481 insertions, 0 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..6003640 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,224 @@ +mod store; + +use store::{Context, Store}; +use miette::{Diagnostic, ErrReport, Result}; +use thiserror::Error; + +#[derive(Default)] +pub struct App { + context: Context, + error: Option<ErrReport>, + exit: bool, +} + +#[derive(Diagnostic, Debug, Error, PartialEq)] +#[error("An overflow error occurred")] +#[diagnostic()] +pub struct OverflowError { + #[help] + pub _msg: String, +} + +impl App { + + /// Build context in App and pass it forward + pub fn init_context(&mut self) -> Result<&mut Self> { + let store = self.context.unwrap_store(); + + store.init()?; + Ok(self) + } + + pub fn get_exit_status(&self) -> bool { + self.exit + } + + pub fn exit(&mut self) -> Result<()> { + self.exit = true; + Ok(()) + } + + pub fn decrement_counter(&mut self) -> Result<()> { + let store = self.context.unwrap_store(); + let count = store.get_counter()?; + + if count == 0 { + Err(OverflowError { + _msg: "Cannot count below 0".into(), + } + .into()) + } else { + store.set_counter(count - 1)?; + Ok(()) + } + } + + pub fn increment_counter(&mut self) -> Result<()> { + let store = self.context.unwrap_store(); + let count = store.get_counter()?; + + if count == u8::MAX { + Err(OverflowError { + _msg: "Cannot count above u8::MAX".into(), + } + .into()) + } else { + store.set_counter(count + 1)?; + Ok(()) + } + } + + pub fn display_counter(&self) -> String { + self.context + .get_store() + .get_counter() + .map(|u| u.to_string()) + .unwrap_or("Error".into()) + } + + pub fn set_error(&mut self, error: ErrReport) -> Result<()> { + self.error = Some(error); + Ok(()) + } + + pub fn get_error(&self) -> &Option<ErrReport> { + &self.error + } + + /// Clears any existing input error in the app state + pub fn clear_error(&mut self) -> Result<()> { + self.error = None; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use store::ContextError; + + fn get_count(app: &App) -> Result<u8, ContextError> { + app.context.get_store().get_counter() + } + + #[test] + fn display_counter_inited() { + let mut app = App::default(); + assert!(app.init_context().is_ok()); + + let count = app.display_counter(); + + assert_eq!(count, "0"); + } + + #[test] + fn display_counter_noninited() { + let app = App::default(); + + let count = app.display_counter(); + + assert_eq!(count, "Error"); + } + + #[test] + fn increment_decrement() { + let mut app = App::default(); + assert!(app.init_context().is_ok()); + + let res = app.increment_counter(); + assert!(res.is_ok()); + assert_eq!(get_count(&app).unwrap(), 1); + + let res = app.decrement_counter(); + assert!(res.is_ok()); + assert_eq!(get_count(&app).unwrap(), 0); + } + + #[test] + fn increment_n() { + let mut app = App::default(); + assert!(app.init_context().is_ok()); + let n = 8; + + for i in 1..=n { + let res = app.increment_counter(); + assert!(res.is_ok()); + assert_eq!(get_count(&app).unwrap(), i); + } + assert_eq!(get_count(&app).unwrap(), n); + } + + #[test] + fn increment_n_decrement() { + let mut app = App::default(); + assert!(app.init_context().is_ok()); + let n = 8; + + for i in 1..=n { + let res = app.increment_counter(); + assert!(res.is_ok()); + assert_eq!(get_count(&app).unwrap(), i); + } + assert_eq!(get_count(&app).unwrap(), n); + + let res = app.decrement_counter(); + assert!(res.is_ok()); + assert_eq!(get_count(&app).unwrap(), n - 1); + } + + #[test] + fn exit() { + let mut app = App::default(); + assert!(app.init_context().is_ok()); + + let res = app.exit(); + + assert!(res.is_ok()); + assert!(app.exit); + } + + #[test] + fn decrement_counter_error() { + let mut app = App::default(); + assert!(app.init_context().is_ok()); + let res = app.decrement_counter(); + + assert!(res.is_err()); + let res_err = res.unwrap_err(); + let casted_val = res_err + .downcast_ref::<OverflowError>() + .expect("something bad occurred"); + + let expected_err_msg: String = "Cannot count below 0".into(); + assert_eq!(casted_val._msg, expected_err_msg); + assert_eq!( + *casted_val, + OverflowError { + _msg: expected_err_msg, + }, + ); + } + + #[test] + fn increment_counter_error() { + let mut app = App::default(); + assert!(app.init_context().is_ok()); + assert!(app.context.unwrap_store().set_counter(u8::MAX).is_ok()); + let res = app.increment_counter(); + + assert!(res.is_err()); + let res_err = res.unwrap_err(); + let casted_val = res_err + .downcast_ref::<OverflowError>() + .expect("something bad occurred"); + + let expected_err_msg: String = "Cannot count above u8::MAX".into(); + assert_eq!(casted_val._msg, expected_err_msg); + assert_eq!( + *casted_val, + OverflowError { + _msg: expected_err_msg, + }, + ); + } +} diff --git a/src/app/store/mod.rs b/src/app/store/mod.rs new file mode 100644 index 0000000..d038167 --- /dev/null +++ b/src/app/store/mod.rs @@ -0,0 +1,58 @@ +mod sqlite; + +use sqlite::{SQLiteStore}; +use miette::{Diagnostic, Result}; +use thiserror::Error; + +pub enum Context { + SQLite(SQLiteStore), +} + +#[derive(Debug, Diagnostic, Error)] +pub enum ContextError { + #[error("Data storage error")] + DataError(#[from] ::sqlite::Error), + #[error("Int conversion error")] + ConversionError(#[from] std::num::TryFromIntError), +} + +impl Default for Context { + fn default() -> Self { + Context::SQLite(SQLiteStore::default()) + } +} + +pub trait Store { + fn init(&mut self) -> Result<(), ContextError>; + fn get_counter(&self) -> Result<u8, ContextError>; + fn set_counter(&mut self, new_count: u8) -> Result<u8, ContextError>; +} + +impl Context { + pub fn unwrap_store(&mut self) -> &mut impl Store { + match self { + Context::SQLite(store) => store, + } + } + + pub fn get_store(&self) -> &impl Store { + match self { + Context::SQLite(store) => store, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn context_default() { + let context = Context::default(); + + let res = match context { + Context::SQLite(_) => true, + }; + assert!(res); + } +} diff --git a/src/app/store/sqlite.rs b/src/app/store/sqlite.rs new file mode 100644 index 0000000..4f5f8f2 --- /dev/null +++ b/src/app/store/sqlite.rs @@ -0,0 +1,199 @@ +use super::{ContextError, Store}; +use miette::Result; +use sqlite::Connection; + +pub struct SQLiteStore { + location: String, + connection: Option<Connection>, +} + +impl SQLiteStore { + /// Wrap connection in a Result to make it easier to unwrap later + fn get_connection(&self) -> Result<&Connection, sqlite::Error> { + match &self.connection { + Some(conn) => Ok(conn), + None => Err(sqlite::Error { + code: None, + message: Some("Uninitialized".into()), + }), + } + } + + fn create_table(&mut self) -> Result<(), sqlite::Error> { + let conn = self.get_connection()?; + let mut statement = conn.prepare("CREATE TABLE RATAT ( COUNTER SMALLINT )")?; + statement.next()?; + Ok(()) + } + + fn insert_counter(&mut self, counter: i64) -> Result<(), sqlite::Error> { + let conn = self.get_connection()?; + let mut statement = conn.prepare("INSERT INTO RATAT ( COUNTER ) VALUES ( :counter )")?; + statement.bind((":counter", counter))?; + statement.next()?; + Ok(()) + } + + fn select_counter(&self) -> Result<i64, sqlite::Error> { + let conn = self.get_connection()?; + let mut statement = conn.prepare("SELECT COUNTER FROM RATAT")?; + statement.next()?; + statement.read("COUNTER") + } + + fn update_counter(&mut self, counter: i64) -> Result<(), sqlite::Error> { + let conn = self.get_connection()?; + let mut statement = conn.prepare("UPDATE RATAT SET COUNTER = :counter")?; + statement.bind((":counter", counter))?; + statement.next()?; + Ok(()) + } +} + +impl Default for SQLiteStore { + fn default() -> Self { + SQLiteStore { + location: ":memory:".into(), + connection: Option::None, + } + } +} + +impl Store for SQLiteStore { + /// Init SQLite for counting. Creates `RATAT` table with single row, with + /// counter set to `0` + fn init(&mut self) -> Result<(), ContextError> { + let conn_path = self.location.clone(); + let conn = sqlite::open(conn_path)?; + self.connection = Option::Some(conn); + + self.create_table()?; + self.insert_counter(0)?; + + Ok(()) + } + + /// Selects counter from `RATAT` table. Since sqlite doesn't scrutinize the + /// value and we have a more restricted datatype (i64 -> u8), we use + /// `try_from` which may produce an error result. + fn get_counter(&self) -> Result<u8, ContextError> { + let result = self.select_counter()?; + + let count = u8::try_from(result)?; + Ok(count) + } + + /// Updates row with counter value on `RATAT` table + fn set_counter(&mut self, new_count: u8) -> Result<u8, ContextError> { + self.update_counter(i64::from(new_count))?; + + Ok(new_count) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Practically, the `Store` trait is what we interface with externally, and + // provides the more general `Result` interface we want to work with so + // that's what we test here + + #[test] + fn default() { + let context = SQLiteStore::default(); + + assert_eq!(context.location, ":memory:"); + assert!(context.connection.is_none()); + } + + #[test] + fn init() { + let mut context = SQLiteStore::default(); + + let res = context.init(); + + assert!(res.is_ok()); + assert!(context.connection.is_some()); + } + + #[test] + fn get_counter() { + let mut context = SQLiteStore::default(); + assert!(context.init().is_ok()); + + let res = context.get_counter(); + + assert!(res.is_ok()); + assert_eq!(res.unwrap(), 0); + } + + #[test] + fn get_counter_error() { + let context = SQLiteStore::default(); + // don't initialize + + let res = context.get_counter(); + + assert!(res.is_err()); + } + + #[test] + fn get_counter_outside_range() { + let mut context = SQLiteStore::default(); + assert!(context.init().is_ok()); + + let new_value = -1; + if let Some(conn) = &context.connection { + let mut statement = conn.prepare("UPDATE RATAT SET COUNTER = ?").unwrap(); + statement.bind((1, new_value)).unwrap(); + statement.next().unwrap(); + } + let res = context.get_counter(); + + assert!(res.is_err()); + } + + #[test] + fn set_counter() { + let mut context = SQLiteStore::default(); + assert!(context.init().is_ok()); + + let expected = 240; + let res = context.set_counter(expected); + + assert!(res.is_ok()); + assert_eq!(res.unwrap(), expected); + } + + #[test] + fn set_counter_error() { + let mut context = SQLiteStore::default(); + // don't initialize + + let res = context.set_counter(1); + + assert!(res.is_err()); + } + + #[test] + fn set_and_get() { + let mut context = SQLiteStore::default(); + assert!(context.init().is_ok()); + + let count_0 = context.get_counter().unwrap(); + + assert_eq!(count_0, 0); + + let new_num = 244; + let res = context.set_counter(new_num); + + assert!(res.is_ok()); + assert_eq!(res.unwrap(), new_num); + + let count_1 = context.get_counter().unwrap(); + + assert_eq!(count_1, new_num); + assert_ne!(count_0, count_1); + } +} |
