From 91902cebbe35194667d4f9d09a98f1c485d76c90 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 15 Aug 2022 19:37:08 -0400 Subject: [PATCH] Use recipe ids instead of indexes --- Cargo.lock | 10 +++--- kitchen/src/web.rs | 40 +++-------------------- recipe-store/Cargo.toml | 4 ++- recipe-store/src/lib.rs | 28 ++++++++++++---- web/src/app_state.rs | 2 +- web/src/components/recipe.rs | 5 +-- web/src/components/recipe_selection.rs | 25 ++++++++------ web/src/components/recipe_selector.rs | 3 +- web/src/pages/recipe.rs | 2 +- web/src/router_integration.rs | 6 +--- web/src/service.rs | 45 +++++++++++++------------- web/src/web.rs | 2 +- 12 files changed, 81 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e39ff5..8f55647 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1033,6 +1033,8 @@ dependencies = [ "async-trait", "recipes", "reqwasm", + "serde", + "serde_json", "tracing", ] @@ -1129,18 +1131,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" +checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" +checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" dependencies = [ "proc-macro2", "quote", diff --git a/kitchen/src/web.rs b/kitchen/src/web.rs index ac129f7..cb9fe45 100644 --- a/kitchen/src/web.rs +++ b/kitchen/src/web.rs @@ -15,8 +15,6 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; -use async_std::fs::{read_dir, read_to_string, DirEntry}; -use async_std::stream::StreamExt; use axum::{ body::{boxed, Full}, extract::{Extension, Path}, @@ -25,40 +23,10 @@ use axum::{ routing::{get, Router}, }; use mime_guess; -use recipe_store::{self, RecipeStore}; +use recipe_store::{self, RecipeEntry, RecipeStore}; use rust_embed::RustEmbed; use tower_http::trace::TraceLayer; -use tracing::{debug, info, instrument, warn}; - -use crate::api::ParseError; - -#[instrument(fields(recipe_dir=?recipe_dir_path), skip_all)] -pub async fn get_recipes(recipe_dir_path: PathBuf) -> Result, ParseError> { - let mut entries = read_dir(recipe_dir_path).await?; - let mut entry_vec = Vec::new(); - // Special files that we ignore when fetching recipes - let filtered = vec!["menu.txt", "categories.txt"]; - while let Some(res) = entries.next().await { - let entry: DirEntry = res?; - - if !entry.file_type().await?.is_dir() - && !filtered - .iter() - .any(|&s| s == entry.file_name().to_string_lossy().to_string()) - { - // add it to the entry - info!("adding recipe file {}", entry.file_name().to_string_lossy()); - let recipe_contents = read_to_string(entry.path()).await?; - entry_vec.push(recipe_contents); - } else { - warn!( - file = %entry.path().to_string_lossy(), - "skipping file not a recipe", - ); - } - } - Ok(entry_vec) -} +use tracing::{debug, info, instrument}; #[derive(RustEmbed)] #[folder = "../web/dist"] @@ -102,13 +70,13 @@ async fn ui_static_assets(Path(path): Path) -> impl IntoResponse { #[instrument] async fn api_recipes(Extension(store): Extension>) -> Response { - let result: Result>, String> = match store + let result: Result>, String> = match store .get_recipes() .await .map_err(|e| format!("Error: {:?}", e)) { Ok(Some(recipes)) => Ok(axum::Json::from(recipes)), - Ok(None) => Ok(axum::Json::from(Vec::::new())), + Ok(None) => Ok(axum::Json::from(Vec::::new())), Err(e) => Err(e), }; result.into_response() diff --git a/recipe-store/Cargo.toml b/recipe-store/Cargo.toml index bde6437..e7ff810 100644 --- a/recipe-store/Cargo.toml +++ b/recipe-store/Cargo.toml @@ -10,4 +10,6 @@ recipes = {path = "../recipes" } async-trait = "0.1.57" async-std = "1.10.0" tracing = "0.1.35" -reqwasm = "0.5.0" \ No newline at end of file +reqwasm = "0.5.0" +serde_json = "1.0.79" +serde = "1.0.143" \ No newline at end of file diff --git a/recipe-store/src/lib.rs b/recipe-store/src/lib.rs index d55a11b..b87873f 100644 --- a/recipe-store/src/lib.rs +++ b/recipe-store/src/lib.rs @@ -21,6 +21,7 @@ use async_std::{ use async_trait::async_trait; #[cfg(target_arch = "wasm32")] use reqwasm; +use serde::{Deserialize, Serialize}; #[cfg(not(target_arch = "wasm32"))] use tracing::warn; use tracing::{debug, instrument}; @@ -60,6 +61,19 @@ where fn get_user_store(&self, user: String) -> S; } +#[derive(Serialize, Deserialize)] +pub struct RecipeEntry(String, String); + +impl RecipeEntry { + pub fn recipe_id(&self) -> &str { + self.0.as_str() + } + + pub fn recipe_text(&self) -> &str { + self.1.as_str() + } +} + #[cfg(not(target_arch = "wasm32"))] #[async_trait] /// Define the shared interface to use for interacting with a store of recipes. @@ -67,7 +81,7 @@ pub trait RecipeStore: Clone + Sized { /// Get categories text unparsed. async fn get_categories(&self) -> Result, Error>; /// Get list of recipe text unparsed. - async fn get_recipes(&self) -> Result>, Error>; + async fn get_recipes(&self) -> Result>, Error>; } // NOTE(jwall): Futures in webassembly can't implement `Send` easily so we define @@ -79,7 +93,7 @@ pub trait RecipeStore: Clone + Sized { /// Get categories text unparsed. async fn get_categories(&self) -> Result, Error>; /// Get list of recipe text unparsed. - async fn get_recipes(&self) -> Result>, Error>; + async fn get_recipes(&self) -> Result>, Error>; } #[cfg(not(target_arch = "wasm32"))] @@ -112,7 +126,7 @@ impl RecipeStore for AsyncFileStore { Ok(Some(String::from_utf8(contents)?)) } - async fn get_recipes(&self) -> Result>, Error> { + async fn get_recipes(&self) -> Result>, Error> { let mut recipe_path = PathBuf::new(); recipe_path.push(&self.path); recipe_path.push("recipes"); @@ -129,9 +143,10 @@ impl RecipeStore for AsyncFileStore { .any(|&s| s == entry.file_name().to_string_lossy().to_string()) { // add it to the entry - debug!("adding recipe file {}", entry.file_name().to_string_lossy()); + let file_name = entry.file_name().to_string_lossy().to_string(); + debug!("adding recipe file {}", file_name); let recipe_contents = read_to_string(entry.path()).await?; - entry_vec.push(recipe_contents); + entry_vec.push(RecipeEntry(file_name, recipe_contents)); } else { warn!( file = %entry.path().to_string_lossy(), @@ -177,7 +192,7 @@ impl RecipeStore for HttpStore { } #[instrument] - async fn get_recipes(&self) -> Result>, Error> { + async fn get_recipes(&self) -> Result>, Error> { let mut path = self.root.clone(); path.push_str("/recipes"); let resp = reqwasm::http::Request::get(&path).send().await?; @@ -188,5 +203,4 @@ impl RecipeStore for HttpStore { Ok(resp.json().await.map_err(|e| format!("{}", e))?) } } - // } diff --git a/web/src/app_state.rs b/web/src/app_state.rs index fae3448..a661634 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -17,7 +17,7 @@ pub enum AppRoutes { Plan, Inventory, Cook, - Recipe(usize), + Recipe(String), Error(String), NotFound, } diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index f9181ef..d5a4387 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -47,11 +47,12 @@ fn steps(steps: ReadSignal>) -> View { } #[component(Recipe)] -pub fn recipe(idx: ReadSignal) -> View { +pub fn recipe(idx: ReadSignal) -> View { let app_service = get_appservice_from_context(); let view = Signal::new(View::empty()); create_effect(cloned!((app_service, view) => move || { - if let Some((_, recipe)) = app_service.get_recipes().get().get(*idx.get()) { + let recipe_id: String = idx.get().as_ref().to_owned(); + if let Some(recipe) = app_service.get_recipes().get().get(&recipe_id) { let recipe = recipe.clone(); let title = create_memo(cloned!((recipe) => move || recipe.get().title.clone())); let desc = create_memo( diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index 75b9053..7bac672 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -19,7 +19,7 @@ use tracing::{debug, instrument}; use crate::service::get_appservice_from_context; pub struct RecipeCheckBoxProps { - pub i: usize, + pub i: String, pub title: ReadSignal, } @@ -32,18 +32,23 @@ pub fn recipe_selection(props: RecipeCheckBoxProps) -> View { let app_service = get_appservice_from_context(); // This is total hack but it works around the borrow issues with // the `view!` macro. - let i = props.i; - let id_as_str = Rc::new(format!("{}", i)); - let id_cloned_2 = id_as_str.clone(); - let count = Signal::new(format!("{}", app_service.get_recipe_count_by_index(i))); + let id = Rc::new(props.i); + let count = Signal::new(format!( + "{}", + app_service.get_recipe_count_by_index(id.as_ref()) + )); + let for_id = id.clone(); + let href = format!("#recipe/{}", id); + let name = format!("recipe_id:{}", id); + let value = id.clone(); view! { div() { - label(for=id_cloned_2) { a(href=format!("#recipe/{}", i)) { (props.title.get()) } } - input(type="number", class="item-count-sel", min="0", bind:value=count.clone(), name=format!("recipe_id:{}", i), value=id_as_str.clone(), on:change=move |_| { + label(for=for_id) { a(href=href) { (props.title.get()) } } + input(type="number", class="item-count-sel", min="0", bind:value=count.clone(), name=name, value=value, on:change=cloned!((id) => move |_| { let mut app_service = app_service.clone(); - debug!(idx=%i, count=%(*count.get()), "setting recipe count"); - app_service.set_recipe_count_by_index(i, count.get().parse().unwrap()); - }) + debug!(idx=%id, count=%(*count.get()), "setting recipe count"); + app_service.set_recipe_count_by_index(id.as_ref().to_owned(), count.get().parse().unwrap()); + })) } } } diff --git a/web/src/components/recipe_selector.rs b/web/src/components/recipe_selector.rs index b50d714..545be65 100644 --- a/web/src/components/recipe_selector.rs +++ b/web/src/components/recipe_selector.rs @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use recipes::Recipe; use sycamore::{futures::spawn_local_in_scope, prelude::*}; use tracing::{error, instrument}; @@ -23,7 +24,7 @@ pub fn recipe_selector() -> View { let app_service = get_appservice_from_context(); let rows = create_memo(cloned!(app_service => move || { let mut rows = Vec::new(); - for row in app_service.get_recipes().get().as_slice().chunks(4) { + for row in app_service.get_recipes().get().iter().map(|(k, v)| (k.clone(), v.clone())).collect::)>>().chunks(4) { rows.push(Signal::new(Vec::from(row))); } rows diff --git a/web/src/pages/recipe.rs b/web/src/pages/recipe.rs index 674d150..caf8ad3 100644 --- a/web/src/pages/recipe.rs +++ b/web/src/pages/recipe.rs @@ -18,7 +18,7 @@ use tracing::instrument; #[derive(Debug)] pub struct RecipePageProps { - pub recipe: Signal, + pub recipe: Signal, } #[instrument] diff --git a/web/src/router_integration.rs b/web/src/router_integration.rs index 02c0655..56dc14d 100644 --- a/web/src/router_integration.rs +++ b/web/src/router_integration.rs @@ -13,7 +13,6 @@ // limitations under the License. use std::fmt::Debug; use std::rc::Rc; -use std::str::FromStr; use sycamore::prelude::*; use tracing::{debug, error, instrument}; @@ -201,10 +200,7 @@ impl DeriveRoute for AppRoutes { let parts: Vec<&str> = h.splitn(2, "/").collect(); if let Some(&"#recipe") = parts.get(0) { if let Some(&idx) = parts.get(1) { - return match usize::from_str(idx) { - Ok(idx) => AppRoutes::Recipe(idx), - Err(e) => AppRoutes::Error(format!("{:?}", e)), - }; + return AppRoutes::Recipe(idx.to_owned()); } } error!(origin=%input.0, path=%input.1, hash=%input.2, "Path not found"); diff --git a/web/src/service.rs b/web/src/service.rs index 4df9eb1..1054a3b 100644 --- a/web/src/service.rs +++ b/web/src/service.rs @@ -35,10 +35,10 @@ pub struct AppService where S: RecipeStore, { - recipes: Signal)>>, + recipes: Signal>>, staples: Signal>, category_map: Signal>, - menu_list: Signal>, + menu_list: Signal>, store: S, } @@ -48,7 +48,7 @@ where { pub fn new(store: S) -> Self { Self { - recipes: Signal::new(Vec::new()), + recipes: Signal::new(BTreeMap::new()), staples: Signal::new(None), category_map: Signal::new(BTreeMap::new()), menu_list: Signal::new(BTreeMap::new()), @@ -122,7 +122,7 @@ where #[instrument(skip(self))] pub fn fetch_recipes_from_storage( &self, - ) -> Result<(Option, Option>), String> { + ) -> Result<(Option, Option>), String> { let storage = self.get_storage()?.unwrap(); let mut staples = None; match storage @@ -130,10 +130,11 @@ where .map_err(|e| format!("{:?}", e))? { Some(s) => { - let parsed = from_str::>(&s).map_err(|e| format!("{}", e))?; - let mut parsed_list = Vec::new(); + let parsed = from_str::>(&s).map_err(|e| format!("{}", e))?; + let mut parsed_map = BTreeMap::new(); + // TODO(jwall): Utilize the id instead of the index from now on. for r in parsed { - let recipe = match parse::as_recipe(&r) { + let recipe = match parse::as_recipe(&r.recipe_text()) { Ok(r) => r, Err(e) => { error!("Error parsing recipe {}", e); @@ -143,10 +144,10 @@ where if recipe.title == "Staples" { staples = Some(recipe); } else { - parsed_list.push(recipe); + parsed_map.insert(r.recipe_id().to_owned(), recipe); } } - Ok((staples, Some(parsed_list.drain(0..).enumerate().collect()))) + Ok((staples, Some(parsed_map))) } None => Ok((None, None)), } @@ -154,7 +155,7 @@ where async fn fetch_recipes( &self, - ) -> Result<(Option, Option>), String> { + ) -> Result<(Option, Option>), String> { Ok(self.fetch_recipes_from_storage()?) } @@ -177,8 +178,8 @@ where Ok(()) } - pub fn get_recipe_by_index(&self, idx: usize) -> Option> { - self.recipes.get().get(idx).map(|(_, r)| r.clone()) + pub fn get_recipe_by_index(&self, idx: &str) -> Option> { + self.recipes.get().get(idx).map(|r| r.clone()) } #[instrument(skip(self))] @@ -190,7 +191,7 @@ where let recipe_counts = self.menu_list.get(); for (idx, count) in recipe_counts.iter() { for _ in 0..*count { - acc.accumulate_from(self.get_recipe_by_index(*idx).unwrap().get().as_ref()); + acc.accumulate_from(self.get_recipe_by_index(idx).unwrap().get().as_ref()); } } if show_staples { @@ -218,35 +219,35 @@ where groups } - pub fn set_recipe_count_by_index(&mut self, i: usize, count: usize) { + pub fn set_recipe_count_by_index(&mut self, i: String, count: usize) { let mut v = (*self.menu_list.get()).clone(); v.insert(i, count); self.menu_list.set(v); } - pub fn get_recipe_count_by_index(&self, i: usize) -> usize { - self.menu_list.get().get(&i).map(|i| *i).unwrap_or_default() + pub fn get_recipe_count_by_index(&self, i: &str) -> usize { + self.menu_list.get().get(i).map(|i| *i).unwrap_or_default() } - pub fn get_recipes(&self) -> Signal)>> { + pub fn get_recipes(&self) -> Signal>> { self.recipes.clone() } - pub fn get_menu_list(&self) -> Vec<(usize, usize)> { + pub fn get_menu_list(&self) -> Vec<(String, usize)> { self.menu_list .get() .iter() // We exclude recipes in the menu_list with count 0 .filter(|&(_, count)| *count != 0) - .map(|(idx, count)| (*idx, *count)) + .map(|(idx, count)| (idx.clone(), *count)) .collect() } - pub fn set_recipes(&mut self, mut recipes: Vec<(usize, Recipe)>) { + pub fn set_recipes(&mut self, mut recipes: BTreeMap) { self.recipes.set( recipes - .drain(0..) - .map(|(i, r)| (i, Signal::new(r))) + .iter() + .map(|(i, r)| (i.clone(), Signal::new(r.clone()))) .collect(), ); } diff --git a/web/src/web.rs b/web/src/web.rs index 3e396b5..65094d6 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -38,7 +38,7 @@ fn route_switch(route: ReadSignal) -> View { CookPage() }, AppRoutes::Recipe(idx) => view! { - RecipePage(RecipePageProps { recipe: Signal::new(*idx) }) + RecipePage(RecipePageProps { recipe: Signal::new(idx.clone()) }) }, AppRoutes::NotFound => view! { // TODO(Create a real one)