diff --git a/Cargo.lock b/Cargo.lock index 90729b3..bf8543a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-utils" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c77af6f96a4f9e27c8ac23a88407381a31f4a74c3fb985c85aa79b8d898136" +dependencies = [ + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "h2" version = "0.3.10" @@ -834,9 +845,9 @@ checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" dependencies = [ "wasm-bindgen", ] @@ -1255,6 +1266,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "reqwasm" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b9b71430c0b3f076a806c389b5a8f806171a1b9365692b86e9878ad1312879" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -1284,6 +1315,20 @@ name = "serde" version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" @@ -1711,19 +1756,21 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if", + "serde", + "serde_json", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" dependencies = [ "bumpalo", "lazy_static", @@ -1748,9 +1795,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1758,9 +1805,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", "quote", @@ -1771,9 +1818,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" [[package]] name = "wasm-bindgen-test" @@ -1816,13 +1863,15 @@ version = "0.1.0" dependencies = [ "dioxus", "recipes", + "reqwasm", + "wasm-bindgen", ] [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/kitchen/src/api.rs b/kitchen/src/api.rs new file mode 100644 index 0000000..a274d07 --- /dev/null +++ b/kitchen/src/api.rs @@ -0,0 +1,31 @@ +// Copyright 2022 Jeremy Wall +// +// 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. + +#[derive(Debug)] +pub enum ParseError { + IO(std::io::Error), + Syntax(String), +} + +impl From for ParseError { + fn from(err: std::io::Error) -> Self { + ParseError::IO(err) + } +} + +impl From for ParseError { + fn from(s: String) -> Self { + ParseError::Syntax(s) + } +} diff --git a/kitchen/src/cli.rs b/kitchen/src/cli.rs index 4853361..0f38baf 100644 --- a/kitchen/src/cli.rs +++ b/kitchen/src/cli.rs @@ -19,14 +19,9 @@ use std::path::Path; use csv; +use crate::api::ParseError; use recipes::{parse, IngredientAccumulator, Recipe}; -#[derive(Debug)] -pub enum ParseError { - IO(std::io::Error), - Syntax(String), -} - // TODO(jwall): We should think a little more closely about // the error modeling for this application. macro_rules! try_open { @@ -41,18 +36,6 @@ macro_rules! try_open { }; } -impl From for ParseError { - fn from(err: std::io::Error) -> Self { - ParseError::IO(err) - } -} - -impl From for ParseError { - fn from(s: String) -> Self { - ParseError::Syntax(s) - } -} - pub fn parse_recipe

(path: P) -> Result where P: AsRef + Debug, diff --git a/kitchen/src/main.rs b/kitchen/src/main.rs index 67eb96a..1cb1a0c 100644 --- a/kitchen/src/main.rs +++ b/kitchen/src/main.rs @@ -11,14 +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. -mod cli; -mod web; - use std::env; +use std::path::PathBuf; use clap; use clap::{clap_app, crate_authors, crate_version}; +pub mod api; +mod cli; +mod web; + fn create_app<'a, 'b>() -> clap::App<'a, 'b> where 'a: 'b, @@ -39,6 +41,7 @@ where ) (@subcommand serve => (about: "Serve the interface via the web") + (@arg recipe_dir: -d --dir +takes_value "Directory containing recipe files to use") ) ) .setting(clap::AppSettings::SubcommandRequiredElseHelp) @@ -72,8 +75,13 @@ fn main() { eprintln!("{:?}", e); } } - } else if matches.subcommand_matches("serve").is_some() { + } else if let Some(matches) = matches.subcommand_matches("serve") { println!("Launching web interface..."); - async_std::task::block_on(async { web::ui_main().await }); + let recipe_dir_path = if let Some(dir) = matches.value_of("recipe_dir") { + PathBuf::from(dir) + } else { + std::env::current_dir().expect("Unable to get current directory. Bailing out.") + }; + async_std::task::block_on(async { web::ui_main(recipe_dir_path).await }); } } diff --git a/kitchen/src/web.rs b/kitchen/src/web.rs index 3e95fea..61f8879 100644 --- a/kitchen/src/web.rs +++ b/kitchen/src/web.rs @@ -11,16 +11,53 @@ // 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 static_dir::static_dir; -use warp::{hyper::Uri, Filter}; +use std::path::PathBuf; -pub async fn ui_main() { +use async_std::fs::{read_dir, read_to_string, DirEntry}; +use async_std::stream::StreamExt; +use static_dir::static_dir; +use warp::{http::StatusCode, hyper::Uri, Filter}; + +use crate::api::ParseError; + +pub async fn get_recipes(recipe_dir_path: PathBuf) -> Result, ParseError> { + let mut entries = read_dir(recipe_dir_path).await?; + let mut entry_vec = Vec::new(); + while let Some(res) = entries.next().await { + let entry: DirEntry = res?; + if entry.file_type().await?.is_dir() || entry.file_name().to_string_lossy() != "menu.txt" { + // add it to the entry + let recipe_contents = read_to_string(entry.path()).await?; + entry_vec.push(recipe_contents); + } + } + Ok(entry_vec) +} + +pub async fn ui_main(recipe_dir_path: PathBuf) { let root = warp::path::end().map(|| warp::redirect::found(Uri::from_static("/ui"))); let ui = warp::path("ui").and(static_dir!("webdist/")); - // api route goes here eventually. - let api = warp::path!("api" / " v1").map(|| format!("API stuff goes here!")); + // TODO(jwall): Figure out how to make and_then work to simplify the below. + let api = warp::path("api") + .and(warp::path("v1")) + .and(warp::path("recipes")) + .then(move || { + let recipe_dir_path = (&recipe_dir_path).clone(); + eprintln!("servicing api request."); + async move { + match get_recipes(recipe_dir_path).await { + Ok(recipes) => { + warp::reply::with_status(warp::reply::json(&recipes), StatusCode::OK) + } + Err(e) => warp::reply::with_status( + warp::reply::json(&format!("Error: {:?}", e)), + StatusCode::INTERNAL_SERVER_ERROR, + ), + } + } + }); - let routes = root.or(ui).or(api).boxed(); + let routes = root.or(ui).or(api).with(warp::log("access log")); // TODO(jwall): Take listen address as an argument to this function instead. warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; diff --git a/run.sh b/run.sh index 9382e08..ab4ed3e 100755 --- a/run.sh +++ b/run.sh @@ -14,4 +14,4 @@ make clean kitchen cd kitchen -cargo run -- serve \ No newline at end of file +cargo run -- serve --dir ../examples \ No newline at end of file diff --git a/web/Cargo.toml b/web/Cargo.toml index 345400f..555c513 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -7,6 +7,11 @@ edition = "2021" [dependencies] recipes = {path = "../recipes" } +reqwasm = "0.4.0" + +[dependencies.wasm-bindgen] +version = "0.2.79" +#features = [ "console" ] [dependencies.dioxus] version = "0.1.7" diff --git a/web/src/main.rs b/web/src/main.rs index 121e407..560580b 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -11,6 +11,7 @@ // 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. +mod typings; mod web; fn main() { diff --git a/web/src/typings.rs b/web/src/typings.rs new file mode 100644 index 0000000..314e7a0 --- /dev/null +++ b/web/src/typings.rs @@ -0,0 +1,32 @@ +// Copyright 2022 Jeremy Wall +// +// 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 wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + // Use `js_namespace` here to bind `console.log(..)` instead of just + // `log(..)` + #[wasm_bindgen(js_namespace = console)] + pub fn log(s: &str); +} + +#[macro_export] +macro_rules! console_log { + // Note that this is using the `log` function imported above during + // `bare_bones` + ($($t:tt)*) => {{ + use crate::typings::log; + (log(&format_args!($($t)*).to_string())) + }} +} diff --git a/web/src/web.rs b/web/src/web.rs index 449f17c..a2b5142 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -11,10 +11,79 @@ // 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. +#![allow(non_snake_case)] +use std::iter::Iterator; + +use crate::console_log; use dioxus::prelude::*; +use reqwasm::http; + +use recipes::{parse, Recipe}; + +#[derive(Props, PartialEq)] +struct RecipeListProps { + recipe_list: Vec, +} + +/// Component to list available recipes. +fn RecipeList(cx: Scope) -> Element { + let props = use_state(&cx, || RecipeListProps { + recipe_list: vec![], + }); + + use_future(&cx, || { + let props = props.for_async(); + async move { + let req = http::Request::get("/api/v1/recipes").send().await; + match req { + Ok(resp) => { + if resp.status() != 200 { + console_log!("Status: {}", resp.status()); + } else { + console_log!("We got a valid response back!"); + let recipe_list = match resp.json::>().await { + Ok(recipes) => recipes, + Err(e) => { + console_log!("Eror getting recipe list as json {}", e); + Vec::new() + } + }; + let mut parsed_list = Vec::new(); + for r in recipe_list { + let recipe = match parse::as_recipe(&r) { + Ok(r) => r, + Err(e) => { + console_log!("Error parsing recipe {}", e); + break; + } + }; + console_log!("We parsed a recipe {}", recipe.title); + parsed_list.push(recipe); + } + props.set(RecipeListProps { + recipe_list: parsed_list, + }); + } + } + Err(e) => { + console_log!("Error: {}", e); + } + } + } + }); + cx.render(rsx! { + ul { + (&props.recipe_list).into_iter().map(|i| { + let title = &i.title; + rsx!(li { "{title}" }) + }) + } + }) +} pub fn ui(cx: Scope) -> Element { cx.render(rsx! { div { "hello chefs!" } + RecipeList { } }) -} \ No newline at end of file +}