mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -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": [
|
"rust-analyzer.diagnostics.disabled": [
|
||||||
"macro-error"
|
"macro-error"
|
||||||
],
|
],
|
||||||
"rust-analyzer.cargo.features": []
|
|
||||||
}
|
}
|
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -1028,8 +1028,11 @@ dependencies = [
|
|||||||
name = "recipe-store"
|
name = "recipe-store"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"recipes",
|
"recipes",
|
||||||
|
"reqwasm",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -23,7 +23,6 @@ use tracing_subscriber::FmtSubscriber;
|
|||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod store;
|
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
fn create_app<'a>() -> clap::App<'a> {
|
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},
|
routing::{get, Router},
|
||||||
};
|
};
|
||||||
use mime_guess;
|
use mime_guess;
|
||||||
use recipe_store::*;
|
use recipe_store::{self, RecipeStore};
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use tracing::{info, instrument, warn};
|
use tracing::{info, instrument, warn};
|
||||||
|
|
||||||
use crate::api::ParseError;
|
use crate::api::ParseError;
|
||||||
use crate::store;
|
|
||||||
|
|
||||||
#[instrument(fields(recipe_dir=?recipe_dir_path), skip_all)]
|
#[instrument(fields(recipe_dir=?recipe_dir_path), skip_all)]
|
||||||
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> {
|
||||||
@ -101,18 +100,22 @@ async fn ui_static_assets(uri: Uri) -> impl IntoResponse {
|
|||||||
StaticFile(path)
|
StaticFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn api_recipes(Extension(store): Extension<Arc<store::AsyncFileStore>>) -> Response {
|
async fn api_recipes(Extension(store): Extension<Arc<recipe_store::AsyncFileStore>>) -> Response {
|
||||||
let recipe_future = store.get_recipes();
|
let result: Result<axum::Json<Vec<String>>, String> = match store
|
||||||
let result: Result<axum::Json<Vec<String>>, String> =
|
.get_recipes()
|
||||||
match recipe_future.await.map_err(|e| format!("Error: {:?}", e)) {
|
.await
|
||||||
Ok(Some(recipes)) => Ok(axum::Json::from(recipes)),
|
.map_err(|e| format!("Error: {:?}", e))
|
||||||
Ok(None) => Ok(axum::Json::from(Vec::<String>::new())),
|
{
|
||||||
Err(e) => Err(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()
|
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
|
let recipe_result = store
|
||||||
.get_categories()
|
.get_categories()
|
||||||
.await
|
.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)]
|
#[instrument(fields(recipe_dir=?recipe_dir_path,listen=?listen_socket), skip_all)]
|
||||||
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 dir_path = recipe_dir_path.clone();
|
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 dir_path = (&dir_path).clone();
|
||||||
let mut router = Router::new()
|
let router = Router::new()
|
||||||
.layer(Extension(store))
|
.layer(Extension(store))
|
||||||
.route("/ui", ui_static_assets.into_service())
|
.route("/ui", ui_static_assets.into_service())
|
||||||
// recipes api path route
|
// recipes api path route
|
||||||
|
@ -8,3 +8,6 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
recipes = {path = "../recipes" }
|
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"
|
@ -11,12 +11,41 @@
|
|||||||
// 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 async_std::{
|
||||||
|
fs::{read_dir, read_to_string, DirEntry, File},
|
||||||
|
io::{self, ReadExt},
|
||||||
|
path::PathBuf,
|
||||||
|
stream::StreamExt,
|
||||||
|
};
|
||||||
use async_trait::async_trait;
|
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
|
where
|
||||||
S: RecipeStore<E>,
|
S: RecipeStore,
|
||||||
E: Send,
|
|
||||||
{
|
{
|
||||||
fn get_user_store(&self, user: String) -> S;
|
fn get_user_store(&self, user: String) -> S;
|
||||||
}
|
}
|
||||||
@ -24,33 +53,132 @@ where
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
/// Define the shared interface to use for interacting with a store of recipes.
|
/// Define the shared interface to use for interacting with a store of recipes.
|
||||||
pub trait RecipeStore<E>
|
pub trait RecipeStore: Clone + Sized {
|
||||||
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.
|
|
||||||
|
|
||||||
/// Get categories text unparsed.
|
/// 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.
|
/// 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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
/// Define the shared interface to use for interacting with a store of recipes.
|
impl RecipeStore<String> for HttpStore {
|
||||||
pub trait RecipeStore<E>
|
#[instrument]
|
||||||
where
|
async fn get_categories(&self) -> Result<Option<String>, String> {
|
||||||
E: Send,
|
let mut path = self.root.clone();
|
||||||
{
|
path.push_str("/categories");
|
||||||
// NOTE(jwall): For reasons I do not entirely understand yet
|
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||||
// You have to specify that these are both Future + Send below
|
Ok(resp) => resp,
|
||||||
// because the compiler can't figure it out for you.
|
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.
|
#[instrument]
|
||||||
async fn get_categories(&self) -> Result<Option<String>, E>;
|
async fn get_recipes(&self) -> Result<Option<Vec<String>>, String> {
|
||||||
/// Get list of recipe text unparsed.
|
let mut path = self.root.clone();
|
||||||
async fn get_recipes(&self) -> Result<Option<Vec<String>>, E>;
|
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.
|
// 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 crate::service::AppService;
|
|
||||||
|
|
||||||
use recipes;
|
use recipes;
|
||||||
use sycamore::{context::use_context, prelude::*};
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
|
use crate::service::get_appservice_from_context;
|
||||||
|
|
||||||
#[component(Steps<G>)]
|
#[component(Steps<G>)]
|
||||||
fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<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>)]
|
#[component(Recipe<G>)]
|
||||||
pub fn recipe(idx: ReadSignal<usize>) -> View<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());
|
let view = Signal::new(View::empty());
|
||||||
create_effect(cloned!((app_service, view) => move || {
|
create_effect(cloned!((app_service, view) => move || {
|
||||||
if let Some((_, recipe)) = app_service.get_recipes().get().get(*idx.get()) {
|
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.
|
// 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 crate::{components::Recipe, service::AppService};
|
use crate::components::Recipe;
|
||||||
|
|
||||||
use sycamore::{context::use_context, prelude::*};
|
use sycamore::prelude::*;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
use crate::service::get_appservice_from_context;
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
#[component(RecipeList<G>)]
|
#[component(RecipeList<G>)]
|
||||||
pub fn recipe_list() -> View<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());
|
let menu_list = create_memo(move || app_service.get_menu_list());
|
||||||
view! {
|
view! {
|
||||||
h1 { "Recipe List" }
|
h1 { "Recipe List" }
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use sycamore::{context::use_context, prelude::*};
|
use sycamore::prelude::*;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
use crate::service::AppService;
|
use crate::service::get_appservice_from_context;
|
||||||
|
|
||||||
pub struct RecipeCheckBoxProps {
|
pub struct RecipeCheckBoxProps {
|
||||||
pub i: usize,
|
pub i: usize,
|
||||||
@ -29,7 +29,7 @@ pub struct RecipeCheckBoxProps {
|
|||||||
))]
|
))]
|
||||||
#[component(RecipeSelection<G>)]
|
#[component(RecipeSelection<G>)]
|
||||||
pub fn recipe_selection(props: RecipeCheckBoxProps) -> View<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
|
// This is total hack but it works around the borrow issues with
|
||||||
// the `view!` macro.
|
// the `view!` macro.
|
||||||
let i = props.i;
|
let i = props.i;
|
||||||
|
@ -11,15 +11,16 @@
|
|||||||
// 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 crate::{components::recipe_selection::*, service::AppService};
|
use sycamore::{futures::spawn_local_in_scope, prelude::*};
|
||||||
|
|
||||||
use sycamore::{context::use_context, futures::spawn_local_in_scope, prelude::*};
|
|
||||||
use tracing::{error, instrument};
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
|
use crate::components::recipe_selection::*;
|
||||||
|
use crate::service::get_appservice_from_context;
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
#[component(RecipeSelector<G>)]
|
#[component(RecipeSelector<G>)]
|
||||||
pub fn recipe_selector() -> View<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 rows = create_memo(cloned!(app_service => move || {
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
for row in app_service.get_recipes().get().as_slice().chunks(4) {
|
for row in app_service.get_recipes().get().as_slice().chunks(4) {
|
||||||
|
@ -11,16 +11,17 @@
|
|||||||
// 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 crate::service::AppService;
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use sycamore::{context::use_context, prelude::*};
|
use sycamore::prelude::*;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
use crate::service::get_appservice_from_context;
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
#[component(ShoppingList<G>)]
|
#[component(ShoppingList<G>)]
|
||||||
pub fn shopping_list() -> View<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 filtered_keys = Signal::new(BTreeSet::new());
|
||||||
let ingredients_map = Signal::new(BTreeMap::new());
|
let ingredients_map = Signal::new(BTreeMap::new());
|
||||||
let extras = Signal::new(Vec::<(usize, (Signal<String>, Signal<String>))>::new());
|
let extras = Signal::new(Vec::<(usize, (Signal<String>, Signal<String>))>::new());
|
||||||
|
@ -16,7 +16,6 @@ mod components;
|
|||||||
mod pages;
|
mod pages;
|
||||||
mod router_integration;
|
mod router_integration;
|
||||||
mod service;
|
mod service;
|
||||||
mod store;
|
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
@ -13,79 +13,75 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
use reqwasm::http;
|
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 tracing::{debug, error, info, instrument, warn};
|
||||||
use web_sys::{window, Storage};
|
use web_sys::{window, Storage};
|
||||||
|
|
||||||
|
use recipe_store::{AsyncFileStore, RecipeStore};
|
||||||
use recipes::{parse, Ingredient, IngredientAccumulator, Recipe};
|
use recipes::{parse, Ingredient, IngredientAccumulator, Recipe};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub struct AppService {
|
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>)>>,
|
recipes: Signal<Vec<(usize, Signal<Recipe>)>>,
|
||||||
staples: Signal<Option<Recipe>>,
|
staples: Signal<Option<Recipe>>,
|
||||||
category_map: Signal<BTreeMap<String, String>>,
|
category_map: Signal<BTreeMap<String, String>>,
|
||||||
menu_list: Signal<BTreeMap<usize, usize>>,
|
menu_list: Signal<BTreeMap<usize, usize>>,
|
||||||
|
store: S,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppService {
|
impl<S> AppService<S>
|
||||||
pub fn new() -> Self {
|
where
|
||||||
|
S: RecipeStore,
|
||||||
|
{
|
||||||
|
pub fn new(store: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
recipes: Signal::new(Vec::new()),
|
recipes: Signal::new(Vec::new()),
|
||||||
staples: Signal::new(None),
|
staples: Signal::new(None),
|
||||||
category_map: Signal::new(BTreeMap::new()),
|
category_map: Signal::new(BTreeMap::new()),
|
||||||
menu_list: 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()
|
window()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.local_storage()
|
.local_storage()
|
||||||
.map_err(|e| format!("{:?}", e))
|
.map_err(|e| format!("{:?}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
async fn fetch_recipes_http() -> Result<String, String> {
|
async fn synchronize(&self) -> Result<(), 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> {
|
|
||||||
info!("Synchronizing Recipes");
|
info!("Synchronizing Recipes");
|
||||||
let storage = Self::get_storage()?.unwrap();
|
let storage = self.get_storage()?.unwrap();
|
||||||
let recipes = Self::fetch_recipes_http().await?;
|
let recipes = self
|
||||||
|
.store
|
||||||
|
.get_recipes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
storage
|
storage
|
||||||
.set_item("recipes", &recipes)
|
.set_item(
|
||||||
|
"recipes",
|
||||||
|
&(to_string(&recipes).map_err(|e| format!("{:?}", e))?),
|
||||||
|
)
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
info!("Synchronizing categories");
|
info!("Synchronizing categories");
|
||||||
match Self::fetch_categories_http().await {
|
match self.store.get_categories().await {
|
||||||
Ok(Some(categories_content)) => {
|
Ok(Some(categories_content)) => {
|
||||||
debug!(categories=?categories_content);
|
debug!(categories=?categories_content);
|
||||||
storage
|
storage
|
||||||
@ -96,21 +92,23 @@ impl AppService {
|
|||||||
warn!("There is no category file");
|
warn!("There is no category file");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
pub fn fetch_categories_from_storage() -> Result<Option<BTreeMap<String, String>>, String> {
|
pub fn fetch_categories_from_storage(
|
||||||
let storage = Self::get_storage()?.unwrap();
|
&self,
|
||||||
|
) -> Result<Option<BTreeMap<String, String>>, String> {
|
||||||
|
let storage = self.get_storage()?.unwrap();
|
||||||
match storage
|
match storage
|
||||||
.get_item("categories")
|
.get_item("categories")
|
||||||
.map_err(|e| format!("{:?}", e))?
|
.map_err(|e| format!("{:?}", e))?
|
||||||
{
|
{
|
||||||
Some(s) => {
|
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) {
|
match parse::as_categories(&parsed) {
|
||||||
Ok(categories) => Ok(Some(categories)),
|
Ok(categories) => Ok(Some(categories)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -123,18 +121,18 @@ impl AppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument(skip(self))]
|
||||||
pub fn fetch_recipes_from_storage(
|
pub fn fetch_recipes_from_storage(
|
||||||
|
&self,
|
||||||
) -> 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();
|
||||||
let mut staples = None;
|
let mut staples = None;
|
||||||
match storage
|
match storage
|
||||||
.get_item("recipes")
|
.get_item("recipes")
|
||||||
.map_err(|e| format!("{:?}", e))?
|
.map_err(|e| format!("{:?}", e))?
|
||||||
{
|
{
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
let parsed =
|
let parsed = from_str::<Vec<String>>(&s).map_err(|e| format!("{}", e))?;
|
||||||
serde_json::from_str::<Vec<String>>(&s).map_err(|e| format!("{}", e))?;
|
|
||||||
let mut parsed_list = Vec::new();
|
let mut parsed_list = Vec::new();
|
||||||
for r in parsed {
|
for r in parsed {
|
||||||
let recipe = match parse::as_recipe(&r) {
|
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> {
|
async fn fetch_recipes(
|
||||||
Ok(Self::fetch_recipes_from_storage()?)
|
&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> {
|
async fn fetch_categories(&self) -> Result<Option<BTreeMap<String, String>>, String> {
|
||||||
Ok(Self::fetch_categories_from_storage()?)
|
Ok(self.fetch_categories_from_storage()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn refresh(&mut self) -> Result<(), String> {
|
pub async fn refresh(&mut self) -> Result<(), String> {
|
||||||
Self::synchronize().await?;
|
self.synchronize().await?;
|
||||||
debug!("refreshing recipes");
|
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);
|
||||||
}
|
}
|
||||||
debug!("refreshing categories");
|
debug!("refreshing categories");
|
||||||
if let Some(categories) = Self::fetch_categories().await? {
|
if let Some(categories) = self.fetch_categories().await? {
|
||||||
self.set_categories(categories);
|
self.set_categories(categories);
|
||||||
}
|
}
|
||||||
Ok(())
|
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 crate::{app_state::*, components::*, router_integration::*, service::AppService};
|
||||||
use tracing::{debug, error, info, instrument};
|
use tracing::{debug, error, info, instrument};
|
||||||
|
|
||||||
|
use recipe_store::{self, AsyncFileStore};
|
||||||
use sycamore::{
|
use sycamore::{
|
||||||
context::{ContextProvider, ContextProviderProps},
|
context::{ContextProvider, ContextProviderProps},
|
||||||
futures::spawn_local_in_scope,
|
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]
|
#[instrument]
|
||||||
#[component(UI<G>)]
|
#[component(UI<G>)]
|
||||||
pub fn ui() -> View<G> {
|
pub fn ui() -> View<G> {
|
||||||
let app_service = AppService::new();
|
let app_service = get_appservice();
|
||||||
info!("Starting UI");
|
info!("Starting UI");
|
||||||
view! {
|
view! {
|
||||||
// NOTE(jwall): Set the app_service in our toplevel scope. Children will be able
|
// 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();
|
let mut app_service = app_service.clone();
|
||||||
async move {
|
async move {
|
||||||
debug!("fetching recipes");
|
debug!("fetching recipes");
|
||||||
match AppService::fetch_recipes_from_storage() {
|
match app_service.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