aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/mod.rs224
-rw-r--r--src/app/store/mod.rs58
-rw-r--r--src/app/store/sqlite.rs199
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);
+ }
+}