Use recipe ids instead of indexes

This commit is contained in:
Jeremy Wall 2022-08-15 19:37:08 -04:00
parent 585939b842
commit 91902cebbe
12 changed files with 81 additions and 91 deletions

10
Cargo.lock generated
View File

@ -1033,6 +1033,8 @@ dependencies = [
"async-trait", "async-trait",
"recipes", "recipes",
"reqwasm", "reqwasm",
"serde",
"serde_json",
"tracing", "tracing",
] ]
@ -1129,18 +1131,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.140" version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.140" version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -15,8 +15,6 @@ use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use async_std::fs::{read_dir, read_to_string, DirEntry};
use async_std::stream::StreamExt;
use axum::{ use axum::{
body::{boxed, Full}, body::{boxed, Full},
extract::{Extension, Path}, extract::{Extension, Path},
@ -25,40 +23,10 @@ use axum::{
routing::{get, Router}, routing::{get, Router},
}; };
use mime_guess; use mime_guess;
use recipe_store::{self, RecipeStore}; use recipe_store::{self, RecipeEntry, RecipeStore};
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::{debug, info, instrument, warn}; use tracing::{debug, info, instrument};
use crate::api::ParseError;
#[instrument(fields(recipe_dir=?recipe_dir_path), skip_all)]
pub async fn get_recipes(recipe_dir_path: PathBuf) -> Result<Vec<String>, 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)
}
#[derive(RustEmbed)] #[derive(RustEmbed)]
#[folder = "../web/dist"] #[folder = "../web/dist"]
@ -102,13 +70,13 @@ async fn ui_static_assets(Path(path): Path<String>) -> impl IntoResponse {
#[instrument] #[instrument]
async fn api_recipes(Extension(store): Extension<Arc<recipe_store::AsyncFileStore>>) -> Response { async fn api_recipes(Extension(store): Extension<Arc<recipe_store::AsyncFileStore>>) -> Response {
let result: Result<axum::Json<Vec<String>>, String> = match store let result: Result<axum::Json<Vec<RecipeEntry>>, String> = match store
.get_recipes() .get_recipes()
.await .await
.map_err(|e| format!("Error: {:?}", e)) .map_err(|e| format!("Error: {:?}", e))
{ {
Ok(Some(recipes)) => Ok(axum::Json::from(recipes)), Ok(Some(recipes)) => Ok(axum::Json::from(recipes)),
Ok(None) => Ok(axum::Json::from(Vec::<String>::new())), Ok(None) => Ok(axum::Json::from(Vec::<RecipeEntry>::new())),
Err(e) => Err(e), Err(e) => Err(e),
}; };
result.into_response() result.into_response()

View File

@ -11,3 +11,5 @@ async-trait = "0.1.57"
async-std = "1.10.0" async-std = "1.10.0"
tracing = "0.1.35" tracing = "0.1.35"
reqwasm = "0.5.0" reqwasm = "0.5.0"
serde_json = "1.0.79"
serde = "1.0.143"

View File

@ -21,6 +21,7 @@ use async_std::{
use async_trait::async_trait; use async_trait::async_trait;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use reqwasm; use reqwasm;
use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use tracing::warn; use tracing::warn;
use tracing::{debug, instrument}; use tracing::{debug, instrument};
@ -60,6 +61,19 @@ where
fn get_user_store(&self, user: String) -> S; 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"))] #[cfg(not(target_arch = "wasm32"))]
#[async_trait] #[async_trait]
/// Define the shared interface to use for interacting with a store of recipes. /// 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. /// Get categories text unparsed.
async fn get_categories(&self) -> Result<Option<String>, Error>; async fn get_categories(&self) -> Result<Option<String>, Error>;
/// Get list of recipe text unparsed. /// Get list of recipe text unparsed.
async fn get_recipes(&self) -> Result<Option<Vec<String>>, Error>; async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error>;
} }
// NOTE(jwall): Futures in webassembly can't implement `Send` easily so we define // 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. /// Get categories text unparsed.
async fn get_categories(&self) -> Result<Option<String>, Error>; async fn get_categories(&self) -> Result<Option<String>, Error>;
/// Get list of recipe text unparsed. /// Get list of recipe text unparsed.
async fn get_recipes(&self) -> Result<Option<Vec<String>>, Error>; async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error>;
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -112,7 +126,7 @@ impl RecipeStore for AsyncFileStore {
Ok(Some(String::from_utf8(contents)?)) Ok(Some(String::from_utf8(contents)?))
} }
async fn get_recipes(&self) -> Result<Option<Vec<String>>, Error> { async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> {
let mut recipe_path = PathBuf::new(); let mut recipe_path = PathBuf::new();
recipe_path.push(&self.path); recipe_path.push(&self.path);
recipe_path.push("recipes"); recipe_path.push("recipes");
@ -129,9 +143,10 @@ impl RecipeStore for AsyncFileStore {
.any(|&s| s == entry.file_name().to_string_lossy().to_string()) .any(|&s| s == entry.file_name().to_string_lossy().to_string())
{ {
// add it to the entry // 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?; let recipe_contents = read_to_string(entry.path()).await?;
entry_vec.push(recipe_contents); entry_vec.push(RecipeEntry(file_name, recipe_contents));
} else { } else {
warn!( warn!(
file = %entry.path().to_string_lossy(), file = %entry.path().to_string_lossy(),
@ -177,7 +192,7 @@ impl RecipeStore for HttpStore {
} }
#[instrument] #[instrument]
async fn get_recipes(&self) -> Result<Option<Vec<String>>, Error> { async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> {
let mut path = self.root.clone(); let mut path = self.root.clone();
path.push_str("/recipes"); path.push_str("/recipes");
let resp = reqwasm::http::Request::get(&path).send().await?; 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))?) Ok(resp.json().await.map_err(|e| format!("{}", e))?)
} }
} }
//
} }

