Use the store interface in the UI service

This commit is contained in:
Jeremy Wall 2022-08-12 21:41:19 -04:00
parent 13443af51d
commit ca21beb04a
16 changed files with 268 additions and 284 deletions

View File

@ -2,5 +2,4 @@
"rust-analyzer.diagnostics.disabled": [
"macro-error"
],
"rust-analyzer.cargo.features": []
}

3
Cargo.lock generated
View File

@ -1028,8 +1028,11 @@ dependencies = [
name = "recipe-store"
version = "0.1.0"
dependencies = [
"async-std",
"async-trait",
"recipes",
"reqwasm",
"tracing",
]
[[package]]

View File

@ -23,7 +23,6 @@ use tracing_subscriber::FmtSubscriber;
pub mod api;
mod cli;
mod store;
mod web;
fn create_app<'a>() -> clap::App<'a> {

View File

@ -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))
}
}

View File

@ -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

View File

@ -7,4 +7,7 @@ edition = "2021"
[dependencies]
recipes = {path = "../recipes" }
async-trait = "0.1.57"
async-trait = "0.1.57"
async-std = "1.10.0"
tracing = "0.1.35"
reqwasm = "0.5.0"

View File

@ -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))?)
}
}
//
}

View File

@ -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()) {

View File

@ -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" }

View File

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

View File

@ -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) {

View File

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

View File

@ -16,7 +16,6 @@ mod components;
mod pages;
mod router_integration;
mod service;
mod store;
mod web;
use sycamore::prelude::*;

View File

@ -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(())

View File

@ -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))?)
}
}
//
}

View File

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