diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 180 |
1 files changed, 10 insertions, 170 deletions
diff --git a/src/main.rs b/src/main.rs index 6bf8fa8..acf8896 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,187 +1,27 @@ +mod db; +mod error; +mod handlers; +mod models; + use axum::{ Router, - extract::{Query, State}, - http::StatusCode, - response::{IntoResponse, Json, Response}, routing::{get, post}, }; -use chrono::{DateTime, NaiveDate, Utc}; use dotenvy::dotenv; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, from_value}; -use sqlx::{PgPool, postgres::PgPoolOptions}; -use std::env; -use uuid::Uuid; - -const DATABASE_URL_FALLBACK: &str = "postgres://health:health@127.0.0.1:5432/health"; - -#[derive(sqlx::Type, Serialize, Deserialize)] -#[sqlx(type_name = "metric_type")] -pub enum MetricType { - BodyWeight, - Hip, - Waist, -} - -#[derive(sqlx::Type, Serialize, Deserialize)] -#[sqlx(type_name = "unit")] -pub enum Unit { - Kilogram, - Centimeter, -} - -#[derive(Serialize)] -pub struct MeasurementView { - id: Uuid, - measured_at: DateTime<Utc>, - metric_type: MetricType, - value: f64, - unit: Unit, -} - -#[derive(Deserialize)] -pub struct MeasurementData { - measured_at: DateTime<Utc>, - metric_type: MetricType, - value: f64, - unit: Unit, -} - -#[derive(Deserialize)] -pub struct DateQuery { - date: Option<String>, - from: Option<String>, - to: Option<String>, -} #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); dotenv().ok(); - let database_url = - env::var("DATABASE_URL").unwrap_or_else(|_| DATABASE_URL_FALLBACK.to_owned()); - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&database_url) - .await?; + let pool = db::connect().await?; let app = Router::new() - .route("/", get(index)) - .route("/all", get(all)) - .route("/add", post(add)) - .route("/search", get(search)) + .route("/", get(handlers::index)) + .route("/all", get(handlers::all)) + .route("/add", post(handlers::add)) + .route("/search", get(handlers::search)) .with_state(pool); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; tracing::info!("listening at 0.0.0.0:3000"); axum::serve(listener, app).await?; Ok(()) } - -async fn index() -> &'static str { - "Hello World!" -} - -async fn all(State(pool): State<PgPool>) -> Result<Json<Vec<MeasurementView>>, StatusCode> { - sqlx::query_as!(MeasurementView, "SELECT * FROM measurement") - .fetch_all(&pool) - .await - .map(Json) - .map_err(|e| { - tracing::error!("failed to fetch measurements: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - }) -} - -async fn add(State(pool): State<PgPool>, Json(row): Json<Value>) -> Response { - if let Ok(insert_row) = from_value::<MeasurementData>(row) { - match sqlx::query!( - " - INSERT INTO measurement (id, measured_at, metric_type, value, unit) - VALUES ($1, $2, $3, $4, $5) - RETURNING id - ", - Uuid::now_v7(), - insert_row.measured_at, - insert_row.metric_type, - insert_row.value, - insert_row.unit - ) - .fetch_one(&pool) - .await - { - Ok(rec) => (StatusCode::CREATED, Json(rec.id)).into_response(), - Err(sqlx::Error::Database(e)) if e.is_unique_violation() => { - StatusCode::CONFLICT.into_response() - } - Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), - } - } else { - StatusCode::UNPROCESSABLE_ENTITY.into_response() - } -} - -async fn search( - State(pool): State<PgPool>, - Query(date_query): Query<DateQuery>, -) -> Result<Json<Vec<MeasurementView>>, StatusCode> { - if date_query.date.is_some() && (date_query.from.is_some() || date_query.to.is_some()) { - return Err(StatusCode::BAD_REQUEST); - } - let (start, end) = if let Some(date) = &date_query.date { - let Ok(date) = date.parse::<NaiveDate>() else { - return Err(StatusCode::BAD_REQUEST); - }; - let Some(next_day) = date.succ_opt() else { - return Err(StatusCode::BAD_REQUEST); - }; - ( - Some(date.and_hms_opt(0, 0, 0).unwrap().and_utc()), - Some(next_day.and_hms_opt(0, 0, 0).unwrap().and_utc()), - ) - } else { - let start = match &date_query.from { - Some(from) => { - let Ok(from) = from.parse::<NaiveDate>() else { - return Err(StatusCode::BAD_REQUEST); - }; - Some(from.and_hms_opt(0, 0, 0).unwrap().and_utc()) - } - None => None, - }; - let end = match &date_query.to { - Some(to) => { - let Ok(to) = to.parse::<NaiveDate>() else { - return Err(StatusCode::BAD_REQUEST); - }; - let Some(next_day) = to.succ_opt() else { - return Err(StatusCode::BAD_REQUEST); - }; - Some(next_day.and_hms_opt(0, 0, 0).unwrap().and_utc()) - } - None => None, - }; - (start, end) - }; - if start.is_none() && end.is_none() { - return Err(StatusCode::BAD_REQUEST); - } - if let (Some(start), Some(end)) = (start, end) - && start >= end - { - return Err(StatusCode::BAD_REQUEST); - } - sqlx::query_as!( - MeasurementView, - "SELECT * FROM measurement - WHERE ($1::timestamptz IS NULL OR measured_at >= $1) - AND ($2::timestamptz IS NULL OR measured_at < $2)", - start, - end - ) - .fetch_all(&pool) - .await - .map(Json) - .map_err(|e| { - tracing::error!("failed to fetch measurements: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - }) -} |
