diff --git a/api/src/ingredients.rs b/api/src/ingredients.rs index ca5d98d..acea7d8 100644 --- a/api/src/ingredients.rs +++ b/api/src/ingredients.rs @@ -1,5 +1,3 @@ -use std::{any::Any, borrow::BorrowMut}; - use axum::{ extract::{Path, State}, http::StatusCode, @@ -10,7 +8,6 @@ use axum::{ use foodlib::Ingredient; use serde::{Deserialize, Serialize}; use sqlx::types::BigDecimal; -use tracing::Instrument; use crate::ApiState; diff --git a/api/src/places.rs b/api/src/places.rs index d8ddcbb..3d27a8f 100644 --- a/api/src/places.rs +++ b/api/src/places.rs @@ -2,7 +2,7 @@ use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, - routing::{delete, get, post, put}, + routing::{get, post, put}, Json, Router, }; use foodlib::Place; diff --git a/api/src/reciepes.rs b/api/src/reciepes.rs index 4b8cb79..7bcd434 100644 --- a/api/src/reciepes.rs +++ b/api/src/reciepes.rs @@ -5,8 +5,9 @@ use axum::{ routing::{delete, get, post, put}, Json, Router, }; -use foodlib::Recipe; +use foodlib::{Recipe, RecipeIngredient, RecipeMetaIngredient, RecipeStep, Unit}; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::types::PgInterval, types::BigDecimal}; use crate::ApiState; @@ -17,10 +18,12 @@ pub fn router() -> Router { .route("/:recipe_id/", get(get_recipe)) .route("/:recipe_id/", post(update_reciepe)) .route("/:recipe_id/", delete(delete_reciepe)) - .route("/:recipe_id/steps/", get(not_implemented)) - .route("/:recipe_id/steps/", post(not_implemented)) - .route("/:recipe_id/ingredients/", get(update_reciepe)) - .route("/:recipe_id/ingredients/", post(update_reciepe)) + .route("/:recipe_id/steps/", get(get_steps)) + .route("/:recipe_id/steps/", post(update_steps)) + .route("/:recipe_id/subrecipes/", get(get_subrecipes)) + .route("/:recipe_id/subrecipes/", post(update_subrecipes)) + .route("/:recipe_id/ingredients/", get(get_ingredients)) + .route("/:recipe_id/ingredients/", post(update_ingredients)) } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -38,6 +41,85 @@ fn recipe_to_body(recipe: Recipe) -> RecipeBody { } } +pub(crate) fn serialize_interval(interval: &PgInterval, serializer: S) -> Result +where + S: serde::Serializer, +{ + let duration = interval.microseconds / 1000_0000; + serializer.serialize_str(&duration.to_string()) +} + +pub(crate) fn deserialize_interval<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + let microseconds: i64 = s + .parse() + .map_err(|e| serde::de::Error::custom(format!("Failed to parse interval: {}", e)))?; + let interval = PgInterval { + microseconds: microseconds * 1000_1000, + days: 0, + months: 0, + }; + Ok(interval) +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct RecipeStepBody { + name: String, + description: String, + #[serde( + serialize_with = "serialize_interval", + deserialize_with = "deserialize_interval" + )] + pub duration_fixed: PgInterval, + #[serde( + serialize_with = "serialize_interval", + deserialize_with = "deserialize_interval" + )] + pub duration_per_kg: PgInterval, +} +fn step_to_body(step: RecipeStep) -> RecipeStepBody { + RecipeStepBody { + name: step.step_name, + description: step.step_description, + duration_fixed: step.fixed_duration, + duration_per_kg: step.duration_per_kg, + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct RecipeIngredientBody { + id: i32, + name: String, + amount: BigDecimal, + unit: Unit, +} +fn ingredient_to_body(ingredient: RecipeIngredient) -> Option { + match ingredient.ingredient { + RecipeMetaIngredient::Ingredient(ingredient_data) => Some(RecipeIngredientBody { + id: ingredient_data.ingredient_id, + name: ingredient_data.name, + amount: ingredient.amount, + unit: ingredient.unit, + }), + _ => None, + } +} + +fn subrecipe_to_body(ingredient: RecipeIngredient) -> Option { + match ingredient.ingredient { + RecipeMetaIngredient::MetaRecipe(ingredient_data) => Some(RecipeIngredientBody { + id: ingredient_data.recipe_id, + name: ingredient_data.name, + amount: ingredient.amount, + unit: ingredient.unit, + }), + _ => None, + } +} + async fn list_reciepes(State(state): State) -> impl IntoResponse { match state.food_base.get_recipes().await { Ok(recipes) => ( @@ -98,6 +180,98 @@ async fn delete_reciepe( } } -async fn not_implemented() -> impl IntoResponse { +async fn update_steps( + State(state): State, + Path(recipe_id): Path, + Json(body): Json>, +) -> impl IntoResponse { + match state + .food_base + .update_recipe_steps( + recipe_id, + body.into_iter().enumerate().map(|(i, step)| { + return RecipeStep { + step_id: 0, + step_order: i as f64, + step_name: step.name, + step_description: step.description, + fixed_duration: step.duration_fixed, + duration_per_kg: step.duration_per_kg, + recipe_id: recipe_id, + }; + }), + ) + .await + { + Ok(_) => StatusCode::OK, + Err(_) => StatusCode::INTERNAL_SERVER_ERROR, + } +} + +async fn get_steps(State(state): State, Path(recipe_id): Path) -> impl IntoResponse { + match state.food_base.get_recipe_steps(recipe_id).await { + Ok(steps) => ( + StatusCode::OK, + Json( + steps + .into_iter() + .map(step_to_body) + .collect::>(), + ), + ) + .into_response(), + Err(_) => StatusCode::NOT_FOUND.into_response(), + } +} + +async fn get_ingredients( + State(state): State, + Path(recipe_id): Path, +) -> impl IntoResponse { + match state.food_base.get_recipe_ingredients(recipe_id).await { + Ok(ingredients) => ( + StatusCode::OK, + Json( + ingredients + .into_iter() + .flat_map(ingredient_to_body) + .collect::>(), + ), + ) + .into_response(), + Err(_) => StatusCode::NOT_FOUND.into_response(), + } +} + +async fn update_ingredients( + State(_state): State, + Path(_recipe_id): Path, +) -> impl IntoResponse { + StatusCode::NOT_IMPLEMENTED +} + +async fn get_subrecipes( + State(state): State, + Path(recipe_id): Path, +) -> impl IntoResponse { + match state.food_base.get_recipe_meta_ingredients(recipe_id).await { + Ok(ingredients) => ( + StatusCode::OK, + Json( + ingredients + .into_iter() + .flat_map(subrecipe_to_body) + .collect::>(), + ), + ) + .into_response(), + Err(_) => StatusCode::NOT_FOUND.into_response(), + } +} + +async fn update_subrecipes( + State(_state): State, + Path(_recipe_id): Path, +) -> impl IntoResponse { StatusCode::NOT_IMPLEMENTED }