summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbatsumaru <>2026-10-15 16:47:14 +0900
committerbatsumaru <>2026-10-15 16:47:14 +0900
commit7dc5038a7438d0eaf63d4bf65f88f6f5ab6b3207 (patch)
tree9bc96bc03afba10d2090d5b5f54925b64251aee5 /src
parenta007b22a23397ec6dc69736e73f85746ce457355 (diff)
Add search endpoint
Diffstat (limited to 'src')
-rw-r--r--src/main.rs140
1 files 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<Utc>,
metric_type: MetricType,
@@ -39,6 +39,21 @@ pub struct Row {
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();
@@ -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<PgPool>) -> Json<Value> {
- let rows = sqlx::query_as!(Row, "SELECT * FROM measurement")
+async fn all(State(pool): State<PgPool>) -> Result<Json<Vec<MeasurementView>>, 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<PgPool>, Json(row): Json<Value>) -> StatusCode {
- if let Ok(insert_row) = from_value::<Row>(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<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 {
- return StatusCode::UNPROCESSABLE_ENTITY;
+ 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
+ })
}