From 7dc5038a7438d0eaf63d4bf65f88f6f5ab6b3207 Mon Sep 17 00:00:00 2001 From: batsumaru <> Date: Thu, 15 Oct 2026 16:47:14 +0900 Subject: Add search endpoint --- src/main.rs | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 121 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index 31a10cc..6bf8fa8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,14 @@ use axum::{ Router, - extract::State, - http::{Response, StatusCode}, - response::Json, + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, routing::{get, post}, }; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDate, Utc}; use dotenvy::dotenv; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::{Value, from_value, json}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, from_value}; use sqlx::{PgPool, postgres::PgPoolOptions}; use std::env; use uuid::Uuid; @@ -30,8 +30,8 @@ pub enum Unit { Centimeter, } -#[derive(Serialize, Deserialize)] -pub struct Row { +#[derive(Serialize)] +pub struct MeasurementView { id: Uuid, measured_at: DateTime, metric_type: MetricType, @@ -39,6 +39,21 @@ pub struct Row { unit: Unit, } +#[derive(Deserialize)] +pub struct MeasurementData { + measured_at: DateTime, + metric_type: MetricType, + value: f64, + unit: Unit, +} + +#[derive(Deserialize)] +pub struct DateQuery { + date: Option, + from: Option, + to: Option, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); @@ -53,6 +68,7 @@ async fn main() -> anyhow::Result<()> { .route("/", get(index)) .route("/all", get(all)) .route("/add", post(add)) + .route("/search", get(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"); @@ -64,22 +80,108 @@ async fn index() -> &'static str { "Hello World!" } -async fn all(State(pool): State) -> Json { - let rows = sqlx::query_as!(Row, "SELECT * FROM measurement") +async fn all(State(pool): State) -> Result>, StatusCode> { + sqlx::query_as!(MeasurementView, "SELECT * FROM measurement") .fetch_all(&pool) .await - .unwrap_or_else(|_| Vec::new()); - Json(json!(rows)) + .map(Json) + .map_err(|e| { + tracing::error!("failed to fetch measurements: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + }) } -async fn add(State(pool): State, Json(row): Json) -> StatusCode { - if let Ok(insert_row) = from_value::(row) { - match sqlx::query!("INSERT INTO measurement (id, measured_at, metric_type, value, unit) VALUES ($1, $2, $3, $4, $5)", insert_row.id, insert_row.measured_at, insert_row.metric_type, insert_row.value, insert_row.unit).execute(&pool).await { - Ok(_) => {return StatusCode::CREATED;}, - Err(sqlx::Error::Database(e)) if e.is_unique_violation() => StatusCode::CONFLICT, - Err(_) => StatusCode::INTERNAL_SERVER_ERROR +async fn add(State(pool): State, Json(row): Json) -> Response { + if let Ok(insert_row) = from_value::(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 { - return StatusCode::UNPROCESSABLE_ENTITY; + StatusCode::UNPROCESSABLE_ENTITY.into_response() + } +} + +async fn search( + State(pool): State, + Query(date_query): Query, +) -> Result>, 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::() 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::() 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::() 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 + }) } -- cgit v1.3