diff --git a/planner/src/bin/weekly.rs b/planner/src/bin/weekly.rs index d25634d..a9a4c6a 100644 --- a/planner/src/bin/weekly.rs +++ b/planner/src/bin/weekly.rs @@ -1,117 +1,35 @@ //! The weekly menu planner //! -extern crate cookbook; extern crate planner; +extern crate cookbook; -use self::cookbook::*; -use self::cookbook::recipes::Recipe; -use self::planner::solver::{Variables, Domain, Problem}; - -type DomainValues<'a, V> = Vec<&'a V>; -/// We want a mapping of the week meals (matin, midi, soir) -/// Breakfast => RecipeCategory::Breakfast -/// Lunch => RecipeCategory::MainCourse -/// Dinner => RecipeCategory::MainCourse - -mod template { - //! Exports the [`Template`] struct. - use super::{Domain, DomainValues, Variables, Recipe}; - - type Day = String; - const DAYS: &[&str] = &[ - "Lundi", "Mardi", "Mercredi", - "Jeudi", "Vendredi", "Samedi", - "Dimanche"]; - - /// An enum to discriminate every meals - #[allow(dead_code)] - pub enum Meals { - Breakfast(Day), - Lunch(Day), - Dinner(Day) - } - - impl From for String { - fn from(item: Meals) -> String { - match item { - Meals::Breakfast(d) => format!("{}_Breakfast", d), - Meals::Lunch(d) => format!("{}_Lunch", d), - Meals::Dinner(d) => format!("{}_Dinner", d), - } - } +use std::fmt::Debug; +use std::fmt::Display; +use std::hash::Hash; +use cookbook::*; +use planner::{ + *, Value, + solver::{ + Variables, + Domain, + Problem } +}; - /// A fixed template of meals - /// - /// TODO: mask -> disabled choosen meals - /// initials -> fixed values - pub struct Template; - - impl Template { - - fn keys() -> Vec { - let mut keys = Vec::new(); - for day in DAYS { - for meal in &[Meals::Breakfast, Meals::Lunch, Meals::Dinner] { - keys.push(meal(day.to_string())) - } - } - keys - } - - /// Build a [`Template`] from a variables assignment map, - /// usually a solution returned by solver - pub(super) fn from_variables<'a, V>(_vars: Variables<'a,V>) -> Template{ - Template - } - - /// Builds a vector of variables, to be used with - /// [`ProblemBuilder`]. - pub(super) fn generate_variables(domain: &Domain) - -> Vec<(String, DomainValues, Option<&Recipe>)> - { - use cookbook::recipes::RecipeCategory; - let mut vars = Vec::new(); - for key in Self::keys().into_iter() { - //TODO: Use key variants to set filters on domain - //TODO: Initial values - let _filter: fn(&&Recipe) -> bool = match key { - Meals::Breakfast(_) => |r: &&Recipe| { - r.category == RecipeCategory::Breakfast // Breakfast - }, - Meals::Lunch(_) => |r: &&Recipe| { - r.category as i16 == 2i16 // MainCourse - }, - Meals::Dinner(_) => |r: &&Recipe| { - r.category as i16 == 1i16 // Starter - } - }; - vars.push((key.into(), domain.filter(_filter), None)); - } - vars - } - } -} - -fn ingredients_contains<'a>(_assign: &Variables<'a,Recipe>) -> bool { - true -} - - -fn pretty_output(res: &Variables) -> String { +fn pretty_output(res: &Variables) -> String { let mut repr = String::new(); for (var,value) in res { let value = match value { Some(rec) => &rec.title, None => "---", }; - repr.push_str(&format!("{} => {}\n", var, value)); + repr.push_str(&format!("{:?} => {}\n", var, value)); } repr } -fn get_planning_all_results() -> String { +fn main() { let conn = establish_connection(); let possible_values = recipes::load_all(&conn); let domain = Domain::new(possible_values); @@ -119,15 +37,7 @@ fn get_planning_all_results() -> String { for (var, dom, ini) in template::Template::generate_variables(&domain) { problem = problem.add_variable(var, dom, ini); } - let mut problem = problem - .add_constraint( - ingredients_contains - ) - .finish(); + let mut problem = problem.finish(); let results = problem.solve_all(); - format!("{}\nTotal = {}", pretty_output(&results.first().unwrap()), results.len()) -} - -fn main() { - println!("{}", get_planning_all_results()); + println!("{:?}", pretty_output(results.first().unwrap())); } diff --git a/planner/src/lib.rs b/planner/src/lib.rs index ce3bbca..90ca004 100644 --- a/planner/src/lib.rs +++ b/planner/src/lib.rs @@ -1,11 +1,10 @@ extern crate cookbook; +use cookbook::recipes::Recipe; pub mod solver; +pub mod template; -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} + +pub use solver::{Domain, DomainValues}; +/// We mainly use Recipe as the domain value type +pub type Value = Recipe; diff --git a/planner/src/solver.rs b/planner/src/solver.rs index 695d322..d862d58 100644 --- a/planner/src/solver.rs +++ b/planner/src/solver.rs @@ -2,15 +2,16 @@ //! //! Provides `Variables`, `Domain` structs and `solve_all` function. use std::fmt; +use std::hash::Hash; use std::clone::Clone; use std::collections::HashMap; /// An assignments map of variables -pub type Variables<'a, V> = HashMap>; +pub type Variables<'a, V, K> = HashMap>; -enum Assignment<'a, V> { - Update(String, &'a V), - Clear(String) +enum Assignment<'a, V, K> { + Update(K, &'a V), + Clear(K) } @@ -38,13 +39,17 @@ impl Domain { /// # extern crate planner; /// # use planner::solver::Domain; /// let domain = Domain::new(vec![1,2,3]); - /// fn even(i: &i32) -> bool { i % 2 == 0 }; - /// assert_eq!(&domain.filter(even).values, &vec![2]); + /// fn even(i: &i32) -> bool { + /// i % 2 == 0 + /// }; + /// assert_eq!(domain.filter(even), vec![&2]); + /// assert_eq!(domain.filter(|i: &i32| i % 2 == 1), vec![&1,&3]); /// ``` - pub fn filter(&self, f: fn(&&V) -> bool) -> DomainValues - where V: std::clone::Clone - { - self.values.iter().filter(f).collect() + pub fn filter(&self, filter_func: fn(&V) -> bool) -> DomainValues { + self.values + .iter() + .filter(|v: &&V| filter_func(*v)) + .collect() } } @@ -55,26 +60,31 @@ impl fmt::Debug for Domain { } -pub type Constraint<'a,V> = fn(&Variables<'a,V>) -> bool; +pub type Constraint<'a,V, K> = fn(&Variables<'a,V, K>) -> bool; -pub struct Problem<'a, V> { +pub struct Problem<'a, V, K> { /// The initial assignements map - variables: Variables<'a, V>, + variables: Variables<'a, V, K>, /// Each variable has its associated domain - domains: HashMap>, + domains: HashMap>, /// Set of constraints to validate - constraints: Vec>, + constraints: Vec>, } -impl<'a,V> Problem<'a, V> { +impl<'a,V, K: Eq + Hash + Clone> Problem<'a, V, K> { - pub fn build() -> ProblemBuilder<'a,V> { + pub fn build() -> ProblemBuilder<'a,V, K> { ProblemBuilder::new() } + pub fn from_template(_template: i32) -> Problem<'a, V, K> { + Self::build() + .finish() + } + /// Returns all possible Updates for next assignements, prepended with /// a Clear to ensure the variable is unset before when leaving the branch. - fn _push_updates(&self) -> Option>> { + fn _push_updates(&self) -> Option>> { // TODO: should be able to inject a choosing strategy if let Some((key,_)) = self.variables.iter().find(|(_, val)| val.is_none()) { let domain_values = self.domains.get(key).expect("No domain for variable !"); @@ -100,12 +110,13 @@ impl<'a,V> Problem<'a, V> { return true; } - /// Visit all possible solutions, using a stack (DFS). - pub fn solve_all(&mut self) -> Vec> - where V: Clone + fmt::Debug + /// Returns all complete solutions, after visiting all possible outcomes using a stack (DFS). + pub fn solve_all(&mut self) -> Vec> + where V: Clone + fmt::Debug, + K: Clone + fmt::Debug, { - let mut solutions: Vec> = vec![]; - let mut stack: Vec> = vec![]; + let mut solutions: Vec> = vec![]; + let mut stack: Vec> = vec![]; stack.append(&mut self._push_updates().unwrap()); loop { let node = stack.pop(); @@ -134,10 +145,10 @@ impl<'a,V> Problem<'a, V> { } } -pub struct ProblemBuilder<'a, V>(Problem<'a, V>); +pub struct ProblemBuilder<'a, V, K>(Problem<'a, V, K>); -impl<'a, V> ProblemBuilder<'a, V> { - fn new() -> ProblemBuilder<'a, V> { +impl<'a, V, K: Eq + Hash + Clone> ProblemBuilder<'a, V, K> { + fn new() -> ProblemBuilder<'a, V, K> { ProblemBuilder( Problem{ variables: Variables::new(), @@ -146,21 +157,19 @@ impl<'a, V> ProblemBuilder<'a, V> { }) } - pub fn add_variable(mut self, name: S, domain: Vec<&'a V>, value: Option<&'a V>) -> Self - where S: Into + pub fn add_variable(mut self, name: K, domain: Vec<&'a V>, value: Option<&'a V>) -> Self { - let name = name.into(); self.0.variables.insert(name.clone(), value); self.0.domains.insert(name, domain); self } - pub fn add_constraint(mut self, cons: Constraint<'a,V>) -> Self { + pub fn add_constraint(mut self, cons: Constraint<'a,V, K>) -> Self { self.0.constraints.push(cons); self } - pub fn finish(self) -> Problem<'a, V> { + pub fn finish(self) -> Problem<'a, V, K> { self.0 } } @@ -172,15 +181,15 @@ mod tests { fn test_solver_find_pairs() { use super::*; let domain = Domain::new(vec![1,2,3]); - let mut problem: Problem<_> = Problem::build() + let mut problem: Problem<_, _> = Problem::build() .add_variable(String::from("Left"), domain.all(), None) .add_variable(String::from("Right"), domain.all(), None) - .add_constraint(|assign: &Variables| { + .add_constraint(|assign: &Variables| { assign.get("Left").unwrap() == assign.get("Right").unwrap() }) .finish(); - let solutions: Vec> = vec![ + let solutions: Vec> = vec![ [("Left".to_string(), Some(&3)), ("Right".to_string(), Some(&3)),].iter().cloned().collect(), [("Left".to_string(), Some(&2)), ("Right".to_string(), Some(&2)),].iter().cloned().collect(), [("Left".to_string(), Some(&1)), ("Right".to_string(), Some(&1)),].iter().cloned().collect(), @@ -193,15 +202,15 @@ mod tests { fn test_solver_find_pairs_with_initial() { use super::*; let domain = Domain::new(vec![1,2,3]); - let mut problem: Problem<_> = Problem::build() + let mut problem: Problem<_, _> = Problem::build() .add_variable("Left".to_string(), domain.all(), None) .add_variable("Right".to_string(), domain.all(), Some(&2)) - .add_constraint( |assign: &Variables| { + .add_constraint( |assign: &Variables| { assign.get("Left").unwrap() == assign.get("Right").unwrap() }) .finish(); - let solutions: Vec> = vec![ + let solutions: Vec> = vec![ [("Left".to_string(), Some(&2)), ("Right".to_string(), Some(&2)),].iter().cloned().collect(), ]; diff --git a/planner/src/template.rs b/planner/src/template.rs new file mode 100644 index 0000000..78359a7 --- /dev/null +++ b/planner/src/template.rs @@ -0,0 +1,77 @@ +use super::{Domain, DomainValues, Value}; + +const DAYS: &[&str] = &[ + "Lundi", "Mardi", "Mercredi", + "Jeudi", "Vendredi", "Samedi", + "Dimanche"]; + +/// An enum to discriminate every meals +#[allow(dead_code)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub enum Meal { + Breakfast, + Lunch, + Dinner +} + +type Key<'a> = (&'a str, &'a Meal); + + +/// Options set on a meal +#[derive(Debug, Default)] +struct MealOpts { + is_used: bool, + initial: Option, +} + +/// Lets do a fixed template for now +/// * Breakfast only on weekends. +/// * Lunch has a starter and main course. +/// * Plus dessert on weekend ? +/// * Dinner is just a starter (hot in winter, cold in summer) +pub struct Template; + +impl<'a> Template { + + /// Returns all possible meals + fn keys() -> Vec> { + let mut keys = Vec::new(); + for day in DAYS { + for meal in &[Meal::Breakfast, Meal::Lunch, Meal::Dinner] { + keys.push((*day, meal)) + } + } + keys + } + + /// Builds a vector of variables, to be used with + /// [ProblemBuilder](struct.ProblemBuilder.html). + pub fn generate_variables(domain: &Domain) + -> Vec<(Key, DomainValues, Option<&Value>)> + { + use cookbook::recipes::RecipeCategory; + let mut vars_opts = Vec::new(); + for key in Self::keys().into_iter() { + //TODO: is key used ? MealOpts.is_used + + //TODO: Initial values + let category_filter: fn(&Value) -> bool = match key { + (_, Meal::Breakfast) => |r: &Value| { + r.category == RecipeCategory::Breakfast // Breakfast + }, + (_, Meal::Lunch) => |r: &Value| { + r.category == RecipeCategory::MainCourse // MainCourse + }, + (_, Meal::Dinner) => |r: &Value| { + r.category == RecipeCategory::Starter // Starter + } + }; + + // TODO: has initial ? + //let key_name = format!("{}_{:?}", key.0, key.1); + let initial = None; + vars_opts.push((key, domain.filter(category_filter), initial)); + } + vars_opts + } +} diff --git a/web/Cargo.toml b/web/Cargo.toml index 50e444a..28bb0eb 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" rocket = "0.4.0" rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" } cookbook = { path = "../cookbook/" } +planner = { path = "../planner/" } serde = "1.0" serde_derive = "1.0" diff --git a/web/html/index.html b/web/html/index.html deleted file mode 100644 index f84e719..0000000 --- a/web/html/index.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - Hello Bulma! - - - - -
-
-

