diff --git a/backend/src/app.rs b/backend/src/app.rs index 7b4b10c..f1257ce 100644 --- a/backend/src/app.rs +++ b/backend/src/app.rs @@ -132,6 +132,10 @@ pub fn make_service( // Disabled: update user profile is now made with a post data ('edit_user_post'). // .route("/user/update", put(services::ron::update_user)) .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/{id}/title", diff --git a/backend/src/data/db/recipe.rs b/backend/src/data/db/recipe.rs index 347154c..037d00f 100644 --- a/backend/src/data/db/recipe.rs +++ b/backend/src/data/db/recipe.rs @@ -1,7 +1,7 @@ use std::u32; use chrono::prelude::*; -use common::ron_api::Difficulty; +use common::web_api::Difficulty; use itertools::Itertools; use sqlx::{Error, Sqlite}; @@ -410,8 +410,8 @@ WHERE [Recipe].[user_id] = $1 sqlx::query_scalar( r#" SELECT [name], COUNT([name]) as [nb_used] FROM [Tag] - INNER JOIN [RecipeTag] ON [RecipeTag].[tag_id] = [Tag].[id] - INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTag].[recipe_id] +INNER JOIN [RecipeTag] ON [RecipeTag].[tag_id] = [Tag].[id] +INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTag].[recipe_id] WHERE [Recipe].[lang] = $1 GROUP BY [Tag].[name] ORDER BY [nb_used] DESC, [name] diff --git a/backend/src/data/model.rs b/backend/src/data/model.rs index 40fa406..cd30f95 100644 --- a/backend/src/data/model.rs +++ b/backend/src/data/model.rs @@ -1,5 +1,5 @@ use chrono::prelude::*; -use common::ron_api::Difficulty; +use common::web_api::Difficulty; use sqlx::{self, FromRow}; #[derive(Debug, Clone, FromRow)] diff --git a/backend/src/ron_utils.rs b/backend/src/ron_utils.rs index f9cca7b..33c1239 100644 --- a/backend/src/ron_utils.rs +++ b/backend/src/ron_utils.rs @@ -3,7 +3,7 @@ use axum::{ http::{StatusCode, header}, response::{ErrorResponse, IntoResponse, Response}, }; -use common::ron_api; +use common::web_api; use ron::de::from_bytes; use serde::{Serialize, de::DeserializeOwned}; @@ -16,7 +16,7 @@ pub struct RonError { impl axum::response::IntoResponse for RonError { fn into_response(self) -> Response { - match ron_api::to_string(&self) { + match web_api::to_string(&self) { Ok(ron_as_str) => ( StatusCode::BAD_REQUEST, [(header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)], @@ -62,7 +62,7 @@ pub fn ron_response(status: StatusCode, ron: T) -> Response where T: Serialize, { - match ron_api::to_string(&ron) { + match web_api::to_string(&ron) { Ok(ron_as_str) => ( status, [(header::CONTENT_TYPE, common::consts::MIME_TYPE_RON)], diff --git a/backend/src/services/fragments.rs b/backend/src/services/fragments.rs index 0d32ace..93d3918 100644 --- a/backend/src/services/fragments.rs +++ b/backend/src/services/fragments.rs @@ -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, -} - #[debug_handler] pub async fn recipes_list_fragments( State(connection): State, - current_recipe: Query, + params: Query, Extension(context): Extension, ) -> Result { 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, diff --git a/backend/src/services/mod.rs b/backend/src/services/mod.rs index 792a0ca..d514328 100644 --- a/backend/src/services/mod.rs +++ b/backend/src/services/mod.rs @@ -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, State(log): State, Extension(context): Extension, - log_file: Query, + Query(params): Query, ) -> Result { 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, } diff --git a/backend/src/services/ron/calendar.rs b/backend/src/services/ron/calendar.rs index 4072973..1f673c9 100644 --- a/backend/src/services/ron/calendar.rs +++ b/backend/src/services/ron/calendar.rs @@ -20,10 +20,10 @@ use super::rights::*; pub async fn get_scheduled_recipes( State(connection): State, Extension(context): Extension, - date_range: Query, + date_range: Query, ) -> Result { 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 for common::ron_api::ScheduleRecipeResult { +impl From 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 for common::ron_api::Sched pub async fn add_scheduled_recipe( State(connection): State, Extension(context): Extension, - ExtractRon(ron): ExtractRon, + ExtractRon(ron): ExtractRon, ) -> Result { 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, Extension(context): Extension, - ExtractRon(ron): ExtractRon, + ExtractRon(ron): ExtractRon, ) -> Result { check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?; if let Some(user) = context.user { diff --git a/backend/src/services/ron/mod.rs b/backend/src/services/ron/mod.rs index 4d5bafe..36e401b 100644 --- a/backend/src/services/ron/mod.rs +++ b/backend/src/services/ron/mod.rs @@ -15,6 +15,7 @@ use crate::{ }; pub mod calendar; +mod model_converter; pub mod recipe; mod rights; pub mod shopping_list; diff --git a/backend/src/services/ron/model_converter.rs b/backend/src/services/ron/model_converter.rs new file mode 100644 index 0000000..7e2d5f1 --- /dev/null +++ b/backend/src/services/ron/model_converter.rs @@ -0,0 +1,50 @@ +use common::web_api; + +use crate::data::model; + +impl From 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 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 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 for web_api::RecipeSearchResult { + fn from(result: model::RecipeSearchResult) -> Self { + Self { + recipe_id: result.id, + title: result.title, + title_highlighted: result.title_highlighted, + } + } +} diff --git a/backend/src/services/ron/recipe.rs b/backend/src/services/ron/recipe.rs index 09ba0f4..663378f 100644 --- a/backend/src/services/ron/recipe.rs +++ b/backend/src/services/ron/recipe.rs @@ -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, + Extension(context): Extension, + Query(params): Query, +) -> Result { + Ok(ron_response_ok( + connection + .search_recipes(context.tr.current_lang_code(), ¶ms.search_term) + .await? + .into_iter() + .map(web_api::RecipeSearchResult::from) + .collect::>(), + )) +} + /// 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, - recipe_ids: Query>, + Query(params): Query, ) -> Result { Ok(ron_response_ok( - connection.get_recipe_titles(&recipe_ids).await?, + connection.get_recipe_titles(¶ms.ids).await?, )) } @@ -88,16 +103,23 @@ pub async fn get_tags( )) } +#[derive(Deserialize)] +pub struct GetAllTagsParams { + nb_max_tags: Option, + lang: Option, +} + #[debug_handler] pub async fn get_all_tags( State(connection): State, Extension(context): Extension, - nb_max_tags: Query>, - lang: Query>, + Query(params): Query, ) -> Result { - 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, Extension(context): Extension, Path(recipe_id): Path, - ExtractRon(difficulty): ExtractRon, + ExtractRon(difficulty): ExtractRon, ) -> Result { check_user_rights_recipe(&connection, &context.user, recipe_id).await?; connection @@ -184,43 +206,6 @@ pub async fn rm( Ok(StatusCode::OK) } -impl From 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 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 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, @@ -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::>(), )) } diff --git a/backend/src/services/ron/shopping_list.rs b/backend/src/services/ron/shopping_list.rs index ca3b71a..2bf38df 100644 --- a/backend/src/services/ron/shopping_list.rs +++ b/backend/src/services/ron/shopping_list.rs @@ -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 for common::ron_api::ShoppingListItem { +impl From 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::>(), )) } else { @@ -58,7 +58,7 @@ pub async fn get( pub async fn set_entry_checked( State(connection): State, Extension(context): Extension, - ExtractRon(ron): ExtractRon>, + ExtractRon(ron): ExtractRon>, ) -> Result { check_user_rights_shopping_list_entry(&connection, &context.user, ron.id).await?; Ok(ron_response_ok( diff --git a/backend/templates/recipe_edit.html b/backend/templates/recipe_edit.html index 1365bb3..5c8ab90 100644 --- a/backend/templates/recipe_edit.html +++ b/backend/templates/recipe_edit.html @@ -42,10 +42,10 @@
diff --git a/backend/templates/recipe_view.html b/backend/templates/recipe_view.html index cbf3031..2b81f34 100644 --- a/backend/templates/recipe_view.html +++ b/backend/templates/recipe_view.html @@ -39,12 +39,12 @@ {% match recipe.difficulty %} - {% when common::ron_api::Difficulty::Unknown %} - {% when common::ron_api::Difficulty::Easy %} + {% when common::web_api::Difficulty::Unknown %} + {% when common::web_api::Difficulty::Easy %} {{ context.tr.t(Sentence::RecipeDifficultyEasy) }} - {% when common::ron_api::Difficulty::Medium %} + {% when common::web_api::Difficulty::Medium %} {{ context.tr.t(Sentence::RecipeDifficultyMedium) }} - {% when common::ron_api::Difficulty::Hard %} + {% when common::web_api::Difficulty::Hard %} {{ context.tr.t(Sentence::RecipeDifficultyHard) }} {% endmatch %} diff --git a/backend/tests/http.rs b/backend/tests/http.rs index 0af4e20..3bab834 100644 --- a/backend/tests/http.rs +++ b/backend/tests/http.rs @@ -2,7 +2,7 @@ use std::{error::Error, sync::Arc}; use axum::http; use axum_test::TestServer; -use common::ron_api; +use common::web_api; use cookie::Cookie; use scraper::{ElementRef, Html, Selector}; use serde::Serialize; @@ -221,7 +221,13 @@ async fn sign_in() -> Result<(), Box> { // Assert. response.assert_status_see_other(); // Redirection after successful sign in. 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(()) } @@ -260,7 +266,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box> { .patch(&format!("/ron-api/recipe/{recipe_id}/title")) .add_cookie(cookie.clone()) .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; response.assert_status_ok(); @@ -268,7 +274,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box> { .patch(&format!("/ron-api/recipe/{recipe_id}/description")) .add_cookie(cookie.clone()) .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; response.assert_status_ok(); @@ -276,7 +282,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box> { .patch(&format!("/ron-api/recipe/{recipe_id}/servings")) .add_cookie(cookie.clone()) .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; response.assert_status_ok(); @@ -284,7 +290,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box> { .patch(&format!("/ron-api/recipe/{recipe_id}/estimated_time")) .add_cookie(cookie.clone()) .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; response.assert_status_ok(); @@ -293,7 +299,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box> { .add_cookie(cookie.clone()) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .bytes( - ron_api::to_string(ron_api::Difficulty::Hard) + web_api::to_string(web_api::Difficulty::Hard) .unwrap() .into(), ) @@ -304,7 +310,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box> { .patch(&format!("/ron-api/recipe/{recipe_id}/language")) .add_cookie(cookie.clone()) .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; response.assert_status_ok(); @@ -312,7 +318,7 @@ async fn create_recipe_and_edit_it() -> Result<(), Box> { .patch(&format!("/ron-api/recipe/{recipe_id}/is_public")) .add_cookie(cookie.clone()) .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; response.assert_status_ok(); @@ -399,7 +405,7 @@ async fn recipe_tags() -> Result<(), Box> { .add_cookie(cookie.clone()) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .bytes( - ron_api::to_string(vec!["ABC".to_string(), "xyz".to_string()]) + web_api::to_string(vec!["ABC".to_string(), "xyz".to_string()]) .unwrap() .into(), ) @@ -424,7 +430,7 @@ async fn recipe_tags() -> Result<(), Box> { .add_cookie(cookie.clone()) .add_header(http::header::CONTENT_TYPE, common::consts::MIME_TYPE_RON) .bytes( - ron_api::to_string(vec!["XYZ".to_string(), "qwe".to_string()]) + web_api::to_string(vec!["XYZ".to_string(), "qwe".to_string()]) .unwrap() .into(), ) diff --git a/backend/translations/english.ron b/backend/translations/english.ron index 9bbc1ac..20d1669 100644 --- a/backend/translations/english.ron +++ b/backend/translations/english.ron @@ -21,6 +21,8 @@ (DatabaseError, "Database error"), (TemplateError, "Template error"), + (SearchPlaceholder, "Search by title"), + (SignInMenu, "Sign in"), (SignInTitle, "Sign in"), (SignInButton, "Sign in"), diff --git a/backend/translations/french.ron b/backend/translations/french.ron index 9ec817b..6994e3d 100644 --- a/backend/translations/french.ron +++ b/backend/translations/french.ron @@ -21,6 +21,8 @@ (DatabaseError, "Erreur de la base de données (Database error)"), (TemplateError, "Erreur du moteur de modèles (Template error)"), + (SearchPlaceholder, "Recherche par titre"), + (SignInMenu, "Se connecter"), (SignInTitle, "Se connecter"), (SignInButton, "Se connecter"), diff --git a/common/src/lib.rs b/common/src/lib.rs index 940d642..4a236f1 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,5 +1,5 @@ pub mod consts; -pub mod ron_api; pub mod toast; +pub mod translation; pub mod utils; -pub mod translation; \ No newline at end of file +pub mod web_api; diff --git a/common/src/translation.rs b/common/src/translation.rs index 787fde5..9bfa63e 100644 --- a/common/src/translation.rs +++ b/common/src/translation.rs @@ -22,6 +22,9 @@ pub enum Sentence { DatabaseError, TemplateError, + // Search + SearchPlaceholder, + // Sign in page. SignInMenu, SignInTitle, diff --git a/common/src/ron_api.rs b/common/src/web_api.rs similarity index 85% rename from common/src/ron_api.rs rename to common/src/web_api.rs index dd9c78e..9c2b622 100644 --- a/common/src/ron_api.rs +++ b/common/src/web_api.rs @@ -20,6 +20,28 @@ pub struct DateRange { /*** Recipe ***/ +#[derive(Serialize, Deserialize, Clone)] +pub struct RecipesListFragmentsParams { + pub current_recipe_id: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct SearchByTitleParams { + pub search_term: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct GetTitlesParams { + pub ids: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct RecipeSearchResult { + pub recipe_id: i64, + pub title: String, + pub title_highlighted: String, +} + #[repr(u32)] #[derive(Serialize, Deserialize, FromRepr, Clone, Copy, PartialEq, Debug)] pub enum Difficulty { diff --git a/frontend/src/calendar.rs b/frontend/src/calendar.rs index c0cb8bd..e4bdb7f 100644 --- a/frontend/src/calendar.rs +++ b/frontend/src/calendar.rs @@ -1,7 +1,7 @@ use std::{cell::RefCell, rc::Rc}; 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::{ events::EventListener, utils::{document, window}, @@ -13,7 +13,7 @@ use web_sys::{Element, HtmlInputElement}; use crate::{ modal_dialog, recipe_scheduler::RecipeScheduler, - request, + ron_request, utils::{SelectorExt, by_id, get_locale, selector, selector_all}, }; @@ -167,12 +167,14 @@ pub fn setup( ) .await { - let body = ron_api::RemoveScheduledRecipe { + let body = web_api::RemoveScheduledRecipe { recipe_id, date, 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(); } }); diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 1904ed3..b349554 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -13,6 +13,7 @@ mod on_click; mod pages; mod recipe_scheduler; mod request; +mod ron_request; mod shopping_list; mod toast; mod utils; @@ -86,7 +87,7 @@ pub fn main() -> Result<(), JsValue> { // Request the message to display. 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 .unwrap(); 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"); EventListener::new(&select_language.clone(), "input", move |_event| { let lang = select_language.value(); - // let body = ron_api::SetLang { lang: lang.clone() }; let location_without_lang = location_without_lang.clone(); spawn_local(async move { - let _ = request::put::<(), _>("lang", &lang).await; + let _ = ron_request::put::<(), _>("/ron-api/lang", &lang).await; window() .location() diff --git a/frontend/src/pages/recipe_edit.rs b/frontend/src/pages/recipe_edit.rs index 66ac41b..9535433 100644 --- a/frontend/src/pages/recipe_edit.rs +++ b/frontend/src/pages/recipe_edit.rs @@ -1,6 +1,6 @@ use std::{cell::RefCell, rc, sync::Mutex}; -use common::{ron_api, utils::substitute}; +use common::{web_api, utils::substitute}; use gloo::{ events::{EventListener, EventListenerOptions}, net::http::Request, @@ -14,7 +14,7 @@ use web_sys::{ }; use crate::{ - modal_dialog, request, + modal_dialog, request, ron_request, toast::{self, Level}, 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(); let title = title.value(); spawn_local(async move { - let _ = - request::patch::<(), _>(&format!("recipe/{recipe_id}/title"), title).await; + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/title"), + title, + ) + .await; reload_recipes_list(recipe_id).await; }); } @@ -55,8 +58,8 @@ pub fn setup_page(recipe_id: i64) { current_description = description.value(); let description = description.value(); spawn_local(async move { - let _ = request::patch::<(), _>( - &format!("recipe/{recipe_id}/description"), + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/description"), description, ) .await; @@ -85,9 +88,11 @@ pub fn setup_page(recipe_id: i64) { }; current_servings = n; spawn_local(async move { - let _ = - request::patch::<(), _>(&format!("recipe/{recipe_id}/servings"), servings) - .await; + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/servings"), + servings, + ) + .await; }); } }) @@ -114,8 +119,8 @@ pub fn setup_page(recipe_id: i64) { }; current_time = n; spawn_local(async move { - let _ = request::patch::<(), _>( - &format!("recipe/{recipe_id}/estimated_time"), + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/estimated_time"), time, ) .await; @@ -134,11 +139,11 @@ pub fn setup_page(recipe_id: i64) { if difficulty.value() != current_difficulty { current_difficulty = difficulty.value(); let difficulty = - ron_api::Difficulty::from_repr(difficulty.value().parse::().unwrap()) - .unwrap_or(ron_api::Difficulty::Unknown); + web_api::Difficulty::from_repr(difficulty.value().parse::().unwrap()) + .unwrap_or(web_api::Difficulty::Unknown); spawn_local(async move { - let _ = request::patch::<(), _>( - &format!("recipe/{recipe_id}/difficulty"), + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/difficulty"), difficulty, ) .await; @@ -151,7 +156,7 @@ pub fn setup_page(recipe_id: i64) { // Tags. { spawn_local(async move { - let tags: Vec = request::get(&format!("recipe/{recipe_id}/tags")) + let tags: Vec = ron_request::get(&format!("/ron-api/recipe/{recipe_id}/tags")) .await .unwrap(); create_tag_elements(recipe_id, &tags); @@ -161,9 +166,11 @@ pub fn setup_page(recipe_id: i64) { spawn_local(async move { let tag_list: Vec = tags.split_whitespace().map(str::to_lowercase).collect(); - let _ = - request::post::<(), _>(&format!("recipe/{recipe_id}/tags"), Some(&tag_list)) - .await; + let _ = ron_request::post::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/tags"), + Some(&tag_list), + ) + .await; create_tag_elements(recipe_id, &tag_list); by_id::("input-tags").set_value(""); @@ -207,9 +214,11 @@ pub fn setup_page(recipe_id: i64) { current_language = language.value(); let language = language.value(); spawn_local(async move { - let _ = - request::patch::<(), _>(&format!("recipe/{recipe_id}/language"), language) - .await; + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/language"), + language, + ) + .await; }); } }) @@ -222,9 +231,11 @@ pub fn setup_page(recipe_id: i64) { EventListener::new(&is_public.clone(), "input", move |_event| { let is_public = is_public.checked(); spawn_local(async move { - let _ = - request::patch::<(), _>(&format!("recipe/{recipe_id}/is_public"), is_public) - .await; + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/is_public"), + is_public, + ) + .await; reload_recipes_list(recipe_id).await; }); }) @@ -249,7 +260,9 @@ pub fn setup_page(recipe_id: i64) { .await .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() .location() @@ -271,8 +284,8 @@ pub fn setup_page(recipe_id: i64) { // Load initial groups, steps and ingredients. { spawn_local(async move { - let groups: Vec = - request::get(&format!("recipe/{recipe_id}/groups")) + let groups: Vec = + ron_request::get(&format!("/ron-api/recipe/{recipe_id}/groups")) .await .unwrap(); @@ -299,10 +312,11 @@ pub fn setup_page(recipe_id: i64) { let button_add_group: HtmlInputElement = by_id("input-add-group"); EventListener::new(&button_add_group, "click", move |_event| { spawn_local(async move { - let id: i64 = request::post::<_, ()>(&format!("recipe/{recipe_id}/group"), None) - .await - .unwrap(); - create_group_element(&ron_api::Group { + let id: i64 = + ron_request::post::<_, ()>(&format!("/ron-api/recipe/{recipe_id}/group"), None) + .await + .unwrap(); + create_group_element(&web_api::Group { id, name: "".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_element: Element = selector_and_clone("#hidden-templates .group"); 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::().unwrap()) .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(); let name = name.value(); 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(); let comment = comment.value(); spawn_local(async move { - let _ = - request::patch::<(), _>(&format!("group/{group_id}/comment"), comment).await; + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/group/{group_id}/comment"), + comment, + ) + .await; }); } }) @@ -384,7 +403,8 @@ fn create_group_element(group: &ron_api::Group) -> Element { .await .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::(&format!("group-{group_id}")); group_element.next_element_sibling().unwrap().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"); EventListener::new(&add_step_button, "click", move |_event| { spawn_local(async move { - let id: i64 = request::post::<_, ()>(&format!("group/{group_id}/step"), None) - .await - .unwrap(); + let id: i64 = + ron_request::post::<_, ()>(&format!("/ron-api/group/{group_id}/step"), None) + .await + .unwrap(); create_step_element( &selector::(&format!("#group-{group_id} .steps")), - &ron_api::Step { + &web_api::Step { id, action: "".to_string(), ingredients: vec![], @@ -457,9 +478,11 @@ where let tag_span = tag_span.clone(); let tag = tag.clone(); spawn_local(async move { - let _ = - request::delete::<(), _>(&format!("recipe/{recipe_id}/tags"), Some(vec![tag])) - .await; + let _ = ron_request::delete::<(), _>( + &format!("/ron-api/recipe/{recipe_id}/tags"), + Some(vec![tag]), + ) + .await; 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_element: Element = selector_and_clone("#hidden-templates .step"); 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::().unwrap()) .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(); let action = action.value(); 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 .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::(&format!("step-{step_id}")); step_element.next_element_sibling().unwrap().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"); EventListener::new(&add_ingredient_button, "click", move |_event| { spawn_local(async move { - let id: i64 = request::post::<_, ()>(&format!("step/{step_id}/ingredient"), None) - .await - .unwrap(); + let id: i64 = + ron_request::post::<_, ()>(&format!("/ron-api/step/{step_id}/ingredient"), None) + .await + .unwrap(); create_ingredient_element( &selector::(&format!("#step-{} .ingredients", step_id)), - &ron_api::Ingredient { + &web_api::Ingredient { id, name: "".to_string(), comment: "".to_string(), @@ -555,7 +582,7 @@ fn create_step_element(group_element: &Element, step: &ron_api::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_element: Element = selector_and_clone("#hidden-templates .ingredient"); 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::().unwrap()) .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(); let name = name.value(); spawn_local(async move { - let _ = request::patch::<(), _>(&format!("ingredient/{ingredient_id}/name"), name) - .await; + let _ = ron_request::patch::<(), _>( + &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(); let comment = comment.value(); spawn_local(async move { - let _ = request::patch::<(), _>( - &format!("ingredient/{ingredient_id}/comment"), + let _ = ron_request::patch::<(), _>( + &format!("/ron-api/ingredient/{ingredient_id}/comment"), comment, ) .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) }; current_quantity = n; spawn_local(async move { - let _ = request::patch::<(), _>(&format!("ingredient/{ingredient_id}/quantity"), q) - .await; + let _ = ron_request::patch::<(), _>( + &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(); let unit = unit.value(); spawn_local(async move { - let _ = request::patch::<(), _>(&format!("ingredient/{ingredient_id}/unit"), unit) - .await; + let _ = ron_request::patch::<(), _>( + &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 .is_some() { - let _ = - request::delete::<(), ()>(&format!("ingredient/{ingredient_id}"), None).await; + let _ = ron_request::delete::<(), ()>( + &format!("/ron-api/ingredient/{ingredient_id}"), + None, + ) + .await; let ingredient_element = by_id::(&format!("ingredient-{ingredient_id}")); ingredient_element.next_element_sibling().unwrap().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) { - match Request::get("/fragments/recipes_list") - .query([("current_recipe_id", current_recipe_id.to_string())]) - .send() - .await - { - Err(error) => { - toast::show_message_level(Level::Error, &format!("Internal server error: {}", error)); - } - Ok(response) => { - let list = document().get_element_by_id("recipes-list").unwrap(); - list.set_outer_html(&response.text().await.unwrap()); - } - } + let fragment: String = request::get_with_params( + "/fragments/recipes_list", + web_api::RecipesListFragmentsParams { + current_recipe_id: Some(current_recipe_id), + }, + ) + .await + .unwrap(); + + let list = document().get_element_by_id("recipes-list").unwrap(); + list.set_outer_html(&fragment); } enum CursorPosition { diff --git a/frontend/src/recipe_scheduler.rs b/frontend/src/recipe_scheduler.rs index dd34016..11c6b10 100644 --- a/frontend/src/recipe_scheduler.rs +++ b/frontend/src/recipe_scheduler.rs @@ -1,16 +1,16 @@ use chrono::{Datelike, Days, Months, NaiveDate}; -use common::ron_api; +use common::web_api; use gloo::storage::{LocalStorage, Storage}; use ron::ser::{PrettyConfig, to_string_pretty}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{calendar, request}; +use crate::{calendar, ron_request}; #[derive(Error, Debug)] pub enum Error { #[error("Request error: {0}")] - Request(#[from] request::Error), + Request(#[from] ron_request::Error), } type Result = std::result::Result; @@ -25,11 +25,11 @@ pub enum ScheduleRecipeResult { RecipeAlreadyScheduledAtThisDate, } -impl From for ScheduleRecipeResult { - fn from(api_res: ron_api::ScheduleRecipeResult) -> Self { +impl From for ScheduleRecipeResult { + fn from(api_res: web_api::ScheduleRecipeResult) -> Self { match api_res { - ron_api::ScheduleRecipeResult::Ok => Self::Ok, - ron_api::ScheduleRecipeResult::RecipeAlreadyScheduledAtThisDate => { + web_api::ScheduleRecipeResult::Ok => Self::Ok, + web_api::ScheduleRecipeResult::RecipeAlreadyScheduledAtThisDate => { Self::RecipeAlreadyScheduledAtThisDate } } @@ -80,12 +80,14 @@ impl RecipeScheduler { return Ok(vec![]); } - let titles: Vec = request::get_with_params( - "recipe/titles", - recipe_ids_and_dates - .iter() - .map(|r| r.recipe_id) - .collect::>(), + let titles: Vec = ron_request::get_with_params( + "/ron-api/recipe/titles", + web_api::GetTitlesParams { + ids: recipe_ids_and_dates + .iter() + .map(|r| r.recipe_id) + .collect::>(), + }, ) .await?; @@ -95,9 +97,9 @@ impl RecipeScheduler { .map(|(id_and_date, title)| (id_and_date.date, title, id_and_date.recipe_id)) .collect::>()) } else { - let scheduled_recipes: ron_api::ScheduledRecipes = request::get_with_params( - "calendar/scheduled_recipes", - ron_api::DateRange { + let scheduled_recipes: web_api::ScheduledRecipes = ron_request::get_with_params( + "/ron-api/calendar/scheduled_recipes", + web_api::DateRange { start_date, end_date, }, @@ -127,9 +129,9 @@ impl RecipeScheduler { save_scheduled_recipes(recipe_ids_and_dates, date.year(), date.month0()); Ok(ScheduleRecipeResult::Ok) } else { - request::post::( - "calendar/scheduled_recipe", - Some(ron_api::ScheduleRecipe { + ron_request::post::( + "/ron-api/calendar/scheduled_recipe", + Some(web_api::ScheduleRecipe { recipe_id, date, servings, @@ -138,7 +140,7 @@ impl RecipeScheduler { ) .await .map_err(Error::from) - .map(From::::from) + .map(From::::from) } } diff --git a/frontend/src/request.rs b/frontend/src/request.rs index c702e54..4849262 100644 --- a/frontend/src/request.rs +++ b/frontend/src/request.rs @@ -1,10 +1,7 @@ -/// 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 `ron_api` module. -/// For requests with parameters (GET), it uses the HTML form format. -use common::ron_api; -use gloo::net::http::{Request, RequestBuilder}; -use serde::{Serialize, de::DeserializeOwned}; +use std::any::TypeId; + +use gloo::net::http::Request; +use serde::Serialize; use thiserror::Error; use crate::toast::{self, Level}; @@ -14,12 +11,6 @@ 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), @@ -29,53 +20,26 @@ pub enum Error { type Result = std::result::Result; -const CONTENT_TYPE: &str = "Content-Type"; - -async fn req_with_body( - api_name: &str, - body: U, - method_fn: fn(&str) -> RequestBuilder, -) -> Result -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 +#[allow(dead_code)] // Not used for the moment. +pub async fn get(url: &str) -> Result { + get_with_params(url, ()).await } -async fn req_with_params( - api_name: &str, - params: U, - method_fn: fn(&str) -> RequestBuilder, -) -> Result +pub async fn get_with_params(url: &str, params: U) -> Result where - T: DeserializeOwned, - U: Serialize, + U: Serialize + 'static, { - let mut url = format!("/ron-api/{}?", api_name); - serde_html_form::ser::push_to_string(&mut url, params).unwrap(); - let request_builder = method_fn(&url); - send_req(request_builder.build()?).await -} + let request_builder = if TypeId::of::() == TypeId::of::<()>() { + Request::get(url) + } else { + 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(api_name: &str, method_fn: fn(&str) -> RequestBuilder) -> Result -where - T: DeserializeOwned, -{ - let url = format!("/ron-api/{}", api_name); - let request_builder = method_fn(&url); - send_req(request_builder.build()?).await -} + let request = request_builder.build()?; -async fn send_req(request: Request) -> Result -where - T: DeserializeOwned, -{ match request.send().await { Err(error) => { toast::show_message_level(Level::Error, &format!("Internal server error: {}", error)); @@ -89,73 +53,8 @@ where ); 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::(&r)?) + Ok(response.text().await?) } } } } - -/// 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(api_name: &str, body: U) -> Result -where - T: DeserializeOwned, - U: Serialize, -{ - req_with_body(api_name, body, Request::put).await -} - -pub async fn patch(api_name: &str, body: U) -> Result -where - T: DeserializeOwned, - U: Serialize, -{ - req_with_body(api_name, body, Request::patch).await -} - -pub async fn post(api_name: &str, body: Option) -> Result -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(api_name: &str, body: Option) -> Result -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(api_name: &str) -> Result -where - T: DeserializeOwned, -{ - req(api_name, Request::get).await -} - -pub async fn get_with_params(api_name: &str, params: U) -> Result -where - T: DeserializeOwned, - U: Serialize, -{ - req_with_params(api_name, params, Request::get).await -} diff --git a/frontend/src/ron_request.rs b/frontend/src/ron_request.rs new file mode 100644 index 0000000..7af468b --- /dev/null +++ b/frontend/src/ron_request.rs @@ -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 = std::result::Result; + +const CONTENT_TYPE: &str = "Content-Type"; // TODO: take it from the http crate. + +async fn req_with_body(url: &str, body: U, method_fn: fn(&str) -> RequestBuilder) -> Result +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( + url: &str, + params: U, + method_fn: fn(&str) -> RequestBuilder, +) -> Result +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(url: &str, method_fn: fn(&str) -> RequestBuilder) -> Result +where + T: DeserializeOwned, +{ + let request_builder = method_fn(url); + send_req(request_builder.build()?).await +} + +async fn send_req(request: Request) -> Result +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::(&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(url: &str, body: U) -> Result +where + T: DeserializeOwned, + U: Serialize, +{ + req_with_body(url, body, Request::put).await +} + +pub async fn patch(url: &str, body: U) -> Result +where + T: DeserializeOwned, + U: Serialize, +{ + req_with_body(url, body, Request::patch).await +} + +pub async fn post(url: &str, body: Option) -> Result +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(url: &str, body: Option) -> Result +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(url: &str) -> Result +where + T: DeserializeOwned, +{ + req(url, Request::get).await +} + +pub async fn get_with_params(url: &str, params: U) -> Result +where + T: DeserializeOwned, + U: Serialize, +{ + req_with_params(url, params, Request::get).await +} diff --git a/frontend/src/shopping_list.rs b/frontend/src/shopping_list.rs index b9251cf..cff52ae 100644 --- a/frontend/src/shopping_list.rs +++ b/frontend/src/shopping_list.rs @@ -1,16 +1,16 @@ use chrono::{Datelike, Days, Months, NaiveDate}; -use common::ron_api; +use common::web_api; use gloo::storage::{LocalStorage, Storage}; use ron::ser::{PrettyConfig, to_string_pretty}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{calendar, request}; +use crate::{calendar, ron_request}; #[derive(Error, Debug)] pub enum Error { #[error("Request error: {0}")] - Request(#[from] request::Error), + Request(#[from] ron_request::Error), } type Result = std::result::Result; @@ -25,11 +25,11 @@ impl ShoppingList { Self { is_local } } - pub async fn get_items(&self) -> Result> { + pub async fn get_items(&self) -> Result> { if self.is_local { Ok(vec![]) // TODO } 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 { todo!(); } else { - request::patch( - "shopping_list/checked", - ron_api::KeyValue { + ron_request::patch( + "/ron-api/shopping_list/checked", + web_api::KeyValue { id: item_id, value: is_checked, },