mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-21 19:29:49 -04:00
Use recipe ids instead of indexes
This commit is contained in:
parent
585939b842
commit
91902cebbe
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -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",
|
||||
|
@ -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()
|
||||
|
@ -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"
|
@ -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))?)
|
||||
}
|
||||
}
|
||||
//
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ pub enum AppRoutes {
|
||||
Plan,
|
||||
Inventory,
|
||||
Cook,
|
||||
Recipe(usize),
|
||||
Recipe(String),
|
||||
Error(String),
|
||||
NotFound,
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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());
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -18,7 +18,7 @@ use tracing::instrument;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RecipePageProps {
|
||||
pub recipe: Signal<usize>,
|
||||
pub recipe: Signal<String>,
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
|
@ -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");
|
||||
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user