use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use strum::{EnumIter, IntoStaticStr}; use uuid::Uuid; #[derive(sqlx::Type, Serialize, Deserialize, EnumIter, Debug, IntoStaticStr, Clone, Copy)] #[sqlx(type_name = "metric_type")] pub enum MetricType { BodyWeight, Hip, Waist, } impl MetricType { pub fn accepts(&self, unit: &Unit) -> bool { matches!( (self, unit), (MetricType::BodyWeight, Unit::Kilogram) | (MetricType::Hip, Unit::Centimeter) | (MetricType::Waist, Unit::Centimeter) ) } } #[derive(sqlx::Type, Serialize, Deserialize, EnumIter, Debug, IntoStaticStr)] #[sqlx(type_name = "unit")] pub enum Unit { Kilogram, Centimeter, } #[derive(Serialize)] pub struct MeasurementView { pub id: Uuid, pub measured_at: DateTime, pub metric_type: MetricType, pub value: f64, pub unit: Unit, } #[derive(Deserialize)] #[serde(try_from = "MeasurementDataUnchecked")] pub struct MeasurementData { pub measured_at: DateTime, pub metric_type: MetricType, pub value: f64, pub unit: Unit, } #[derive(Deserialize)] struct MeasurementDataUnchecked { pub measured_at: DateTime, pub metric_type: MetricType, pub value: f64, pub unit: Unit, } impl TryFrom for MeasurementData { type Error = String; fn try_from(value: MeasurementDataUnchecked) -> Result { if value.value < 0.0 || value.value.is_nan() { return Err("Invalid value".to_string()); } if !value.metric_type.accepts(&value.unit) { let metric_type: &'static str = (&value.metric_type).into(); let unit: &'static str = (&value.unit).into(); return Err(format!("Invalid pair {}, {}", metric_type, unit)); } Ok(Self { measured_at: value.measured_at, metric_type: value.metric_type, value: value.value, unit: value.unit, }) } } #[derive(Serialize)] pub struct MetricUnitPair { pub metric_type: MetricType, pub unit: Unit, } #[derive(Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum Order { #[default] Asc, Desc, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct ListQuery { pub from: Option>, pub to: Option>, pub metric_type: Option, pub limit: Option, #[serde(default)] pub order: Order, } #[cfg(test)] mod tests { use super::*; use sqlx::PgPool; use strum::IntoEnumIterator; #[sqlx::test] async fn unit_enums_match(pool: PgPool) { let values: Vec = sqlx::query_scalar("SELECT enum_range(NULL::unit)") .fetch_one(&pool) .await .expect("Database has a variant that Rust doesn't"); assert_eq!( values.len(), Unit::iter().count(), "Rust has a variant that the database doesn't" ); } #[sqlx::test] async fn metric_type_enums_match(pool: PgPool) { let values: Vec = sqlx::query_scalar("SELECT enum_range(NULL::metric_type)") .fetch_one(&pool) .await .expect("Database has a variant that Rust doesn't"); assert_eq!( values.len(), MetricType::iter().count(), "Rust has a variant that the database doesn't" ); } }