aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorKevin Hoerr <kjhoerr@submelon.dev>2026-04-16 11:37:21 -0400
committerKevin Hoerr <kjhoerr@submelon.dev>2026-04-16 11:37:21 -0400
commit615d44041d35ee1a08ddc66423f67a9d48f53ae9 (patch)
tree9720d6a192e0bae9c7393b8b99ff8ae401ec7f72 /src/main
parent51623bc995c29937bb0a13d5b71ff908be8d1a1c (diff)
downloadequity-tracker-615d44041d35ee1a08ddc66423f67a9d48f53ae9.tar.gz
equity-tracker-615d44041d35ee1a08ddc66423f67a9d48f53ae9.tar.bz2
equity-tracker-615d44041d35ee1a08ddc66423f67a9d48f53ae9.zip
Switch to distrobox as base for development
Diffstat (limited to 'src/main')
-rw-r--r--src/main/resources/db/20251114-0-init-schema.sql53
-rw-r--r--src/main/resources/db/20251117-0-summary-view.sql65
-rw-r--r--src/main/rust/app.rs220
-rw-r--r--src/main/rust/main.rs189
-rw-r--r--src/main/rust/store/mod.rs58
-rw-r--r--src/main/rust/store/sqlite.rs199
-rw-r--r--src/main/rust/ui.rs134
7 files changed, 0 insertions, 918 deletions
diff --git a/src/main/resources/db/20251114-0-init-schema.sql b/src/main/resources/db/20251114-0-init-schema.sql
deleted file mode 100644
index 886a961..0000000
--- a/src/main/resources/db/20251114-0-init-schema.sql
+++ /dev/null
@@ -1,53 +0,0 @@
--- Equity tracker schema
--- SQLite does not control types so directly; but it makes it more portable to a different SQL server with specificity
-
--- enum as table - there may be other equity types, does not affect views directly
-create table equity_type
-(
- equity_type_name VARCHAR(20) PRIMARY KEY NOT NULL
-);
-
-INSERT INTO equity_type (equity_type_name) VALUES
- ('STOCK'),
- ('ETF'),
- ('CRYPTO');
-
--- deletion of equity_type does not cascade - equity_type is required for equity_symbol, which is required for equity_change_event, which is required for fiduciary_account.
--- equity_symbol, equity_type would never be deleted, but they could be cleaned up if they are orphaned records.
-create table equity_symbol
-(
- equity_symbol_id SMALLINT PRIMARY KEY NOT NULL,
- equity_symbol_name VARCHAR(16) NOT NULL,
- equity_symbol_type VARCHAR(20) NOT NULL
- REFERENCES equity_type(equity_type_name),
- equity_symbol_managing_company VARCHAR(96) NOT NULL,
- equity_symbol_created_timestamp TIMESTAMP NOT NULL
-);
-
-create table fiduciary_account
-(
- fiduciary_account_id TINYINT PRIMARY KEY NOT NULL,
- fiduciary_account_number UNSIGNED BIG INT NOT NULL,
- fiduciary_account_name NVARCHAR(96) NOT NULL,
- fiduciary_account_description NVARCHAR(512),
- fiduciary_account_created_timestamp TIMESTAMP NOT NULL
-);
-
--- deletion of fiduciary_account row cascades - an account precludes change events.
--- deletion of equity_symbol row does not cascade - all events should be recorded for an account, and all events must have symbols
-create table equity_change_event
-(
- equity_change_event_id UNSIGNED BIG INT PRIMARY KEY NOT NULL,
- fiduciary_account_id TINYINT NOT NULL
- REFERENCES fiduciary_account(fiduciary_account_id)
- ON DELETE CASCADE,
- equity_symbol_id SMALLINT NOT NULL
- REFERENCES equity_symbol(equity_symbol_id),
- equity_change_quantity DECIMAL(20, 8) NOT NULL,
- equity_change_cost_basis_usd DECIMAL(14, 2) NOT NULL,
- equity_change_type VARCHAR(20) NOT NULL
- CHECK (equity_change_type IN ('BUY', 'SELL')), -- This is essentially '+' or '-'
- equity_change_timestamp TIMESTAMP NOT NULL,
- equity_change_type_score NUMERIC AS (CASE WHEN equity_change_type = 'BUY' THEN 1.0 ELSE -1.0 END) STORED,
- equity_change_est_price DECIMAL(14, 2) AS (equity_change_cost_basis_usd / equity_change_quantity) STORED
-);
diff --git a/src/main/resources/db/20251117-0-summary-view.sql b/src/main/resources/db/20251117-0-summary-view.sql
deleted file mode 100644
index a4e8004..0000000
--- a/src/main/resources/db/20251117-0-summary-view.sql
+++ /dev/null
@@ -1,65 +0,0 @@
--- Yucky slow view, since it's sub-querying for each of the summation fields
--- AND has a subquery on the inner join to find the right symbols. Anyways it
--- should preview relative strength of treating equity transaction data as
--- immutable. Any application on top of this should get the full list of
--- records so it can construct helpful views of summations based on the history
--- of changes, and/or show price data over time
-CREATE VIEW account_symbol_summary_view
-AS
-SELECT
- acc.fiduciary_account_id,
- acc.fiduciary_account_name,
- acc.fiduciary_account_description,
- acc.fiduciary_account_number,
- acc.fiduciary_account_created_timestamp,
- sym.equity_symbol_id,
- sym.equity_symbol_type,
- sym.equity_symbol_name,
- sym.equity_symbol_managing_company,
- (SELECT
- SUM(ev.equity_change_quantity * ev.equity_change_type_score)
- FROM equity_change_event ev
- WHERE ev.fiduciary_account_id = acc.fiduciary_account_id
- AND ev.equity_symbol_id = sym.equity_symbol_id
- ) AS account_symbol_quantity,
- (SELECT
- printf( '%.2f', SUM(ev.equity_change_cost_basis_usd * ev.equity_change_type_score) / SUM(ev.equity_change_quantity * ev.equity_change_type_score))
- FROM equity_change_event ev
- WHERE ev.fiduciary_account_id = acc.fiduciary_account_id
- AND ev.equity_symbol_id = sym.equity_symbol_id
- ) AS account_symbol_cost_basis_price,
- (SELECT
- printf( '%.2f', SUM(ev.equity_change_cost_basis_usd * ev.equity_change_type_score))
- FROM equity_change_event ev
- WHERE ev.fiduciary_account_id = acc.fiduciary_account_id
- AND ev.equity_symbol_id = sym.equity_symbol_id
- ) AS account_symbol_cost_basis_total_usd,
- (SELECT
- printf( '%.2f', SUM(ev.equity_change_quantity) * (
- SELECT
- iev.equity_change_est_price
- FROM equity_change_event iev
- WHERE iev.fiduciary_account_id = acc.fiduciary_account_id
- AND iev.equity_symbol_id = sym.equity_symbol_id
- ORDER BY iev.equity_change_timestamp DESC
- LIMIT 1
- ))
- FROM equity_change_event ev
- WHERE ev.fiduciary_account_id = acc.fiduciary_account_id
- AND ev.equity_symbol_id = sym.equity_symbol_id
- ) AS account_symbol_last_effective_price,
- (SELECT
- ev.equity_change_timestamp
- FROM equity_change_event ev
- WHERE ev.fiduciary_account_id = acc.fiduciary_account_id
- AND ev.equity_symbol_id = sym.equity_symbol_id
- ORDER BY ev.equity_change_timestamp DESC
- LIMIT 1
- ) AS account_symbol_last_update_timestamp
-FROM fiduciary_account acc
-INNER JOIN equity_symbol sym ON EXISTS(
- SELECT *
- FROM equity_change_event e
- WHERE e.fiduciary_account_id = acc.fiduciary_account_id
- AND e.equity_symbol_id = sym.equity_symbol_id
-); \ No newline at end of file
diff --git a/src/main/rust/app.rs b/src/main/rust/app.rs
deleted file mode 100644
index 2ed7c28..0000000
--- a/src/main/rust/app.rs
+++ /dev/null
@@ -1,220 +0,0 @@
-use crate::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
- }
-
- pub fn clear_error(&mut self) -> Result<()> {
- self.error = None;
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::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/main/rust/main.rs b/src/main/rust/main.rs
deleted file mode 100644
index 124ce81..0000000
--- a/src/main/rust/main.rs
+++ /dev/null
@@ -1,189 +0,0 @@
-mod app;
-pub mod store;
-mod ui;
-
-use app::App;
-use miette::{IntoDiagnostic, Result, miette};
-use ratatui::{Terminal, backend::Backend};
-
-use ratatui::crossterm::{
- event::{
- self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
- },
- execute,
- terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
-};
-use ratatui::prelude::CrosstermBackend;
-
-#[cfg(not(tarpaulin_include))]
-fn main() -> Result<()> {
- enable_raw_mode().into_diagnostic()?;
- let mut stdout = std::io::stdout();
-
- execute!(stdout, EnterAlternateScreen, EnableMouseCapture).into_diagnostic()?;
-
- let backend = CrosstermBackend::new(stdout);
- let mut term = Terminal::new(backend).into_diagnostic()?;
-
- let mut app = App::default();
- app.init_context()?;
-
- // could be error, so don't unwrap; let term restore first
- let run = run_app(&mut term, &mut app);
-
- // restore terminal
- disable_raw_mode().into_diagnostic()?;
- execute!(
- term.backend_mut(),
- LeaveAlternateScreen,
- DisableMouseCapture
- )
- .into_diagnostic()?;
- term.show_cursor().into_diagnostic()?;
- run?;
- Ok(())
-}
-
-#[cfg(not(tarpaulin_include))]
-fn run_app<B: Backend>(term: &mut Terminal<B>, app: &mut App) -> Result<()> {
- while !app.get_exit_status() {
- term.draw(|frame| ui::draw(frame, app))
- .map_err(|_| miette!("Unrecoverable error occurred"))?;
-
- match event::read().into_diagnostic()? {
- // it's important to check that the event is a key press event as
- // crossterm also emits key release and repeat events on Windows.
- Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
- // if error occurred during input processing, set error in the app state
- match handle_key_event(app, key_event) {
- Err(e) => app.set_error(e)?,
- _ => {}
- }
- }
- _ => {}
- };
- }
- Ok(())
-}
-
-/// Process user input and run associated app functions
-fn handle_key_event(app: &mut App, key_event: KeyEvent) -> Result<()> {
- match key_event.code {
- KeyCode::Char('q') => app.exit()?,
- KeyCode::Left => app.decrement_counter()?,
- KeyCode::Right => app.increment_counter()?,
- _ => {}
- };
- // any new key event, we should clear the existing error
- app.clear_error()?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
-
- use super::*;
- use app::OverflowError;
-
- #[test]
- fn handle_key_event_inc_dec() {
- let mut app = App::default();
- assert!(app.init_context().is_ok());
- let res = handle_key_event(&mut app, KeyCode::Right.into());
- assert!(res.is_ok());
- assert_eq!(app.display_counter(), "1");
-
- let res = handle_key_event(&mut app, KeyCode::Left.into());
- assert!(res.is_ok());
- assert_eq!(app.display_counter(), "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 = handle_key_event(&mut app, KeyCode::Right.into());
- assert!(res.is_ok());
- assert_eq!(app.display_counter(), i.to_string());
- }
- assert_eq!(app.display_counter(), n.to_string());
- }
-
- #[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 = handle_key_event(&mut app, KeyCode::Right.into());
- assert!(res.is_ok());
- assert_eq!(app.display_counter(), i.to_string());
- }
- assert_eq!(app.display_counter(), n.to_string());
-
- let res = handle_key_event(&mut app, KeyCode::Left.into());
- assert!(res.is_ok());
- assert_eq!(app.display_counter(), (n - 1).to_string());
- }
-
- #[test]
- fn handle_key_event_exit() {
- let mut app = App::default();
- assert!(app.init_context().is_ok());
- let res = handle_key_event(&mut app, KeyCode::Char('q').into());
- assert!(res.is_ok());
- assert!(app.get_exit_status());
- }
-
- #[test]
- fn handle_key_event_dec_error() {
- let mut app = App::default();
- assert!(app.init_context().is_ok());
- let res = handle_key_event(&mut app, KeyCode::Left.into());
-
- 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 handle_key_event_inc_error() {
- let mut app = App::default();
- assert!(app.init_context().is_ok());
- // no pub set_counter exposed, so we manually set this
- for _ in 0..u8::MAX {
- assert!(app.increment_counter().is_ok());
- }
- let res = handle_key_event(&mut app, KeyCode::Right.into());
-
- 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/main/rust/store/mod.rs b/src/main/rust/store/mod.rs
deleted file mode 100644
index 2cce3e9..0000000
--- a/src/main/rust/store/mod.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-mod sqlite;
-
-use crate::store::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/main/rust/store/sqlite.rs b/src/main/rust/store/sqlite.rs
deleted file mode 100644
index 6647556..0000000
--- a/src/main/rust/store/sqlite.rs
+++ /dev/null
@@ -1,199 +0,0 @@
-use crate::store::{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);
- }
-}
diff --git a/src/main/rust/ui.rs b/src/main/rust/ui.rs
deleted file mode 100644
index 1632623..0000000
--- a/src/main/rust/ui.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-use crate::app::App;
-use miette::ErrReport;
-use ratatui::{
- Frame,
- layout::{Constraint, Direction, Layout, Rect},
- style::{Style, Stylize},
- symbols::border,
- text::{Line, Span, Text},
- widgets::{Block, Clear, Paragraph, Widget},
-};
-
-pub fn draw(frame: &mut Frame, app: &App) {
- let rect = draw_layout(app);
- frame.render_widget(rect, frame.area());
-
- if let Some(error) = app.get_error() {
- let popup = error_popup(error);
-
- let area = centered_rect(40, 25, frame.area());
- frame.render_widget(Clear, area);
- frame.render_widget(popup, area);
- }
-}
-
-fn draw_layout(app: &App) -> impl Widget {
- let title = Line::from(" Counter App Tutorial ".bold());
- let instructions = Line::from(vec![
- " Decrement ".into(),
- "<Left>".blue().bold(),
- " Increment ".into(),
- "<Right>".blue().bold(),
- " Quit ".into(),
- "<Q> ".blue().bold(),
- ]);
- let block = Block::bordered()
- .title(title.centered())
- .title_bottom(instructions.centered())
- .border_set(border::THICK);
-
- let counter_text = Text::from(vec![Line::from(vec![
- "Value: ".into(),
- app.display_counter().yellow(),
- ])]);
-
- Paragraph::new(counter_text).centered().block(block)
-}
-
-/// helper function to create a centered rect using up certain percentage of the available rect `r`
-fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
- // Cut the given rectangle into three vertical pieces
- let popup_layout = Layout::default()
- .direction(Direction::Vertical)
- .constraints([
- Constraint::Percentage((100 - percent_y) / 2),
- Constraint::Percentage(percent_y),
- Constraint::Percentage((100 - percent_y) / 2),
- ])
- .split(r);
- // Then cut the middle vertical piece into three width-wise pieces
- Layout::default()
- .direction(Direction::Horizontal)
- .constraints([
- Constraint::Percentage((100 - percent_x) / 2),
- Constraint::Percentage(percent_x),
- Constraint::Percentage((100 - percent_x) / 2),
- ])
- .split(popup_layout[1])[1] // Return the middle chunk
-}
-
-fn error_popup(error: &ErrReport) -> impl Widget {
- let title = Line::from("Error").bold();
- let popup = Block::bordered()
- .title(title.centered())
- .border_set(border::DOUBLE);
-
- let error_text = error_text(error);
-
- Paragraph::new(error_text).block(popup)
-}
-
-fn error_text(error: &ErrReport) -> Text<'_> {
- let errname_spans = vec![
- Span::raw("Error "),
- Span::styled("x", Style::new().red()),
- Span::raw(" "),
- Span::raw(error.to_string()),
- ];
-
- let line1 = Line::default().spans(errname_spans);
- let help_spans = vec![
- Span::styled(" help: ", Style::new().cyan()),
- Span::raw(error.help().map_or("".into(), |x| x.to_string())),
- ];
- let line2 = Line::default().spans(help_spans);
-
- Text::from(vec![line1, line2])
-}
-
-#[cfg(test)]
-mod tests {
-
- use super::*;
- use ratatui::{buffer::Buffer, layout::Rect, style::Style};
-
- #[test]
- fn render_success() {
- let mut app = App::default();
- assert!(app.init_context().is_ok());
-
- let rect = Rect::new(0, 0, 50, 4);
- let mut buf = Buffer::empty(rect);
-
- let res = draw_layout(&app);
- res.render(rect, &mut buf);
-
- let mut expected = Buffer::with_lines(vec![
- "┏━━━━━━━━━━━━━ Counter App Tutorial ━━━━━━━━━━━━━┓",
- "┃ Value: 0 ┃",
- "┃ ┃",
- "┗━ Decrement <Left> Increment <Right> Quit <Q> ━━┛",
- ]);
- let title_style = Style::new().bold();
- let counter_style = Style::new().yellow();
- let key_style = Style::new().blue().bold();
-
- expected.set_style(Rect::new(14, 0, 22, 1), title_style);
- expected.set_style(Rect::new(28, 1, 1, 1), counter_style);
- expected.set_style(Rect::new(13, 3, 6, 1), key_style);
- expected.set_style(Rect::new(30, 3, 7, 1), key_style);
- expected.set_style(Rect::new(43, 3, 4, 1), key_style);
-
- assert_eq!(buf, expected);
- }
-}