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

View File

@ -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<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)
}
use tracing::{debug, info, instrument};
#[derive(RustEmbed)]
#[folder = "../web/dist"]
@ -102,13 +70,13 @@ async fn ui_static_assets(Path(path): Path<String>) -> impl IntoResponse {
#[instrument]
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()
.await
.map_err(|e| format!("Error: {:?}", e))
{
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),
};
result.into_response()

View File

@ -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"
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;
#[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<Option<String>, Error>;
/// 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
@ -79,7 +93,7 @@ pub trait RecipeStore: Clone + Sized {
/// Get categories text unparsed.
async fn get_categories(&self) -> Result<Option<String>, Error>;
/// 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"))]
@ -112,7 +126,7 @@ impl RecipeStore for AsyncFileStore {
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();
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<Option<Vec<String>>, Error> {
async fn get_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, 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))?)
}
}
//
}

View File

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

View File

@ -47,11 +47,12 @@ fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<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 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(

View File

@ -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<String>,
}
@ -32,18 +32,23 @@ pub fn recipe_selection(props: RecipeCheckBoxProps) -> View<G> {
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());
}))
}
}
}

View File

@ -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<G> {
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::<Vec<(String, Signal<Recipe>)>>().chunks(4) {
rows.push(Signal::new(Vec::from(row)));
}
rows

View File

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

View File

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

View File

@ -35,10 +35,10 @@ pub struct AppService<S>
where
S: RecipeStore,
{
recipes: Signal<Vec<(usize, Signal<Recipe>)>>,
recipes: Signal<BTreeMap<String, Signal<Recipe>>>,
staples: Signal<Option<Recipe>>,
category_map: Signal<BTreeMap<String, String>>,
menu_list: Signal<BTreeMap<usize, usize>>,
menu_list: Signal<BTreeMap<String, usize>>,
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<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), 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::<Vec<String>>(&s).map_err(|e| format!("{}", e))?;
let mut parsed_list = Vec::new();
let parsed = from_str::<Vec<RecipeEntry>>(&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<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
) -> Result<(Option<Recipe>, Option<BTreeMap<String, Recipe>>), String> {
Ok(self.fetch_recipes_from_storage()?)
}
@ -177,8 +178,8 @@ where
Ok(())
}
pub fn get_recipe_by_index(&self, idx: usize) -> Option<Signal<Recipe>> {
self.recipes.get().get(idx).map(|(_, r)| r.clone())
pub fn get_recipe_by_index(&self, idx: &str) -> Option<Signal<Recipe>> {
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<Vec<(usize, Signal<Recipe>)>> {
pub fn get_recipes(&self) -> Signal<BTreeMap<String, Signal<Recipe>>> {
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<String, Recipe>) {
self.recipes.set(
recipes
.drain(0..)
.map(|(i, r)| (i, Signal::new(r)))
.iter()
.map(|(i, r)| (i.clone(), Signal::new(r.clone())))
.collect(),
);
}

View File

@ -38,7 +38,7 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
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)