View File

@ -17,7 +17,7 @@ pub enum AppRoutes {
Plan, Plan,
Inventory, Inventory,
Cook, Cook,
Recipe(usize), Recipe(String),
Error(String), Error(String),
NotFound, NotFound,
} }

View File

@ -47,11 +47,12 @@ fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<G> {
} }
#[component(Recipe<G>)] #[component(Recipe<G>)]
pub fn recipe(idx: ReadSignal<usize>) -> View<G> { pub fn recipe(idx: ReadSignal<String>) -> View<G> {
let app_service = get_appservice_from_context(); let app_service = get_appservice_from_context();
let view = Signal::new(View::empty()); let view = Signal::new(View::empty());
create_effect(cloned!((app_service, view) => move || { 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 recipe = recipe.clone();
let title = create_memo(cloned!((recipe) => move || recipe.get().title.clone())); let title = create_memo(cloned!((recipe) => move || recipe.get().title.clone()));
let desc = create_memo( let desc = create_memo(

View File

@ -19,7 +19,7 @@ use tracing::{debug, instrument};
use crate::service::get_appservice_from_context; use crate::service::get_appservice_from_context;
pub struct RecipeCheckBoxProps { pub struct RecipeCheckBoxProps {
pub i: usize, pub i: String,
pub title: ReadSignal<String>, pub title: ReadSignal<String>,
} }
@ -32,18 +32,23 @@ pub fn recipe_selection(props: RecipeCheckBoxProps) -> View<G> {
let app_service = get_appservice_from_context(); let app_service = get_appservice_from_context();
// This is total hack but it works around the borrow issues with // This is total hack but it works around the borrow issues with
// the `view!` macro. // the `view!` macro.
let i = props.i; let id = Rc::new(props.i);
let id_as_str = Rc::new(format!("{}", i)); let count = Signal::new(format!(
let id_cloned_2 = id_as_str.clone(); "{}",
let count = Signal::new(format!("{}", app_service.get_recipe_count_by_index(i))); 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! { view! {
div() { div() {
label(for=id_cloned_2) { a(href=format!("#recipe/{}", i)) { (props.title.get()) } } label(for=for_id) { a(href=href) { (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 |_| { 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(); let mut app_service = app_service.clone();
debug!(idx=%i, count=%(*count.get()), "setting recipe count"); debug!(idx=%id, count=%(*count.get()), "setting recipe count");
app_service.set_recipe_count_by_index(i, count.get().parse().unwrap()); app_service.set_recipe_count_by_index(id.as_ref().to_owned(), count.get().parse().unwrap());
}) }))
} }
} }
} }

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use recipes::Recipe;
use sycamore::{futures::spawn_local_in_scope, prelude::*}; use sycamore::{futures::spawn_local_in_scope, prelude::*};
use tracing::{error, instrument}; use tracing::{error, instrument};
@ -23,7 +24,7 @@ pub fn recipe_selector() -> View<G> {
let app_service = get_appservice_from_context(); let app_service = get_appservice_from_context();
let rows = create_memo(cloned!(app_service => move || { let rows = create_memo(cloned!(app_service => move || {
let mut rows = Vec::new(); 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::<Vec<(String, Signal<Recipe>)>>().chunks(4) {
rows.push(Signal::new(Vec::from(row))); rows.push(Signal::new(Vec::from(row)));
} }
rows rows

View File

@ -18,7 +18,7 @@ use tracing::instrument;
#[derive(Debug)] #[derive(Debug)]
pub struct RecipePageProps { pub struct RecipePageProps {
pub recipe: Signal<usize>, pub recipe: Signal<String>,
} }
#[instrument] #[instrument]

View File

@ -13,7 +13,6 @@
// limitations under the License. // limitations under the License.
use std::fmt::Debug; use std::fmt::Debug;
use std::rc::Rc; use std::rc::Rc;
use std::str::FromStr;
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, error, instrument}; use tracing::{debug, error, instrument};
@ -201,10 +200,7 @@ impl DeriveRoute for AppRoutes {
let parts: Vec<&str> = h.splitn(2, "/").collect(); let parts: Vec<&str> = h.splitn(2, "/").collect();
if let Some(&"#recipe") = parts.get(0) { if let Some(&"#recipe") = parts.get(0) {
if let Some(&idx) = parts.get(1) { if let Some(&idx) = parts.get(1) {
return match usize::from_str(idx) { return AppRoutes::Recipe(idx.to_owned());
Ok(idx) => AppRoutes::Recipe(idx),
Err(e) => AppRoutes::Error(format!("{:?}", e)),
};
} }
} }
error!(origin=%input.0, path=%input.1, hash=%input.2, "Path not found"); error!(origin=%input.0, path=%input.1, hash=%input.2, "Path not found");

View File

@ -35,10 +35,10 @@ pub struct AppService<S>
where where
S: RecipeStore, S: RecipeStore,
{ {
recipes: Signal<Vec<(usize, Signal<Recipe>)>>, recipes: Signal<BTreeMap<String, Signal<Recipe>>>,
staples: Signal<Option<Recipe>>, staples: Signal<Option<Recipe>>,
category_map: Signal<BTreeMap<String, String>>, category_map: Signal<BTreeMap<String, String>>,
menu_list: Signal<BTreeMap<usize, usize>>, menu_list: Signal<BTreeMap<String, usize>>,
store: S, store: S,
} }
@ -48,7 +48,7 @@ where
{ {
pub fn new(store: S) -> Self { pub fn new(store: S) -> Self {
Self { Self {
recipes: Signal::new(Vec::new()), recipes: Signal::new(BTreeMap::new()),
staples: Signal::new(None), staples: Signal::new(None),
category_map: Signal::new(BTreeMap::new()), category_map: Signal::new(BTreeMap::new()),
menu_list: Signal::new(BTreeMap::new()), menu_list: Signal::new(BTreeMap::new()),
@ -122,7 +122,7 @@ where
#[instrument(skip(self))] #[instrument(skip(self))]
pub fn fetch_recipes_from_storage( pub fn fetch_recipes_from_storage(
&self, &self,
) -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> { ) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> {
let storage = self.get_storage()?.unwrap(); let storage = self.get_storage()?.unwrap();
let mut staples = None; let mut staples = None;
match storage match storage
@ -130,10 +130,11 @@ where
.map_err(|e| format!("{:?}", e))? .map_err(|e| format!("{:?}", e))?
{ {
Some(s) => { Some(s) => {
let parsed = from_str::<Vec<String>>(&s).map_err(|e| format!("{}", e))?; let parsed = from_str::<Vec<RecipeEntry>>(&s).map_err(|e| format!("{}", e))?;
let mut parsed_list = Vec::new(); let mut parsed_map = BTreeMap::new();
// TODO(jwall): Utilize the id instead of the index from now on.
for r in parsed { for r in parsed {
let recipe = match parse::as_recipe(&r) { let recipe = match parse::as_recipe(&r.recipe_text()) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
error!("Error parsing recipe {}", e); error!("Error parsing recipe {}", e);
@ -143,10 +144,10 @@ where
if recipe.title == "Staples" { if recipe.title == "Staples" {
staples = Some(recipe); staples = Some(recipe);
} else { } 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)), None => Ok((None, None)),
} }
@ -154,7 +155,7 @@ where
async fn fetch_recipes( async fn fetch_recipes(
&self, &self,
) -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> { ) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> {
Ok(self.fetch_recipes_from_storage()?) Ok(self.fetch_recipes_from_storage()?)
} }
@ -177,8 +178,8 @@ where
Ok(()) Ok(())
} }
pub fn get_recipe_by_index(&self, idx: usize) -> Option<Signal<Recipe>> { pub fn get_recipe_by_index(&self, idx: &str) -> Option<Signal<Recipe>> {
self.recipes.get().get(idx).map(|(_, r)| r.clone()) self.recipes.get().get(idx).map(|r| r.clone())
} }
#[instrument(skip(self))] #[instrument(skip(self))]
@ -190,7 +191,7 @@ where
let recipe_counts = self.menu_list.get(); let recipe_counts = self.menu_list.get();
for (idx, count) in recipe_counts.iter() { for (idx, count) in recipe_counts.iter() {
for _ in 0..*count { 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 { if show_staples {
@ -218,35 +219,35 @@ where
groups 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(); let mut v = (*self.menu_list.get()).clone();
v.insert(i, count); v.insert(i, count);
self.menu_list.set(v); self.menu_list.set(v);
} }
pub fn get_recipe_count_by_index(&self, i: usize) -> usize { pub fn get_recipe_count_by_index(&self, i: &str) -> usize {
self.menu_list.get().get(&i).map(|i| *i).unwrap_or_default() self.menu_list.get().get(i).map(|i| *i).unwrap_or_default()
} }
pub fn get_recipes(&self) -> Signal<Vec<(usize, Signal<Recipe>)>> { pub fn get_recipes(&self) -> Signal<BTreeMap<String, Signal<Recipe>>> {
self.recipes.clone() self.recipes.clone()
} }
pub fn get_menu_list(&self) -> Vec<(usize, usize)> { pub fn get_menu_list(&self) -> Vec<(String, usize)> {
self.menu_list self.menu_list
.get() .get()
.iter() .iter()
// We exclude recipes in the menu_list with count 0 // We exclude recipes in the menu_list with count 0
.filter(|&(_, count)| *count != 0) .filter(|&(_, count)| *count != 0)
.map(|(idx, count)| (*idx, *count)) .map(|(idx, count)| (idx.clone(), *count))
.collect() .collect()
} }
pub fn set_recipes(&mut self, mut recipes: Vec<(usize, Recipe)>) { pub fn set_recipes(&mut self, mut recipes: BTreeMap<String, Recipe>) {
self.recipes.set( self.recipes.set(
recipes recipes
.drain(0..) .iter()
.map(|(i, r)| (i, Signal::new(r))) .map(|(i, r)| (i.clone(), Signal::new(r.clone())))
.collect(), .collect(),
); );
} }

View File

@ -38,7 +38,7 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
CookPage() CookPage()
}, },
AppRoutes::Recipe(idx) => view! { AppRoutes::Recipe(idx) => view! {
RecipePage(RecipePageProps { recipe: Signal::new(*idx) }) RecipePage(RecipePageProps { recipe: Signal::new(idx.clone()) })
}, },
AppRoutes::NotFound => view! { AppRoutes::NotFound => view! {
// TODO(Create a real one) // TODO(Create a real one)