// rocie - An enterprise grocery management system // // Copyright (C) 2026 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Rocie. // // You should have received a copy of the License along with this program. // If not, see . use actix_identity::Identity; use actix_web::{HttpRequest, HttpResponse, Responder, error::Result, get, web}; use crate::{ api::get::auth::UrlEncodedString, app::App, storage::sql::{ recipe::{Recipe, RecipeId, RecipeIdStub}, recipe_parent::{RecipeParent, RecipeParentId, RecipeParentIdStub}, }, }; /// Get an recipe by it's name. #[utoipa::path( responses( ( status = OK, description = "Recipe found in database and fetched", body = Recipe, ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = NOT_FOUND, description = "Recipe not found in database" ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "name" = String, description = "Recipe name" ), ) )] #[get("/recipe/by-name/{name}")] pub(crate) async fn recipe_by_name( app: web::Data, req: HttpRequest, name: web::Path, _user: Identity, ) -> Result { drop(name); let name = UrlEncodedString::from_str( req.path() .strip_prefix("/recipe/by-name/") .expect("Will always exists"), ); let name = name.percent_decode()?; match Recipe::from_name(&app, name).await? { Some(recipe) => Ok(HttpResponse::Ok().json(recipe)), None => Ok(HttpResponse::NotFound().finish()), } } /// Get an recipe by it's id. #[utoipa::path( responses( ( status = OK, description = "Recipe found in database and fetched", body = Recipe, ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = NOT_FOUND, description = "Recipe not found in database" ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "id" = RecipeId, description = "Recipe id" ), ) )] #[get("/recipe/by-id/{id}")] pub(crate) async fn recipe_by_id( app: web::Data, id: web::Path, _user: Identity, ) -> Result { match Recipe::from_id(&app, id.into_inner().into()).await? { Some(recipe) => Ok(HttpResponse::Ok().json(recipe)), None => Ok(HttpResponse::NotFound().finish()), } } /// Get all added recipes #[utoipa::path( responses( ( status = OK, description = "All recipes found in database and fetched", body = Vec, ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), )] #[get("/recipe/all")] pub(crate) async fn recipes(app: web::Data, _user: Identity) -> Result { let all = Recipe::get_all(&app).await?; Ok(HttpResponse::Ok().json(all)) } /// Get Recipes by it's recipe parent id /// /// This will also return all recipes below this recipe parent id #[utoipa::path( responses( ( status = OK, description = "Recipes found from database", body = Vec ), ( status = NOT_FOUND, description = "Recipe parent id not found in database" ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "id" = RecipeParentId, description = "Recipe parent id" ), ) )] #[get("/recipe/by-recipe-parent-id-indirect/{id}")] pub(crate) async fn recipes_by_recipe_parent_id_indirect( app: web::Data, id: web::Path, _user: Identity, ) -> Result { let id = id.into_inner(); if let Some(parent) = RecipeParent::from_id(&app, id.into()).await? { async fn collect_recipes(app: &App, parent: RecipeParent) -> Result> { let mut all = Recipe::get_all(app) .await? .into_iter() .filter(|prod| prod.parent.is_some_and(|val| val == parent.id)) .collect::>(); if let Some(child) = RecipeParent::get_all(app) .await? .into_iter() .find(|pp| pp.parent.is_some_and(|id| id == parent.id)) { all.extend(Box::pin(collect_recipes(app, child)).await?); } Ok(all) } let all = collect_recipes(&app, parent).await?; Ok(HttpResponse::Ok().json(all)) } else { Ok(HttpResponse::NotFound().finish()) } } /// Get Recipes by it's recipe parent id /// /// This will only return recipes directly associated with this recipe parent id #[utoipa::path( responses( ( status = OK, description = "Recipes found from database", body = Vec ), ( status = NOT_FOUND, description = "Recipe parent id not found in database" ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "id" = RecipeParentId, description = "Recipe parent id" ), ) )] #[get("/recipe/by-recipe-parent-id-direct/{id}")] pub(crate) async fn recipes_by_recipe_parent_id_direct( app: web::Data, id: web::Path, _user: Identity, ) -> Result { let id = id.into_inner(); if let Some(parent) = RecipeParent::from_id(&app, id.into()).await? { let all = Recipe::get_all(&app) .await? .into_iter() .filter(|prod| prod.parent.is_some_and(|val| val == parent.id)) .collect::>(); Ok(HttpResponse::Ok().json(all)) } else { Ok(HttpResponse::NotFound().finish()) } } /// Get Recipes by it's absents of a recipe parent /// /// This will only return recipes without a recipe parent associated with it #[utoipa::path( responses( ( status = OK, description = "Recipes found from database", body = Vec ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), )] #[get("/recipe/without-recipe-parent")] pub(crate) async fn recipes_without_recipe_parent( app: web::Data, _user: Identity, ) -> Result { let all = { let base = Recipe::get_all(&app).await?; base.into_iter() .filter(|r| r.parent.is_none()) .collect::>() }; Ok(HttpResponse::Ok().json(all)) }