summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorbatsumaru <>2026-10-16 11:57:17 +0900
committerbatsumaru <>2026-10-16 11:57:17 +0900
commita69e78b7482227f22e7ddbfaa7dc7ee31f29a99b (patch)
treee829a766484e1937bcbcd4e97668e9d674ea6099 /src/main.rs
parent7dc5038a7438d0eaf63d4bf65f88f6f5ab6b3207 (diff)
Add enum fetchers
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs180
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
- })
-}