diff --git a/.vscode/settings.json b/.vscode/settings.json index 83ecb76..e61586a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,4 @@ "rust-analyzer.diagnostics.disabled": [ "macro-error" ], - "rust-analyzer.cargo.features": [] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c6c7ade..b3c3281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1028,8 +1028,11 @@ dependencies = [ name = "recipe-store" version = "0.1.0" dependencies = [ + "async-std", "async-trait", "recipes", + "reqwasm", + "tracing", ] [[package]] diff --git a/kitchen/src/main.rs b/kitchen/src/main.rs index 32993ea..70bc13b 100644 --- a/kitchen/src/main.rs +++ b/kitchen/src/main.rs @@ -23,7 +23,6 @@ use tracing_subscriber::FmtSubscriber; pub mod api; mod cli; -mod store; mod web; fn create_app<'a>() -> clap::App<'a> { diff --git a/kitchen/src/store.rs b/kitchen/src/store.rs deleted file mode 100644 index 9d87090..0000000 --- a/kitchen/src/store.rs +++ /dev/null @@ -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>(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, 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>, 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)) - } -} diff --git a/kitchen/src/web.rs b/kitchen/src/web.rs index 8b50d99..3ea2b13 100644 --- a/kitchen/src/web.rs +++ b/kitchen/src/web.rs @@ -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, ParseError> { @@ -101,18 +100,22 @@ async fn ui_static_assets(uri: Uri) -> impl IntoResponse { StaticFile(path) } -async fn api_recipes(Extension(store): Extension>) -> Response { - let recipe_future = store.get_recipes(); - let result: Result>, 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::::new())), - Err(e) => Err(e), - }; +async fn api_recipes(Extension(store): Extension>) -> Response { + let result: Result>, 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::::new())), + Err(e) => Err(e), + }; result.into_response() } -async fn api_categories(Extension(store): Extension>) -> Response { +async fn api_categories( + Extension(store): Extension>, +) -> Response { let recipe_result = store .get_categories() .await @@ -128,9 +131,9 @@ async fn api_categories(Extension(store): Extension>) #[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 diff --git a/recipe-store/Cargo.toml b/recipe-store/Cargo.toml index c19ac47..bde6437 100644 --- a/recipe-store/Cargo.toml +++ b/recipe-store/Cargo.toml @@ -7,4 +7,7 @@ edition = "2021" [dependencies] recipes = {path = "../recipes" } -async-trait = "0.1.57" \ No newline at end of file +async-trait = "0.1.57" +async-std = "1.10.0" +tracing = "0.1.35" +reqwasm = "0.5.0" \ No newline at end of file diff --git a/recipe-store/src/lib.rs b/recipe-store/src/lib.rs index 0ea0d83..88dcc63 100644 --- a/recipe-store/src/lib.rs +++ b/recipe-store/src/lib.rs @@ -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 +#[derive(Debug)] +pub struct Error(String); + +impl From for Error { + fn from(item: std::io::Error) -> Self { + Error(format!("{:?}", item)) + } +} + +impl From for Error { + fn from(item: String) -> Self { + Error(item) + } +} + +impl From for Error { + fn from(item: std::string::FromUtf8Error) -> Self { + Error(format!("{:?}", item)) + } +} + +pub trait TenantStoreFactory where - S: RecipeStore, - 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 -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, E>; + async fn get_categories(&self) -> Result, Error>; /// Get list of recipe text unparsed. - async fn get_recipes(&self) -> Result>, E>; + async fn get_recipes(&self) -> Result>, 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, Error>; + /// Get list of recipe text unparsed. + async fn get_recipes(&self) -> Result>, Error>; +} + +#[cfg(not(target_arch = "wasm32"))] +#[derive(Clone)] +pub struct AsyncFileStore { + path: PathBuf, +} + +impl AsyncFileStore { + pub fn new>(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, 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>, 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 -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 for HttpStore { + #[instrument] + async fn get_categories(&self) -> Result, 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, E>; - /// Get list of recipe text unparsed. - async fn get_recipes(&self) -> Result>, E>; + #[instrument] + async fn get_recipes(&self) -> Result>, 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))?) + } + } + // } diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index d5e82da..f9181ef 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -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)] fn steps(steps: ReadSignal>) -> View { @@ -48,7 +48,7 @@ fn steps(steps: ReadSignal>) -> View { #[component(Recipe)] pub fn recipe(idx: ReadSignal) -> View { - let app_service = use_context::(); + 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()) { diff --git a/web/src/components/recipe_list.rs b/web/src/components/recipe_list.rs index b50624e..35d1e32 100644 --- a/web/src/components/recipe_list.rs +++ b/web/src/components/recipe_list.rs @@ -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)] pub fn recipe_list() -> View { - let app_service = use_context::(); + let app_service = get_appservice_from_context(); let menu_list = create_memo(move || app_service.get_menu_list()); view! { h1 { "Recipe List" } diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index fc41524..75b9053 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -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)] pub fn recipe_selection(props: RecipeCheckBoxProps) -> View { - let app_service = use_context::(); + 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; diff --git a/web/src/components/recipe_selector.rs b/web/src/components/recipe_selector.rs index fc0e470..b50d714 100644 --- a/web/src/components/recipe_selector.rs +++ b/web/src/components/recipe_selector.rs @@ -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)] pub fn recipe_selector() -> View { - let app_service = use_context::(); + 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) { diff --git a/web/src/components/shopping_list.rs b/web/src/components/shopping_list.rs index f8565f4..dda218b 100644 --- a/web/src/components/shopping_list.rs +++ b/web/src/components/shopping_list.rs @@ -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)] pub fn shopping_list() -> View { - let app_service = use_context::(); + 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, Signal))>::new()); diff --git a/web/src/lib.rs b/web/src/lib.rs index f414161..ba81bbb 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -16,7 +16,6 @@ mod components; mod pages; mod router_integration; mod service; -mod store; mod web; use sycamore::prelude::*; diff --git a/web/src/service.rs b/web/src/service.rs index 7457484..eb1d29b 100644 --- a/web/src/service.rs +++ b/web/src/service.rs @@ -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 { + use_context::>() +} +#[cfg(target_arch = "wasm32")] +pub fn get_appservice_from_context() -> AppService { + use_context::>() +} + +#[derive(Clone, Debug)] +pub struct AppService +where + S: RecipeStore, +{ recipes: Signal)>>, staples: Signal>, category_map: Signal>, menu_list: Signal>, + store: S, } -impl AppService { - pub fn new() -> Self { +impl AppService +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, String> { + fn get_storage(&self) -> Result, String> { window() .unwrap() .local_storage() .map_err(|e| format!("{:?}", e)) } - #[instrument] - async fn fetch_recipes_http() -> Result { - 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, 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>, String> { - let storage = Self::get_storage()?.unwrap(); + #[instrument(skip(self))] + pub fn fetch_categories_from_storage( + &self, + ) -> Result>, 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::(&s).map_err(|e| format!("{}", e))?; + let parsed = from_str::(&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, Option>), 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::>(&s).map_err(|e| format!("{}", e))?; + let parsed = from_str::>(&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, Option>), String> { - Ok(Self::fetch_recipes_from_storage()?) + async fn fetch_recipes( + &self, + ) -> Result<(Option, Option>), String> { + Ok(self.fetch_recipes_from_storage()?) } - async fn fetch_categories() -> Result>, String> { - Ok(Self::fetch_categories_from_storage()?) + async fn fetch_categories(&self) -> Result>, 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(()) diff --git a/web/src/store.rs b/web/src/store.rs deleted file mode 100644 index dfdde26..0000000 --- a/web/src/store.rs +++ /dev/null @@ -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 for HttpStore { - async fn get_categories(&self) -> Result, 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>, 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))?) - } - } - // -} diff --git a/web/src/web.rs b/web/src/web.rs index cae6917..b45bbe7 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -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(route: ReadSignal) -> View { }) } +#[cfg(not(target_arch = "wasm32"))] +fn get_appservice() -> AppService { + AppService::new(recipe_store::AsyncFileStore::new("/".to_owned())) +} +#[cfg(target_arch = "wasm32")] +fn get_appservice() -> AppService { + AppService::new(recipe_store::HttpStore::new("/api/v1".to_owned())) +} + #[instrument] #[component(UI)] pub fn ui() -> View { - 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 { 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); }