diff options
| author | Kevin Hoerr <kjhoerr@submelon.dev> | 2026-02-26 21:57:41 +0000 |
|---|---|---|
| committer | Kevin Hoerr <kjhoerr@submelon.dev> | 2026-02-26 21:57:41 +0000 |
| commit | 47ea7901b73085ead704ec2bd45402ae0c4ce5a2 (patch) | |
| tree | 28c5710b4ffe79c36cadcfdd3a28e30ec0fa2305 | |
| parent | 8a4f311c1d1fa4d978c93e58d95bf59a8a38b587 (diff) | |
| download | equity-tracker-47ea7901b73085ead704ec2bd45402ae0c4ce5a2.tar.gz equity-tracker-47ea7901b73085ead704ec2bd45402ae0c4ce5a2.tar.bz2 equity-tracker-47ea7901b73085ead704ec2bd45402ae0c4ce5a2.zip | |
Use SQLite to store counter
| -rw-r--r-- | src/main/rust/main.rs | 102 | ||||
| -rw-r--r-- | src/main/rust/store/mod.rs | 34 | ||||
| -rw-r--r-- | src/main/rust/store/sqlite.rs | 163 |
3 files changed, 264 insertions, 35 deletions
diff --git a/src/main/rust/main.rs b/src/main/rust/main.rs index 0805796..2b77a18 100644 --- a/src/main/rust/main.rs +++ b/src/main/rust/main.rs @@ -17,7 +17,6 @@ use thiserror::Error; #[derive(Default)] pub struct App { context: Context, - counter: u8, exit: bool, } @@ -35,17 +34,15 @@ fn main() -> Result<()> { } impl App { - // Initializes context + /// Build context in App and pass it forward pub fn init_context(&mut self) -> Result<&mut Self> { - match &mut self.context { - Context::SQLite(s) => { - s.init()?; - } - } + let store = self.context.unwrap_store(); + + store.init()?; Ok(self) } - /// runs the application's main loop until the user quits + /// Run the app's main loop until the user quits #[cfg(not(tarpaulin_include))] pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> { while !self.exit { @@ -55,10 +52,12 @@ impl App { Ok(()) } + /// Render app to frame fn draw(&self, frame: &mut Frame) { frame.render_widget(self, frame.area()); } + /// Use crossterm to read user input events fn handle_events(&mut self) -> Result<()> { match event::read().into_diagnostic()? { // it's important to check that the event is a key press event as @@ -70,6 +69,7 @@ impl App { } } + /// Process user input and run associated app functions fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> { match key_event.code { KeyCode::Char('q') => self.exit()?, @@ -85,27 +85,43 @@ impl App { Ok(()) } - fn decrement_counter(&mut self) -> Result<(), OverflowError> { - if self.counter == 0 { + 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 { - self.counter -= 1; + store.set_counter(count - 1)?; Ok(()) } } - fn increment_counter(&mut self) -> Result<(), OverflowError> { - if self.counter == u8::MAX { + 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 { - self.counter += 1; + store.set_counter(count + 1)?; Ok(()) } } + + fn display_counter(&self) -> String { + self.context + .get_store() + .get_counter() + .map(|u| u.to_string()) + .unwrap_or("Error".into()) + } } impl Widget for &App { @@ -126,7 +142,7 @@ impl Widget for &App { let counter_text = Text::from(vec![Line::from(vec![ "Value: ".into(), - self.counter.to_string().yellow(), + self.display_counter().yellow(), ])]); Paragraph::new(counter_text) @@ -138,12 +154,39 @@ impl Widget for &App { #[cfg(test)] mod tests { + use crate::store::ContextError; + use super::*; use ratatui::style::Style; + fn get_count(app: &App) -> Result<u8, ContextError> { + app.context.get_store().get_counter() + } + #[test] - fn render() { + 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 render() { + let mut app = App::default(); + assert!(app.init_context().is_ok()); + let mut buf = Buffer::empty(Rect::new(0, 0, 50, 4)); app.render(buf.area, &mut buf); @@ -170,48 +213,53 @@ mod tests { #[test] fn handle_key_event_inc_dec() { let mut app = App::default(); + assert!(app.init_context().is_ok()); + let res = app.handle_key_event(KeyCode::Right.into()); assert!(res.is_ok()); - assert_eq!(app.counter, 1); + assert_eq!(get_count(&app).unwrap(), 1); let res = app.handle_key_event(KeyCode::Left.into()); assert!(res.is_ok()); - assert_eq!(app.counter, 0); + assert_eq!(get_count(&app).unwrap(), 0); } #[test] fn handle_key_event_inc_n() { let mut app = App::default(); + assert!(app.init_context().is_ok()); let n = 8; for i in 1..=n { let res = app.handle_key_event(KeyCode::Right.into()); assert!(res.is_ok()); - assert_eq!(app.counter, i); + assert_eq!(get_count(&app).unwrap(), i); } - assert_eq!(app.counter, n); + assert_eq!(get_count(&app).unwrap(), n); } #[test] fn handle_key_event_inc_n_dec() { let mut app = App::default(); + assert!(app.init_context().is_ok()); let n = 8; for i in 1..=n { let res = app.handle_key_event(KeyCode::Right.into()); assert!(res.is_ok()); - assert_eq!(app.counter, i); + assert_eq!(get_count(&app).unwrap(), i); } - assert_eq!(app.counter, n); + assert_eq!(get_count(&app).unwrap(), n); let res = app.handle_key_event(KeyCode::Left.into()); assert!(res.is_ok()); - assert_eq!(app.counter, n - 1); + assert_eq!(get_count(&app).unwrap(), n - 1); } #[test] fn handle_key_event_exit() { let mut app = App::default(); + assert!(app.init_context().is_ok()); let res = app.handle_key_event(KeyCode::Char('q').into()); assert!(res.is_ok()); assert!(app.exit); @@ -220,6 +268,7 @@ mod tests { #[test] fn handle_key_event_dec_error() { let mut app = App::default(); + assert!(app.init_context().is_ok()); let res = app.handle_key_event(KeyCode::Left.into()); assert!(res.is_err()); @@ -241,7 +290,8 @@ mod tests { #[test] fn handle_key_event_inc_error() { let mut app = App::default(); - app.counter = u8::MAX; + assert!(app.init_context().is_ok()); + assert!(app.context.unwrap_store().set_counter(u8::MAX).is_ok()); let res = app.handle_key_event(KeyCode::Right.into()); assert!(res.is_err()); diff --git a/src/main/rust/store/mod.rs b/src/main/rust/store/mod.rs index 42f1809..7c7d9ce 100644 --- a/src/main/rust/store/mod.rs +++ b/src/main/rust/store/mod.rs @@ -1,11 +1,23 @@ -pub mod sqlite; +mod sqlite; -use miette::Result; +use std::num::TryFromIntError; + +use ::sqlite::Error as SQLiteLibraryError; +use miette::{Diagnostic, Result}; +use thiserror::Error; pub enum Context { SQLite(sqlite::SQLite), } +#[derive(Debug, Diagnostic, Error)] +pub enum ContextError { + #[error("SQLite error")] + SQLiteError(#[from] SQLiteLibraryError), + #[error("Int conversion error")] + ConversionError(#[from] TryFromIntError), +} + impl Default for Context { fn default() -> Self { Context::SQLite(sqlite::SQLite::default()) @@ -13,7 +25,23 @@ impl Default for Context { } pub trait Store { - fn init(&mut self) -> Result<()>; + 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)] diff --git a/src/main/rust/store/sqlite.rs b/src/main/rust/store/sqlite.rs index 734c635..6033494 100644 --- a/src/main/rust/store/sqlite.rs +++ b/src/main/rust/store/sqlite.rs @@ -1,5 +1,5 @@ -use crate::store::Store; -use miette::{IntoDiagnostic, Result}; +use crate::store::{ContextError, Store}; +use miette::Result; use sqlite::Connection; pub struct SQLite { @@ -7,6 +7,49 @@ pub struct SQLite { connection: Option<Connection>, } +impl SQLite { + /// 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(&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(&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(&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 SQLite { fn default() -> Self { SQLite { @@ -17,19 +60,47 @@ impl Default for SQLite { } impl Store for SQLite { - fn init(&mut self) -> Result<()> { + /// 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(); - self.connection = sqlite::open(conn_path).into_diagnostic().ok(); + 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 sqlite_default() { + fn default() { let context = SQLite::default(); assert_eq!(context.location, ":memory:"); @@ -37,7 +108,7 @@ mod tests { } #[test] - fn sqlite_init() { + fn init() { let mut context = SQLite::default(); let res = context.init(); @@ -45,4 +116,84 @@ mod tests { assert!(res.is_ok()); assert!(context.connection.is_some()); } + + #[test] + fn get_counter() { + let mut context = SQLite::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 = SQLite::default(); + // don't initialize + + let res = context.get_counter(); + + assert!(res.is_err()); + } + + #[test] + fn get_counter_outside_range() { + let mut context = SQLite::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 = SQLite::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 = SQLite::default(); + // don't initialize + + let res = context.set_counter(1); + + assert!(res.is_err()); + } + + #[test] + fn set_and_get() { + let mut context = SQLite::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); + } } |
