mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-21 19:29:49 -04:00
Use the store interface in the UI service
This commit is contained in:
parent
13443af51d
commit
ca21beb04a
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -2,5 +2,4 @@
|
||||
"rust-analyzer.diagnostics.disabled": [
|
||||
"macro-error"
|
||||
],
|
||||
"rust-analyzer.cargo.features": []
|
||||
}
|
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -1028,8 +1028,11 @@ dependencies = [
|
||||
name = "recipe-store"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"recipes",
|
||||
"reqwasm",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -23,7 +23,6 @@ use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
pub mod api;
|
||||
mod cli;
|
||||
mod store;
|
||||
mod web;
|
||||
|
||||
fn create_app<'a>() -> clap::App<'a> {
|
||||
|
@ -1,93 +0,0 @@
|
||||
// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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 async_std::{
|
||||
fs::{read_dir, read_to_string, DirEntry, File},
|
||||
io::{self, ReadExt},
|
||||
path::PathBuf,
|
||||
stream::StreamExt,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use recipe_store::RecipeStore;
|
||||
|
||||
pub struct AsyncFileStore {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl AsyncFileStore {
|
||||
pub fn new<P: Into<PathBuf>>(root: P) -> Self {
|
||||
Self { path: root.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
// TODO(jwall): We need to model our own set of errors for this.
|
||||
impl RecipeStore<io::Error> for AsyncFileStore {
|
||||
#[instrument(skip_all)]
|
||||
async fn get_categories(&self) -> Result<Option<String>, io::Error> {
|
||||
let mut category_path = PathBuf::new();
|
||||
category_path.push(&self.path);
|
||||
category_path.push("categories.txt");
|
||||
let category_file = match File::open(&category_path).await {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
if let io::ErrorKind::NotFound = e.kind() {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let mut buf_reader = io::BufReader::new(category_file);
|
||||
let mut contents = Vec::new();
|
||||
if let Err(e) = buf_reader.read_to_end(&mut contents).await {
|
||||
return Err(e);
|
||||
}
|
||||
match String::from_utf8(contents) {
|
||||
Ok(s) => Ok(Some(s)),
|
||||
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<String>>, io::Error> {
|
||||
let mut recipe_path = PathBuf::new();
|
||||
recipe_path.push(&self.path);
|
||||
recipe_path.push("recipes");
|
||||
let mut entries = read_dir(&recipe_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(Some(entry_vec))
|
||||
}
|
||||
}
|
@ -26,12 +26,11 @@ use axum::{
|
||||
routing::{get, Router},
|
||||
};
|
||||
use mime_guess;
|
||||
use recipe_store::*;
|
||||
use recipe_store::{self, RecipeStore};
|
||||
use rust_embed::RustEmbed;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use crate::api::ParseError;
|
||||
use crate::store;
|
||||
|
||||
#[instrument(fields(recipe_dir=?recipe_dir_path), skip_all)]
|
||||
pub async fn get_recipes(recipe_dir_path: PathBuf) -> Result<Vec<String>, ParseError> {
|
||||
@ -101,18 +100,22 @@ async fn ui_static_assets(uri: Uri) -> impl IntoResponse {
|
||||
StaticFile(path)
|
||||
}
|
||||
|
||||
async fn api_recipes(Extension(store): Extension<Arc<store::AsyncFileStore>>) -> Response {
|
||||
let recipe_future = store.get_recipes();
|
||||
let result: Result<axum::Json<Vec<String>>, String> =
|
||||
match recipe_future.await.map_err(|e| format!("Error: {:?}", e)) {
|
||||
Ok(Some(recipes)) => Ok(axum::Json::from(recipes)),
|
||||
Ok(None) => Ok(axum::Json::from(Vec::<String>::new())),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
async fn api_recipes(Extension(store): Extension<Arc<recipe_store::AsyncFileStore>>) -> Response {
|
||||
let result: Result<axum::Json<Vec<String>>, 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())),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
result.into_response()
|
||||
}
|
||||
|
||||
async fn api_categories(Extension(store): Extension<Arc<store::AsyncFileStore>>) -> Response {
|
||||
async fn api_categories(
|
||||
Extension(store): Extension<Arc<recipe_store::AsyncFileStore>>,
|
||||
) -> Response {
|
||||
let recipe_result = store
|
||||
.get_categories()
|
||||
.await
|
||||
@ -128,9 +131,9 @@ async fn api_categories(Extension(store): Extension<Arc<store::AsyncFileStore>>)
|
||||
#[instrument(fields(recipe_dir=?recipe_dir_path,listen=?listen_socket), skip_all)]
|
||||
pub async fn ui_main(recipe_dir_path: PathBuf, listen_socket: SocketAddr) {
|
||||
let dir_path = recipe_dir_path.clone();
|
||||
let store = Arc::new(store::AsyncFileStore::new(dir_path));
|
||||
let store = Arc::new(recipe_store::AsyncFileStore::new(dir_path));
|
||||
//let dir_path = (&dir_path).clone();
|
||||
let mut router = Router::new()
|
||||
let router = Router::new()
|
||||
.layer(Extension(store))
|
||||
.route("/ui", ui_static_assets.into_service())
|
||||
// recipes api path route
|
||||
|
@ -8,3 +8,6 @@ edition = "2021"
|
||||
[dependencies]
|
||||
recipes = {path = "../recipes" }
|
||||
async-trait = "0.1.57"
|
||||
async-std = "1.10.0"
|
||||
tracing = "0.1.35"
|
||||
reqwasm = "0.5.0"
|
@ -11,12 +11,41 @@
|
||||
// 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 async_std::{
|
||||
fs::{read_dir, read_to_string, DirEntry, File},
|
||||
io::{self, ReadExt},
|
||||
path::PathBuf,
|
||||
stream::StreamExt,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use reqwasm;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
pub trait TenantStoreFactory<S, E>
|
||||
#[derive(Debug)]
|
||||
pub struct Error(String);
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(item: std::io::Error) -> Self {
|
||||
Error(format!("{:?}", item))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(item: String) -> Self {
|
||||
Error(item)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(item: std::string::FromUtf8Error) -> Self {
|
||||
Error(format!("{:?}", item))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TenantStoreFactory<S>
|
||||
where
|
||||
S: RecipeStore<E>,
|
||||
E: Send,
|
||||
S: RecipeStore,
|
||||
{
|
||||
fn get_user_store(&self, user: String) -> S;
|
||||
}
|
||||
@ -24,33 +53,132 @@ where
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
/// Define the shared interface to use for interacting with a store of recipes.
|
||||
pub trait RecipeStore<E>
|
||||
where
|
||||
E: Send,
|
||||
{
|
||||
// NOTE(jwall): For reasons I do not entirely understand yet
|
||||
// You have to specify that these are both Future + Send below
|
||||
// because the compiler can't figure it out for you.
|
||||
|
||||
pub trait RecipeStore: Clone + Sized {
|
||||
/// Get categories text unparsed.
|
||||
async fn get_categories(&self) -> Result<Option<String>, E>;
|
||||
async fn get_categories(&self) -> Result<Option<String>, Error>;
|
||||
/// Get list of recipe text unparsed.
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<String>>, E>;
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<String>>, Error>;
|
||||
}
|
||||
|
||||
// NOTE(jwall): Futures in webassembly can't implement `Send` easily so we define
|
||||
// this trait differently based on architecture.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
/// Define the shared interface to use for interacting with a store of recipes.
|
||||
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>;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncFileStore {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl AsyncFileStore {
|
||||
pub fn new<P: Into<PathBuf>>(root: P) -> Self {
|
||||
Self { path: root.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
// TODO(jwall): We need to model our own set of errors for this.
|
||||
impl RecipeStore for AsyncFileStore {
|
||||
#[instrument(skip_all)]
|
||||
async fn get_categories(&self) -> Result<Option<String>, Error> {
|
||||
let mut category_path = PathBuf::new();
|
||||
category_path.push(&self.path);
|
||||
category_path.push("categories.txt");
|
||||
let category_file = File::open(&category_path).await?;
|
||||
let mut buf_reader = io::BufReader::new(category_file);
|
||||
let mut contents = Vec::new();
|
||||
buf_reader.read_to_end(&mut contents).await?;
|
||||
Ok(Some(String::from_utf8(contents)?))
|
||||
}
|
||||
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<String>>, Error> {
|
||||
let mut recipe_path = PathBuf::new();
|
||||
recipe_path.push(&self.path);
|
||||
recipe_path.push("recipes");
|
||||
let mut entries = read_dir(&recipe_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(Some(entry_vec))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct HttpStore {
|
||||
root: String,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl HttpStore {
|
||||
pub fn new(root: String) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
/// Define the shared interface to use for interacting with a store of recipes.
|
||||
pub trait RecipeStore<E>
|
||||
where
|
||||
E: Send,
|
||||
{
|
||||
// NOTE(jwall): For reasons I do not entirely understand yet
|
||||
// You have to specify that these are both Future + Send below
|
||||
// because the compiler can't figure it out for you.
|
||||
impl RecipeStore<String> for HttpStore {
|
||||
#[instrument]
|
||||
async fn get_categories(&self) -> Result<Option<String>, String> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/categories");
|
||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => return Err(format!("Error: {}", e)),
|
||||
};
|
||||
if resp.status() == 404 {
|
||||
debug!("Categories returned 404");
|
||||
Ok(None)
|
||||
} else if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()))
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
let resp = resp.text().await;
|
||||
Ok(Some(resp.map_err(|e| format!("{}", e))?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get categories text unparsed.
|
||||
async fn get_categories(&self) -> Result<Option<String>, E>;
|
||||
/// Get list of recipe text unparsed.
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<String>>, E>;
|
||||
#[instrument]
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<String>>, String> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/recipes");
|
||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => return Err(format!("Error: {}", e)),
|
||||
};
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()))
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(resp.json().await.map_err(|e| format!("{}", e))?)
|
||||
}
|
||||
}
|
||||
//
|
||||
}
|
||||
|
@ -11,10 +11,10 @@
|
||||
// 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 crate::service::AppService;
|
||||
|
||||
use recipes;
|
||||
use sycamore::{context::use_context, prelude::*};
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use crate::service::get_appservice_from_context;
|
||||
|
||||
#[component(Steps<G>)]
|
||||
fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<G> {
|
||||
@ -48,7 +48,7 @@ fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<G> {
|
||||
|
||||
#[component(Recipe<G>)]
|
||||
pub fn recipe(idx: ReadSignal<usize>) -> View<G> {
|
||||
let app_service = use_context::<AppService>();
|
||||
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()) {
|
||||
|
@ -11,15 +11,17 @@
|
||||
// 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 crate::{components::Recipe, service::AppService};
|
||||
use crate::components::Recipe;
|
||||
|
||||
use sycamore::{context::use_context, prelude::*};
|
||||
use sycamore::prelude::*;
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use crate::service::get_appservice_from_context;
|
||||
|
||||
#[instrument]
|
||||
#[component(RecipeList<G>)]
|
||||
pub fn recipe_list() -> View<G> {
|
||||
let app_service = use_context::<AppService>();
|
||||
let app_service = get_appservice_from_context();
|
||||
let menu_list = create_memo(move || app_service.get_menu_list());
|
||||
view! {
|
||||
h1 { "Recipe List" }
|
||||
|
@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
use std::rc::Rc;
|
||||
|
||||
use sycamore::{context::use_context, prelude::*};
|
||||
use sycamore::prelude::*;
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use crate::service::AppService;
|
||||
use crate::service::get_appservice_from_context;
|
||||
|
||||
pub struct RecipeCheckBoxProps {
|
||||
pub i: usize,
|
||||
@ -29,7 +29,7 @@ pub struct RecipeCheckBoxProps {
|
||||
))]
|
||||
#[component(RecipeSelection<G>)]
|
||||
pub fn recipe_selection(props: RecipeCheckBoxProps) -> View<G> {
|
||||
let app_service = use_context::<AppService>();
|
||||
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;
|
||||
|
@ -11,15 +11,16 @@
|
||||
// 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 crate::{components::recipe_selection::*, service::AppService};
|
||||
|
||||
use sycamore::{context::use_context, futures::spawn_local_in_scope, prelude::*};
|
||||
use sycamore::{futures::spawn_local_in_scope, prelude::*};
|
||||
use tracing::{error, instrument};
|
||||
|
||||
use crate::components::recipe_selection::*;
|
||||
use crate::service::get_appservice_from_context;
|
||||
|
||||
#[instrument]
|
||||
#[component(RecipeSelector<G>)]
|
||||
pub fn recipe_selector() -> View<G> {
|
||||
let app_service = use_context::<AppService>();
|
||||
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) {
|
||||
|
@ -11,16 +11,17 @@
|
||||
// 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 crate::service::AppService;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use sycamore::{context::use_context, prelude::*};
|
||||
use sycamore::prelude::*;
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use crate::service::get_appservice_from_context;
|
||||
|
||||
#[instrument]
|
||||
#[component(ShoppingList<G>)]
|
||||
pub fn shopping_list() -> View<G> {
|
||||
let app_service = use_context::<AppService>();
|
||||
let app_service = get_appservice_from_context();
|
||||
let filtered_keys = Signal::new(BTreeSet::new());
|
||||
let ingredients_map = Signal::new(BTreeMap::new());
|
||||
let extras = Signal::new(Vec::<(usize, (Signal<String>, Signal<String>))>::new());
|
||||
|
@ -16,7 +16,6 @@ mod components;
|
||||
mod pages;
|
||||
mod router_integration;
|
||||
mod service;
|
||||
mod store;
|
||||
mod web;
|
||||
|
||||
use sycamore::prelude::*;
|
||||
|
@ -13,79 +13,75 @@
|
||||
// limitations under the License.
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use reqwasm::http;
|
||||
use sycamore::prelude::*;
|
||||
use serde_json::{from_str, to_string};
|
||||
use sycamore::{context::use_context, prelude::*};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use web_sys::{window, Storage};
|
||||
|
||||
use recipe_store::{AsyncFileStore, RecipeStore};
|
||||
use recipes::{parse, Ingredient, IngredientAccumulator, Recipe};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppService {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn get_appservice_from_context() -> AppService<AsyncFileStore> {
|
||||
use_context::<AppService<AsyncFileStore>>()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn get_appservice_from_context() -> AppService<AsyncFileStore> {
|
||||
use_context::<AppService<HttpStore>>()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppService<S>
|
||||
where
|
||||
S: RecipeStore,
|
||||
{
|
||||
recipes: Signal<Vec<(usize, Signal<Recipe>)>>,
|
||||
staples: Signal<Option<Recipe>>,
|
||||
category_map: Signal<BTreeMap<String, String>>,
|
||||
menu_list: Signal<BTreeMap<usize, usize>>,
|
||||
store: S,
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
pub fn new() -> Self {
|
||||
impl<S> AppService<S>
|
||||
where
|
||||
S: RecipeStore,
|
||||
{
|
||||
pub fn new(store: S) -> Self {
|
||||
Self {
|
||||
recipes: Signal::new(Vec::new()),
|
||||
staples: Signal::new(None),
|
||||
category_map: Signal::new(BTreeMap::new()),
|
||||
menu_list: Signal::new(BTreeMap::new()),
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_storage() -> Result<Option<Storage>, String> {
|
||||
fn get_storage(&self) -> Result<Option<Storage>, String> {
|
||||
window()
|
||||
.unwrap()
|
||||
.local_storage()
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn fetch_recipes_http() -> Result<String, String> {
|
||||
let resp = match http::Request::get("/api/v1/recipes").send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => return Err(format!("Error: {}", e)),
|
||||
};
|
||||
if resp.status() != 200 {
|
||||
return Err(format!("Status: {}", resp.status()));
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
return Ok(resp.text().await.map_err(|e| format!("{}", e))?);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
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 {
|
||||
debug!("Categories returned 404");
|
||||
return Ok(None);
|
||||
} else if resp.status() != 200 {
|
||||
return Err(format!("Status: {}", resp.status()));
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
return Ok(Some(resp.text().await.map_err(|e| format!("{}", e))?));
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn synchronize() -> Result<(), String> {
|
||||
#[instrument(skip(self))]
|
||||
async fn synchronize(&self) -> Result<(), String> {
|
||||
info!("Synchronizing Recipes");
|
||||
let storage = Self::get_storage()?.unwrap();
|
||||
let recipes = Self::fetch_recipes_http().await?;
|
||||
let storage = self.get_storage()?.unwrap();
|
||||
let recipes = self
|
||||
.store
|
||||
.get_recipes()
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
storage
|
||||
.set_item("recipes", &recipes)
|
||||
.set_item(
|
||||
"recipes",
|
||||
&(to_string(&recipes).map_err(|e| format!("{:?}", e))?),
|
||||
)
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
info!("Synchronizing categories");
|
||||
match Self::fetch_categories_http().await {
|
||||
match self.store.get_categories().await {
|
||||
Ok(Some(categories_content)) => {
|
||||
debug!(categories=?categories_content);
|
||||
storage
|
||||
@ -96,21 +92,23 @@ impl AppService {
|
||||
warn!("There is no category file");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub fn fetch_categories_from_storage() -> Result<Option<BTreeMap<String, String>>, String> {
|
||||
let storage = Self::get_storage()?.unwrap();
|
||||
#[instrument(skip(self))]
|
||||
pub fn fetch_categories_from_storage(
|
||||
&self,
|
||||
) -> Result<Option<BTreeMap<String, String>>, String> {
|
||||
let storage = self.get_storage()?.unwrap();
|
||||
match storage
|
||||
.get_item("categories")
|
||||
.map_err(|e| format!("{:?}", e))?
|
||||
{
|
||||
Some(s) => {
|
||||
let parsed = serde_json::from_str::<String>(&s).map_err(|e| format!("{}", e))?;
|
||||
let parsed = from_str::<String>(&s).map_err(|e| format!("{}", e))?;
|
||||
match parse::as_categories(&parsed) {
|
||||
Ok(categories) => Ok(Some(categories)),
|
||||
Err(e) => {
|
||||
@ -123,18 +121,18 @@ impl AppService {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[instrument(skip(self))]
|
||||
pub fn fetch_recipes_from_storage(
|
||||
&self,
|
||||
) -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
|
||||
let storage = Self::get_storage()?.unwrap();
|
||||
let storage = self.get_storage()?.unwrap();
|
||||
let mut staples = None;
|
||||
match storage
|
||||
.get_item("recipes")
|
||||
.map_err(|e| format!("{:?}", e))?
|
||||
{
|
||||
Some(s) => {
|
||||
let parsed =
|
||||
serde_json::from_str::<Vec<String>>(&s).map_err(|e| format!("{}", e))?;
|
||||
let parsed = from_str::<Vec<String>>(&s).map_err(|e| format!("{}", e))?;
|
||||
let mut parsed_list = Vec::new();
|
||||
for r in parsed {
|
||||
let recipe = match parse::as_recipe(&r) {
|
||||
@ -156,24 +154,26 @@ impl AppService {
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_recipes() -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
|
||||
Ok(Self::fetch_recipes_from_storage()?)
|
||||
async fn fetch_recipes(
|
||||
&self,
|
||||
) -> Result<(Option<Recipe>, Option<Vec<(usize, Recipe)>>), String> {
|
||||
Ok(self.fetch_recipes_from_storage()?)
|
||||
}
|
||||
|
||||
async fn fetch_categories() -> Result<Option<BTreeMap<String, String>>, String> {
|
||||
Ok(Self::fetch_categories_from_storage()?)
|
||||
async fn fetch_categories(&self) -> Result<Option<BTreeMap<String, String>>, String> {
|
||||
Ok(self.fetch_categories_from_storage()?)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn refresh(&mut self) -> Result<(), String> {
|
||||
Self::synchronize().await?;
|
||||
self.synchronize().await?;
|
||||
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.staples.set(staples);
|
||||
}
|
||||
debug!("refreshing categories");
|
||||
if let Some(categories) = Self::fetch_categories().await? {
|
||||
if let Some(categories) = self.fetch_categories().await? {
|
||||
self.set_categories(categories);
|
||||
}
|
||||
Ok(())
|
||||
|
@ -1,71 +0,0 @@
|
||||
// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// 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 async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
|
||||
use reqwasm;
|
||||
use tracing::debug;
|
||||
|
||||
use recipe_store::RecipeStore;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct HttpStore {
|
||||
root: String,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl HttpStore {
|
||||
pub fn new(root: String) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl RecipeStore<String> for HttpStore {
|
||||
async fn get_categories(&self) -> Result<Option<String>, String> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/categories");
|
||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => return Err(format!("Error: {}", e)),
|
||||
};
|
||||
if resp.status() == 404 {
|
||||
debug!("Categories returned 404");
|
||||
Ok(None)
|
||||
} else if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()))
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
let resp = resp.text().await;
|
||||
Ok(Some(resp.map_err(|e| format!("{}", e))?))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_recipes(&self) -> Result<Option<Vec<String>>, String> {
|
||||
let mut path = self.root.clone();
|
||||
path.push_str("/recipes");
|
||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => return Err(format!("Error: {}", e)),
|
||||
};
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()))
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(resp.json().await.map_err(|e| format!("{}", e))?)
|
||||
}
|
||||
}
|
||||
//
|
||||
}
|
@ -15,6 +15,7 @@ use crate::pages::*;
|
||||
use crate::{app_state::*, components::*, router_integration::*, service::AppService};
|
||||
use tracing::{debug, error, info, instrument};
|
||||
|
||||
use recipe_store::{self, AsyncFileStore};
|
||||
use sycamore::{
|
||||
context::{ContextProvider, ContextProviderProps},
|
||||
futures::spawn_local_in_scope,
|
||||
@ -52,10 +53,19 @@ fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn get_appservice() -> AppService<AsyncFileStore> {
|
||||
AppService::new(recipe_store::AsyncFileStore::new("/".to_owned()))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn get_appservice() -> AppService<HttpStore> {
|
||||
AppService::new(recipe_store::HttpStore::new("/api/v1".to_owned()))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[component(UI<G>)]
|
||||
pub fn ui() -> View<G> {
|
||||
let app_service = AppService::new();
|
||||
let app_service = get_appservice();
|
||||
info!("Starting UI");
|
||||
view! {
|
||||
// NOTE(jwall): Set the app_service in our toplevel scope. Children will be able
|
||||
@ -68,7 +78,7 @@ pub fn ui() -> View<G> {
|
||||
let mut app_service = app_service.clone();
|
||||
async move {
|
||||
debug!("fetching recipes");
|
||||
match AppService::fetch_recipes_from_storage() {
|
||||
match app_service.fetch_recipes_from_storage() {
|
||||
Ok((_, Some(recipes))) => {
|
||||
app_service.set_recipes(recipes);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user