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

@ -132,6 +132,10 @@ pub fn make_service(
// Disabled: update user profile is now made with a post data ('edit_user_post'). // Disabled: update user profile is now made with a post data ('edit_user_post').
// .route("/user/update", put(services::ron::update_user)) // .route("/user/update", put(services::ron::update_user))
.route("/lang", put(services::ron::set_lang)) .route("/lang", put(services::ron::set_lang))
.route(
"/recipe/search",
get(services::ron::recipe::search_by_title),
)
.route("/recipe/titles", get(services::ron::recipe::get_titles)) .route("/recipe/titles", get(services::ron::recipe::get_titles))
.route( .route(
"/recipe/{id}/title", "/recipe/{id}/title",

View file

@ -1,7 +1,7 @@
use std::u32; use std::u32;
use chrono::prelude::*; use chrono::prelude::*;
use common::ron_api::Difficulty; use common::web_api::Difficulty;
use itertools::Itertools; use itertools::Itertools;
use sqlx::{Error, Sqlite}; use sqlx::{Error, Sqlite};
@ -410,8 +410,8 @@ WHERE [Recipe].[user_id] = $1
sqlx::query_scalar( sqlx::query_scalar(
r#" r#"
SELECT [name], COUNT([name]) as [nb_used] FROM [Tag] SELECT [name], COUNT([name]) as [nb_used] FROM [Tag]
INNER JOIN [RecipeTag] ON [RecipeTag].[tag_id] = [Tag].[id] INNER JOIN [RecipeTag] ON [RecipeTag].[tag_id] = [Tag].[id]
INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTag].[recipe_id] INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTag].[recipe_id]
WHERE [Recipe].[lang] = $1 WHERE [Recipe].[lang] = $1
GROUP BY [Tag].[name] GROUP BY [Tag].[name]
ORDER BY [nb_used] DESC, [name] ORDER BY [nb_used] DESC, [name]

View file

@ -1,5 +1,5 @@
use chrono::prelude::*; use chrono::prelude::*;
use common::ron_api::Difficulty; use common::web_api::Difficulty;
use sqlx::{self, FromRow}; use sqlx::{self, FromRow};
#[derive(Debug, Clone, FromRow)] #[derive(Debug, Clone, FromRow)]

View file

@ -3,7 +3,7 @@ use axum::{
http::{StatusCode, header}, http::{StatusCode, header},
response::{ErrorResponse, IntoResponse, Response}, response::{ErrorResponse, IntoResponse, Response},
}; };
use common::ron_api; use common::web_api;
use ron::de::from_bytes; use ron::de::from_bytes;
use serde::{Serialize, de::DeserializeOwned}; use serde::{Serialize, de::DeserializeOwned};
@ -16,7 +16,7 @@ pub struct RonError {
impl axum::response::IntoResponse for RonError { impl axum::response::IntoResponse for RonError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
match ron_api::to_string(&self) { match web_api::to_string(&self) {
Ok(ron_as_str) => ( Ok(ron_as_str) => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
[(header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)], [(header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)],
@ -62,7 +62,7 @@ pub fn ron_response<T>(status: StatusCode, ron: T) -> Response
where where
T: Serialize, T: Serialize,
{ {
match ron_api::to_string(&ron) { match web_api::to_string(&ron) {
Ok(ron_as_str) => ( Ok(ron_as_str) => (
status, status,
[(header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)], [(header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)],

View file

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

View file

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

View file

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

View file

@ -15,6 +15,7 @@ use crate::{
}; };
pub mod calendar; pub mod calendar;
mod model_converter;
pub mod recipe; pub mod recipe;
mod rights; mod rights;
pub mod shopping_list; 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}, response::{IntoResponse, Result},
}; };
use axum_extra::extract::Query; use axum_extra::extract::Query;
use common::ron_api; use common::web_api;
use serde::Deserialize;
use tracing::warn; use tracing::warn;
use crate::{ use crate::{app::Context, data::db, ron_extractor::ExtractRon, ron_utils::ron_response_ok};
app::Context, data::db, data::model, ron_extractor::ExtractRon, ron_utils::ron_response_ok,
};
use super::rights::*; 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 /// Ask recipe titles associated with each given id. The returned titles are in the same order
/// as the given ids. /// as the given ids.
#[debug_handler] #[debug_handler]
pub async fn get_titles( pub async fn get_titles(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
recipe_ids: Query<Vec<i64>>, Query(params): Query<web_api::GetTitlesParams>,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
Ok(ron_response_ok( 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] #[debug_handler]
pub async fn get_all_tags( pub async fn get_all_tags(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
nb_max_tags: Query<Option<u32>>, Query(params): Query<GetAllTagsParams>,
lang: Query<Option<String>>,
) -> Result<impl IntoResponse> { ) -> 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( 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>, State(connection): State<db::Connection>,
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
Path(recipe_id): Path<i64>, Path(recipe_id): Path<i64>,
ExtractRon(difficulty): ExtractRon<ron_api::Difficulty>, ExtractRon(difficulty): ExtractRon<web_api::Difficulty>,
) -> Result<StatusCode> { ) -> Result<StatusCode> {
check_user_rights_recipe(&connection, &context.user, recipe_id).await?; check_user_rights_recipe(&connection, &context.user, recipe_id).await?;
connection connection
@ -184,43 +206,6 @@ pub async fn rm(
Ok(StatusCode::OK) 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] #[debug_handler]
pub async fn get_groups( pub async fn get_groups(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
@ -232,7 +217,7 @@ pub async fn get_groups(
.get_groups(recipe_id) .get_groups(recipe_id)
.await? .await?
.into_iter() .into_iter()
.map(ron_api::Group::from) .map(web_api::Group::from)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
)) ))
} }

View file

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

View file

@ -42,10 +42,10 @@
<label for="select-difficulty">{{ context.tr.t(Sentence::RecipeDifficulty) }}</label> <label for="select-difficulty">{{ context.tr.t(Sentence::RecipeDifficulty) }}</label>
<select id="select-difficulty"> <select id="select-difficulty">
<option value="0" {%~ call is_difficulty(common::ron_api::Difficulty::Unknown) %}> - </option> <option value="0" {%~ call is_difficulty(common::web_api::Difficulty::Unknown) %}> - </option>
<option value="1" {%~ call is_difficulty(common::ron_api::Difficulty::Easy) %}>{{ context.tr.t(Sentence::RecipeDifficultyEasy) }}</option> <option value="1" {%~ call is_difficulty(common::web_api::Difficulty::Easy) %}>{{ context.tr.t(Sentence::RecipeDifficultyEasy) }}</option>
<option value="2" {%~ call is_difficulty(common::ron_api::Difficulty::Medium) %}>{{ context.tr.t(Sentence::RecipeDifficultyMedium) }}</option> <option value="2" {%~ call is_difficulty(common::web_api::Difficulty::Medium) %}>{{ context.tr.t(Sentence::RecipeDifficultyMedium) }}</option>
<option value="3" {%~ call is_difficulty(common::ron_api::Difficulty::Hard) %}>{{ context.tr.t(Sentence::RecipeDifficultyHard) }}</option> <option value="3" {%~ call is_difficulty(common::web_api::Difficulty::Hard) %}>{{ context.tr.t(Sentence::RecipeDifficultyHard) }}</option>
</select> </select>
<div id="container-tags"> <div id="container-tags">

View file

@ -39,12 +39,12 @@
<span class="difficulty"> <span class="difficulty">
{% match recipe.difficulty %} {% match recipe.difficulty %}
{% when common::ron_api::Difficulty::Unknown %} {% when common::web_api::Difficulty::Unknown %}
{% when common::ron_api::Difficulty::Easy %} {% when common::web_api::Difficulty::Easy %}
{{ context.tr.t(Sentence::RecipeDifficultyEasy) }} {{ context.tr.t(Sentence::RecipeDifficultyEasy) }}
{% when common::ron_api::Difficulty::Medium %} {% when common::web_api::Difficulty::Medium %}
{{ context.tr.t(Sentence::RecipeDifficultyMedium) }} {{ context.tr.t(Sentence::RecipeDifficultyMedium) }}
{% when common::ron_api::Difficulty::Hard %} {% when common::web_api::Difficulty::Hard %}
{{ context.tr.t(Sentence::RecipeDifficultyHard) }} {{ context.tr.t(Sentence::RecipeDifficultyHard) }}
{% endmatch %} {% endmatch %}
</span> </span>

View file

@ -2,7 +2,7 @@ use std::{error::Error, sync::Arc};
use axum::http; use axum::http;
use axum_test::TestServer; use axum_test::TestServer;
use common::ron_api; use common::web_api;
use cookie::Cookie; use cookie::Cookie;
use scraper::{ElementRef, Html, Selector}; use scraper::{ElementRef, Html, Selector};
use serde::Serialize; use serde::Serialize;
@ -221,7 +221,13 @@ async fn sign_in() -> Result<(), Box<dyn Error>> {
// Assert. // Assert.
response.assert_status_see_other(); // Redirection after successful sign in. response.assert_status_see_other(); // Redirection after successful sign in.
response.assert_text(""); response.assert_text("");
response.assert_header("location", "/?user_message=16&user_message_icon=0"); response.assert_header(
"location",
format!(
"/?user_message={}&user_message_icon=0",
common::translation::Sentence::SignInSuccess as i64
),
);
Ok(()) Ok(())
} }
@ -260,7 +266,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box<dyn Error>> {
.patch(&format!("/ron-api/recipe/{recipe_id}/title")) .patch(&format!("/ron-api/recipe/{recipe_id}/title"))
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(ron_api::to_string("AAA").unwrap().into()) .bytes(web_api::to_string("AAA").unwrap().into())
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
@ -268,7 +274,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box<dyn Error>> {
.patch(&format!("/ron-api/recipe/{recipe_id}/description")) .patch(&format!("/ron-api/recipe/{recipe_id}/description"))
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(ron_api::to_string("BBB").unwrap().into()) .bytes(web_api::to_string("BBB").unwrap().into())
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
@ -276,7 +282,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box<dyn Error>> {
.patch(&format!("/ron-api/recipe/{recipe_id}/servings")) .patch(&format!("/ron-api/recipe/{recipe_id}/servings"))
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(ron_api::to_string(Some(42)).unwrap().into()) .bytes(web_api::to_string(Some(42)).unwrap().into())
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
@ -284,7 +290,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box<dyn Error>> {
.patch(&format!("/ron-api/recipe/{recipe_id}/estimated_time")) .patch(&format!("/ron-api/recipe/{recipe_id}/estimated_time"))
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(ron_api::to_string(Some(420)).unwrap().into()) .bytes(web_api::to_string(Some(420)).unwrap().into())
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
@ -293,7 +299,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box<dyn Error>> {
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes( .bytes(
ron_api::to_string(ron_api::Difficulty::Hard) web_api::to_string(web_api::Difficulty::Hard)
.unwrap() .unwrap()
.into(), .into(),
) )
@ -304,7 +310,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box<dyn Error>> {
.patch(&format!("/ron-api/recipe/{recipe_id}/language")) .patch(&format!("/ron-api/recipe/{recipe_id}/language"))
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(ron_api::to_string("fr").unwrap().into()) .bytes(web_api::to_string("fr").unwrap().into())
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
@ -312,7 +318,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box<dyn Error>> {
.patch(&format!("/ron-api/recipe/{recipe_id}/is_public")) .patch(&format!("/ron-api/recipe/{recipe_id}/is_public"))
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes(ron_api::to_string(true).unwrap().into()) .bytes(web_api::to_string(true).unwrap().into())
.await; .await;
response.assert_status_ok(); response.assert_status_ok();
@ -399,7 +405,7 @@ async fn recipe_tags() -> Result<(), Box<dyn Error>> {
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes( .bytes(
ron_api::to_string(vec!["ABC".to_string(), "xyz".to_string()]) web_api::to_string(vec!["ABC".to_string(), "xyz".to_string()])
.unwrap() .unwrap()
.into(), .into(),
) )
@ -424,7 +430,7 @@ async fn recipe_tags() -> Result<(), Box<dyn Error>> {
.add_cookie(cookie.clone()) .add_cookie(cookie.clone())
.add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)
.bytes( .bytes(
ron_api::to_string(vec!["XYZ".to_string(), "qwe".to_string()]) web_api::to_string(vec!["XYZ".to_string(), "qwe".to_string()])
.unwrap() .unwrap()
.into(), .into(),
) )

View file

@ -21,6 +21,8 @@
(DatabaseError, "Database error"), (DatabaseError, "Database error"),
(TemplateError, "Template error"), (TemplateError, "Template error"),
(SearchPlaceholder, "Search by title"),
(SignInMenu, "Sign in"), (SignInMenu, "Sign in"),
(SignInTitle, "Sign in"), (SignInTitle, "Sign in"),
(SignInButton, "Sign in"), (SignInButton, "Sign in"),

View file

@ -21,6 +21,8 @@
(DatabaseError, "Erreur de la base de données (Database error)"), (DatabaseError, "Erreur de la base de données (Database error)"),
(TemplateError, "Erreur du moteur de modèles (Template error)"), (TemplateError, "Erreur du moteur de modèles (Template error)"),
(SearchPlaceholder, "Recherche par titre"),
(SignInMenu, "Se connecter"), (SignInMenu, "Se connecter"),
(SignInTitle, "Se connecter"), (SignInTitle, "Se connecter"),
(SignInButton, "Se connecter"), (SignInButton, "Se connecter"),

View file

@ -1,5 +1,5 @@
pub mod consts; pub mod consts;
pub mod ron_api;
pub mod toast; pub mod toast;
pub mod translation;
pub mod utils; pub mod utils;
pub mod translation; pub mod web_api;

View file

@ -22,6 +22,9 @@ pub enum Sentence {
DatabaseError, DatabaseError,
TemplateError, TemplateError,
// Search
SearchPlaceholder,
// Sign in page. // Sign in page.
SignInMenu, SignInMenu,
SignInTitle, SignInTitle,

View file

@ -20,6 +20,28 @@ pub struct DateRange {
/*** Recipe ***/ /*** Recipe ***/
#[derive(Serialize, Deserialize, Clone)]
pub struct RecipesListFragmentsParams {
pub current_recipe_id: Option<i64>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct SearchByTitleParams {
pub search_term: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct GetTitlesParams {
pub ids: Vec<i64>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct RecipeSearchResult {
pub recipe_id: i64,
pub title: String,
pub title_highlighted: String,
}
#[repr(u32)] #[repr(u32)]
#[derive(Serialize, Deserialize, FromRepr, Clone, Copy, PartialEq, Debug)] #[derive(Serialize, Deserialize, FromRepr, Clone, Copy, PartialEq, Debug)]
pub enum Difficulty { pub enum Difficulty {

View file

@ -1,7 +1,7 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use chrono::{Datelike, Days, Months, NaiveDate, Weekday, offset::Local}; use chrono::{Datelike, Days, Months, NaiveDate, Weekday, offset::Local};
use common::{ron_api, utils::substitute_with_names}; use common::{web_api, utils::substitute_with_names};
use gloo::{ use gloo::{
events::EventListener, events::EventListener,
utils::{document, window}, utils::{document, window},
@ -13,7 +13,7 @@ use web_sys::{Element, HtmlInputElement};
use crate::{ use crate::{
modal_dialog, modal_dialog,
recipe_scheduler::RecipeScheduler, recipe_scheduler::RecipeScheduler,
request, ron_request,
utils::{SelectorExt, by_id, get_locale, selector, selector_all}, utils::{SelectorExt, by_id, get_locale, selector, selector_all},
}; };
@ -167,12 +167,14 @@ pub fn setup(
) )
.await .await
{ {
let body = ron_api::RemoveScheduledRecipe { let body = web_api::RemoveScheduledRecipe {
recipe_id, recipe_id,
date, date,
remove_ingredients_from_shopping_list, remove_ingredients_from_shopping_list,
}; };
let _ = request::delete::<(), _>("calendar/scheduled_recipe", Some(body)).await; let _ =
ron_request::delete::<(), _>("/ron-api/calendar/scheduled_recipe", Some(body))
.await;
window().location().reload().unwrap(); window().location().reload().unwrap();
} }
}); });

View file

@ -13,6 +13,7 @@ mod on_click;
mod pages; mod pages;
mod recipe_scheduler; mod recipe_scheduler;
mod request; mod request;
mod ron_request;
mod shopping_list; mod shopping_list;
mod toast; mod toast;
mod utils; mod utils;
@ -86,7 +87,7 @@ pub fn main() -> Result<(), JsValue> {
// Request the message to display. // Request the message to display.
spawn_local(async move { spawn_local(async move {
let translation: String = request::get(&format!("translation/{mess_id}")) let translation: String = ron_request::get(&format!("/ron-api/translation/{mess_id}"))
.await .await
.unwrap(); .unwrap();
if let Some(level_id) = level_id { if let Some(level_id) = level_id {
@ -105,10 +106,9 @@ pub fn main() -> Result<(), JsValue> {
let select_language: HtmlSelectElement = by_id("select-website-language"); let select_language: HtmlSelectElement = by_id("select-website-language");
EventListener::new(&select_language.clone(), "input", move |_event| { EventListener::new(&select_language.clone(), "input", move |_event| {
let lang = select_language.value(); let lang = select_language.value();
// let body = ron_api::SetLang { lang: lang.clone() };
let location_without_lang = location_without_lang.clone(); let location_without_lang = location_without_lang.clone();
spawn_local(async move { spawn_local(async move {
let _ = request::put::<(), _>("lang", &lang).await; let _ = ron_request::put::<(), _>("/ron-api/lang", &lang).await;
window() window()
.location() .location()

View file

@ -1,6 +1,6 @@
use std::{cell::RefCell, rc, sync::Mutex}; use std::{cell::RefCell, rc, sync::Mutex};
use common::{ron_api, utils::substitute}; use common::{web_api, utils::substitute};
use gloo::{ use gloo::{
events::{EventListener, EventListenerOptions}, events::{EventListener, EventListenerOptions},
net::http::Request, net::http::Request,
@ -14,7 +14,7 @@ use web_sys::{
}; };
use crate::{ use crate::{
modal_dialog, request, modal_dialog, request, ron_request,
toast::{self, Level}, toast::{self, Level},
utils::{SelectorExt, by_id, get_current_lang, selector, selector_and_clone}, utils::{SelectorExt, by_id, get_current_lang, selector, selector_and_clone},
}; };
@ -36,8 +36,11 @@ pub fn setup_page(recipe_id: i64) {
current_title = title.value(); current_title = title.value();
let title = title.value(); let title = title.value();
spawn_local(async move { spawn_local(async move {
let _ = let _ = ron_request::patch::<(), _>(
request::patch::<(), _>(&format!("recipe/{recipe_id}/title"), title).await; &format!("/ron-api/recipe/{recipe_id}/title"),
title,
)
.await;
reload_recipes_list(recipe_id).await; reload_recipes_list(recipe_id).await;
}); });
} }
@ -55,8 +58,8 @@ pub fn setup_page(recipe_id: i64) {
current_description = description.value(); current_description = description.value();
let description = description.value(); let description = description.value();
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>( let _ = ron_request::patch::<(), _>(
&format!("recipe/{recipe_id}/description"), &format!("/ron-api/recipe/{recipe_id}/description"),
description, description,
) )
.await; .await;
@ -85,9 +88,11 @@ pub fn setup_page(recipe_id: i64) {
}; };
current_servings = n; current_servings = n;
spawn_local(async move { spawn_local(async move {
let _ = let _ = ron_request::patch::<(), _>(
request::patch::<(), _>(&format!("recipe/{recipe_id}/servings"), servings) &format!("/ron-api/recipe/{recipe_id}/servings"),
.await; servings,
)
.await;
}); });
} }
}) })
@ -114,8 +119,8 @@ pub fn setup_page(recipe_id: i64) {
}; };
current_time = n; current_time = n;
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>( let _ = ron_request::patch::<(), _>(
&format!("recipe/{recipe_id}/estimated_time"), &format!("/ron-api/recipe/{recipe_id}/estimated_time"),
time, time,
) )
.await; .await;
@ -134,11 +139,11 @@ pub fn setup_page(recipe_id: i64) {
if difficulty.value() != current_difficulty { if difficulty.value() != current_difficulty {
current_difficulty = difficulty.value(); current_difficulty = difficulty.value();
let difficulty = let difficulty =
ron_api::Difficulty::from_repr(difficulty.value().parse::<u32>().unwrap()) web_api::Difficulty::from_repr(difficulty.value().parse::<u32>().unwrap())
.unwrap_or(ron_api::Difficulty::Unknown); .unwrap_or(web_api::Difficulty::Unknown);
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>( let _ = ron_request::patch::<(), _>(
&format!("recipe/{recipe_id}/difficulty"), &format!("/ron-api/recipe/{recipe_id}/difficulty"),
difficulty, difficulty,
) )
.await; .await;
@ -151,7 +156,7 @@ pub fn setup_page(recipe_id: i64) {
// Tags. // Tags.
{ {
spawn_local(async move { spawn_local(async move {
let tags: Vec<String> = request::get(&format!("recipe/{recipe_id}/tags")) let tags: Vec<String> = ron_request::get(&format!("/ron-api/recipe/{recipe_id}/tags"))
.await .await
.unwrap(); .unwrap();
create_tag_elements(recipe_id, &tags); create_tag_elements(recipe_id, &tags);
@ -161,9 +166,11 @@ pub fn setup_page(recipe_id: i64) {
spawn_local(async move { spawn_local(async move {
let tag_list: Vec<String> = let tag_list: Vec<String> =
tags.split_whitespace().map(str::to_lowercase).collect(); tags.split_whitespace().map(str::to_lowercase).collect();
let _ = let _ = ron_request::post::<(), _>(
request::post::<(), _>(&format!("recipe/{recipe_id}/tags"), Some(&tag_list)) &format!("/ron-api/recipe/{recipe_id}/tags"),
.await; Some(&tag_list),
)
.await;
create_tag_elements(recipe_id, &tag_list); create_tag_elements(recipe_id, &tag_list);
by_id::<HtmlInputElement>("input-tags").set_value(""); by_id::<HtmlInputElement>("input-tags").set_value("");
@ -207,9 +214,11 @@ pub fn setup_page(recipe_id: i64) {
current_language = language.value(); current_language = language.value();
let language = language.value(); let language = language.value();
spawn_local(async move { spawn_local(async move {
let _ = let _ = ron_request::patch::<(), _>(
request::patch::<(), _>(&format!("recipe/{recipe_id}/language"), language) &format!("/ron-api/recipe/{recipe_id}/language"),
.await; language,
)
.await;
}); });
} }
}) })
@ -222,9 +231,11 @@ pub fn setup_page(recipe_id: i64) {
EventListener::new(&is_public.clone(), "input", move |_event| { EventListener::new(&is_public.clone(), "input", move |_event| {
let is_public = is_public.checked(); let is_public = is_public.checked();
spawn_local(async move { spawn_local(async move {
let _ = let _ = ron_request::patch::<(), _>(
request::patch::<(), _>(&format!("recipe/{recipe_id}/is_public"), is_public) &format!("/ron-api/recipe/{recipe_id}/is_public"),
.await; is_public,
)
.await;
reload_recipes_list(recipe_id).await; reload_recipes_list(recipe_id).await;
}); });
}) })
@ -249,7 +260,9 @@ pub fn setup_page(recipe_id: i64) {
.await .await
.is_some() .is_some()
{ {
if let Ok(()) = request::delete::<_, ()>(&format!("recipe/{recipe_id}"), None).await if let Ok(()) =
ron_request::delete::<_, ()>(&format!("/ron-api/recipe/{recipe_id}"), None)
.await
{ {
window() window()
.location() .location()
@ -271,8 +284,8 @@ pub fn setup_page(recipe_id: i64) {
// Load initial groups, steps and ingredients. // Load initial groups, steps and ingredients.
{ {
spawn_local(async move { spawn_local(async move {
let groups: Vec<common::ron_api::Group> = let groups: Vec<common::web_api::Group> =
request::get(&format!("recipe/{recipe_id}/groups")) ron_request::get(&format!("/ron-api/recipe/{recipe_id}/groups"))
.await .await
.unwrap(); .unwrap();
@ -299,10 +312,11 @@ pub fn setup_page(recipe_id: i64) {
let button_add_group: HtmlInputElement = by_id("input-add-group"); let button_add_group: HtmlInputElement = by_id("input-add-group");
EventListener::new(&button_add_group, "click", move |_event| { EventListener::new(&button_add_group, "click", move |_event| {
spawn_local(async move { spawn_local(async move {
let id: i64 = request::post::<_, ()>(&format!("recipe/{recipe_id}/group"), None) let id: i64 =
.await ron_request::post::<_, ()>(&format!("/ron-api/recipe/{recipe_id}/group"), None)
.unwrap(); .await
create_group_element(&ron_api::Group { .unwrap();
create_group_element(&web_api::Group {
id, id,
name: "".to_string(), name: "".to_string(),
comment: "".to_string(), comment: "".to_string(),
@ -314,7 +328,7 @@ pub fn setup_page(recipe_id: i64) {
} }
} }
fn create_group_element(group: &ron_api::Group) -> Element { fn create_group_element(group: &web_api::Group) -> Element {
let group_id = group.id; let group_id = group.id;
let group_element: Element = selector_and_clone("#hidden-templates .group"); let group_element: Element = selector_and_clone("#hidden-templates .group");
group_element.set_id(&format!("group-{}", group.id)); group_element.set_id(&format!("group-{}", group.id));
@ -330,7 +344,7 @@ fn create_group_element(group: &ron_api::Group) -> Element {
.map(|e| e.id()[6..].parse::<i64>().unwrap()) .map(|e| e.id()[6..].parse::<i64>().unwrap())
.collect(); .collect();
let _ = request::patch::<(), _>("groups/order", ids).await; let _ = ron_request::patch::<(), _>("/ron-api/groups/order", ids).await;
}); });
}); });
@ -343,7 +357,9 @@ fn create_group_element(group: &ron_api::Group) -> Element {
current_name = name.value(); current_name = name.value();
let name = name.value(); let name = name.value();
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>(&format!("group/{group_id}/name"), name).await; let _ =
ron_request::patch::<(), _>(&format!("/ron-api/group/{group_id}/name"), name)
.await;
}) })
} }
}) })
@ -358,8 +374,11 @@ fn create_group_element(group: &ron_api::Group) -> Element {
current_comment = comment.value(); current_comment = comment.value();
let comment = comment.value(); let comment = comment.value();
spawn_local(async move { spawn_local(async move {
let _ = let _ = ron_request::patch::<(), _>(
request::patch::<(), _>(&format!("group/{group_id}/comment"), comment).await; &format!("/ron-api/group/{group_id}/comment"),
comment,
)
.await;
}); });
} }
}) })
@ -384,7 +403,8 @@ fn create_group_element(group: &ron_api::Group) -> Element {
.await .await
.is_some() .is_some()
{ {
let _ = request::delete::<(), ()>(&format!("group/{group_id}"), None).await; let _ = ron_request::delete::<(), ()>(&format!("/ron-api/group/{group_id}"), None)
.await;
let group_element = by_id::<Element>(&format!("group-{group_id}")); let group_element = by_id::<Element>(&format!("group-{group_id}"));
group_element.next_element_sibling().unwrap().remove(); group_element.next_element_sibling().unwrap().remove();
group_element.remove(); group_element.remove();
@ -397,12 +417,13 @@ fn create_group_element(group: &ron_api::Group) -> Element {
let add_step_button: HtmlInputElement = group_element.selector(".input-add-step"); let add_step_button: HtmlInputElement = group_element.selector(".input-add-step");
EventListener::new(&add_step_button, "click", move |_event| { EventListener::new(&add_step_button, "click", move |_event| {
spawn_local(async move { spawn_local(async move {
let id: i64 = request::post::<_, ()>(&format!("group/{group_id}/step"), None) let id: i64 =
.await ron_request::post::<_, ()>(&format!("/ron-api/group/{group_id}/step"), None)
.unwrap(); .await
.unwrap();
create_step_element( create_step_element(
&selector::<Element>(&format!("#group-{group_id} .steps")), &selector::<Element>(&format!("#group-{group_id} .steps")),
&ron_api::Step { &web_api::Step {
id, id,
action: "".to_string(), action: "".to_string(),
ingredients: vec![], ingredients: vec![],
@ -457,9 +478,11 @@ where
let tag_span = tag_span.clone(); let tag_span = tag_span.clone();
let tag = tag.clone(); let tag = tag.clone();
spawn_local(async move { spawn_local(async move {
let _ = let _ = ron_request::delete::<(), _>(
request::delete::<(), _>(&format!("recipe/{recipe_id}/tags"), Some(vec![tag])) &format!("/ron-api/recipe/{recipe_id}/tags"),
.await; Some(vec![tag]),
)
.await;
tag_span.remove(); tag_span.remove();
}); });
}) })
@ -467,7 +490,7 @@ where
} }
} }
fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element { fn create_step_element(group_element: &Element, step: &web_api::Step) -> Element {
let step_id = step.id; let step_id = step.id;
let step_element: Element = selector_and_clone("#hidden-templates .step"); let step_element: Element = selector_and_clone("#hidden-templates .step");
step_element.set_id(&format!("step-{}", step.id)); step_element.set_id(&format!("step-{}", step.id));
@ -484,7 +507,7 @@ fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element
.map(|e| e.id()[5..].parse::<i64>().unwrap()) .map(|e| e.id()[5..].parse::<i64>().unwrap())
.collect(); .collect();
let _ = request::patch::<(), _>("/steps/order", ids).await; let _ = ron_request::patch::<(), _>("/ron-api/steps/order", ids).await;
}); });
}); });
@ -497,7 +520,9 @@ fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element
current_action = action.value(); current_action = action.value();
let action = action.value(); let action = action.value();
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>(&format!("/step/{step_id}/action"), action).await; let _ =
ron_request::patch::<(), _>(&format!("/ron-api/step/{step_id}/action"), action)
.await;
}); });
} }
}) })
@ -522,7 +547,8 @@ fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element
.await .await
.is_some() .is_some()
{ {
let _ = request::delete::<(), ()>(&format!("step/{step_id}"), None).await; let _ =
ron_request::delete::<(), ()>(&format!("/ron-api/step/{step_id}"), None).await;
let step_element = by_id::<Element>(&format!("step-{step_id}")); let step_element = by_id::<Element>(&format!("step-{step_id}"));
step_element.next_element_sibling().unwrap().remove(); step_element.next_element_sibling().unwrap().remove();
step_element.remove(); step_element.remove();
@ -535,12 +561,13 @@ fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element
let add_ingredient_button: HtmlInputElement = step_element.selector(".input-add-ingredient"); let add_ingredient_button: HtmlInputElement = step_element.selector(".input-add-ingredient");
EventListener::new(&add_ingredient_button, "click", move |_event| { EventListener::new(&add_ingredient_button, "click", move |_event| {
spawn_local(async move { spawn_local(async move {
let id: i64 = request::post::<_, ()>(&format!("step/{step_id}/ingredient"), None) let id: i64 =
.await ron_request::post::<_, ()>(&format!("/ron-api/step/{step_id}/ingredient"), None)
.unwrap(); .await
.unwrap();
create_ingredient_element( create_ingredient_element(
&selector::<Element>(&format!("#step-{} .ingredients", step_id)), &selector::<Element>(&format!("#step-{} .ingredients", step_id)),
&ron_api::Ingredient { &web_api::Ingredient {
id, id,
name: "".to_string(), name: "".to_string(),
comment: "".to_string(), comment: "".to_string(),
@ -555,7 +582,7 @@ fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element
step_element step_element
} }
fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingredient) -> Element { fn create_ingredient_element(step_element: &Element, ingredient: &web_api::Ingredient) -> Element {
let ingredient_id = ingredient.id; let ingredient_id = ingredient.id;
let ingredient_element: Element = selector_and_clone("#hidden-templates .ingredient"); let ingredient_element: Element = selector_and_clone("#hidden-templates .ingredient");
ingredient_element.set_id(&format!("ingredient-{}", ingredient.id)); ingredient_element.set_id(&format!("ingredient-{}", ingredient.id));
@ -572,7 +599,7 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
.map(|e| e.id()[11..].parse::<i64>().unwrap()) .map(|e| e.id()[11..].parse::<i64>().unwrap())
.collect(); .collect();
let _ = request::patch::<(), _>("ingredients/order", ids).await; let _ = ron_request::patch::<(), _>("/ron-api/ingredients/order", ids).await;
}); });
}); });
@ -585,8 +612,11 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
current_name = name.value(); current_name = name.value();
let name = name.value(); let name = name.value();
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>(&format!("ingredient/{ingredient_id}/name"), name) let _ = ron_request::patch::<(), _>(
.await; &format!("/ron-api/ingredient/{ingredient_id}/name"),
name,
)
.await;
}); });
} }
}) })
@ -601,8 +631,8 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
current_comment = comment.value(); current_comment = comment.value();
let comment = comment.value(); let comment = comment.value();
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>( let _ = ron_request::patch::<(), _>(
&format!("ingredient/{ingredient_id}/comment"), &format!("/ron-api/ingredient/{ingredient_id}/comment"),
comment, comment,
) )
.await; .await;
@ -628,8 +658,11 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
let q = if n.is_nan() { None } else { Some(n) }; let q = if n.is_nan() { None } else { Some(n) };
current_quantity = n; current_quantity = n;
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>(&format!("ingredient/{ingredient_id}/quantity"), q) let _ = ron_request::patch::<(), _>(
.await; &format!("/ron-api/ingredient/{ingredient_id}/quantity"),
q,
)
.await;
}); });
} }
}) })
@ -644,8 +677,11 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
current_unit = unit.value(); current_unit = unit.value();
let unit = unit.value(); let unit = unit.value();
spawn_local(async move { spawn_local(async move {
let _ = request::patch::<(), _>(&format!("ingredient/{ingredient_id}/unit"), unit) let _ = ron_request::patch::<(), _>(
.await; &format!("/ron-api/ingredient/{ingredient_id}/unit"),
unit,
)
.await;
}); });
} }
}) })
@ -670,8 +706,11 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
.await .await
.is_some() .is_some()
{ {
let _ = let _ = ron_request::delete::<(), ()>(
request::delete::<(), ()>(&format!("ingredient/{ingredient_id}"), None).await; &format!("/ron-api/ingredient/{ingredient_id}"),
None,
)
.await;
let ingredient_element = by_id::<Element>(&format!("ingredient-{ingredient_id}")); let ingredient_element = by_id::<Element>(&format!("ingredient-{ingredient_id}"));
ingredient_element.next_element_sibling().unwrap().remove(); ingredient_element.next_element_sibling().unwrap().remove();
ingredient_element.remove(); ingredient_element.remove();
@ -684,19 +723,17 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
} }
async fn reload_recipes_list(current_recipe_id: i64) { async fn reload_recipes_list(current_recipe_id: i64) {
match Request::get("/fragments/recipes_list") let fragment: String = request::get_with_params(
.query([("current_recipe_id", current_recipe_id.to_string())]) "/fragments/recipes_list",
.send() web_api::RecipesListFragmentsParams {
.await current_recipe_id: Some(current_recipe_id),
{ },
Err(error) => { )
toast::show_message_level(Level::Error, &format!("Internal server error: {}", error)); .await
} .unwrap();
Ok(response) => {
let list = document().get_element_by_id("recipes-list").unwrap(); let list = document().get_element_by_id("recipes-list").unwrap();
list.set_outer_html(&response.text().await.unwrap()); list.set_outer_html(&fragment);
}
}
} }
enum CursorPosition { enum CursorPosition {

View file

@ -1,16 +1,16 @@
use chrono::{Datelike, Days, Months, NaiveDate}; use chrono::{Datelike, Days, Months, NaiveDate};
use common::ron_api; use common::web_api;
use gloo::storage::{LocalStorage, Storage}; use gloo::storage::{LocalStorage, Storage};
use ron::ser::{PrettyConfig, to_string_pretty}; use ron::ser::{PrettyConfig, to_string_pretty};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::{calendar, request}; use crate::{calendar, ron_request};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("Request error: {0}")] #[error("Request error: {0}")]
Request(#[from] request::Error), Request(#[from] ron_request::Error),
} }
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
@ -25,11 +25,11 @@ pub enum ScheduleRecipeResult {
RecipeAlreadyScheduledAtThisDate, RecipeAlreadyScheduledAtThisDate,
} }
impl From<ron_api::ScheduleRecipeResult> for ScheduleRecipeResult { impl From<web_api::ScheduleRecipeResult> for ScheduleRecipeResult {
fn from(api_res: ron_api::ScheduleRecipeResult) -> Self { fn from(api_res: web_api::ScheduleRecipeResult) -> Self {
match api_res { match api_res {
ron_api::ScheduleRecipeResult::Ok => Self::Ok, web_api::ScheduleRecipeResult::Ok => Self::Ok,
ron_api::ScheduleRecipeResult::RecipeAlreadyScheduledAtThisDate => { web_api::ScheduleRecipeResult::RecipeAlreadyScheduledAtThisDate => {
Self::RecipeAlreadyScheduledAtThisDate Self::RecipeAlreadyScheduledAtThisDate
} }
} }
@ -80,12 +80,14 @@ impl RecipeScheduler {
return Ok(vec![]); return Ok(vec![]);
} }
let titles: Vec<String> = request::get_with_params( let titles: Vec<String> = ron_request::get_with_params(
"recipe/titles", "/ron-api/recipe/titles",
recipe_ids_and_dates web_api::GetTitlesParams {
.iter() ids: recipe_ids_and_dates
.map(|r| r.recipe_id) .iter()
.collect::<Vec<_>>(), .map(|r| r.recipe_id)
.collect::<Vec<_>>(),
},
) )
.await?; .await?;
@ -95,9 +97,9 @@ impl RecipeScheduler {
.map(|(id_and_date, title)| (id_and_date.date, title, id_and_date.recipe_id)) .map(|(id_and_date, title)| (id_and_date.date, title, id_and_date.recipe_id))
.collect::<Vec<_>>()) .collect::<Vec<_>>())
} else { } else {
let scheduled_recipes: ron_api::ScheduledRecipes = request::get_with_params( let scheduled_recipes: web_api::ScheduledRecipes = ron_request::get_with_params(
"calendar/scheduled_recipes", "/ron-api/calendar/scheduled_recipes",
ron_api::DateRange { web_api::DateRange {
start_date, start_date,
end_date, end_date,
}, },
@ -127,9 +129,9 @@ impl RecipeScheduler {
save_scheduled_recipes(recipe_ids_and_dates, date.year(), date.month0()); save_scheduled_recipes(recipe_ids_and_dates, date.year(), date.month0());
Ok(ScheduleRecipeResult::Ok) Ok(ScheduleRecipeResult::Ok)
} else { } else {
request::post::<ron_api::ScheduleRecipeResult, _>( ron_request::post::<web_api::ScheduleRecipeResult, _>(
"calendar/scheduled_recipe", "/ron-api/calendar/scheduled_recipe",
Some(ron_api::ScheduleRecipe { Some(web_api::ScheduleRecipe {
recipe_id, recipe_id,
date, date,
servings, servings,
@ -138,7 +140,7 @@ impl RecipeScheduler {
) )
.await .await
.map_err(Error::from) .map_err(Error::from)
.map(From::<ron_api::ScheduleRecipeResult>::from) .map(From::<web_api::ScheduleRecipeResult>::from)
} }
} }

View file

@ -1,10 +1,7 @@
/// This module provides a simple API for making HTTP requests to the server. use std::any::TypeId;
/// For requests with a body (POST, PUT, PATCH, etc.), it uses the RON format.
/// The RON data structures should come from the `ron_api` module. use gloo::net::http::Request;
/// For requests with parameters (GET), it uses the HTML form format. use serde::Serialize;
use common::ron_api;
use gloo::net::http::{Request, RequestBuilder};
use serde::{Serialize, de::DeserializeOwned};
use thiserror::Error; use thiserror::Error;
use crate::toast::{self, Level}; use crate::toast::{self, Level};
@ -14,12 +11,6 @@ pub enum Error {
#[error("Gloo error: {0}")] #[error("Gloo error: {0}")]
Gloo(#[from] gloo::net::Error), Gloo(#[from] gloo::net::Error),
#[error("RON Spanned error: {0}")]
RonSpanned(#[from] ron::error::SpannedError),
#[error("RON Error: {0}")]
Ron(#[from] ron::error::Error),
#[error("HTTP error: {0}")] #[error("HTTP error: {0}")]
Http(String), Http(String),
@ -29,53 +20,26 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
const CONTENT_TYPE: &str = "Content-Type"; #[allow(dead_code)] // Not used for the moment.
pub async fn get(url: &str) -> Result<String> {
async fn req_with_body<T, U>( get_with_params(url, ()).await
api_name: &str,
body: U,
method_fn: fn(&str) -> RequestBuilder,
) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
let url = format!("/ron-api/{}", api_name);
let request_builder = method_fn(&url).header(
CONTENT_TYPE,
common::consts::MIME_TYPE_RON.to_str().unwrap(),
);
send_req(request_builder.body(ron_api::to_string(body)?)?).await
} }
async fn req_with_params<T, U>( pub async fn get_with_params<U>(url: &str, params: U) -> Result<String>
api_name: &str,
params: U,
method_fn: fn(&str) -> RequestBuilder,
) -> Result<T>
where where
T: DeserializeOwned, U: Serialize + 'static,
U: Serialize,
{ {
let mut url = format!("/ron-api/{}?", api_name); let request_builder = if TypeId::of::<U>() == TypeId::of::<()>() {
serde_html_form::ser::push_to_string(&mut url, params).unwrap(); Request::get(url)
let request_builder = method_fn(&url); } else {
send_req(request_builder.build()?).await let mut url = url.to_string();
} url.push('?');
serde_html_form::ser::push_to_string(&mut url, params).unwrap();
Request::get(&url)
};
async fn req<T>(api_name: &str, method_fn: fn(&str) -> RequestBuilder) -> Result<T> let request = request_builder.build()?;
where
T: DeserializeOwned,
{
let url = format!("/ron-api/{}", api_name);
let request_builder = method_fn(&url);
send_req(request_builder.build()?).await
}
async fn send_req<T>(request: Request) -> Result<T>
where
T: DeserializeOwned,
{
match request.send().await { match request.send().await {
Err(error) => { Err(error) => {
toast::show_message_level(Level::Error, &format!("Internal server error: {}", error)); toast::show_message_level(Level::Error, &format!("Internal server error: {}", error));
@ -89,73 +53,8 @@ where
); );
Err(Error::Http(response.status_text())) Err(Error::Http(response.status_text()))
} else { } else {
let mut r = response.binary().await?; Ok(response.text().await?)
// An empty response is considered to be an unit value.
if r.is_empty() {
r = b"()".to_vec();
}
Ok(ron::de::from_bytes::<T>(&r)?)
} }
} }
} }
} }
/// Sends a request to the server with the given API name and body.
/// # Example
/// ```rust
/// use common::ron_api;
/// let body = ron_api::SetLang { lang : "en".to_string() };
/// request::put::<(), _>("lang", body).await;
/// ```
pub async fn put<T, U>(api_name: &str, body: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
req_with_body(api_name, body, Request::put).await
}
pub async fn patch<T, U>(api_name: &str, body: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
req_with_body(api_name, body, Request::patch).await
}
pub async fn post<T, U>(api_name: &str, body: Option<U>) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
match body {
Some(body) => req_with_body(api_name, body, Request::post).await,
None => req(api_name, Request::post).await,
}
}
pub async fn delete<T, U>(api_name: &str, body: Option<U>) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
match body {
Some(body) => req_with_body(api_name, body, Request::delete).await,
None => req(api_name, Request::delete).await,
}
}
pub async fn get<T>(api_name: &str) -> Result<T>
where
T: DeserializeOwned,
{
req(api_name, Request::get).await
}
pub async fn get_with_params<T, U>(api_name: &str, params: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
req_with_params(api_name, params, Request::get).await
}

156
frontend/src/ron_request.rs Normal file
View file

@ -0,0 +1,156 @@
/// This module provides a simple API for making HTTP requests to the server.
/// For requests with a body (POST, PUT, PATCH, etc.), it uses the RON format.
/// The RON data structures should come from the `web_api` module.
/// For requests with parameters (GET), it uses the HTML form format.
use common::web_api;
use gloo::net::http::{Request, RequestBuilder};
use serde::{Serialize, de::DeserializeOwned};
use thiserror::Error;
use crate::toast::{self, Level};
#[derive(Error, Debug)]
pub enum Error {
#[error("Gloo error: {0}")]
Gloo(#[from] gloo::net::Error),
#[error("RON Spanned error: {0}")]
RonSpanned(#[from] ron::error::SpannedError),
#[error("RON Error: {0}")]
Ron(#[from] ron::error::Error),
#[error("HTTP error: {0}")]
Http(String),
#[error("Unknown error: {0}")]
Other(String),
}
type Result<T> = std::result::Result<T, Error>;
const CONTENT_TYPE: &str = "Content-Type"; // TODO: take it from the http crate.
async fn req_with_body<T, U>(url: &str, body: U, method_fn: fn(&str) -> RequestBuilder) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
let request_builder = method_fn(url).header(
CONTENT_TYPE,
common::consts::MIME_TYPE_RON.to_str().unwrap(),
);
send_req(request_builder.body(web_api::to_string(body)?)?).await
}
async fn req_with_params<T, U>(
url: &str,
params: U,
method_fn: fn(&str) -> RequestBuilder,
) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
let mut url = url.to_string();
url.push('?');
serde_html_form::ser::push_to_string(&mut url, params).unwrap();
let request_builder = method_fn(&url);
send_req(request_builder.build()?).await
}
async fn req<T>(url: &str, method_fn: fn(&str) -> RequestBuilder) -> Result<T>
where
T: DeserializeOwned,
{
let request_builder = method_fn(url);
send_req(request_builder.build()?).await
}
async fn send_req<T>(request: Request) -> Result<T>
where
T: DeserializeOwned,
{
match request.send().await {
Err(error) => {
toast::show_message_level(Level::Error, &format!("Internal server error: {}", error));
Err(Error::Gloo(error))
}
Ok(response) => {
if !response.ok() {
toast::show_message_level(
Level::Error,
&format!("HTTP error: {}", response.status_text()),
);
Err(Error::Http(response.status_text()))
} else {
let mut r = response.binary().await?;
// An empty response is considered to be an unit value.
if r.is_empty() {
r = b"()".to_vec();
}
Ok(ron::de::from_bytes::<T>(&r)?)
}
}
}
}
/// Sends a request to the server with the given API name and body.
/// # Example
/// ```rust
/// use common::web_api;
/// let body = web_api::SetLang { lang : "en".to_string() };
/// request::put::<(), _>("lang", body).await;
/// ```
pub async fn put<T, U>(url: &str, body: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
req_with_body(url, body, Request::put).await
}
pub async fn patch<T, U>(url: &str, body: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
req_with_body(url, body, Request::patch).await
}
pub async fn post<T, U>(url: &str, body: Option<U>) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
match body {
Some(body) => req_with_body(url, body, Request::post).await,
None => req(url, Request::post).await,
}
}
pub async fn delete<T, U>(url: &str, body: Option<U>) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
match body {
Some(body) => req_with_body(url, body, Request::delete).await,
None => req(url, Request::delete).await,
}
}
pub async fn get<T>(url: &str) -> Result<T>
where
T: DeserializeOwned,
{
req(url, Request::get).await
}
pub async fn get_with_params<T, U>(url: &str, params: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
req_with_params(url, params, Request::get).await
}

View file

@ -1,16 +1,16 @@
use chrono::{Datelike, Days, Months, NaiveDate}; use chrono::{Datelike, Days, Months, NaiveDate};
use common::ron_api; use common::web_api;
use gloo::storage::{LocalStorage, Storage}; use gloo::storage::{LocalStorage, Storage};
use ron::ser::{PrettyConfig, to_string_pretty}; use ron::ser::{PrettyConfig, to_string_pretty};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::{calendar, request}; use crate::{calendar, ron_request};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("Request error: {0}")] #[error("Request error: {0}")]
Request(#[from] request::Error), Request(#[from] ron_request::Error),
} }
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
@ -25,11 +25,11 @@ impl ShoppingList {
Self { is_local } Self { is_local }
} }
pub async fn get_items(&self) -> Result<Vec<ron_api::ShoppingListItem>> { pub async fn get_items(&self) -> Result<Vec<web_api::ShoppingListItem>> {
if self.is_local { if self.is_local {
Ok(vec![]) // TODO Ok(vec![]) // TODO
} else { } else {
Ok(request::get("shopping_list").await?) Ok(ron_request::get("/ron-api/shopping_list").await?)
} }
} }
@ -37,9 +37,9 @@ impl ShoppingList {
if self.is_local { if self.is_local {
todo!(); todo!();
} else { } else {
request::patch( ron_request::patch(
"shopping_list/checked", "/ron-api/shopping_list/checked",
ron_api::KeyValue { web_api::KeyValue {
id: item_id, id: item_id,
value: is_checked, value: is_checked,
}, },