use rusqlite::{Connection, OptionalExtension, params, Result, Row, ToSql, Transaction}; use serde::Serialize; use crate::db::_external_id::{Id, SecurityId, UserId}; use crate::db::users::User; #[derive(Debug, Serialize, PartialEq)] pub enum SecurityType { Currency, Bond, } impl SecurityType { fn from_string(s: String) -> SecurityType { if s == "currency".to_string() { return SecurityType::Currency; } if s == "bond".to_string() { return SecurityType::Bond; } panic!("Impossible to convert string {} to SecurityType.", s); } } impl ToSql for SecurityType { #[inline] fn to_sql(&self) -> Result> { Ok(rusqlite::types::ToSqlOutput::from(match self { SecurityType::Currency => "currency", SecurityType::Bond => "bond", })) } } #[derive(Debug, Serialize, PartialEq)] pub struct Security { id: i64, security_type: SecurityType, symbol: String, } impl Security { pub fn id(&self) -> SecurityId { SecurityId::from_id(self.id) } } impl Security { pub fn from_row(row: &Row) -> Result { Ok(Security { id: row.get("sec_id")?, security_type: SecurityType::from_string(row.get("sec_security_type")?), symbol: row.get("sec_symbol")?, }) } } pub struct NewSecurity { pub security_type: SecurityType, pub symbol: String, pub name: String, } pub fn create_security(tx: &Transaction, sec: &NewSecurity, owner: &UserId) -> 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)?; let new_sec_id = tx.last_insert_rowid() as i32; return get_security_by_id(tx, new_sec_id); } pub fn get_security_by_id(tx: &Transaction, id: i32) -> Result { let q = "SELECT id AS sec_id, security_type AS sec_security_type, symbol AS sec_symbol FROM securities WHERE id = ?1"; tx.query_row(q, params![id], Security::from_row) } pub fn list_securities(conn: &Connection) -> Result> { let q = "SELECT id AS sec_id, security_type AS sec_security_type, symbol AS sec_symbol FROM securities ORDER BY id ASC"; let mut stmt = conn.prepare(q)?; let ret: Vec = stmt .query_map([], Security::from_row)? .filter(Result::is_ok) .map(Result::unwrap) .collect(); return Ok(ret); } #[cfg(test)] mod test_securities { use rusqlite::{Connection, params}; use crate::db::_test_utils::*; use crate::migration; use super::*; fn setup() -> (Connection, 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(); tx.commit().unwrap(); return (conn, users); } #[test] fn test_security_types() -> Result<(), rusqlite::Error> { let (mut conn, users) = setup(); let q = "CREATE TABLE test_security_types( name TEXT, FOREIGN KEY (name) REFERENCES security_types (name) )"; conn.execute(q, ())?; let security_types = vec![SecurityType::Currency, SecurityType::Bond]; for sec in security_types.iter() { let q = "INSERT INTO test_security_types(name) VALUES (?1)"; conn.execute(q, params![sec])?; } return Ok(()); } #[test] fn test_create_security() -> Result<(), rusqlite::Error> { let (mut conn, users) = setup(); let owner = users.get(0).unwrap(); let not_owner = users.get(1).unwrap(); let sec = mock_currency_1(); let tx = conn.transaction()?; let sec = create_security(&tx, &sec, &owner.id())?; tx.commit() } #[test] fn test_list_security() -> Result<(), rusqlite::Error> { let (mut conn, users) = setup(); let user = users.get(0).unwrap(); assert_eq!(0, list_securities(&conn)?.len()); let sec = mock_currency_1(); let tx = conn.transaction()?; create_security(&tx, &sec, &user.id())?; tx.commit()?; let ret = list_securities(&conn)?; let expt = Security { id: 1, security_type: SecurityType::Currency, symbol: "P".to_string(), }; assert_eq!(1, ret.len()); assert_eq!(&expt, ret.get(0).unwrap()); let sec = mock_currency_2(); let expt = Security { id: 2, security_type: SecurityType::Bond, symbol: "B".to_string(), }; let tx = conn.transaction()?; create_security(&tx, &sec, &user.id())?; tx.commit()?; let ret = list_securities(&conn)?; assert_eq!(2, ret.len()); assert_eq!(&expt, ret.get(1).unwrap()); return Ok(()); } }