2025-10-19 17:50:07 +02:00
|
|
|
use std::sync::Arc;
|
2025-10-18 20:54:23 +02:00
|
|
|
|
|
|
|
|
use askama::Template;
|
2025-10-19 14:23:41 +02:00
|
|
|
use axum::{
|
|
|
|
|
Router,
|
2025-10-19 15:05:12 +02:00
|
|
|
extract::{MatchedPath, Path, State},
|
|
|
|
|
http::Request,
|
2025-10-19 14:23:41 +02:00
|
|
|
response::Html,
|
|
|
|
|
routing::{get, post},
|
|
|
|
|
};
|
2025-10-19 17:50:07 +02:00
|
|
|
use parking_lot::Mutex;
|
2025-10-19 17:58:02 +02:00
|
|
|
use rusqlite::{CachedStatement, Connection};
|
2025-10-19 15:05:12 +02:00
|
|
|
use tower_http::trace::TraceLayer;
|
|
|
|
|
use tower_request_id::{RequestId, RequestIdLayer};
|
2025-10-19 17:32:25 +02:00
|
|
|
use tracing::{debug, error, info, info_span};
|
2025-10-19 15:05:12 +02:00
|
|
|
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
|
2025-10-18 20:54:23 +02:00
|
|
|
|
|
|
|
|
#[derive(Template)]
|
|
|
|
|
#[template(path = "index.html")]
|
|
|
|
|
struct IndexTemplate {
|
2025-10-18 21:21:58 +02:00
|
|
|
foods: Vec<Food>,
|
2025-10-18 22:12:51 +02:00
|
|
|
sum: i32,
|
2025-10-18 20:54:23 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 13:57:14 +02:00
|
|
|
#[derive(Template)]
|
2025-10-19 14:18:40 +02:00
|
|
|
#[template(path = "food-update.html")]
|
|
|
|
|
struct FoodUpdateTemplate {
|
2025-10-19 13:57:14 +02:00
|
|
|
food: Food,
|
2025-10-19 14:18:40 +02:00
|
|
|
sum: i32,
|
2025-10-19 13:57:14 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-18 20:54:23 +02:00
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
2025-10-18 21:21:58 +02:00
|
|
|
struct Food {
|
2025-10-18 20:54:23 +02:00
|
|
|
id: i32,
|
2025-10-19 14:23:41 +02:00
|
|
|
portion: String,
|
|
|
|
|
name: String,
|
2025-10-18 21:21:58 +02:00
|
|
|
kc_per_serving: i32,
|
|
|
|
|
target_servings: i32,
|
|
|
|
|
actual_servings: i32,
|
2025-10-19 14:33:32 +02:00
|
|
|
color: String,
|
2025-10-18 20:54:23 +02:00
|
|
|
}
|
2025-10-18 20:50:04 +02:00
|
|
|
|
2025-10-19 17:58:02 +02:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
struct PreparedStatements {}
|
|
|
|
|
|
|
|
|
|
impl<'conn> PreparedStatements {
|
|
|
|
|
fn check(conn: &Connection) -> rusqlite::Result<Self> {
|
|
|
|
|
conn.prepare_cached(include_str!("create_tables.sql"))?;
|
|
|
|
|
conn.prepare_cached(include_str!("increase.sql"))?;
|
|
|
|
|
conn.prepare_cached(include_str!("decrease.sql"))?;
|
|
|
|
|
conn.prepare_cached(include_str!("get_food.sql"))?;
|
|
|
|
|
conn.prepare_cached(include_str!("get_foods.sql"))?;
|
|
|
|
|
conn.prepare_cached(include_str!("get_sum.sql"))?;
|
|
|
|
|
Ok(PreparedStatements {})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_tables(conn: &'conn Connection) -> CachedStatement<'conn> {
|
|
|
|
|
conn.prepare_cached(include_str!("create_tables.sql"))
|
|
|
|
|
.expect("cached statement is invalid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn increase(conn: &'conn Connection) -> CachedStatement<'conn> {
|
|
|
|
|
conn.prepare_cached(include_str!("increase.sql"))
|
|
|
|
|
.expect("cached statement is invalid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn decrease(conn: &'conn Connection) -> CachedStatement<'conn> {
|
|
|
|
|
conn.prepare_cached(include_str!("decrease.sql"))
|
|
|
|
|
.expect("cached statement is invalid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_food(conn: &'conn Connection) -> CachedStatement<'conn> {
|
|
|
|
|
conn.prepare_cached(include_str!("get_food.sql"))
|
|
|
|
|
.expect("cached statement is invalid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_foods(conn: &'conn Connection) -> CachedStatement<'conn> {
|
|
|
|
|
conn.prepare_cached(include_str!("get_foods.sql"))
|
|
|
|
|
.expect("cached statement is invalid")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_sum(conn: &'conn Connection) -> CachedStatement<'conn> {
|
|
|
|
|
conn.prepare_cached(include_str!("get_sum.sql"))
|
|
|
|
|
.expect("cached statement is invalid")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ConnState = Arc<Mutex<Connection>>;
|
|
|
|
|
|
2025-10-18 20:50:04 +02:00
|
|
|
#[tokio::main]
|
2025-10-19 17:32:25 +02:00
|
|
|
async fn main() -> Result<(), std::io::Error> {
|
2025-10-19 15:05:12 +02:00
|
|
|
tracing_subscriber::registry()
|
|
|
|
|
.with(
|
|
|
|
|
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
|
|
|
|
// axum logs rejections from built-in extractors with the `axum::rejection`
|
|
|
|
|
// target, at `TRACE` level. `axum::rejection=trace` enables showing those events
|
|
|
|
|
format!(
|
|
|
|
|
"{}=debug,tower_http=debug,axum::rejection=trace",
|
|
|
|
|
env!("CARGO_CRATE_NAME")
|
|
|
|
|
)
|
|
|
|
|
.into()
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.with(tracing_subscriber::fmt::layer())
|
|
|
|
|
.init();
|
|
|
|
|
|
2025-10-18 21:21:58 +02:00
|
|
|
let db_connecion_str = "./foods.db".to_string();
|
2025-10-19 17:32:25 +02:00
|
|
|
debug!(db_connecion_str, "opening database");
|
|
|
|
|
let conn = Connection::open(db_connecion_str).expect("failed to open database");
|
2025-10-19 17:58:02 +02:00
|
|
|
PreparedStatements::check(&conn).expect("failed to prepare sql statements");
|
|
|
|
|
if let Err(e) = PreparedStatements::create_tables(&conn).execute(()) {
|
2025-10-19 17:32:25 +02:00
|
|
|
error!(?e, "failed to create tables");
|
|
|
|
|
panic!("failed to create tables: {:#?}", e);
|
|
|
|
|
}
|
2025-10-19 14:23:41 +02:00
|
|
|
|
|
|
|
|
let app = Router::new()
|
|
|
|
|
.route("/", get(root))
|
2025-10-18 22:12:51 +02:00
|
|
|
.route("/increase/{id}", post(increase))
|
|
|
|
|
.route("/decrease/{id}", post(decrease))
|
2025-10-19 15:05:12 +02:00
|
|
|
.layer(
|
|
|
|
|
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
|
|
|
|
let matched_path = request
|
|
|
|
|
.extensions()
|
|
|
|
|
.get::<MatchedPath>()
|
|
|
|
|
.map(MatchedPath::as_str);
|
|
|
|
|
let request_id = request
|
|
|
|
|
.extensions()
|
|
|
|
|
.get::<RequestId>()
|
|
|
|
|
.map(ToString::to_string)
|
|
|
|
|
.unwrap_or_else(|| "unknown".into());
|
|
|
|
|
info_span!(
|
|
|
|
|
"request",
|
|
|
|
|
method = ?request.method(),
|
|
|
|
|
matched_path,
|
|
|
|
|
uri = ?request.uri(),
|
|
|
|
|
id = %request_id,
|
|
|
|
|
)
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.layer(RequestIdLayer)
|
2025-10-19 17:58:02 +02:00
|
|
|
.with_state(Arc::new(Mutex::new(conn)));
|
2025-10-18 20:50:04 +02:00
|
|
|
|
2025-10-19 17:32:25 +02:00
|
|
|
let address = "0.0.0.0:3001";
|
|
|
|
|
let listener = tokio::net::TcpListener::bind(address)
|
|
|
|
|
.await
|
|
|
|
|
.expect("failed to bind to address");
|
|
|
|
|
info!(
|
|
|
|
|
"listening on {}",
|
|
|
|
|
listener
|
|
|
|
|
.local_addr()
|
|
|
|
|
.expect("failed to get local listening address")
|
|
|
|
|
);
|
|
|
|
|
axum::serve(listener, app).await
|
2025-10-18 20:50:04 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 17:58:02 +02:00
|
|
|
fn get_foods(conn: &ConnState) -> Vec<Food> {
|
2025-10-19 17:50:07 +02:00
|
|
|
let conn = conn.lock();
|
2025-10-19 17:58:02 +02:00
|
|
|
let mut stmt = PreparedStatements::get_foods(&conn);
|
2025-10-19 15:05:12 +02:00
|
|
|
let foods: Vec<_> = stmt
|
2025-10-19 14:23:41 +02:00
|
|
|
.query_map((), |row| {
|
|
|
|
|
Ok(Food {
|
|
|
|
|
id: row.get(0).unwrap(),
|
|
|
|
|
portion: row.get(1).unwrap(),
|
|
|
|
|
name: row.get(2).unwrap(),
|
|
|
|
|
kc_per_serving: row.get(3).unwrap(),
|
|
|
|
|
target_servings: row.get(4).unwrap(),
|
|
|
|
|
actual_servings: row.get(5).unwrap(),
|
2025-10-19 14:33:32 +02:00
|
|
|
color: row.get(6).unwrap(),
|
2025-10-19 14:23:41 +02:00
|
|
|
})
|
2025-10-18 20:54:23 +02:00
|
|
|
})
|
2025-10-19 14:23:41 +02:00
|
|
|
.unwrap()
|
|
|
|
|
.collect::<Result<_, _>>()
|
|
|
|
|
.unwrap();
|
2025-10-19 15:05:12 +02:00
|
|
|
debug!(num_foods = foods.len());
|
2025-10-19 14:25:11 +02:00
|
|
|
foods
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_sum(conn: &Arc<Mutex<Connection>>) -> i32 {
|
2025-10-19 17:50:07 +02:00
|
|
|
let conn = conn.lock();
|
2025-10-19 17:58:02 +02:00
|
|
|
let mut stmt = PreparedStatements::get_sum(&conn);
|
2025-10-18 22:12:51 +02:00
|
|
|
let sum = stmt.query_one((), |row| row.get(0)).unwrap();
|
2025-10-19 15:05:12 +02:00
|
|
|
debug!(sum);
|
2025-10-19 14:25:11 +02:00
|
|
|
sum
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 17:58:02 +02:00
|
|
|
async fn root(State(conn): State<ConnState>) -> Html<String> {
|
2025-10-19 14:25:11 +02:00
|
|
|
let foods = get_foods(&conn);
|
|
|
|
|
let sum = get_sum(&conn);
|
2025-10-19 14:23:41 +02:00
|
|
|
let index = IndexTemplate { foods, sum };
|
|
|
|
|
Html(index.render().unwrap())
|
2025-10-18 20:49:21 +02:00
|
|
|
}
|
2025-10-18 22:12:51 +02:00
|
|
|
|
2025-10-19 14:25:11 +02:00
|
|
|
fn do_increase(conn: &Arc<Mutex<Connection>>, id: i32) {
|
2025-10-19 17:50:07 +02:00
|
|
|
let conn = conn.lock();
|
2025-10-19 17:58:02 +02:00
|
|
|
let mut stmt = PreparedStatements::increase(&conn);
|
2025-10-19 15:05:12 +02:00
|
|
|
let new: i32 = stmt.query_one((id,), |row| row.get(0)).unwrap();
|
|
|
|
|
debug!(id, new_serving_count = new, "increase");
|
2025-10-19 14:25:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_food(conn: &Arc<Mutex<Connection>>, id: i32) -> Food {
|
2025-10-19 17:50:07 +02:00
|
|
|
let conn = conn.lock();
|
2025-10-19 17:58:02 +02:00
|
|
|
let mut stmt = PreparedStatements::get_food(&conn);
|
2025-10-19 14:23:41 +02:00
|
|
|
let food = stmt
|
|
|
|
|
.query_one((id,), |row| {
|
|
|
|
|
Ok(Food {
|
|
|
|
|
id: row.get(0).unwrap(),
|
|
|
|
|
portion: row.get(1).unwrap(),
|
|
|
|
|
name: row.get(2).unwrap(),
|
|
|
|
|
kc_per_serving: row.get(3).unwrap(),
|
|
|
|
|
target_servings: row.get(4).unwrap(),
|
|
|
|
|
actual_servings: row.get(5).unwrap(),
|
2025-10-19 14:33:32 +02:00
|
|
|
color: row.get(6).unwrap(),
|
2025-10-19 14:23:41 +02:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
2025-10-19 15:05:12 +02:00
|
|
|
debug!(?food);
|
2025-10-19 14:25:11 +02:00
|
|
|
food
|
2025-10-18 22:12:51 +02:00
|
|
|
}
|
2025-10-19 14:25:11 +02:00
|
|
|
|
|
|
|
|
async fn increase(State(conn): State<Arc<Mutex<Connection>>>, Path(id): Path<i32>) -> Html<String> {
|
|
|
|
|
do_increase(&conn, id);
|
|
|
|
|
let food = get_food(&conn, id);
|
|
|
|
|
let sum = get_sum(&conn);
|
|
|
|
|
let update = FoodUpdateTemplate { food, sum };
|
|
|
|
|
Html(update.render().unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn do_decrease(conn: &Arc<Mutex<Connection>>, id: i32) {
|
2025-10-19 17:50:07 +02:00
|
|
|
let conn = conn.lock();
|
2025-10-19 17:58:02 +02:00
|
|
|
let mut stmt = PreparedStatements::decrease(&conn);
|
2025-10-19 15:05:12 +02:00
|
|
|
let new: i32 = stmt.query_one((id,), |row| row.get(0)).unwrap();
|
|
|
|
|
debug!(id, new_serving_count = new, "decrease");
|
2025-10-19 14:25:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn decrease(State(conn): State<Arc<Mutex<Connection>>>, Path(id): Path<i32>) -> Html<String> {
|
|
|
|
|
do_decrease(&conn, id);
|
|
|
|
|
let food = get_food(&conn, id);
|
|
|
|
|
let sum = get_sum(&conn);
|
|
|
|
|
let update = FoodUpdateTemplate { food, sum };
|
|
|
|
|
Html(update.render().unwrap())
|
2025-10-18 22:12:51 +02:00
|
|
|
}
|