feat: reorganize to one flake one rust project

This commit is contained in:
Jet 2026-03-18 17:33:57 -07:00
parent 5183130427
commit e8b60519e7
No known key found for this signature in database
23 changed files with 792 additions and 2144 deletions

View file

@ -12,7 +12,8 @@ pub fn init(path: &str) -> Result<Connection> {
id INTEGER PRIMARY KEY CHECK (id = 1),
status TEXT,
timestamp INTEGER,
last_seen INTEGER
last_seen INTEGER,
last_checked INTEGER
);
CREATE TABLE IF NOT EXISTS state_log (
@ -28,17 +29,29 @@ pub fn init(path: &str) -> Result<Connection> {
fetched_at INTEGER NOT NULL
);
INSERT OR IGNORE INTO current_state (id, status, timestamp, last_seen) VALUES (1, NULL, NULL, NULL);
INSERT OR IGNORE INTO current_state (id, status, timestamp, last_seen, last_checked) VALUES (1, 'offline', NULL, NULL, NULL);
INSERT OR IGNORE INTO pi_info (id, data, fetched_at) VALUES (1, '{}', 0);
",
)
.context("failed to initialize database schema")?;
// Migration: add last_checked column if missing (existing databases)
let has_last_checked: bool = conn
.prepare("SELECT last_checked FROM current_state LIMIT 1")
.is_ok();
if !has_last_checked {
conn.execute_batch("ALTER TABLE current_state ADD COLUMN last_checked INTEGER")?;
}
// Migration: convert NULL status to 'offline'
conn.execute("UPDATE current_state SET status = 'offline' WHERE status IS NULL", [])?;
Ok(conn)
}
pub fn get_status(conn: &Connection) -> Result<StatusResponse> {
let (status, timestamp, last_seen) = conn.query_row(
"SELECT status, timestamp, last_seen FROM current_state WHERE id = 1",
let (status_str, timestamp, last_checked) = conn.query_row(
"SELECT status, timestamp, last_checked FROM current_state WHERE id = 1",
[],
|row| {
Ok((
@ -48,10 +61,14 @@ pub fn get_status(conn: &Connection) -> Result<StatusResponse> {
))
},
)?;
let status = status_str
.as_deref()
.and_then(DoorStatus::from_str)
.unwrap_or(DoorStatus::Offline);
Ok(StatusResponse {
status: status.unwrap_or_else(|| "offline".to_string()),
timestamp,
last_seen,
status,
since: timestamp,
last_checked,
})
}
@ -59,7 +76,7 @@ pub fn update_state(conn: &Connection, status: DoorStatus, timestamp: u64, now:
let status_str = status.as_str();
conn.execute(
"UPDATE current_state SET status = ?1, timestamp = ?2, last_seen = ?3 WHERE id = 1",
rusqlite::params![status_str, timestamp, now],
rusqlite::params![status_str, now, now],
)?;
conn.execute(
"INSERT INTO state_log (status, timestamp, recorded_at) VALUES (?1, ?2, ?3)",
@ -76,10 +93,18 @@ pub fn update_last_seen(conn: &Connection, now: u64) -> Result<()> {
Ok(())
}
pub fn update_last_checked(conn: &Connection, now: u64) -> Result<()> {
conn.execute(
"UPDATE current_state SET last_checked = ?1 WHERE id = 1",
rusqlite::params![now],
)?;
Ok(())
}
pub fn mark_offline(conn: &Connection, now: u64) -> Result<()> {
conn.execute(
"UPDATE current_state SET status = NULL WHERE id = 1",
[],
"UPDATE current_state SET status = 'offline', timestamp = ?1 WHERE id = 1",
rusqlite::params![now],
)?;
conn.execute(
"INSERT INTO state_log (status, timestamp, recorded_at) VALUES ('offline', ?1, ?1)",
@ -88,13 +113,16 @@ pub fn mark_offline(conn: &Connection, now: u64) -> Result<()> {
Ok(())
}
pub fn get_current_status_str(conn: &Connection) -> Result<Option<String>> {
let status = conn.query_row(
pub fn get_current_status(conn: &Connection) -> Result<DoorStatus> {
let status_str: Option<String> = conn.query_row(
"SELECT status FROM current_state WHERE id = 1",
[],
|row| row.get::<_, Option<String>>(0),
|row| row.get(0),
)?;
Ok(status)
Ok(status_str
.as_deref()
.and_then(DoorStatus::from_str)
.unwrap_or(DoorStatus::Offline))
}
pub fn get_history(conn: &Connection, limit: u32) -> Result<Vec<noisebell_common::HistoryEntry>> {
@ -131,3 +159,105 @@ pub fn update_pi_info(conn: &Connection, data: &serde_json::Value, now: u64) ->
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn test_db() -> Connection {
init(":memory:").expect("failed to init test db")
}
#[test]
fn initial_status_is_offline() {
let conn = test_db();
let status = get_status(&conn).unwrap();
assert_eq!(status.status, DoorStatus::Offline);
assert!(status.since.is_none());
assert!(status.last_checked.is_none());
}
#[test]
fn update_state_changes_status() {
let conn = test_db();
update_state(&conn, DoorStatus::Open, 1000, 1001).unwrap();
let status = get_status(&conn).unwrap();
assert_eq!(status.status, DoorStatus::Open);
assert_eq!(status.since, Some(1001));
}
#[test]
fn mark_offline_sets_offline_status() {
let conn = test_db();
update_state(&conn, DoorStatus::Open, 1000, 1001).unwrap();
mark_offline(&conn, 2000).unwrap();
let status = get_status(&conn).unwrap();
assert_eq!(status.status, DoorStatus::Offline);
assert_eq!(status.since, Some(2000));
}
#[test]
fn get_current_status_matches_get_status() {
let conn = test_db();
assert_eq!(get_current_status(&conn).unwrap(), DoorStatus::Offline);
update_state(&conn, DoorStatus::Closed, 1000, 1001).unwrap();
assert_eq!(get_current_status(&conn).unwrap(), DoorStatus::Closed);
}
#[test]
fn update_last_checked_is_readable() {
let conn = test_db();
update_last_checked(&conn, 5000).unwrap();
let status = get_status(&conn).unwrap();
assert_eq!(status.last_checked, Some(5000));
}
#[test]
fn history_records_state_changes() {
let conn = test_db();
update_state(&conn, DoorStatus::Open, 1000, 1001).unwrap();
update_state(&conn, DoorStatus::Closed, 2000, 2001).unwrap();
mark_offline(&conn, 3000).unwrap();
let history = get_history(&conn, 10).unwrap();
assert_eq!(history.len(), 3);
assert_eq!(history[0].status, "offline");
assert_eq!(history[1].status, "closed");
assert_eq!(history[2].status, "open");
}
#[test]
fn status_response_serializes_correctly() {
let resp = StatusResponse {
status: DoorStatus::Open,
since: Some(1234),
last_checked: Some(5678),
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["status"], "open");
assert_eq!(json["since"], 1234);
assert_eq!(json["last_checked"], 5678);
}
#[test]
fn null_status_migration_converts_to_offline() {
// Simulate an old database with NULL status
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch("
CREATE TABLE current_state (id INTEGER PRIMARY KEY CHECK (id = 1), status TEXT, timestamp INTEGER, last_seen INTEGER);
INSERT INTO current_state (id, status, timestamp, last_seen) VALUES (1, NULL, NULL, NULL);
CREATE TABLE state_log (id INTEGER PRIMARY KEY AUTOINCREMENT, status TEXT NOT NULL, timestamp INTEGER NOT NULL, recorded_at INTEGER NOT NULL);
CREATE TABLE pi_info (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL, fetched_at INTEGER NOT NULL);
INSERT INTO pi_info (id, data, fetched_at) VALUES (1, '{}', 0);
").unwrap();
// Re-init should migrate
let conn = init(":memory:").unwrap();
let status = get_current_status(&conn).unwrap();
assert_eq!(status, DoorStatus::Offline);
}
}