Skip to main content

tally_cli/
models.rs

1use sqlite::{ConnectionThreadSafe, State};
2
3#[derive(Debug)]
4pub struct Counter {
5    pub name: String,
6    pub count: i64,
7    pub step: i64,
8    pub template: String,
9}
10
11impl Counter {
12    pub fn new(name: &str) -> Counter {
13        Counter {
14            name: name.to_string(),
15            count: 0,
16            step: 1,
17            template: String::from("{}"),
18        }
19    }
20
21    pub fn set_default(&self, conn: &ConnectionThreadSafe) -> sqlite::Result<()> {
22        conn.execute("DELETE FROM default_counter;")?;
23        let mut stmt = conn.prepare(
24            "INSERT INTO default_counter (name, timestamp) VALUES (?, CURRENT_TIMESTAMP);",
25        )?;
26        stmt.bind((1, self.name.as_str()))?;
27        stmt.next()?;
28        Ok(())
29    }
30
31    pub fn get_default(conn: &ConnectionThreadSafe) -> sqlite::Result<Option<String>> {
32        let mut stmt =
33            conn.prepare("SELECT name FROM default_counter ORDER BY timestamp DESC LIMIT 1;")?;
34        if let State::Row = stmt.next()? {
35            Ok(Some(stmt.read::<String, usize>(0)?))
36        } else {
37            Ok(None)
38        }
39    }
40
41    pub fn insert(&self, conn: &ConnectionThreadSafe) -> sqlite::Result<()> {
42        let mut stmt =
43            conn.prepare("INSERT INTO counters (name, count, step, template) VALUES (?, ?, ?, ?)")?;
44        stmt.bind((1, self.name.as_str()))?;
45        stmt.bind((2, self.count))?;
46        stmt.bind((3, self.step))?;
47        stmt.bind((4, self.template.as_str()))?;
48        stmt.next()?;
49
50        Ok(())
51    }
52
53    pub fn delete(conn: &ConnectionThreadSafe, name: &str) -> sqlite::Result<()> {
54        let mut stmt = conn.prepare("DELETE FROM counters WHERE name = ?")?;
55        stmt.bind((1, name))?;
56        stmt.next()?;
57        Ok(())
58    }
59
60    pub fn get(conn: &ConnectionThreadSafe, name: &str) -> sqlite::Result<Option<Counter>> {
61        let mut stmt =
62            conn.prepare("SELECT name, count, step, template FROM counters WHERE name = ?")?;
63        stmt.bind((1, name))?;
64
65        if let State::Row = stmt.next()? {
66            Ok(Some(Counter {
67                name: stmt.read::<String, _>(0)?.to_string(),
68                count: stmt.read::<i64, _>(1)?,
69                step: stmt.read::<i64, _>(2)?,
70                template: stmt.read::<String, _>(3)?.to_string(),
71            }))
72        } else {
73            Ok(None)
74        }
75    }
76
77    pub fn get_all(conn: &ConnectionThreadSafe) -> sqlite::Result<Vec<Counter>> {
78        let mut stmt = conn.prepare("SELECT name, count, step, template FROM counters")?;
79        let mut counters = Vec::new();
80
81        while let State::Row = stmt.next()? {
82            counters.push(Counter {
83                name: stmt.read::<String, usize>(0)?,
84                count: stmt.read::<i64, usize>(1)?,
85                step: stmt.read::<i64, usize>(2)?,
86                template: stmt.read::<String, usize>(3)?,
87            });
88        }
89
90        Ok(counters)
91    }
92
93    pub fn update(&self, conn: &ConnectionThreadSafe) -> sqlite::Result<()> {
94        let mut stmt =
95            conn.prepare("UPDATE counters SET count = ?, step = ?, template = ? WHERE name = ?")?;
96        stmt.bind((1, self.count))?;
97        stmt.bind((2, self.step))?;
98        stmt.bind((3, self.template.as_str()))?;
99        stmt.bind((4, self.name.as_str()))?;
100        stmt.next()?;
101        Ok(())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::database::Connection;
109    use tempfile::TempDir;
110
111    fn fresh_db() -> (TempDir, Connection) {
112        let dir = TempDir::new().unwrap();
113        let path = dir.path().join("test.db");
114        let conn = Connection::new(&path.to_string_lossy()).unwrap();
115        (dir, conn)
116    }
117
118    #[test]
119    fn new_has_sensible_defaults() {
120        let c = Counter::new("foo");
121        assert_eq!(c.name, "foo");
122        assert_eq!(c.count, 0);
123        assert_eq!(c.step, 1);
124        assert_eq!(c.template, "{}");
125    }
126
127    #[test]
128    fn insert_and_get_round_trip() {
129        let (_dir, conn) = fresh_db();
130        let c = Counter {
131            name: "foo".into(),
132            count: 7,
133            step: 2,
134            template: "x-{}".into(),
135        };
136        c.insert(conn.get()).unwrap();
137
138        let loaded = Counter::get(conn.get(), "foo").unwrap().unwrap();
139        assert_eq!(loaded.name, "foo");
140        assert_eq!(loaded.count, 7);
141        assert_eq!(loaded.step, 2);
142        assert_eq!(loaded.template, "x-{}");
143    }
144
145    #[test]
146    fn get_missing_returns_none() {
147        let (_dir, conn) = fresh_db();
148        assert!(Counter::get(conn.get(), "nope").unwrap().is_none());
149    }
150
151    #[test]
152    fn update_persists_changes() {
153        let (_dir, conn) = fresh_db();
154        let mut c = Counter::new("foo");
155        c.insert(conn.get()).unwrap();
156        c.count = 42;
157        c.step = 5;
158        c.update(conn.get()).unwrap();
159
160        let loaded = Counter::get(conn.get(), "foo").unwrap().unwrap();
161        assert_eq!(loaded.count, 42);
162        assert_eq!(loaded.step, 5);
163    }
164
165    #[test]
166    fn delete_removes_row() {
167        let (_dir, conn) = fresh_db();
168        Counter::new("foo").insert(conn.get()).unwrap();
169        Counter::delete(conn.get(), "foo").unwrap();
170        assert!(Counter::get(conn.get(), "foo").unwrap().is_none());
171    }
172
173    #[test]
174    fn get_all_returns_every_counter() {
175        let (_dir, conn) = fresh_db();
176        Counter::new("a").insert(conn.get()).unwrap();
177        Counter::new("b").insert(conn.get()).unwrap();
178        let mut names: Vec<String> = Counter::get_all(conn.get())
179            .unwrap()
180            .into_iter()
181            .map(|c| c.name)
182            .collect();
183        names.sort();
184        assert_eq!(names, vec!["a", "b", "tally"]);
185    }
186
187    #[test]
188    fn set_default_keeps_default_counter_single_row() {
189        let (_dir, conn) = fresh_db();
190        Counter::new("a").insert(conn.get()).unwrap();
191        Counter::new("b").insert(conn.get()).unwrap();
192
193        Counter::new("a").set_default(conn.get()).unwrap();
194        Counter::new("b").set_default(conn.get()).unwrap();
195        Counter::new("a").set_default(conn.get()).unwrap();
196
197        assert_eq!(
198            Counter::get_default(conn.get()).unwrap().as_deref(),
199            Some("a")
200        );
201
202        let mut stmt = conn
203            .get()
204            .prepare("SELECT COUNT(*) FROM default_counter;")
205            .unwrap();
206        stmt.next().unwrap();
207        let count: i64 = stmt.read(0).unwrap();
208        assert_eq!(count, 1);
209    }
210}