summaryrefslogtreecommitdiff
path: root/src/handlers.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/handlers.rs')
-rw-r--r--src/handlers.rs157
1 files changed, 73 insertions, 84 deletions
diff --git a/src/handlers.rs b/src/handlers.rs
index a49d29a..b048681 100644
--- a/src/handlers.rs
+++ b/src/handlers.rs
@@ -1,57 +1,85 @@
use axum::{
extract::{Path, Query, State},
- http::StatusCode,
+ http::{HeaderName, StatusCode, header},
response::Json,
};
use chrono::NaiveTime;
use sqlx::PgPool;
+use strum::IntoEnumIterator;
use uuid::Uuid;
-use crate::models::{AllQuery, DateQuery, MeasurementData, MeasurementView, Unit};
-use crate::{error::AppError, models::MetricType};
+use crate::error::AppError;
+use crate::models::{
+ ListQuery, MeasurementData, MeasurementView, MetricType, MetricUnitPair, Order, Unit,
+};
-pub async fn index() -> &'static str {
- "Hello World!"
+pub async fn health() -> StatusCode {
+ StatusCode::OK
}
-pub async fn all(
+pub async fn list_measurements(
State(pool): State<PgPool>,
- Query(options): Query<AllQuery>,
+ Query(query): Query<ListQuery>,
) -> Result<Json<Vec<MeasurementView>>, AppError> {
- let limit = match options.limit {
+ let limit = match query.limit {
Some(x) if (x > 0) => Some(x),
None => None,
_ => return Err(AppError::BadRequest),
};
+ let (from, to) = match (query.date, query.from, query.to) {
+ (Some(date), None, None) => (Some(date), Some(date)),
+ (None, from, to) => (from, to),
+ _ => return Err(AppError::BadRequest),
+ };
+ if let (Some(from), Some(to)) = (from, to)
+ && from > to
+ {
+ return Err(AppError::BadRequest);
+ }
+ let start = from.map(|from| from.and_time(NaiveTime::MIN).and_utc());
+ let end = match to {
+ Some(to) => {
+ let Some(next_day) = to.succ_opt() else {
+ return Err(AppError::BadRequest);
+ };
+ Some(next_day.and_time(NaiveTime::MIN).and_utc())
+ }
+ None => None,
+ };
// SQL can't bind ASC/DESC as a parameter. The CASE yields measured_at only when
- // `reverse` is true (sorted DESC) and NULL otherwise, so that key ties for every
+ // `order` is desc (sorted DESC) and NULL otherwise, so that key ties for every
// row and the ASC key decides the order. If this gets slow, use an if/else with a
// separate ASC and DESC query and add an index on measured_at.
let rows = sqlx::query_as!(
MeasurementView,
"
SELECT id, measured_at, metric_type, value, unit FROM measurement
- ORDER BY CASE WHEN $2 THEN measured_at END DESC,
+ WHERE ($1::timestamptz IS NULL OR measured_at >= $1)
+ AND ($2::timestamptz IS NULL OR measured_at < $2)
+ ORDER BY CASE WHEN $4 THEN measured_at END DESC,
measured_at ASC
- LIMIT $1
+ LIMIT $3
",
+ start,
+ end,
limit,
- options.reverse.unwrap_or(false)
+ matches!(query.order, Order::Desc)
)
.fetch_all(&pool)
.await?;
Ok(Json(rows))
}
-pub async fn add(
+pub async fn create_measurement(
State(pool): State<PgPool>,
Json(data): Json<MeasurementData>,
-) -> Result<(StatusCode, Json<Uuid>), AppError> {
- let rec = sqlx::query!(
+) -> Result<(StatusCode, [(HeaderName, String); 1], Json<MeasurementView>), AppError> {
+ let measurement = sqlx::query_as!(
+ MeasurementView,
"
INSERT INTO measurement (id, measured_at, metric_type, value, unit)
VALUES ($1, $2, $3, $4, $5)
- RETURNING id
+ RETURNING id, measured_at, metric_type, value, unit
",
Uuid::now_v7(),
data.measured_at,
@@ -61,66 +89,15 @@ pub async fn add(
)
.fetch_one(&pool)
.await?;
- Ok((StatusCode::CREATED, Json(rec.id)))
-}
-
-pub async fn search(
- State(pool): State<PgPool>,
- Query(date_query): Query<DateQuery>,
-) -> Result<Json<Vec<MeasurementView>>, AppError> {
- let (from, to) = match (date_query.date, date_query.from, date_query.to) {
- (Some(date), None, None) => (Some(date), Some(date)),
- (None, from, to) => (from, to),
- _ => return Err(AppError::BadRequest),
- };
- if from.is_none() && to.is_none() {
- return Err(AppError::BadRequest);
- }
- if let (Some(from), Some(to)) = (from, to)
- && from > to
- {
- return Err(AppError::BadRequest);
- }
- let start = from.map(|from| from.and_time(NaiveTime::MIN).and_utc());
- let end = match to {
- Some(to) => {
- let Some(next_day) = to.succ_opt() else {
- return Err(AppError::BadRequest);
- };
- Some(next_day.and_time(NaiveTime::MIN).and_utc())
- }
- None => None,
- };
- let rows = sqlx::query_as!(
- MeasurementView,
- "SELECT id, measured_at, metric_type, value, unit 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?;
- Ok(Json(rows))
-}
-
-pub async fn units(State(pool): State<PgPool>) -> Result<Json<Vec<Unit>>, AppError> {
- Ok(Json(
- sqlx::query_scalar("SELECT enum_range(NULL::unit)")
- .fetch_one(&pool)
- .await?,
+ let location = format!("/measurements/{}", measurement.id);
+ Ok((
+ StatusCode::CREATED,
+ [(header::LOCATION, location)],
+ Json(measurement),
))
}
-pub async fn metric_types(State(pool): State<PgPool>) -> Result<Json<Vec<MetricType>>, AppError> {
- Ok(Json(
- sqlx::query_scalar("SELECT enum_range(NULL::metric_type)")
- .fetch_one(&pool)
- .await?,
- ))
-}
-
-pub async fn measurement_by_id(
+pub async fn get_measurement(
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
) -> Result<Json<MeasurementView>, AppError> {
@@ -137,17 +114,7 @@ pub async fn measurement_by_id(
Ok(Json(measurement))
}
-pub async fn delete(
- State(pool): State<PgPool>,
- Path(id): Path<Uuid>,
-) -> Result<StatusCode, AppError> {
- sqlx::query!("DELETE FROM measurement WHERE id = $1", id)
- .execute(&pool)
- .await?;
- Ok(StatusCode::NO_CONTENT)
-}
-
-pub async fn update(
+pub async fn update_measurement(
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
Json(data): Json<MeasurementData>,
@@ -174,3 +141,25 @@ pub async fn update(
}
Ok(StatusCode::NO_CONTENT)
}
+
+pub async fn delete_measurement(
+ State(pool): State<PgPool>,
+ Path(id): Path<Uuid>,
+) -> Result<StatusCode, AppError> {
+ sqlx::query!("DELETE FROM measurement WHERE id = $1", id)
+ .execute(&pool)
+ .await?;
+ Ok(StatusCode::NO_CONTENT)
+}
+
+pub async fn metric_types() -> Json<Vec<MetricUnitPair>> {
+ Json(
+ MetricType::iter()
+ .flat_map(|metric_type| {
+ Unit::iter()
+ .filter(move |unit| metric_type.accepts(unit))
+ .map(move |unit| MetricUnitPair { metric_type, unit })
+ })
+ .collect(),
+ )
+}