mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-21 19:29:49 -04:00
Pull out a shared store interface
Use it in ui_main on the server to prove it works.
This commit is contained in:
parent
e1ba92938f
commit
2dc561e393
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -773,6 +773,7 @@ dependencies = [
|
||||
"async-std",
|
||||
"clap",
|
||||
"csv",
|
||||
"recipe-store",
|
||||
"recipes",
|
||||
"static_dir",
|
||||
"tracing",
|
||||
@ -1156,6 +1157,13 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "recipe-store"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"recipes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "recipes"
|
||||
version = "0.2.9"
|
||||
|
@ -1,2 +1,2 @@
|
||||
[workspace]
|
||||
members = [ "recipes", "kitchen", "web" ]
|
||||
members = [ "recipes", "kitchen", "web", "recipe-store"]
|
8
examples/categories.txt
Normal file
8
examples/categories.txt
Normal file
@ -0,0 +1,8 @@
|
||||
Produce: onion|green pepper|Green Pepper|bell pepper|corn|potato|green onion|scallions|lettuce|fresh basil|cucumber|celery
|
||||
Meat: ground beef|beef|beef tenderloin|ribeye|filet mignon|pork|chicken|chicken breast|chicken tenders|sausage|hot dogs|bacon|lamb
|
||||
Dairy: milk|oat milk|butter|heavy cream|cheddar cheese|mozarella|cheddar|white american|american|swiss
|
||||
Drinks: can Sprite|can Coke Zero|can ginger ale|can dr pepper|can coke|orange juice|apple juice
|
||||
Dry Goods: sugar|flour|brown sugar|bag powder sugar|bag powdered sugar|baking soda|baking powder
|
||||
Spices: cumin|cinnamon|garlic|clove garlic|garlic powder|paprika|basil|oregano|parsley|celery salt|salt|pepper
|
||||
Cereal: oatmeal|cream of wheat
|
||||
|
@ -10,6 +10,7 @@ edition = "2021"
|
||||
tracing = "0.1.35"
|
||||
tracing-subscriber = "0.3.14"
|
||||
recipes = {path = "../recipes" }
|
||||
recipe-store = {path = "../recipe-store" }
|
||||
csv = "1.1.1"
|
||||
warp = "0.3.2"
|
||||
static_dir = "0.2.0"
|
||||
|
@ -23,6 +23,7 @@ use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
pub mod api;
|
||||
mod cli;
|
||||
mod store;
|
||||
mod web;
|
||||
|
||||
fn create_app<'a>() -> clap::App<'a> {
|
||||
|
93
kitchen/src/store.rs
Normal file
93
kitchen/src/store.rs
Normal file
@ -0,0 +1,93 @@
|
||||
// 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::{
|
||||
io::{self, ReadExt},
|
||||
path::PathBuf,
|
||||
stream::StreamExt,
|
||||
};
|
||||
|
||||
use async_std::fs::{read_dir, read_to_string, DirEntry, File};
|
||||
use recipe_store::{MaybeAsync, RecipeStore};
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
pub struct AsyncFileStore {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl AsyncFileStore {
|
||||
pub fn new<P: Into<PathBuf>>(root: P) -> Self {
|
||||
Self { path: root.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl RecipeStore<io::Error> for AsyncFileStore {
|
||||
#[instrument(fields(path = ?self.path), skip_all)]
|
||||
fn get_categories(&self) -> MaybeAsync<Result<Option<String>, io::Error>> {
|
||||
let mut category_path = PathBuf::new();
|
||||
category_path.push(&self.path);
|
||||
category_path.push("categories.txt");
|
||||
MaybeAsync::Async(Box::pin(async move {
|
||||
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)),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_recipes(&self) -> MaybeAsync<Result<Option<Vec<String>>, io::Error>> {
|
||||
let mut recipe_path = PathBuf::new();
|
||||
recipe_path.push(&self.path);
|
||||
recipe_path.push("recipes");
|
||||
MaybeAsync::Async(Box::pin(async move {
|
||||
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))
|
||||
}))
|
||||
}
|
||||
}
|
@ -14,13 +14,15 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use async_std::fs::{self, read_dir, read_to_string, DirEntry};
|
||||
use async_std::fs::{read_dir, read_to_string, DirEntry};
|
||||
use async_std::stream::StreamExt;
|
||||
use recipe_store::*;
|
||||
use static_dir::static_dir;
|
||||
use tracing::{info, instrument, warn};
|
||||
use warp::{http::StatusCode, hyper::Uri, Filter};
|
||||
|
||||
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> {
|
||||
@ -54,40 +56,54 @@ pub async fn get_recipes(recipe_dir_path: PathBuf) -> Result<Vec<String>, ParseE
|
||||
pub async fn ui_main(recipe_dir_path: PathBuf, listen_socket: SocketAddr) {
|
||||
let root = warp::path::end().map(|| warp::redirect::found(Uri::from_static("/ui")));
|
||||
let ui = warp::path("ui").and(static_dir!("../web/dist"));
|
||||
let dir_path = (&recipe_dir_path).clone();
|
||||
let dir_path = recipe_dir_path.clone();
|
||||
|
||||
// recipes api path route
|
||||
let recipe_path = warp::path("recipes").then(move || {
|
||||
let dir_path = (&dir_path).clone();
|
||||
info!(?dir_path, "servicing recipe api request.");
|
||||
async move {
|
||||
match get_recipes(dir_path).await {
|
||||
Ok(recipes) => {
|
||||
async {
|
||||
let store = store::AsyncFileStore::new(dir_path);
|
||||
let recipe_future = store.get_recipes().as_async();
|
||||
match recipe_future.await {
|
||||
Ok(Ok(Some(recipes))) => {
|
||||
warp::reply::with_status(warp::reply::json(&recipes), StatusCode::OK)
|
||||
}
|
||||
Err(e) => warp::reply::with_status(
|
||||
Ok(Ok(None)) => warp::reply::with_status(
|
||||
warp::reply::json(&Vec::<String>::new()),
|
||||
StatusCode::OK,
|
||||
),
|
||||
Ok(Err(e)) => warp::reply::with_status(
|
||||
warp::reply::json(&format!("Error: {:?}", e)),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
Err(e) => warp::reply::with_status(
|
||||
warp::reply::json(&format!("Error: {}", e)),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// categories api path route
|
||||
let mut file_path = (&recipe_dir_path).clone();
|
||||
file_path.push("categories.txt");
|
||||
let categories_path = warp::path("categories").then(move || {
|
||||
info!(?file_path, "servicing category api request");
|
||||
let file_path = (&file_path).clone();
|
||||
let dir_path = (&recipe_dir_path).clone();
|
||||
async move {
|
||||
match fs::metadata(&file_path).await {
|
||||
Ok(_) => {
|
||||
let content = read_to_string(&file_path).await.unwrap();
|
||||
warp::reply::with_status(warp::reply::json(&content), StatusCode::OK)
|
||||
let store = store::AsyncFileStore::new(dir_path);
|
||||
match store.get_categories().as_async().await {
|
||||
Ok(Ok(Some(categories))) => {
|
||||
warp::reply::with_status(warp::reply::json(&categories), StatusCode::OK)
|
||||
}
|
||||
Err(_) => warp::reply::with_status(
|
||||
warp::reply::json(&"No categories found"),
|
||||
StatusCode::NOT_FOUND,
|
||||
Ok(Ok(None)) => warp::reply::with_status(
|
||||
warp::reply::json(&Vec::<String>::new()),
|
||||
StatusCode::OK,
|
||||
),
|
||||
Ok(Err(e)) => warp::reply::with_status(
|
||||
warp::reply::json(&format!("Error: {:?}", e)),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
Err(e) => warp::reply::with_status(
|
||||
warp::reply::json(&format!("Error: {}", e)),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
9
recipe-store/Cargo.toml
Normal file
9
recipe-store/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "recipe-store"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
recipes = {path = "../recipes" }
|
63
recipe-store/src/lib.rs
Normal file
63
recipe-store/src/lib.rs
Normal file
@ -0,0 +1,63 @@
|
||||
// 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 std::{future::Future, pin::Pin};
|
||||
|
||||
pub enum MaybeAsync<T>
|
||||
where
|
||||
T: Send,
|
||||
{
|
||||
Sync(T),
|
||||
// NOTE(jwall): For reasons I do not entirely understand yet
|
||||
// You have to specify that this is both Future + Send because
|
||||
// the compiler can't figure it out for you.
|
||||
Async(Pin<Box<dyn Future<Output = T> + Send>>),
|
||||
}
|
||||
|
||||
impl<T> MaybeAsync<T>
|
||||
where
|
||||
T: Send,
|
||||
{
|
||||
pub async fn as_async(self) -> Result<T, &'static str> {
|
||||
use MaybeAsync::{Async, Sync};
|
||||
match self {
|
||||
Async(f) => Ok(f.await),
|
||||
Sync(_) => Err("Something went very wrong. Attempted to use Sync as Async."),
|
||||
}
|
||||
}
|
||||
pub fn as_sync(self) -> Result<T, &'static str> {
|
||||
use MaybeAsync::{Async, Sync};
|
||||
match self {
|
||||
Async(_) => Err("Something went very wrong. Attempted to use Async as Sync."),
|
||||
Sync(v) => Ok(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TenantStoreFactory<S, E>
|
||||
where
|
||||
S: RecipeStore<E>,
|
||||
E: Send,
|
||||
{
|
||||
fn get_user_store(&self, user: String) -> S;
|
||||
}
|
||||
|
||||
pub trait RecipeStore<E>
|
||||
where
|
||||
E: Send,
|
||||
{
|
||||
/// Get categories text unparsed.
|
||||
fn get_categories(&self) -> MaybeAsync<Result<Option<String>, E>>;
|
||||
/// Get list of recipe text unparsed.
|
||||
fn get_recipes(&self) -> MaybeAsync<Result<Option<Vec<String>>, E>>;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user