Use the api library on the frontend side

This commit is contained in:
Jeremy Wall 2022-12-21 11:41:41 -05:00
parent db63deb319
commit 997d95e201
7 changed files with 73 additions and 28 deletions

2
Cargo.lock generated
View File

@ -58,6 +58,7 @@ name = "api"
version = "0.1.0"
dependencies = [
"axum",
"chrono",
"recipes",
"serde",
]
@ -1300,6 +1301,7 @@ dependencies = [
name = "kitchen-wasm"
version = "0.2.11"
dependencies = [
"api",
"async-trait",
"base64",
"chrono",

View File

@ -8,11 +8,14 @@ edition = "2021"
[dependencies]
serde = "1.0.144"
recipes = { path = "../recipes" }
chrono = "0.4.22"
[dependencies.axum]
version = "0.5.16"
optional = true
[features]
default = []
server = ["axum"]
browser = []

View File

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com)
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -40,6 +42,15 @@ impl<T> Response<T> {
pub fn success(payload: T) -> Self {
Self::Success(payload)
}
#[cfg(feature = "browser")]
pub fn as_success(self) -> Option<T> {
if let Self::Success(val) = self {
Some(val)
} else {
None
}
}
}
#[cfg(feature = "server")]
@ -57,6 +68,7 @@ where
};
(code, axum::Json::from(self)).into_response()
}
// TODO(jwall): Perhaps this can show a more useful json payload?
Self::NotFound => (StatusCode::NOT_FOUND, axum::Json::from(self)).into_response(),
Self::Unauthorized => {
(StatusCode::UNAUTHORIZED, axum::Json::from(self)).into_response()
@ -87,6 +99,10 @@ where
}
}
pub type CategoryResponse = Response<String>;
pub type EmptyResponse = Response<()>;
#[derive(Serialize, Deserialize)]
pub struct UserData {
pub user_id: String,
@ -125,6 +141,8 @@ impl From<Option<Vec<(String, i32)>>> for PlanDataResponse {
}
}
pub type PlanHistoryResponse = Response<BTreeMap<chrono::NaiveDate, Vec<(String, i32)>>>;
#[derive(Serialize, Deserialize)]
pub struct InventoryData {
pub filtered_ingredients: Vec<IngredientKey>,

View File

@ -23,7 +23,6 @@ use axum::{
response::{IntoResponse, Redirect, Response},
routing::{get, Router},
};
use chrono::NaiveDate;
use mime_guess;
use recipes::{IngredientKey, RecipeEntry};
use rust_embed::RustEmbed;
@ -146,7 +145,7 @@ async fn api_save_categories(
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
session: storage::UserIdFromSession,
Json(categories): Json<String>,
) -> api::Response<String> {
) -> api::CategoryResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
if let Err(e) = app_store
@ -158,9 +157,9 @@ async fn api_save_categories(
format!("{:?}", e),
);
}
api::Response::success("Successfully saved categories".into())
api::CategoryResponse::success("Successfully saved categories".into())
} else {
api::Response::Unauthorized
api::CategoryResponse::Unauthorized
}
}
@ -168,7 +167,7 @@ async fn api_save_recipes(
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
session: storage::UserIdFromSession,
Json(recipes): Json<Vec<RecipeEntry>>,
) -> api::Response<()> {
) -> api::EmptyResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
let result = app_store
@ -176,7 +175,7 @@ async fn api_save_recipes(
.await;
result.map_err(|e| format!("Error: {:?}", e)).into()
} else {
api::Response::Unauthorized
api::EmptyResponse::Unauthorized
}
}
@ -200,7 +199,7 @@ async fn api_plan_since(
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
session: storage::UserIdFromSession,
Path(date): Path<chrono::NaiveDate>,
) -> api::Response<BTreeMap<NaiveDate, Vec<(String, i32)>>> {
) -> api::PlanHistoryResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
app_store
@ -209,7 +208,7 @@ async fn api_plan_since(
.map_err(|e| format!("Error: {:?}", e))
.into()
} else {
api::Response::Unauthorized
api::PlanHistoryResponse::Unauthorized
}
}
@ -217,7 +216,7 @@ async fn api_save_plan(
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
session: storage::UserIdFromSession,
Json(meal_plan): Json<Vec<(String, i32)>>,
) -> api::Response<()> {
) -> api::EmptyResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
app_store
@ -226,7 +225,7 @@ async fn api_save_plan(
.map_err(|e| format!("{:?}", e))
.into()
} else {
api::Response::Unauthorized
api::EmptyResponse::Unauthorized
}
}
@ -273,7 +272,7 @@ async fn save_inventory_data(
filtered_ingredients: BTreeSet<IngredientKey>,
modified_amts: BTreeMap<IngredientKey, String>,
extra_items: Vec<(String, String)>,
) -> api::Response<()> {
) -> api::EmptyResponse {
app_store
.save_inventory_data(id, filtered_ingredients, modified_amts, extra_items)
.await
@ -289,7 +288,7 @@ async fn api_save_inventory_v2(
Vec<(IngredientKey, String)>,
Vec<(String, String)>,
)>,
) -> api::Response<()> {
) -> api::EmptyResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
let filtered_ingredients = filtered_ingredients.into_iter().collect();
@ -304,7 +303,7 @@ async fn api_save_inventory_v2(
.await
.into()
} else {
api::Response::Unauthorized
api::EmptyResponse::Unauthorized
}
}
@ -315,7 +314,7 @@ async fn api_save_inventory(
Vec<IngredientKey>,
Vec<(IngredientKey, String)>,
)>,
) -> api::Response<()> {
) -> api::EmptyResponse {
use storage::{UserId, UserIdFromSession::FoundUserId};
if let FoundUserId(UserId(id)) = session {
let filtered_ingredients = filtered_ingredients.into_iter().collect();

View File

@ -14,6 +14,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
recipes = { path = "../recipes" }
client-api = { path = "../api", package="api", features = ["browser"] }
# This makes debugging panics more tractable.
console_error_panic_hook = "0.1.7"
serde_json = "1.0.79"

View File

@ -18,6 +18,7 @@ use serde_json::{from_str, to_string};
use sycamore::prelude::*;
use tracing::{debug, error, info, instrument, warn};
use client_api::*;
use recipes::{parse, IngredientKey, Recipe, RecipeEntry};
use wasm_bindgen::JsValue;
@ -216,7 +217,7 @@ impl HttpStore {
Err(format!("Status: {}", resp.status()).into())
} else {
debug!("We got a valid response back!");
let resp: String = resp.json().await?;
let resp = resp.json::<CategoryResponse>().await?.as_success().unwrap();
storage.set("categories", &resp)?;
Ok(Some(resp))
}
@ -250,8 +251,11 @@ impl HttpStore {
Err(format!("Status: {}", resp.status()).into())
} else {
debug!("We got a valid response back!");
let entries: Option<Vec<RecipeEntry>> =
resp.json().await.map_err(|e| format!("{}", e))?;
let entries = resp
.json::<RecipeEntryResponse>()
.await
.map_err(|e| format!("{}", e))?
.as_success();
if let Some(ref entries) = entries {
for r in entries.iter() {
storage.set(
@ -292,7 +296,12 @@ impl HttpStore {
Ok(None)
} else {
debug!("We got a valid response back!");
let entry: Option<RecipeEntry> = resp.json().await.map_err(|e| format!("{}", e))?;
let entry = resp
.json::<Response<Option<RecipeEntry>>>()
.await
.map_err(|e| format!("{}", e))?
.as_success()
.unwrap();
if let Some(ref entry) = entry {
let serialized: String = to_string(entry).map_err(|e| format!("{}", e))?;
storage.set(&recipe_key(entry.recipe_id()), &serialized)?
@ -399,8 +408,11 @@ impl HttpStore {
Err(format!("Status: {}", resp.status()).into())
} else {
debug!("We got a valid response back");
let plan: Option<Vec<(String, i32)>> =
resp.json().await.map_err(|e| format!("{}", e))?;
let plan = resp
.json::<PlanDataResponse>()
.await
.map_err(|e| format!("{}", e))?
.as_success();
if let Some(ref entry) = plan {
let serialized: String = to_string(entry).map_err(|e| format!("{}", e))?;
storage.set("plan", &serialized)?
@ -448,11 +460,16 @@ impl HttpStore {
})
} else {
debug!("We got a valid response back");
let (filtered_ingredients, modified_amts, extra_items): (
Vec<IngredientKey>,
Vec<(IngredientKey, String)>,
Vec<(String, String)>,
) = resp.json().await.map_err(|e| format!("{}", e))?;
let InventoryData {
filtered_ingredients,
modified_amts,
extra_items,
} = resp
.json::<InventoryResponse>()
.await
.map_err(|e| format!("{}", e))?
.as_success()
.unwrap();
let _ = storage.set(
"inventory",
&to_string(&(&filtered_ingredients, &modified_amts))

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use base64;
use client_api::AccountResponse;
use reqwasm::http;
use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{debug, error, info};
@ -20,7 +21,7 @@ fn token68(user: String, pass: String) -> String {
base64::encode(format!("{}:{}", user, pass))
}
async fn authenticate(user: String, pass: String) -> bool {
async fn authenticate(user: String, pass: String) -> Option<AccountResponse> {
debug!(
username = user,
password = pass,
@ -35,13 +36,16 @@ async fn authenticate(user: String, pass: String) -> bool {
.await;
if let Ok(resp) = &result {
if resp.status() == 200 {
return true;
return resp
.json()
.await
.expect("Unparseable authentication response");
}
error!(status = resp.status(), "Login was unsuccessful")
} else {
error!(err=?result.unwrap_err(), "Failed to send auth request");
}
return false;
return None;
}
#[component]
@ -55,6 +59,7 @@ pub fn LoginForm<G: Html>(cx: Scope) -> View<G> {
spawn_local_scoped(cx, async move {
debug!("authenticating against ui");
// TODO(jwall): Navigate to plan if the below is successful.
// TODO(jwall): Store account data in our app_state.
authenticate(username, password).await;
});
}