diff --git a/migrations/create_tables_001.sql b/migrations/create_tables_001.sql index 75c1acd..2e856a3 100644 --- a/migrations/create_tables_001.sql +++ b/migrations/create_tables_001.sql @@ -1,9 +1,9 @@ CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - public_key TEXT NOT NULL, - foo INTEGER + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + public_key TEXT NOT NULL, + creation_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS security_types @@ -24,13 +24,17 @@ CREATE TABLE IF NOT EXISTS securities FOREIGN KEY (security_type) REFERENCES security_types (name) ); -CREATE TABLE IF NOT EXISTS account +CREATE TABLE IF NOT EXISTS accounts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - security INTEGER NOT NULL, - owner INTEGER NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + security INTEGER NOT NULL, + owner INTEGER NOT NULL, + amount INTEGER NOT NULL, + timestamp DATETIME NOT NULL, + creation_time DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (security) REFERENCES securities (id) + FOREIGN KEY (security) REFERENCES securities (id), + FOREIGN KEY (owner) REFERENCES users (id) ); CREATE TABLE IF NOT EXISTS account_values diff --git a/src/account.rs b/src/account.rs index 2fad85d..5fd92cd 100644 --- a/src/account.rs +++ b/src/account.rs @@ -1,16 +1,109 @@ -use rusqlite::Connection; +use rusqlite::{Connection, params, Result, Row, Transaction}; + use crate::securities::Security; use crate::users::User; -struct Account<'a> { - security: &'a Security, - user: &'a User, +pub struct Account { + id: i64, amount: i64, - time_stamp: i64 + timestamp: i64, } +fn _account_from_row(row: &Row) -> Result { + return Ok(Account{ + id: row.get(0)?, + amount: row.get(1)?, + timestamp: row.get(2)?, + }); +} -pub fn list_accounts(conn: & Connection){ +pub fn list_accounts<'a>(conn: &Connection) -> Result> { + let q = "SELECT id, amount, UNIXEPOCH(timestamp) + FROM accounts + ORDER BY id ASC"; + let mut stmt = conn.prepare(q)?; + let ret: Vec = stmt + .query_map([], &_account_from_row)? + .filter(Result::is_ok) + .map(Result::unwrap) + .collect(); + return Ok(ret); +} +pub fn get_account_by_id(tx: &Transaction, id: u32) -> Result{ + let q = "SELECT id, amount, UNIXEPOCH(timestamp) FROM accounts WHERE id = ?1"; + return tx.query_row(q, params![id], _account_from_row); +} + +pub fn create_account(tx: &Transaction, sec: &Security, user: &User) -> Result{ + let q = "INSERT INTO accounts(security, owner, amount, timestamp) + VALUES (?1, ?2, 0, CURRENT_TIMESTAMP)"; + tx.execute(q, (sec.id, user.id))?; + return get_account_by_id(tx, tx.last_insert_rowid() as u32); +} + +#[cfg(test)] +mod test { + use rusqlite::{Connection}; + use crate::users; + use crate::securities; + use crate::migration; + + use super::*; + + fn setup_users(tx: &Transaction) -> Result> { + let user_1 = users::create_user(tx, &"bernard".to_string(), &"123".to_string())?; + let user_2 = users::create_user(tx, &"sam".to_string(), &"abc".to_string())?; + return Ok(vec![user_1, user_2]); + } + + fn setup_securities(tx: &Transaction) -> Result> { + let sec_1 = securities::create_security(tx, &securities::test_util::mock_currency_1())?; + let sec_2 = securities::create_security(tx, &securities::test_util::mock_currency_2())?; + return Ok(vec![sec_1, sec_2]); + } + + fn setup() -> (Connection, Vec, Vec) { + let mut conn = Connection::open_in_memory().unwrap(); + migration::apply_migrations(&mut conn).unwrap(); + + let tx = conn.transaction().unwrap(); + let users = setup_users(&tx).unwrap(); + let securities = setup_securities(&tx).unwrap(); + + tx.commit().unwrap(); + return (conn, users, securities); + } + + fn create_all_accounts(tx: & Transaction, users: & Vec, secs: & Vec) -> Result<()>{ + for sec in secs.iter() { + for user in users.iter() { + create_account(&tx, sec, user)?; + } + } + return Ok(()); + } + + #[test] + fn test_happy_path() -> Result<()>{ + let (mut conn, users, secs) = setup(); + let tx = conn.transaction()?; + create_all_accounts(& tx, &users, &secs)?; + tx.commit()?; + let accs = list_accounts(& conn)?; + assert_eq!(4, accs.len()); + assert_eq!(1, accs.get(0).unwrap().id); + assert_eq!(4, accs.get(3).unwrap().id); + return Ok(()); + } + + #[test] + fn test_happy_path_no_commit() -> Result<()>{ + let (mut conn, users, secs) = setup(); + create_all_accounts(& conn.transaction()?, &users, &secs)?; + let accs = list_accounts(& conn)?; + assert_eq!(0, accs.len()); + return Ok(()); + } } diff --git a/src/main.rs b/src/main.rs index b8ba8df..5d16958 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ impl Output { } trait CommandLineUserLogic { - fn create_user(&mut self, conn: &Connection, name: &String, public_key: &String); + fn create_user(&mut self, conn: &mut Connection, name: &String, public_key: &String); fn list_user(&mut self, conn: &Connection); @@ -103,7 +103,7 @@ fn command_line_parser() -> Command { .subcommand(securities_command_line_parser()); } -fn process_new_user_command_line(conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { +fn process_new_user_command_line(conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { debug!("Processing new user command line {:?}.", m); let name = m.get_one::("name").unwrap(); let pk = m.get_one::("public_key").unwrap(); @@ -115,7 +115,7 @@ fn process_list_user_command_line(conn: &Connection, m: &ArgMatches, logic: &mut logic.list_user(conn); } -fn process_user_command_line(conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { +fn process_user_command_line(conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { debug!("Processing user command line {:?}.", m); match m.subcommand() { Some(("new", new_user_match)) => { @@ -143,7 +143,7 @@ fn process_securities_command_line(conn: &Connection, m: &ArgMatches, logic: &mu } } -fn process_command_line(conn: &Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { +fn process_command_line(conn: &mut Connection, m: &ArgMatches, logic: &mut dyn CommandLineUserLogic) { debug!("Processing command line {:?}.", m); match m.subcommand() { Some(("users", user_match)) => { @@ -181,7 +181,7 @@ mod test { } impl CommandLineUserLogic for TestCommandLineLogic { - fn create_user(&mut self, _conn: &Connection, _name: &String, _public_key: &String) { + fn create_user(&mut self, _conn: &mut Connection, _name: &String, _public_key: &String) { self.create_user_count += 1; } @@ -232,8 +232,8 @@ mod test { let cmd = command_line_parser(); let m = cmd.get_matches_from(vec!["test", "users", "new", "--name", "paul", "--public-key", "123abc"]); let mut logic = TestCommandLineLogic::new(); - let conn = Connection::open_in_memory().unwrap(); - process_command_line(&conn, &m, &mut logic); + let mut conn = Connection::open_in_memory().unwrap(); + process_command_line(&mut conn, &m, &mut logic); assert_eq!(logic.create_user_count, 1); } @@ -242,8 +242,8 @@ mod test { let cmd = command_line_parser(); let m = cmd.get_matches_from(vec!["test", "users", "ls"]); let mut logic = TestCommandLineLogic::new(); - let conn = Connection::open_in_memory().unwrap(); - process_command_line(&conn, &m, &mut logic); + let mut conn = Connection::open_in_memory().unwrap(); + process_command_line(&mut conn, &m, &mut logic); assert_eq!(logic.list_user_count, 1); } @@ -252,8 +252,8 @@ mod test { let cmd = command_line_parser(); let m = cmd.get_matches_from(vec!["test", "securities", "ls"]); let mut logic = TestCommandLineLogic::new(); - let conn = Connection::open_in_memory().unwrap(); - process_command_line(&conn, &m, &mut logic); + let mut conn = Connection::open_in_memory().unwrap(); + process_command_line(&mut conn, &m, &mut logic); assert_eq!(logic.list_securities_count, 1); } } @@ -267,21 +267,17 @@ impl ProCommandLineLogic { } impl CommandLineUserLogic for ProCommandLineLogic { - fn create_user(&mut self, conn: &Connection, name: &String, public_key: &String) { - users::create_user(conn, name, public_key); + fn create_user(&mut self, conn: &mut Connection, name: &String, public_key: &String) { + let tx = conn.transaction().unwrap(); + users::create_user(& tx, name, public_key); + tx.commit().unwrap(); } fn list_user(&mut self, conn: &Connection) { let users = users::list_users(conn); match users { Ok(u) => { - let r: Vec<& users::User> = u.iter() - .filter(|& x| x.is_ok()) - .map(| x| x.as_ref().unwrap()) - .collect(); - - let out = Output::success(r); - + let out = Output::success(u); println!("{}", serde_json::to_string(&out).unwrap()); } Err(e) => { @@ -325,5 +321,5 @@ fn main() { apply_migrations(&mut conn).unwrap(); let mut logic = ProCommandLineLogic::new(); - process_command_line(&conn, &m, &mut logic); + process_command_line(&mut conn, &m, &mut logic); } \ No newline at end of file diff --git a/src/securities.rs b/src/securities.rs index e75bccb..d82d10c 100644 --- a/src/securities.rs +++ b/src/securities.rs @@ -1,7 +1,6 @@ -use rusqlite::{Connection, Result, ToSql}; +use rusqlite::{Connection, params, Result, Row, ToSql, Transaction}; use serde::Serialize; -use crate::migration; #[derive(Debug, Serialize, PartialEq)] pub enum SecurityType { @@ -33,50 +32,80 @@ impl ToSql for SecurityType { #[derive(Debug, Serialize, PartialEq)] pub struct Security { - id: i32, + pub id: i32, security_type: SecurityType, symbol: String, } pub struct NewSecurity { security_type: SecurityType, - public_key: String, symbol: String, name: String, } -pub fn create_security(conn: &mut Connection, sec: &NewSecurity) -> Result<()> { - let tx = conn.transaction()?; +pub fn create_security(tx: &Transaction, sec: &NewSecurity) -> Result { let q = "INSERT INTO securities(name, symbol, security_type) VALUES(?1, ?2, ?3)"; let params = (&sec.name, &sec.symbol, &sec.security_type); tx.execute(q, params)?; - return tx.commit(); + return get_security_by_id(tx, tx.last_insert_rowid() as i32); } -pub fn list_securities(conn: & Connection) -> Result> { +fn _security_from_row(row: &Row) -> Result { + Ok(Security { + id: row.get(0)?, + security_type: SecurityType::from_string(row.get(1)?), + symbol: row.get(2)?, + }) +} + +pub fn get_security_by_id(tx: &Transaction, id: i32) -> Result { let q = "SELECT id, security_type, symbol FROM securities - ORDER BY creation_time DESC"; + WHERE id = ?1"; + tx.query_row(q, params![id], _security_from_row) +} + +pub fn list_securities(conn: &Connection) -> Result> { + let q = "SELECT id, security_type, symbol + FROM securities + ORDER BY id ASC"; let mut stmt = conn.prepare(q)?; - let ret: Vec = stmt.query_map([], |row| { - Ok(Security { - id: row.get(0)?, - security_type: SecurityType::from_string(row.get(1)?), - symbol: row.get(2)?, - }) - }).unwrap().filter(|x| x.is_ok()).map(|x| x.unwrap()).collect(); + let ret: Vec = stmt + .query_map([], &_security_from_row)? + .filter(Result::is_ok) + .map(Result::unwrap) + .collect(); return Ok(ret); } +pub mod test_util { + use super::*; + pub fn mock_currency_1() -> NewSecurity { + return NewSecurity { + security_type: SecurityType::Currency, + symbol: "P".to_string(), + name: "piastre".to_string(), + }; + } + + pub fn mock_currency_2() -> NewSecurity { + return NewSecurity { + security_type: SecurityType::Bond, + symbol: "B".to_string(), + name: "bong".to_string(), + }; + } +} #[cfg(test)] mod test { use rusqlite::{Connection, params}; - use super::*; + use super::test_util::*; + use crate::migration; fn setup() -> Connection { let mut conn = Connection::open_in_memory().unwrap(); @@ -84,24 +113,6 @@ mod test { return conn; } - fn mock_currency_1() -> NewSecurity { - return NewSecurity { - security_type: SecurityType::Currency, - public_key: "123".to_string(), - symbol: "P".to_string(), - name: "pisatre".to_string(), - }; - } - - fn mock_currency_2() -> NewSecurity { - return NewSecurity { - security_type: SecurityType::Bond, - public_key: "123".to_string(), - symbol: "B".to_string(), - name: "bong".to_string(), - }; - } - #[test] fn test_security_types() -> Result<(), rusqlite::Error> { let conn = setup(); @@ -122,18 +133,21 @@ mod test { fn test_create_security() -> Result<(), rusqlite::Error> { let mut conn = setup(); let sec = mock_currency_1(); - create_security(&mut conn, &sec)?; - return Ok(()); + let tx = conn.transaction()?; + create_security(&tx, &sec)?; + return tx.commit(); } #[test] fn test_list_security() -> Result<(), rusqlite::Error> { let mut conn = setup(); - assert_eq!(0, list_securities(& conn)?.len()); + assert_eq!(0, list_securities(&conn)?.len()); let sec = mock_currency_1(); - create_security(&mut conn, &sec)?; - let ret = list_securities(& conn)?; + let tx = conn.transaction()?; + create_security(&tx, &sec)?; + tx.commit()?; + let ret = list_securities(&conn)?; let expt = Security { id: 1, security_type: SecurityType::Currency, @@ -149,8 +163,10 @@ mod test { security_type: SecurityType::Bond, symbol: "B".to_string(), }; - create_security(&mut conn, &sec)?; - let ret = list_securities(& conn)?; + let tx = conn.transaction()?; + create_security(&tx, &sec)?; + tx.commit()?; + let ret = list_securities(&conn)?; assert_eq!(2, ret.len()); assert_eq!(&expt, ret.get(1).unwrap()); diff --git a/src/users.rs b/src/users.rs index 5df85e8..fd9bd9a 100644 --- a/src/users.rs +++ b/src/users.rs @@ -1,28 +1,41 @@ -use rusqlite::{Connection, Result}; +use rusqlite::{Connection, params, Result, Transaction, Row}; use serde::{Serialize}; #[derive(Debug, Serialize)] pub struct User { - id: i32, + pub id: i32, name: String, public_key: String, } -pub fn create_user(conn: & Connection, name: &String, public_key: &String){ - conn.execute("INSERT INTO users (name, public_key) VALUES (?1, ?2)", - (name, public_key)).unwrap(); +pub fn create_user(tx: & Transaction, name: &String, public_key: &String) -> Result{ + tx.execute("INSERT INTO users (name, public_key) VALUES (?1, ?2)", + (name, public_key))?; + return get_user_by_id(tx, tx.last_insert_rowid() as i32); } -pub fn list_users(conn: & Connection) -> Result>>{ +fn _user_from_row(row: &Row) -> Result{ + Ok(User { + id: row.get(0)?, + name: row.get(1)?, + public_key: row.get(2)?, + }) +} + +fn get_user_by_id(tx: & Transaction, id: i32) -> Result{ + let q = "SELECT id, name, public_key + FROM users + WHERE id = ?1"; + tx.query_row(q, params![id], _user_from_row) +} + +pub fn list_users(conn: & Connection) -> Result>{ let mut stmt = conn.prepare("SELECT id, name, public_key FROM users")?; - let ret: Vec> = stmt.query_map([], |row| { - Ok(User { - id: row.get(0)?, - name: row.get(1)?, - public_key: row.get(2)?, - }) - }).unwrap().collect(); + let ret: Vec = stmt.query_map([], _user_from_row)? + .filter(Result::is_ok) + .map(Result::unwrap) + .collect(); return Ok(ret); } \ No newline at end of file