Cook Assistant

-

Recettes

-
- -
-
- -

{{ items[active_view].title }}

-
{{ categories[items[active_view].category].name }}
-

Ingredients :

-
    -
  • {{ ing }}
  • -
- -
-
- -
-
-
-
- -
-
-
- -

{{ categories[active_category].name }}

- -
-
-
-
- - - diff --git a/web/src/main.rs b/web/src/main.rs index eeb1131..d3fb4de 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -5,8 +5,9 @@ #[macro_use] extern crate serde_derive; extern crate rocket_cors; extern crate cookbook; +extern crate planner; -use std::path::Path; +use std::path::{Path, PathBuf}; use rocket::{ response::{NamedFile, status::NotFound}, http::Method, @@ -15,8 +16,14 @@ use rocket_cors::{AllowedHeaders, AllowedOrigins, Error}; #[get("/")] fn index() -> Result> { - NamedFile::open(&Path::new("./html/index.html")) - .map_err(|_| NotFound(format!("Server error : index not found"))) + files(PathBuf::from("index.html")) +} + +#[get("/", rank=6)] +fn files(file: PathBuf) -> Result> { + let path = Path::new("vue/dist/").join(file); + NamedFile::open(&path) + .map_err(|_| NotFound(format!("Bad path: {:?}", path))) } mod api { @@ -68,6 +75,27 @@ mod api { pub fn delete_recipe(conn: CookbookDbConn, id: i32) -> Json { Json( recipes::delete(&conn, id) ) } + + #[get("/solver/one")] + pub fn one_solution(conn: CookbookDbConn) -> Json { + use planner::{ + template, + solver::{Domain, Problem} + }; + + let possible_values = recipes::load_all(&conn); + let domain = Domain::new(possible_values); + let mut problem = Problem::build(); + for (var, dom, ini) in template::Template::generate_variables(&domain) { + problem = problem.add_variable(var, dom, ini); + } + let mut problem = problem + .add_constraint(|_| true) + .finish(); + let results = problem.solve_all(); + + Json(format!("{:?}", results.first().unwrap())) + } } fn main() -> Result<(), Error> { @@ -86,8 +114,8 @@ fn main() -> Result<(), Error> { rocket::ignite() .attach(api::CookbookDbConn::fairing()) - .mount("/", routes![index]) - .mount("/api", routes![api::recipes_list, api::delete_recipe]) + .mount("/", routes![index, files]) + .mount("/api", routes![api::recipes_list, api::delete_recipe, api::one_solution]) .attach(cors) .launch(); Ok(()) diff --git a/web/vue/src/App.vue b/web/vue/src/App.vue index 96fa163..c70818f 100644 --- a/web/vue/src/App.vue +++ b/web/vue/src/App.vue @@ -1,10 +1,9 @@ @@ -28,6 +28,7 @@ import 'bulma/css/bulma.css' import Heading from './components/Heading.vue' import RecipeDetails from './components/RecipeDetails.vue' import RecipeList from './components/RecipeList.vue' +import Planner from './components/Planner.vue' @@ -37,10 +38,15 @@ export default { Heading, RecipeDetails, RecipeList, + Planner, }, data () { return { - is_loading: true, + status: { + loading: true, + error: false, + error_msg: "", + }, items: [], // Index of the item // activated for details view @@ -71,10 +77,14 @@ export default { fetch("http://localhost:8000/api/list") .then((res) => res.json()) .then((data) => { + console.log(data); this.items = data; - this.is_loading = false; + this.status.loading = false; }) .catch(function(err) { + this.status.loading = false; + this.status.error = true; + this.statue.error_msg = err; console.error(err); }); } diff --git a/web/vue/src/components/Planner.vue b/web/vue/src/components/Planner.vue new file mode 100644 index 0000000..7d0c01b --- /dev/null +++ b/web/vue/src/components/Planner.vue @@ -0,0 +1,25 @@ + + + diff --git a/web/vue/src/components/RecipeDetails.vue b/web/vue/src/components/RecipeDetails.vue index bf186a6..368fda9 100644 --- a/web/vue/src/components/RecipeDetails.vue +++ b/web/vue/src/components/RecipeDetails.vue @@ -1,6 +1,6 @@ + +