2025-10-18 20:54:23 +02:00
use std ::sync ::{ Arc , Mutex } ;
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 } ,
} ;
use rusqlite ::Connection ;
2025-10-19 15:05:12 +02:00
use tower_http ::trace ::TraceLayer ;
use tower_request_id ::{ RequestId , RequestIdLayer } ;
use tracing ::{ debug , info , info_span } ;
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
#[ tokio::main ]
async fn main ( ) {
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-18 20:54:23 +02:00
let conn = Connection ::open ( db_connecion_str ) . unwrap ( ) ;
conn . execute ( include_str! ( " create_tables.sql " ) , ( ) ) . unwrap ( ) ;
let conn = Arc ::new ( Mutex ::new ( conn ) ) ;
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-18 22:12:51 +02:00
. with_state ( conn ) ;
2025-10-18 20:50:04 +02:00
let listener = tokio ::net ::TcpListener ::bind ( " 0.0.0.0:3001 " ) . await . unwrap ( ) ;
2025-10-19 15:05:12 +02:00
info! ( " listening on {} " , listener . local_addr ( ) . unwrap ( ) ) ;
2025-10-18 20:50:04 +02:00
axum ::serve ( listener , app ) . await . unwrap ( ) ;
}
2025-10-19 14:25:11 +02:00
fn get_foods ( conn : & Arc < Mutex < Connection > > ) -> Vec < Food > {
2025-10-18 20:54:23 +02:00
let conn = conn . lock ( ) . unwrap ( ) ;
2025-10-19 14:23:41 +02:00
let mut stmt = conn
. prepare (
2025-10-19 14:33:32 +02:00
" SELECT id, portion, name, kc_per_serving, target_servings, actual_servings, color FROM food " ,
2025-10-19 14:23:41 +02:00
)
. unwrap ( ) ;
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 {
let conn = conn . lock ( ) . unwrap ( ) ;
2025-10-19 14:23:41 +02:00
let mut stmt = conn
. prepare ( " SELECT SUM(kc_per_serving * actual_servings) as kc FROM food " )
. unwrap ( ) ;
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
}
async fn root ( State ( conn ) : State < Arc < Mutex < Connection > > > ) -> Html < String > {
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-18 22:12:51 +02:00
let conn = conn . lock ( ) . unwrap ( ) ;
2025-10-19 15:05:12 +02:00
let mut stmt = conn . prepare ( " UPDATE food SET actual_servings = (SELECT actual_servings FROM food WHERE id = ?1) + 1 WHERE id = ?1 RETURNING actual_servings " ) . unwrap ( ) ;
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 {
let conn = conn . lock ( ) . unwrap ( ) ;
2025-10-19 14:33:32 +02:00
let mut stmt = conn . prepare ( " SELECT id, portion, name, kc_per_serving, target_servings, actual_servings, color FROM food WHERE id = ?1 " ) . unwrap ( ) ;
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-18 22:12:51 +02:00
let conn = conn . lock ( ) . unwrap ( ) ;
2025-10-19 15:05:12 +02:00
let mut stmt = conn . prepare ( " UPDATE food SET actual_servings = MAX((SELECT actual_servings FROM food WHERE id = ?1) - 1, 0) WHERE id = ?1 RETURNING actual_servings " ) . unwrap ( ) ;
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
}