mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
parent
d8540e6e7f
commit
b37995edb5
@ -1,4 +1,3 @@
|
|||||||
use std::net::SocketAddr;
|
|
||||||
// Copyright 2022 Jeremy Wall
|
// Copyright 2022 Jeremy Wall
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -12,9 +11,10 @@ use std::net::SocketAddr;
|
|||||||
// 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 std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use async_std::fs::{read_dir, read_to_string, DirEntry};
|
use async_std::fs::{self, read_dir, read_to_string, DirEntry};
|
||||||
use async_std::stream::StreamExt;
|
use async_std::stream::StreamExt;
|
||||||
use static_dir::static_dir;
|
use static_dir::static_dir;
|
||||||
use warp::{http::StatusCode, hyper::Uri, Filter};
|
use warp::{http::StatusCode, hyper::Uri, Filter};
|
||||||
@ -24,12 +24,25 @@ use crate::api::ParseError;
|
|||||||
pub async fn get_recipes(recipe_dir_path: PathBuf) -> Result<Vec<String>, ParseError> {
|
pub async fn get_recipes(recipe_dir_path: PathBuf) -> Result<Vec<String>, ParseError> {
|
||||||
let mut entries = read_dir(recipe_dir_path).await?;
|
let mut entries = read_dir(recipe_dir_path).await?;
|
||||||
let mut entry_vec = Vec::new();
|
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 {
|
while let Some(res) = entries.next().await {
|
||||||
let entry: DirEntry = res?;
|
let entry: DirEntry = res?;
|
||||||
if entry.file_type().await?.is_dir() || entry.file_name().to_string_lossy() != "menu.txt" {
|
|
||||||
|
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
|
// add it to the entry
|
||||||
|
eprintln!("adding recipe file {}", entry.file_name().to_string_lossy());
|
||||||
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(recipe_contents);
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"skipping file {} not a recipe",
|
||||||
|
entry.path().to_string_lossy()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(entry_vec)
|
Ok(entry_vec)
|
||||||
@ -38,24 +51,47 @@ pub async fn get_recipes(recipe_dir_path: PathBuf) -> Result<Vec<String>, ParseE
|
|||||||
pub async fn ui_main(recipe_dir_path: PathBuf, listen_socket: SocketAddr) {
|
pub async fn ui_main(recipe_dir_path: PathBuf, listen_socket: SocketAddr) {
|
||||||
let root = warp::path::end().map(|| warp::redirect::found(Uri::from_static("/ui")));
|
let root = warp::path::end().map(|| warp::redirect::found(Uri::from_static("/ui")));
|
||||||
let ui = warp::path("ui").and(static_dir!("../web/dist"));
|
let ui = warp::path("ui").and(static_dir!("../web/dist"));
|
||||||
|
let dir_path = (&recipe_dir_path).clone();
|
||||||
|
|
||||||
|
// recipes api path route
|
||||||
|
let recipe_path = warp::path("recipes").then(move || {
|
||||||
|
let dir_path = (&dir_path).clone();
|
||||||
|
eprintln!("servicing recipe api request.");
|
||||||
|
async move {
|
||||||
|
match get_recipes(dir_path).await {
|
||||||
|
Ok(recipes) => {
|
||||||
|
warp::reply::with_status(warp::reply::json(&recipes), StatusCode::OK)
|
||||||
|
}
|
||||||
|
Err(e) => warp::reply::with_status(
|
||||||
|
warp::reply::json(&format!("Error: {:?}", e)),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// categories api path route
|
||||||
|
let mut file_path = (&recipe_dir_path).clone();
|
||||||
|
file_path.push("categories.txt");
|
||||||
|
let categories_path = warp::path("categories").then(move || {
|
||||||
|
eprintln!("servicing category api request");
|
||||||
|
let file_path = (&file_path).clone();
|
||||||
|
async move {
|
||||||
|
match fs::metadata(&file_path).await {
|
||||||
|
Ok(_) => {
|
||||||
|
let content = read_to_string(&file_path).await.unwrap();
|
||||||
|
warp::reply::with_status(warp::reply::json(&content), StatusCode::OK)
|
||||||
|
}
|
||||||
|
Err(_) => warp::reply::with_status(
|
||||||
|
warp::reply::json(&"No categories found"),
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
let api = warp::path("api")
|
let api = warp::path("api")
|
||||||
.and(warp::path("v1"))
|
.and(warp::path("v1"))
|
||||||
.and(warp::path("recipes"))
|
.and(recipe_path.or(categories_path));
|
||||||
.then(move || {
|
|
||||||
let recipe_dir_path = (&recipe_dir_path).clone();
|
|
||||||
eprintln!("servicing api request.");
|
|
||||||
async move {
|
|
||||||
match get_recipes(recipe_dir_path).await {
|
|
||||||
Ok(recipes) => {
|
|
||||||
warp::reply::with_status(warp::reply::json(&recipes), StatusCode::OK)
|
|
||||||
}
|
|
||||||
Err(e) => warp::reply::with_status(
|
|
||||||
warp::reply::json(&format!("Error: {:?}", e)),
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let routes = root.or(ui).or(api).with(warp::log("access log"));
|
let routes = root.or(ui).or(api).with(warp::log("access log"));
|
||||||
|
|
||||||
|
@ -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 std::collections::BTreeMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -34,6 +35,89 @@ pub fn as_recipe(i: &str) -> std::result::Result<Recipe, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_categories(i: &str) -> std::result::Result<BTreeMap<String, String>, String> {
|
||||||
|
match categories(StrIter::new(i)) {
|
||||||
|
Result::Abort(e) | Result::Fail(e) => Err(format!("Parse Failure: {:?}", e)),
|
||||||
|
Result::Incomplete(_) => Err(format!("Incomplete categories list can not parse")),
|
||||||
|
Result::Complete(_, c) => Ok(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_fn!(
|
||||||
|
pub categories<StrIter, BTreeMap<String, String>>,
|
||||||
|
do_each!(
|
||||||
|
first_category => category_line,
|
||||||
|
rest => repeat!(category_line),
|
||||||
|
({
|
||||||
|
let mut out = BTreeMap::new();
|
||||||
|
let mut op = |(cat, mut ingredients): (String, Vec<String>)| {
|
||||||
|
for i in ingredients.drain(0..) {
|
||||||
|
out.insert(i, cat.clone());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
op(first_category);
|
||||||
|
for line in rest.iter() {
|
||||||
|
op(line.clone());
|
||||||
|
}
|
||||||
|
out
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
make_fn!(
|
||||||
|
category_line<StrIter, (String, Vec<String>)>,
|
||||||
|
do_each!(
|
||||||
|
cat => until!(text_token!(":")),
|
||||||
|
_ => text_token!(":"),
|
||||||
|
ingredients => cat_ingredients_list,
|
||||||
|
_ => either!(
|
||||||
|
discard!(eoi),
|
||||||
|
discard!(text_token!("\n"))
|
||||||
|
),
|
||||||
|
((cat.trim().to_owned(), ingredients))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
make_fn!(
|
||||||
|
pub cat_ingredients_list<StrIter, Vec<String>>,
|
||||||
|
do_each!(
|
||||||
|
first_ingredient => must!(cat_ingredient),
|
||||||
|
rest => repeat!(cat_ingredient),
|
||||||
|
({
|
||||||
|
let mut ingredients = vec![first_ingredient];
|
||||||
|
ingredients.extend(rest);
|
||||||
|
ingredients
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
make_fn!(
|
||||||
|
pub cat_ingredient<StrIter, String>,
|
||||||
|
do_each!(
|
||||||
|
_ => peek!(not!(
|
||||||
|
either!(
|
||||||
|
discard!(text_token!("|")),
|
||||||
|
discard!(eoi),
|
||||||
|
discard!(text_token!("\n"))
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
ingredient => until!(
|
||||||
|
either!(
|
||||||
|
discard!(text_token!("|")),
|
||||||
|
discard!(eoi),
|
||||||
|
discard!(text_token!("\n"))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
_ => either!(
|
||||||
|
discard!(text_token!("|")),
|
||||||
|
discard!(eoi),
|
||||||
|
discard!(text_token!("\n"))
|
||||||
|
),
|
||||||
|
(ingredient.trim().to_owned())
|
||||||
|
)
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
make_fn!(
|
make_fn!(
|
||||||
pub recipe<StrIter, Recipe>,
|
pub recipe<StrIter, Recipe>,
|
||||||
do_each!(
|
do_each!(
|
||||||
|
@ -77,7 +77,6 @@ macro_rules! assert_normalize {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_volume_normalize() {
|
fn test_volume_normalize() {
|
||||||
assert_normalize!(Tbsp, into_tsp, "not a tablespoon after normalize call");
|
assert_normalize!(Tbsp, into_tsp, "not a tablespoon after normalize call");
|
||||||
assert_normalize!(Floz, into_tbsp, "not a floz after normalize call");
|
|
||||||
assert_normalize!(Cup, into_floz, "not a cup after normalize call");
|
assert_normalize!(Cup, into_floz, "not a cup after normalize call");
|
||||||
assert_normalize!(Pint, into_cup, "not a pint after normalize call");
|
assert_normalize!(Pint, into_cup, "not a pint after normalize call");
|
||||||
assert_normalize!(Qrt, into_pint, "not a qrt after normalize call");
|
assert_normalize!(Qrt, into_pint, "not a qrt after normalize call");
|
||||||
@ -506,3 +505,50 @@ step: ";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_category_line_happy_path() {
|
||||||
|
let line = "Produce: onion|green pepper|bell pepper|corn|potato|green onion|scallions|lettuce";
|
||||||
|
match parse::as_categories(line) {
|
||||||
|
Ok(map) => {
|
||||||
|
assert_eq!(map.len(), 8);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
map.contains_key("onion"),
|
||||||
|
"map does not contain onion {:?}",
|
||||||
|
map
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
assert!(false, "{:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_category_single_ingredient_happy_paths() {
|
||||||
|
let ingredients = vec!["foo", "foo\n", "foo|"];
|
||||||
|
for ingredient in ingredients {
|
||||||
|
match parse::cat_ingredient(StrIter::new(ingredient)) {
|
||||||
|
ParseResult::Complete(_itr, _i) => {
|
||||||
|
// yay we pass
|
||||||
|
}
|
||||||
|
res => {
|
||||||
|
assert!(false, "{:?}", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ingredients_list_happy_path() {
|
||||||
|
let line = "onion|green pepper|bell pepper|corn|potato|green onion|scallions|lettuce|";
|
||||||
|
match parse::cat_ingredients_list(StrIter::new(line)) {
|
||||||
|
ParseResult::Complete(_itr, i) => {
|
||||||
|
assert_eq!(i.len(), 8);
|
||||||
|
}
|
||||||
|
res => {
|
||||||
|
assert!(false, "{:?}", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -32,7 +32,7 @@ pub fn recipe_selector() -> View<G> {
|
|||||||
spawn_local_in_scope(cloned!((app_service) => {
|
spawn_local_in_scope(cloned!((app_service) => {
|
||||||
let mut app_service = app_service.clone();
|
let mut app_service = app_service.clone();
|
||||||
async move {
|
async move {
|
||||||
if let Err(e) = app_service.refresh_recipes().await {
|
if let Err(e) = app_service.refresh().await {
|
||||||
console_error!("{}", e);
|
console_error!("{}", e);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ use recipes::{parse, Ingredient, IngredientAccumulator, IngredientKey, Recipe};
|
|||||||
pub struct AppService {
|
pub struct AppService {
|
||||||
recipes: Signal<Vec<(usize, Signal<Recipe>)>>,
|
recipes: Signal<Vec<(usize, Signal<Recipe>)>>,
|
||||||
staples: Signal<Option<Recipe>>,
|
staples: Signal<Option<Recipe>>,
|
||||||
|
category_map: Signal<Option<BTreeMap<String, String>>>,
|
||||||
menu_list: Signal<BTreeMap<usize, usize>>,
|
menu_list: Signal<BTreeMap<usize, usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ impl AppService {
|
|||||||
Self {
|
Self {
|
||||||
recipes: Signal::new(Vec::new()),
|
recipes: Signal::new(Vec::new()),
|
||||||
staples: Signal::new(None),
|
staples: Signal::new(None),
|
||||||
|
category_map: Signal::new(None),
|
||||||
menu_list: Signal::new(BTreeMap::new()),
|
menu_list: Signal::new(BTreeMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,16 +59,63 @@ impl AppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn synchronize_recipes() -> Result<(), String> {
|
async fn fetch_categories_http() -> Result<Option<String>, String> {
|
||||||
|
let resp = match http::Request::get("/api/v1/categories").send().await {
|
||||||
|
Ok(resp) => resp,
|
||||||
|
Err(e) => return Err(format!("Error: {}", e)),
|
||||||
|
};
|
||||||
|
if resp.status() == 404 {
|
||||||
|
console_debug!("Categories returned 404");
|
||||||
|
return Ok(None);
|
||||||
|
} else if resp.status() != 200 {
|
||||||
|
return Err(format!("Status: {}", resp.status()));
|
||||||
|
} else {
|
||||||
|
console_debug!("We got a valid response back!");
|
||||||
|
return Ok(Some(resp.text().await.map_err(|e| format!("{}", e))?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn synchronize() -> Result<(), String> {
|
||||||
console_log!("Synchronizing Recipes");
|
console_log!("Synchronizing Recipes");
|
||||||
let storage = Self::get_storage()?.unwrap();
|
let storage = Self::get_storage()?.unwrap();
|
||||||
let recipes = Self::fetch_recipes_http().await?;
|
let recipes = Self::fetch_recipes_http().await?;
|
||||||
storage
|
storage
|
||||||
.set_item("recipes", &recipes)
|
.set_item("recipes", &recipes)
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
console_log!("Synchronizing categories");
|
||||||
|
match Self::fetch_categories_http().await {
|
||||||
|
Ok(Some(categories_content)) => {
|
||||||
|
storage
|
||||||
|
.set_item("categories", &categories_content)
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
console_error!("There is no category file");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console_error!("{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fetch_categories_from_storage() -> Result<Option<BTreeMap<String, String>>, String> {
|
||||||
|
let storage = Self::get_storage()?.unwrap();
|
||||||
|
match storage
|
||||||
|
.get_item("categories")
|
||||||
|
.map_err(|e| format!("{:?}", e))?
|
||||||
|
{
|
||||||
|
Some(s) => match parse::as_categories(&s) {
|
||||||
|
Ok(categories) => Ok(Some(categories)),
|
||||||
|
Err(e) => {
|
||||||
|
console_debug!("Error parsing categories {}", e);
|
||||||
|
Err(format!("Error parsing categories {}", e))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fetch_recipes_from_storage(
|
pub fn fetch_recipes_from_storage(
|
||||||
) -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
|
) -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
|
||||||
let storage = Self::get_storage()?.unwrap();
|
let storage = Self::get_storage()?.unwrap();
|
||||||
@ -100,23 +149,25 @@ impl AppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_recipes() -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
|
async fn fetch_recipes() -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
|
||||||
if let (staples, Some(recipes)) = Self::fetch_recipes_from_storage()? {
|
Ok(Self::fetch_recipes_from_storage()?)
|
||||||
return Ok((staples, Some(recipes)));
|
|
||||||
} else {
|
|
||||||
console_debug!("No recipes in cache synchronizing from api");
|
|
||||||
// Try to synchronize first
|
|
||||||
Self::synchronize_recipes().await?;
|
|
||||||
Ok(Self::fetch_recipes_from_storage()?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh_recipes(&mut self) -> Result<(), String> {
|
async fn fetch_categories() -> Result<Option<BTreeMap<String, String>>, String> {
|
||||||
Self::synchronize_recipes().await?;
|
Ok(Self::fetch_categories_from_storage()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh(&mut self) -> Result<(), String> {
|
||||||
|
Self::synchronize().await?;
|
||||||
|
console_debug!("refreshing recipes");
|
||||||
if let (staples, Some(r)) = Self::fetch_recipes().await? {
|
if let (staples, Some(r)) = Self::fetch_recipes().await? {
|
||||||
self.set_recipes(r);
|
self.set_recipes(r);
|
||||||
self.staples.set(staples);
|
self.staples.set(staples);
|
||||||
}
|
}
|
||||||
|
console_debug!("refreshing categories");
|
||||||
|
if let Some(categories) = Self::fetch_categories().await? {
|
||||||
|
self.set_categories(categories);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,4 +221,8 @@ impl AppService {
|
|||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_categories(&mut self, categories: BTreeMap<String, String>) {
|
||||||
|
self.category_map.set(Some(categories));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ macro_rules! console_error {
|
|||||||
{
|
{
|
||||||
use crate::typings::error;
|
use crate::typings::error;
|
||||||
error(&format_args!($($t)*).to_string());
|
error(&format_args!($($t)*).to_string());
|
||||||
}else if cfg!(feature="ssr") {
|
} else if cfg!(feature="ssr") {
|
||||||
print!("ERROR: ");
|
print!("ERROR: ");
|
||||||
println!($($t)*);
|
println!($($t)*);
|
||||||
};
|
};
|
||||||
|
@ -56,7 +56,7 @@ pub fn ui() -> View<G> {
|
|||||||
spawn_local_in_scope(cloned!((page_state, view) => {
|
spawn_local_in_scope(cloned!((page_state, view) => {
|
||||||
let mut app_service = app_service.clone();
|
let mut app_service = app_service.clone();
|
||||||
async move {
|
async move {
|
||||||
match AppService::fetch_recipes().await {
|
match AppService::fetch_recipes_from_storage() {
|
||||||
Ok((_, Some(recipes))) => {
|
Ok((_, Some(recipes))) => {
|
||||||
app_service.set_recipes(recipes);
|
app_service.set_recipes(recipes);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user