Add a RON API to search recipes by title + a bit of refactoring

This commit is contained in:
Greg Burri 2025-05-21 19:58:31 +02:00
parent a3f61e3711
commit 084f7ef445
26 changed files with 499 additions and 333 deletions

View file

@ -4,7 +4,7 @@ use axum::{
extract::{Extension, Query, State},
response::{Html, IntoResponse},
};
use serde::Deserialize;
use common::web_api;
use crate::{
app::{Context, Result},
@ -12,15 +12,10 @@ use crate::{
html_templates::*,
};
#[derive(Deserialize)]
pub struct CurrentRecipeId {
current_recipe_id: Option<i64>,
}
#[debug_handler]
pub async fn recipes_list_fragments(
State(connection): State<db::Connection>,
current_recipe: Query<CurrentRecipeId>,
params: Query<web_api::RecipesListFragmentsParams>,
Extension(context): Extension<Context>,
) -> Result<impl IntoResponse> {
Ok(Html(
@ -29,7 +24,7 @@ pub async fn recipes_list_fragments(
connection,
&context.user,
context.tr.current_lang_code(),
current_recipe.current_recipe_id,
params.current_recipe_id,
)
.await?,
context,

View file

@ -6,7 +6,7 @@ use axum::{
middleware::Next,
response::{Html, IntoResponse, Response},
};
use serde::Deserialize;
use serde::{self, Deserialize};
use crate::{
app::{AppState, Context, Result},
@ -109,7 +109,7 @@ pub async fn dev_panel(
///// LOGS /////
#[derive(Deserialize)]
pub struct LogFile {
pub struct LogsParams {
#[serde(default)]
pub log_file: String,
}
@ -119,7 +119,7 @@ pub async fn logs(
State(connection): State<db::Connection>,
State(log): State<Log>,
Extension(context): Extension<Context>,
log_file: Query<LogFile>,
Query(params): Query<LogsParams>,
) -> Result<Response> {
if context.user.is_some() && context.user.as_ref().unwrap().is_admin {
Ok(Html(
@ -133,11 +133,11 @@ pub async fn logs(
.await?,
context,
current_log_file: match (
log_file.log_file.is_empty(),
params.log_file.is_empty(),
log.file_names().unwrap_or_default(),
) {
(true, file_names) if !file_names.is_empty() => file_names[0].clone(),
_ => log_file.log_file.clone(),
_ => params.log_file.clone(),
},
log,
}

View file

@ -20,10 +20,10 @@ use super::rights::*;
pub async fn get_scheduled_recipes(
State(connection): State<db::Connection>,
Extension(context): Extension<Context>,
date_range: Query<common::ron_api::DateRange>,
date_range: Query<common::web_api::DateRange>,
) -> Result<impl IntoResponse> {
if let Some(user) = context.user {
Ok(ron_response_ok(common::ron_api::ScheduledRecipes {
Ok(ron_response_ok(common::web_api::ScheduledRecipes {
recipes: connection
.get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date)
.await?,
@ -36,7 +36,7 @@ pub async fn get_scheduled_recipes(
}
}
impl From<data::db::recipe::AddScheduledRecipeResult> for common::ron_api::ScheduleRecipeResult {
impl From<data::db::recipe::AddScheduledRecipeResult> for common::web_api::ScheduleRecipeResult {
fn from(db_res: data::db::recipe::AddScheduledRecipeResult) -> Self {
match db_res {
db::recipe::AddScheduledRecipeResult::Ok => Self::Ok,
@ -51,7 +51,7 @@ impl From<data::db::recipe::AddScheduledRecipeResult> for common::ron_api::Sched
pub async fn add_scheduled_recipe(
State(connection): State<db::Connection>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<common::ron_api::ScheduleRecipe>,
ExtractRon(ron): ExtractRon<common::web_api::ScheduleRecipe>,
) -> Result<Response> {
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
if let Some(user) = context.user {
@ -65,7 +65,7 @@ pub async fn add_scheduled_recipe(
)
.await
.map(|res| {
ron_response_ok(common::ron_api::ScheduleRecipeResult::from(res)).into_response()
ron_response_ok(common::web_api::ScheduleRecipeResult::from(res)).into_response()
})
.map_err(ErrorResponse::from)
} else {
@ -77,7 +77,7 @@ pub async fn add_scheduled_recipe(
pub async fn rm_scheduled_recipe(
State(connection): State<db::Connection>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<common::ron_api::RemoveScheduledRecipe>,
ExtractRon(ron): ExtractRon<common::web_api::RemoveScheduledRecipe>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
if let Some(user) = context.user {

View file

@ -15,6 +15,7 @@ use crate::{
};
pub mod calendar;
mod model_converter;
pub mod recipe;
mod rights;
pub mod shopping_list;

View file

@ -0,0 +1,50 @@
use common::web_api;
use crate::data::model;
impl From<model::Group> for web_api::Group {
fn from(group: model::Group) -> Self {
Self {
id: group.id,
name: group.name,
comment: group.comment,
steps: group.steps.into_iter().map(web_api::Step::from).collect(),
}
}
}
impl From<model::Step> for web_api::Step {
fn from(step: model::Step) -> Self {
Self {
id: step.id,
action: step.action,
ingredients: step
.ingredients
.into_iter()
.map(web_api::Ingredient::from)
.collect(),
}
}
}
impl From<model::Ingredient> for web_api::Ingredient {
fn from(ingredient: model::Ingredient) -> Self {
Self {
id: ingredient.id,
name: ingredient.name,
comment: ingredient.comment,
quantity_value: ingredient.quantity_value,
quantity_unit: ingredient.quantity_unit,
}
}
}
impl From<model::RecipeSearchResult> for web_api::RecipeSearchResult {
fn from(result: model::RecipeSearchResult) -> Self {
Self {
recipe_id: result.id,
title: result.title,
title_highlighted: result.title_highlighted,
}
}
}

View file

@ -5,24 +5,39 @@ use axum::{
response::{IntoResponse, Result},
};
use axum_extra::extract::Query;
use common::ron_api;
use common::web_api;
use serde::Deserialize;
use tracing::warn;
use crate::{
app::Context, data::db, data::model, ron_extractor::ExtractRon, ron_utils::ron_response_ok,
};
use crate::{app::Context, data::db, ron_extractor::ExtractRon, ron_utils::ron_response_ok};
use super::rights::*;
#[debug_handler]
pub async fn search_by_title(
State(connection): State<db::Connection>,
Extension(context): Extension<Context>,
Query(params): Query<web_api::SearchByTitleParams>,
) -> Result<impl IntoResponse> {
Ok(ron_response_ok(
connection
.search_recipes(context.tr.current_lang_code(), &params.search_term)
.await?
.into_iter()
.map(web_api::RecipeSearchResult::from)
.collect::<Vec<_>>(),
))
}
/// Ask recipe titles associated with each given id. The returned titles are in the same order
/// as the given ids.
#[debug_handler]
pub async fn get_titles(
State(connection): State<db::Connection>,
recipe_ids: Query<Vec<i64>>,
Query(params): Query<web_api::GetTitlesParams>,
) -> Result<impl IntoResponse> {
Ok(ron_response_ok(
connection.get_recipe_titles(&recipe_ids).await?,
connection.get_recipe_titles(&params.ids).await?,
))
}
@ -88,16 +103,23 @@ pub async fn get_tags(
))
}
#[derive(Deserialize)]
pub struct GetAllTagsParams {
nb_max_tags: Option<u32>,
lang: Option<String>,
}
#[debug_handler]
pub async fn get_all_tags(
State(connection): State<db::Connection>,
Extension(context): Extension<Context>,
nb_max_tags: Query<Option<u32>>,
lang: Query<Option<String>>,
Query(params): Query<GetAllTagsParams>,
) -> Result<impl IntoResponse> {
let lang = lang.0.unwrap_or(context.tr.current_lang_code().to_string());
let lang = params
.lang
.unwrap_or(context.tr.current_lang_code().to_string());
Ok(ron_response_ok(
connection.get_all_tags(&lang, nb_max_tags.0).await?,
connection.get_all_tags(&lang, params.nb_max_tags).await?,
))
}
@ -130,7 +152,7 @@ pub async fn set_difficulty(
State(connection): State<db::Connection>,
Extension(context): Extension<Context>,
Path(recipe_id): Path<i64>,
ExtractRon(difficulty): ExtractRon<ron_api::Difficulty>,
ExtractRon(difficulty): ExtractRon<web_api::Difficulty>,
) -> Result<StatusCode> {
check_user_rights_recipe(&connection, &context.user, recipe_id).await?;
connection
@ -184,43 +206,6 @@ pub async fn rm(
Ok(StatusCode::OK)
}
impl From<model::Group> for ron_api::Group {
fn from(group: model::Group) -> Self {
Self {
id: group.id,
name: group.name,
comment: group.comment,
steps: group.steps.into_iter().map(ron_api::Step::from).collect(),
}
}
}
impl From<model::Step> for ron_api::Step {
fn from(step: model::Step) -> Self {
Self {
id: step.id,
action: step.action,
ingredients: step
.ingredients
.into_iter()
.map(ron_api::Ingredient::from)
.collect(),
}
}
}
impl From<model::Ingredient> for ron_api::Ingredient {
fn from(ingredient: model::Ingredient) -> Self {
Self {
id: ingredient.id,
name: ingredient.name,
comment: ingredient.comment,
quantity_value: ingredient.quantity_value,
quantity_unit: ingredient.quantity_unit,
}
}
}
#[debug_handler]
pub async fn get_groups(
State(connection): State<db::Connection>,
@ -232,7 +217,7 @@ pub async fn get_groups(
.get_groups(recipe_id)
.await?
.into_iter()
.map(ron_api::Group::from)
.map(web_api::Group::from)
.collect::<Vec<_>>(),
))
}

View file

@ -4,7 +4,7 @@ use axum::{
http::StatusCode,
response::{ErrorResponse, IntoResponse, Result},
};
use common::ron_api;
use common::web_api;
use crate::{
app::Context,
@ -17,7 +17,7 @@ use crate::{
use super::rights::*;
impl From<model::ShoppingListItem> for common::ron_api::ShoppingListItem {
impl From<model::ShoppingListItem> for common::web_api::ShoppingListItem {
fn from(item: model::ShoppingListItem) -> Self {
Self {
id: item.id,
@ -43,7 +43,7 @@ pub async fn get(
.get_shopping_list(user.id)
.await?
.into_iter()
.map(common::ron_api::ShoppingListItem::from)
.map(common::web_api::ShoppingListItem::from)
.collect::<Vec<_>>(),
))
} else {
@ -58,7 +58,7 @@ pub async fn get(
pub async fn set_entry_checked(
State(connection): State<db::Connection>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::KeyValue<bool>>,
ExtractRon(ron): ExtractRon<web_api::KeyValue<bool>>,
) -> Result<impl IntoResponse> {
check_user_rights_shopping_list_entry(&connection, &context.user, ron.id).await?;
Ok(ron_response_ok(