diff --git a/Cargo.lock b/Cargo.lock index 63700d2..b5ab401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,6 +1133,7 @@ dependencies = [ "humantime", "humantime-serde", "migration", + "pulldown-cmark", "redis 0.27.5", "rswind", "rswind_cli", @@ -1141,6 +1142,7 @@ dependencies = [ "serde_json", "tantivy", "tempfile", + "thiserror", "tracing", "tracing-subscriber", "tracing-test", @@ -1811,6 +1813,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -3207,6 +3218,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags 2.6.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quote" version = "1.0.37" @@ -4699,18 +4729,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", @@ -5022,6 +5052,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/Cargo.toml b/Cargo.toml index fa2ef44..ec1236b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ actix-session = { version = "0.10.1", features = ["redis-session-rustls"] } actix-identity = "0.8.0" tantivy = "0.22.0" tempfile = "3.13.0" +pulldown-cmark = "0.12.2" +thiserror = "1.0.65" [build-dependencies] diff --git a/src/main.rs b/src/main.rs index a8b4c4e..172fc8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![feature(iterator_try_collect)] + use actix::SyncArbiter; use actix_files::Files; use actix_identity::IdentityMiddleware; @@ -13,6 +15,7 @@ pub mod entities; pub mod filters; pub mod routes; pub mod types; +pub mod utils; const SESSION_KEY: &'static str = "session"; diff --git a/src/routes.rs b/src/routes.rs index 0a87fa7..26185f0 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -54,7 +54,7 @@ struct SignInForm { page: Page, } -#[derive(Debug, Template)] +#[derive(Debug, Template, Deserialize, Clone)] #[template(path = "recipies/create_form.jinja", ext = "html")] struct RecipeForm { title: String, @@ -293,6 +293,33 @@ async fn recipe_form(admin: Identity) -> RecipeForm { } } +#[post("/create")] +async fn create_recipe( + admin: Identity, + form: Form, + db: Data, +) -> RecipeForm { + let form = form.into_inner(); + let mut failure = form.clone(); + + let title = form.title; + + let mut summary = String::new(); + let parser = pulldown_cmark::Parser::new(&form.summary); + pulldown_cmark::html::push_html(&mut summary, parser); + + let Ok(steps) = crate::utils::parse_steps(&form.steps, 0) else { + failure.error = Some("Invalid steps list".into()); + return failure; + }; + let Ok(ingeredients) = crate::utils::parse_steps(&form.ingeredients, 0) else { + failure.error = Some("Invalid ingeredients list".into()); + return failure; + }; + + failure +} + #[get("/styles.css")] async fn styles_css() -> HttpResponse { HttpResponse::Ok() @@ -309,5 +336,6 @@ pub fn configure(config: &mut actix_web::web::ServiceConfig) { .service(show) .service(search_page) .service(search_results) - .service(recipe_form); + .service(recipe_form) + .service(create_recipe); } diff --git a/src/types.rs b/src/types.rs index 5e8cee7..5dffe43 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,18 @@ use std::str::FromStr; +use serde::Deserialize; + pub type User = String; -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid step list")] + InvalidStepList, + #[error("Invalid ingeredients list")] + InvalidIngeredientList, +} + +#[derive(Debug, PartialEq, Clone, Copy, Deserialize)] pub enum Page { Index, Recipe, @@ -10,7 +20,7 @@ pub enum Page { SignIn, } -#[derive(Debug)] +#[derive(Debug, Deserialize)] pub struct Admin { pub email: String, pub pass: String, diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..664d88b --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,68 @@ +use crate::entities::recipe_ingeredients::ActiveModel as RIAM; +use crate::entities::recipe_steps::ActiveModel as RSAM; +use crate::types::Error::*; +use sea_orm::prelude::*; +use sea_orm::ActivrValue::*; + +pub fn parse_steps(s: &str, recipe_id: u64) -> Result, InvalidStepList> { + s.lines() + .filter(|s| !s.trim().is_empty()) + .try_fold(Vec::new(), |mut v, line| { + let line = line.trim(); + + match line.chars().next() { + Some('*') => { + let line = line.replacen("*", "", 1).trim(); + v.push(RSAM { + body: Set(line.to_string()), + recipe_id: Set(recipe_id), + ..Default::default() + }); + } + Some('>') => { + let line = line.replacen(">", "", 1).trim(); + v.last_mut().ok_or(InvalidStepList)?.hint = Set(line.to_string()); + } + _ => return Err(InvalidStepList), + }; + + Ok(v) + }) +} + +pub fn parse_ingenedients(s: &str, recipe_id: u64) -> Result, InvalidIngeredientList> { + s.lines() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| { + let mut pieces = s + .split_whitespace() + .filter(|s| !s.is_empty()) + .collect::>(); + match pieces.len() { + 0 | 1 => return Err(InvalidIngeredientList), + // no unit + 2 => { + let qty = pieces.remove(0).parse().map_err(|_| InvalidIngeredientList); + Ok(RIAM { + recipe_id: Set(recipe_id), + name: Set(pieces.join(" ")), + qty: Set(qty), + ..Default::default() + }) + } + _ => { + let qty = pieces.remove(0).parse().map_err(|_| InvalidIngeredientList); + let unit = pieces.remove(0); + Ok(RIAM { + recipe_id: Set(recipe_id), + name: Set(pieces.join(" ")), + unit: Set(unit), + qty: Set(qty), + ..Default::default() + }) + } + } + }) + .try_collect() +}