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').
// .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",

View file

@ -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]

View file

@ -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)]

View file

@ -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<T>(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)],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,12 +39,12 @@
<span class="difficulty">
{% 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 %}
</span>

View file

@ -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<dyn Error>> {
// 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<dyn Error>> {
.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<dyn Error>> {
.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<dyn Error>> {
.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<dyn Error>> {
.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<dyn Error>> {
.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<dyn Error>> {
.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<dyn Error>> {
.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<dyn Error>> {
.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<dyn Error>> {
.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(),
)

View file

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

View file

@ -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"),

View file

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

View file

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

View file

@ -20,6 +20,28 @@ pub struct DateRange {
/*** 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)]
#[derive(Serialize, Deserialize, FromRepr, Clone, Copy, PartialEq, Debug)]
pub enum Difficulty {

View file

@ -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();
}
});

View file

@ -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()

View file

@ -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::<u32>().unwrap())
.unwrap_or(ron_api::Difficulty::Unknown);
web_api::Difficulty::from_repr(difficulty.value().parse::<u32>().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<String> = request::get(&format!("recipe/{recipe_id}/tags"))
let tags: Vec<String> = 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<String> =
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::<HtmlInputElement>("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<common::ron_api::Group> =
request::get(&format!("recipe/{recipe_id}/groups"))
let groups: Vec<common::web_api::Group> =
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::<i64>().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::<Element>(&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::<Element>(&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::<i64>().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::<Element>(&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::<Element>(&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::<i64>().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::<Element>(&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 {

View file

@ -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<T> = std::result::Result<T, Error>;
@ -25,11 +25,11 @@ pub enum ScheduleRecipeResult {
RecipeAlreadyScheduledAtThisDate,
}
impl From<ron_api::ScheduleRecipeResult> for ScheduleRecipeResult {
fn from(api_res: ron_api::ScheduleRecipeResult) -> Self {
impl From<web_api::ScheduleRecipeResult> 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<String> = request::get_with_params(
"recipe/titles",
recipe_ids_and_dates
.iter()
.map(|r| r.recipe_id)
.collect::<Vec<_>>(),
let titles: Vec<String> = ron_request::get_with_params(
"/ron-api/recipe/titles",
web_api::GetTitlesParams {
ids: recipe_ids_and_dates
.iter()
.map(|r| r.recipe_id)
.collect::<Vec<_>>(),
},
)
.await?;
@ -95,9 +97,9 @@ impl RecipeScheduler {
.map(|(id_and_date, title)| (id_and_date.date, title, id_and_date.recipe_id))
.collect::<Vec<_>>())
} 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::<ron_api::ScheduleRecipeResult, _>(
"calendar/scheduled_recipe",
Some(ron_api::ScheduleRecipe {
ron_request::post::<web_api::ScheduleRecipeResult, _>(
"/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::<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.
/// 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<T> = std::result::Result<T, Error>;
const CONTENT_TYPE: &str = "Content-Type";
async fn req_with_body<T, U>(
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
#[allow(dead_code)] // Not used for the moment.
pub async fn get(url: &str) -> Result<String> {
get_with_params(url, ()).await
}
async fn req_with_params<T, U>(
api_name: &str,
params: U,
method_fn: fn(&str) -> RequestBuilder,
) -> Result<T>
pub async fn get_with_params<U>(url: &str, params: U) -> Result<String>
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::<U>() == 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<T>(api_name: &str, method_fn: fn(&str) -> RequestBuilder) -> Result<T>
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<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));
@ -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::<T>(&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<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 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<T> = std::result::Result<T, Error>;
@ -25,11 +25,11 @@ impl ShoppingList {
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 {
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,
},