diff --git a/src/db.rs b/src/db.rs index 46569d0..12b353a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2,40 +2,45 @@ use rusqlite::types::Value; use rusqlite::{Connection, Result}; use std::sync::Mutex; -pub struct Db { - conn: Mutex, +/// Thin wrapper around a `SQLite` connection. +/// Ensures safe access via Mutex (one query at a time). +pub struct SqliteDb { + connection: Mutex, } -impl Db { - pub fn open(path: &str) -> Result { +impl SqliteDb { + /// Open a database file and create a shared DB handle. + pub fn open(db_path: &str) -> Result { Ok(Self { - conn: Mutex::new(Connection::open(path)?), + connection: Mutex::new(Connection::open(db_path)?), }) } - pub fn get_table(&self, table: &str) -> Result<(Vec, Vec>)> { - let conn = self.conn.lock().unwrap(); + /// Fetch full table data (columns + rows as strings). + pub fn fetch_table(&self, table_name: &str) -> Result<(Vec, Vec>)> { + // Lock ensures only one request uses the connection at a time + let conn = self.connection.lock().unwrap(); - // WARNING: table name is interpolated → trust only internal input - let query = format!("SELECT * FROM {table}"); + // SQL query + let query = format!("SELECT * FROM {table_name}"); let mut stmt = conn.prepare(&query)?; - let columns = stmt + let column_names = stmt .column_names() .iter() .map(|s| s.to_string()) .collect::>(); - let col_count = stmt.column_count(); + let column_count = stmt.column_count(); let rows_iter = stmt.query_map([], move |row| { - let mut r = Vec::with_capacity(col_count); + let mut row_values = Vec::with_capacity(column_count); - for i in 0..col_count { - let val: Value = row.get(i)?; + for i in 0..column_count { + let value: Value = row.get(i)?; - let s = match val { + let cell = match value { Value::Null => "".to_string(), Value::Integer(i) => i.to_string(), Value::Real(f) => f.to_string(), @@ -43,14 +48,14 @@ impl Db { Value::Blob(b) => format!("", b.len()), }; - r.push(s); + row_values.push(cell); } - Ok(r) + Ok(row_values) })?; let rows = rows_iter.collect::>>()?; - Ok((columns, rows)) + Ok((column_names, rows)) } } diff --git a/src/main.rs b/src/main.rs index 97b92b8..46fec3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,25 +6,67 @@ use tokio::net::TcpListener; mod db; mod routes; -use db::Db; +use db::SqliteDb; -#[derive(Parser)] -struct Args { - #[arg(long)] - db: String, +/// Application-wide shared state. +/// Everything handlers need should live here. +#[derive(Clone)] +pub struct AppState { + // Shared handle to the SQLite wrapper (not the raw connection itself) + pub db: Arc, } -#[tokio::main] +/// CLI arguments +#[derive(Parser)] +struct Args { + /// Path to `SQLite` database file (`--dbpath `) + #[arg(long)] + dbpath: String, +} + +#[tokio::main] // Create Tokio runtime and run async main future async fn main() { + // Parse CLI args into `Args` let args = Args::parse(); - let db = Arc::new(Db::open(&args.db).expect("db open failed")); + // Create shared database handle (single SQLite connection wrapped for safe shared access) + // `Arc` allows multiple concurrent request handlers to share the same DB handle (shared ownership) + let db_handle = Arc::new(SqliteDb::open(&args.dbpath).expect("failed to open database")); - let app = Router::new().route("/", get(routes::index)).with_state(db); + // Build shared app state (stored inside the router) + let app_state = AppState { + // SQLite connection + db: db_handle, + }; - let listener = TcpListener::bind("127.0.0.1:8040").await.unwrap(); + // Build router (maps incoming HTTP requests to handler functions) + let app = Router::new() + .route( + // Match path "/" + "/", + // Handler for GET request + get( + // On GET "/", Axum extracts handler inputs (e.g., `State`) and then calls `routes::index` + routes::index, + ), + ) + // Store shared application state in the router; handlers can retrieve it using the `State` extractor + .with_state(app_state); + + // Bind TCP socket to localhost:8040 + let listener = TcpListener::bind("127.0.0.1:8040") + // Async because this is I/O + .await + .unwrap(); println!("http://127.0.0.1:8040"); - axum::serve(listener, app).await.unwrap(); + // Start server - This is the core runtime loop. + // accept connections → route requests → run handlers → send responses + axum::serve( + listener, // Accept connections from listener + app, // Axum routes incoming requests through `app`, which dispatches to the matched handler + ) + .await + .unwrap(); } diff --git a/src/routes.rs b/src/routes.rs index 02ed76b..69ade8d 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,10 +1,10 @@ use askama::Template; - use axum::{extract::State, response::Html}; -use std::sync::Arc; -use crate::db::Db; +use crate::AppState; +/// Askama template for rendering a table page. +/// Field names must match variables used in `templates/table.html`. #[derive(Template)] #[template(path = "table.html")] pub struct TableTemplate { @@ -13,15 +13,26 @@ pub struct TableTemplate { pub rows: Vec>, } -pub async fn index(State(db): State>) -> Html { +/// HTTP handler for GET / +/// `State` is extracted by Axum from the router. +/// This gives the handler access to shared application state. +pub async fn index(State(app_state): State) -> Html { + // `table_name` hardcoded for now; will become dynamic later let table_name = "users"; - let (columns, rows) = db.get_table(table_name).unwrap(); - let tmpl = TableTemplate { + // Query DB: returns (column names, row data as strings) + let (columns, rows) = app_state // Access shared SQLite handle from AppState + .db // Shared SQLite wrapper handle + .fetch_table(table_name) + .unwrap(); // Fetch data from the table (column names, rows) as strings + + // Prepare data for HTML template + let template = TableTemplate { table_name: table_name.into(), columns, rows, }; - Html(tmpl.render().unwrap()) + // Render template → HTML string → wrap as HTTP response + Html(template.render().unwrap()) }