Compare commits

..

26 Commits

Author SHA1 Message Date
e5b16ae955 some more thinking... 2019-02-19 16:24:05 +01:00
f69d94d758 thinking about rules implementation... 2019-02-19 15:57:27 +01:00
619542357b working on constraint design 2019-02-16 15:13:33 +01:00
4e5aab323e small fixes 2019-02-15 14:18:17 +01:00
4bc04bd7e3 refactors Problem, adds Solution 2019-02-15 13:46:39 +01:00
4b21fd873b breaking things up, refactors Problem with vectors... 2019-02-14 21:50:19 +01:00
04e8c554cc code cleanup 2019-02-14 21:34:31 +01:00
bb965413a8 works on Domain docs and api 2019-02-14 21:11:52 +01:00
3e477945ea small things 2019-02-14 14:36:44 +01:00
3ee9533faf working on rust side 2019-02-13 16:01:24 +01:00
8c89b9c059 nothing really 2019-02-13 14:42:49 +01:00
e29d664f0e makes updating Template work 2019-02-13 14:31:46 +01:00
c522c23dfe makes slot selection work 2019-02-13 14:22:49 +01:00
7121137145 adds Template custom object, starts SlotSelect component 2019-02-12 21:48:09 +01:00
e2e0cc587e lets stop minor design fixes and get real... 2019-02-12 15:24:07 +01:00
1e91d98581 adds solve_one 2019-02-12 14:15:27 +01:00
8801596de7 adds mdi icons, improves recipe design 2019-02-12 14:07:50 +01:00
02bdbf0f2c work in progress on Planner component 2019-02-11 14:49:53 +01:00
45029b87d2 small fix 2019-02-10 21:42:37 +01:00
ccb178ae5a Adds Planner component 2019-02-10 21:29:48 +01:00
2f3993bb9e adds Key generic parameters, use custom Key type from template.rs 2019-02-10 14:43:18 +01:00
29b2f076bf work on planner templates in progress 2019-02-07 21:58:38 +01:00
f220c6c960 Adds vuejs with vue-cli, adds cors fairing to rocket 2019-02-05 21:34:54 +01:00
bcc0564f2a Uses new IngredientListManager :) 2019-02-03 21:42:45 +01:00
b14e566593 makes models and schema modules privates, adds re-exports 2019-02-03 21:12:48 +01:00
d3259c82b3 Works on ingredients 2019-02-03 15:17:43 +01:00
29 changed files with 1465 additions and 342 deletions

20
.gitignore vendored
View File

@@ -16,3 +16,23 @@
# Node.js # Node.js
**/node_modules **/node_modules
**/package-lock.json **/package-lock.json
**/.DS_Store
**/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

Binary file not shown.

View File

@@ -2,7 +2,7 @@
CREATE TABLE recipes ( CREATE TABLE recipes (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
title VARCHAR NOT NULL, title VARCHAR NOT NULL,
category INTEGER NOT NULL, category SMALLINT NOT NULL,
ingredients TEXT NOT NULL, ingredients TEXT NOT NULL,
preparation TEXT NOT NULL preparation TEXT NOT NULL
) )

View File

@@ -1,18 +1,18 @@
extern crate cookbook; extern crate cookbook;
extern crate diesel; extern crate diesel;
use self::cookbook::*; use cookbook::{
use self::models::*; establish_connection,
use self::diesel::prelude::*; recipes::{self, Recipe},
};
fn main() { fn main() {
let conn = establish_connection(); let conn = establish_connection();
let result = recipes::load_all(&conn); let result = recipes::load_all(&conn);
println!("Here are {} recipes [{}]:", result.len(), result.len() * std::mem::size_of::<Recipe>()); println!("Here are {} recipes [{}]:", result.len(), result.len() * std::mem::size_of::<Recipe>());
for rec in result { for rec in result {
println!("*************\n{}\n({:?})", rec.title, rec.category); println!("*************\n{}\n({:?})", rec.title, rec.category);
println!("-------------\n"); println!("-------------\n");
println!("{}", rec.ingredients); println!("{}", rec.ingredients.into_manager(&conn).display_lines());
} }
} }

View File

@@ -1,16 +1,17 @@
extern crate cookbook; extern crate cookbook;
extern crate diesel; extern crate diesel;
use std::io::{Read, stdin}; use std::io::{stdin};
use diesel::SqliteConnection; use diesel::SqliteConnection;
use self::cookbook::*; use cookbook::*;
use self::models::{NewRecipe, fields::RecipeCategory}; use cookbook::recipes::{NewRecipe, RecipeCategory};
use cookbook::ingredients::{IngredientList};
struct CreateRecipe<'a> { struct CreateRecipe<'a> {
connection: &'a SqliteConnection, connection: &'a SqliteConnection,
title: &'a str, title: &'a str,
category: Option<RecipeCategory>, category: Option<RecipeCategory>,
ingredients: String, ingredients: IngredientList,
} }
impl<'a> CreateRecipe<'a> { impl<'a> CreateRecipe<'a> {
@@ -19,7 +20,7 @@ impl<'a> CreateRecipe<'a> {
connection: conn, connection: conn,
title: "New recipe", title: "New recipe",
category: None, category: None,
ingredients: String::new(), ingredients: IngredientList::new(),
} }
} }
@@ -33,25 +34,25 @@ impl<'a> CreateRecipe<'a> {
fn add_ingredient(&mut self, name: String) { fn add_ingredient(&mut self, name: String) {
use crate::ingredients::*; use crate::ingredients::*;
let name = name.trim();
// Check it exists or create // Check it exists or create
if let Some(_ingdt) = find(self.connection, &name) { match get_id_or_create(self.connection, &name) {
println!("="); Ok(id) => {
} else { println!("Got id {}", id);
create(self.connection, &name); self.ingredients.push(id);
println!("+{}", &name); },
Err(_) => println!("Error adding ingredient")
} }
self.ingredients.push_str(&name);
} }
/// Builds a NewRecipe instance from current data and insert it. /// Builds a NewRecipe instance from current data and insert it.
fn insert(self) { fn insert(self) {
let new_recipe = NewRecipe::new( let new_recipe = NewRecipe {
self.title, title: self.title,
self.category.unwrap_or(RecipeCategory::Breakfast), category: self.category.unwrap_or(RecipeCategory::Breakfast),
&self.ingredients, ingredients: self.ingredients,
""); preparation: ""
};
match new_recipe.insert(self.connection) { match new_recipe.insert(self.connection) {
Ok(new) => println!("Added {}", new.title), Ok(new) => println!("Added {}", new.title),
Err(e) => println!("Error: {}", e), Err(e) => println!("Error: {}", e),

View File

@@ -2,8 +2,8 @@
extern crate diesel; extern crate diesel;
extern crate dotenv; extern crate dotenv;
pub mod schema; mod schema;
pub mod models; mod models;
mod importer; mod importer;
@@ -20,7 +20,7 @@ pub fn establish_connection() -> SqliteConnection {
} }
pub mod recipes { pub mod recipes {
use crate::models::{Recipe}; pub use crate::models::{Recipe, NewRecipe, fields::RecipeCategory};
use super::{SqliteConnection, schema}; use super::{SqliteConnection, schema};
use super::diesel::prelude::*; use super::diesel::prelude::*;
@@ -38,36 +38,84 @@ pub mod recipes {
.execute(conn) .execute(conn)
.is_ok() .is_ok()
} }
pub fn get(conn: &SqliteConnection, recipe_id: i32) -> Option<Recipe> {
use self::schema::recipes::dsl::*;
recipes.filter(id.eq(recipe_id))
.first(conn)
.ok()
}
} }
pub mod ingredients { pub mod ingredients {
use crate::models::{Ingredient, NewIngredient}; pub use crate::models::{
Ingredient,
NewIngredient,
fields::{IngredientId, IngredientList},
};
use super::{SqliteConnection, schema}; use super::{SqliteConnection, schema};
use super::diesel::prelude::*; use super::diesel::prelude::*;
pub fn find(conn: &SqliteConnection, name: &str) -> Option<Ingredient> { /// A wrapper of [`IngredientList`] with DB connection capacity.
use self::schema::ingredients::dsl::*; pub struct IngredientListManager<'a>(&'a SqliteConnection, IngredientList);
let results = ingredients.filter(alias.like(name)) impl<'a> IngredientListManager<'a> {
.limit(1) pub fn display_lines(&self) -> String {
.load::<Ingredient>(conn) use self::get;
.expect("Error finding ingredient"); let mut repr = String::new();
for id in self.1.iter() {
if !results.is_empty() { let ingdt = get(self.0, id).expect("Database integrity error");
Some(results.into_iter().nth(0).unwrap()) repr.push_str(&format!("{}\n", ingdt.alias));
} else { }
None repr
} }
} }
pub fn create(conn: &SqliteConnection, name: &str) -> usize { impl<'a> IngredientList {
use self::schema::ingredients; pub fn into_manager(self, conn: &'a SqliteConnection) -> IngredientListManager<'a> {
IngredientListManager(conn, self)
}
}
diesel::insert_into(ingredients::table) /// Returns the id of the ingredient identifiable by `name: &str`
/// If the ingredient does not yet exists, it is inserted in database.
pub fn get_id_or_create(conn: &SqliteConnection, name: &str) -> Result<i32,String> {
if let Some(ingdt) = find(conn, name) {
return Ok(ingdt.id);
} else {
return create(conn, name);
}
}
pub fn get(conn: &SqliteConnection, ingdt_id: &IngredientId) -> Result<Ingredient,String> {
use self::schema::ingredients::dsl::*;
ingredients.filter(id.eq(ingdt_id))
.first::<Ingredient>(conn)
.map_err(|e| format!("Could not retrieve : {}", e))
}
fn find(conn: &SqliteConnection, name: &str) -> Option<Ingredient> {
use self::schema::ingredients::dsl::*;
ingredients.filter(alias.like(name))
.first(conn)
.ok()
}
fn create(conn: &SqliteConnection, name: &str) -> Result<i32,String> {
use self::schema::ingredients::dsl::*;
let _ = diesel::insert_into(ingredients)
.values(&NewIngredient { alias: name }) .values(&NewIngredient { alias: name })
.execute(conn) .execute(conn)
.expect("Error inserting ingredient") .map_err(|e| format!("Error inserting ingredient ! {:?}", e))?;
let inserted = ingredients
.order(id.desc())
.first::<Ingredient>(conn)
.map_err(|e| format!("No ingredient at all ! {:?}", e))?;
Ok(inserted.id)
} }
} }

View File

@@ -15,7 +15,7 @@ pub mod fields {
/// representing the main use of the resulting preparation. /// representing the main use of the resulting preparation.
/// ///
/// It is stored as Integer /// It is stored as Integer
#[derive(Debug, Copy, Clone, FromSqlRow, AsExpression)] #[derive(Debug, Copy, Clone, Eq, PartialEq, FromSqlRow, AsExpression)]
#[sql_type = "SmallInt"] #[sql_type = "SmallInt"]
pub enum RecipeCategory { pub enum RecipeCategory {
Breakfast = 0, Breakfast = 0,
@@ -57,8 +57,7 @@ pub mod fields {
} }
impl<DB: Backend> FromSql<SmallInt, DB> for RecipeCategory impl<DB: Backend> FromSql<SmallInt, DB> for RecipeCategory
where where i16: FromSql<SmallInt, DB>
i16: FromSql<SmallInt, DB>
{ {
fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> { fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> {
let v = i16::from_sql(bytes)?; let v = i16::from_sql(bytes)?;
@@ -68,13 +67,70 @@ pub mod fields {
} }
impl<DB: Backend> ToSql<SmallInt, DB> for RecipeCategory impl<DB: Backend> ToSql<SmallInt, DB> for RecipeCategory
where where i16: ToSql<SmallInt, DB>
i16: ToSql<SmallInt, DB>{ {
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result { fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
i16::to_sql(&(*self as i16), out) i16::to_sql(&(*self as i16), out)
} }
} }
pub type IngredientId = i32;
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
#[sql_type = "Text"]
pub struct IngredientList(Vec<IngredientId>);
/// Just a basic method for now
impl IngredientList {
pub fn new() -> Self {
IngredientList(Vec::new())
}
pub fn as_string(&self) -> String {
self.0.iter()
.map(|i| format!("{}", i))
.collect::<Vec<String>>()
.join(" ")
}
}
impl std::ops::Deref for IngredientList {
type Target = Vec<IngredientId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for IngredientList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<DB: Backend> FromSql<Text, DB> for IngredientList
where String: FromSql<Text, DB>
{
fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> {
let data = String::from_sql(bytes)?;
Ok(
IngredientList(
data.split(" ")
.map(|i| i.parse::<IngredientId>().unwrap())
.collect()
))
}
}
impl<DB: Backend> ToSql<Text, DB> for IngredientList
where String: ToSql<Text, DB>
{
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
String::to_sql(&self.as_string(), out)
}
}
} }
@@ -84,29 +140,26 @@ pub struct Recipe {
pub id: i32, pub id: i32,
pub title: String, pub title: String,
pub category: fields::RecipeCategory, pub category: fields::RecipeCategory,
pub ingredients: String, pub ingredients: fields::IngredientList,
pub preparation: String, pub preparation: String,
} }
impl PartialEq for Recipe {
fn eq(&self, other: &Recipe) -> bool {
self.id == other.id
}
}
#[derive(Insertable, Debug)] #[derive(Insertable, Debug)]
#[table_name="recipes"] #[table_name="recipes"]
pub struct NewRecipe<'a> { pub struct NewRecipe<'a> {
pub title: &'a str, pub title: &'a str,
pub category: fields::RecipeCategory, pub category: fields::RecipeCategory,
pub ingredients: &'a str, pub ingredients: fields::IngredientList,
pub preparation: &'a str, pub preparation: &'a str,
} }
impl<'a> NewRecipe<'a> { impl<'a> NewRecipe<'a> {
pub fn new(title: &'a str, category: fields::RecipeCategory, ingredients: &'a str, preparation: &'a str) -> Self {
NewRecipe{
title,
category,
ingredients,
preparation,
}
}
pub fn insert(self, conn: &SqliteConnection) -> Result<Self, String> { pub fn insert(self, conn: &SqliteConnection) -> Result<Self, String> {
diesel::insert_into(recipes::table) diesel::insert_into(recipes::table)
.values(&self) .values(&self)

View File

@@ -1,80 +1,45 @@
//! The weekly menu planner //! The weekly menu planner
//! //!
extern crate cookbook;
extern crate planner; extern crate planner;
extern crate cookbook;
use self::cookbook::*; use std::time;
use self::cookbook::models::Recipe; use std::fmt::Debug;
use self::planner::solver::{Variables, Domain, Problem}; use std::hash::Hash;
use cookbook::*;
/// We want a mapping of the week meals (matin, midi, soir) use planner::{
/// Breakfast => RecipeCategory::Breakfast *, Value,
/// Lunch => RecipeCategory::MainCourse solver::{
/// Dinner => RecipeCategory::MainCourse Solution,
type Day = String; Domain,
const DAYS: &[&str] = &["Lundi", "Mardi", "Mercredi"]; Problem
enum Meals {
Breakfast(Day),
Lunch(Day),
Dinner(Day)
}
impl Into<String> for Meals {
fn into(self) -> String {
match self {
Meals::Breakfast(d) => format!("{}_Breakfast", d),
Meals::Lunch(d) => format!("{}_Lunch", d),
Meals::Dinner(d) => format!("{}_Dinner", d),
}
} }
} };
/// It may also contains an initial value for each variable
fn generate_variables<V>(domain: &Domain<V>) -> Vec<(String, &Domain<V>, Option<&V>)> {
let mut vars = Vec::new();
for day in DAYS {
vars.push((Meals::Lunch(day.to_string()).into(), domain, None));
vars.push((Meals::Dinner(day.to_string()).into(), domain, None));
}
vars
}
fn ingredients_contains<'a>(assign: &Variables<'a,Recipe>) -> bool {
assign.get("Lundi_Lunch").unwrap().unwrap().ingredients.contains("Patates")
&& !assign.get("Mardi_Lunch").unwrap().unwrap().ingredients.contains("Patates")
}
fn pretty_output(res: &Variables<Recipe>) -> String { fn pretty_output<K: Eq + Hash + Debug>(res: &Solution<Value, K>) -> String {
let mut repr = String::new(); let mut repr = String::new();
for (var,value) in res { for (var,value) in res {
let value = match value { let value = match value {
Some(rec) => &rec.title, Some(rec) => &rec.title,
None => "---", None => "---",
}; };
repr.push_str(&format!("{} => {}\n", var, value)); repr.push_str(&format!("{:?} => {}\n", var, value));
} }
repr repr
} }
fn get_planning_all_results() -> String { fn main() {
let start_time = time::Instant::now();
let conn = establish_connection(); let conn = establish_connection();
let possible_values = recipes::load_all(&conn); let possible_values = recipes::load_all(&conn);
let domain = Domain::new(possible_values); let domain = Domain::new(possible_values);
let mut problem = Problem::build(); let mut problem = Problem::build();
for (var, dom, ini) in generate_variables(&domain) { for (var, dom, ini) in template::Template::generate_variables(&domain) {
problem = problem.add_variable(var, dom, ini); problem = problem.add_variable(var, dom, ini);
} }
let mut problem = problem let mut problem = problem.finish();
.add_constraint(
ingredients_contains
)
.finish();
let results = problem.solve_all(); let results = problem.solve_all();
format!("{}\nTotal = {}", pretty_output(&results.first().unwrap()), results.len()) println!("Took {:?}", start_time.elapsed());
} println!("{}", pretty_output(results.first().unwrap()));
fn main() {
println!("{}", get_planning_all_results());
} }

135
planner/src/constraint.rs Normal file
View File

@@ -0,0 +1,135 @@
//! Constraints building
//!
//! # Ideas
//!
//! Each constraints will be updated on every assignment,
//! thus their status is always inspectable.
//! A constraint applies to a set of variables, identified
//! by a key of type `K`.
//! A constraint owns references to actual values assigned,
//! used to perform checks.
//!
//!
//! The problem is to clarify the way Constraints operate.
//! Do they compute their status from some data on demand ?
//! Do they keep their status updated by watching the Variables
//! they act on ?
//! Worse, do they have superpowers ?
//! Could they filter on a variable domain, according to some other variable
//! state ? This would mean that constraints won't judge a result, but guide
//! the solving process to avoid erroring paths, like a constraint-driven
//! solving. This would be powerfull but maybe far too complex...
//!
//! On the other hand, we can implement a simple Observer pattern, with strong
//! coupling to [`Problem`](crate::solver::Problem).
//! Because of this, we can safely play with private fields of Problem, and in
//! return, provide a public api to build specific constraints.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Status {
Validated,// Solution is valid
Unknown, // Constraint cannot resolve yet (unbound variables)
Violated, // Solution is invalid
}
use std::collections::HashMap;
// *Like this
enum ValueChecker {
AllDifferent,
AllSame,
}
pub struct Constraint<'c, V, K> {
status: Status,
variables: HashMap<&'c K, Option<&'c V>>,
// TODO: add a ValueChecker Trait object,
// or just a simple Enum.*
// to provide the check_status procedure, given
// a vector to variables values.
}
impl<'c, V, K> Constraint<'c, V, K>
where K: Eq + std::hash::Hash,
V: PartialEq,
{
pub fn new(vars: Vec<&'c K>) -> Self {
let len = vars.len();
Self {
status: Status::Unknown,
variables: vars.into_iter()
.zip(vec![None; len])
.collect(),
}
}
pub fn status(&self) -> &Status {
&self.status
}
fn check_status(vars: Vec<&Option<&V>>) -> Status {
/// LEts do an hacky NotEqualConstraint
let vars_len = vars.len();
let set_vars: Vec<&Option<&V>> = vars.into_iter().filter(|v| v.is_some()).collect();
let is_complete = vars_len == set_vars.len();
for (idx, value) in set_vars.iter().enumerate() {
let violated = set_vars.iter()
.enumerate()
.filter(|(i,_)| *i != idx)
.fold(false, |res, (_,v)| {
res || v == value
});
if violated { return Status::Violated; }
}
match is_complete {
true => Status::Validated,
false => Status::Unknown,
}
}
fn update_status(&mut self) {
self.status = Self::check_status(self.variables.values().collect());
}
pub fn update(&mut self, key: &K, new_value: Option<&'c V>) {
if let Some(value) = self.variables.get_mut(key) {
// Actually update values
dbg!(*value = new_value);
self.update_status();
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_all_different_problem() {
use crate::solver::{Domain, Problem};
use super::Constraint;
let domain = Domain::new(vec![1, 2, 3]);
let problem = Problem::build()
.add_variable("Left", domain.all(), None)
.add_variable("Right", domain.all(), None)
.add_constraint(Constraint::new(vec![&"Left", &"Right"]))
.finish();
let solutions = vec![
(("Left", Some(&1)), ("Right", Some(&2))),
(("Left", Some(&1)), ("Right", Some(&3))),
(("Left", Some(&2)), ("Right", Some(&1))),
(("Left", Some(&2)), ("Right", Some(&3))),
(("Left", Some(&3)), ("Right", Some(&1))),
(("Left", Some(&3)), ("Right", Some(&2))),
];
let results = problem.solve_all();
println!("{:#?}", results);
assert!(results.len() == solutions.len());
results.into_iter().for_each(|res| {
let res = (("Left", *res.get("Left").unwrap()),
("Right", *res.get("Right").unwrap())) ;
assert!(solutions.contains(&res));
});
}
}

View File

@@ -1,11 +1,10 @@
extern crate cookbook; extern crate cookbook;
use cookbook::recipes::Recipe;
pub mod solver; pub mod solver;
pub mod template;
pub mod constraint;
#[cfg(test)] pub use solver::{Domain, DomainValues};
mod tests { /// We mainly use Recipe as the domain value type
#[test] pub type Value = Recipe;
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@@ -1,16 +1,56 @@
//! Rules used by the `planner` //! Rules used by the `planner`
//! A rule is a constraint on valid solutions, but also provides insights //! A rule is a constraint on valid solutions, but also provides insights
//! and eventually inferences to optimize the solving process. //! and eventually inferences to optimize the solving process.
//!
//! * Basic repartition
//! * All different meals
//! * Map recipes categories to each meals
//! * (Eating a dish over two days (leftovers))
//! * Nutritional values
//! * Per day : according to user profile (man: 2000kcal, woman: 1800kcal)
//! * Per meal : some meals should have higher nutrional values than others
//!
//! * Ingredients
//! * Per week : should use most of a limited set of ingredients (excluding
//! condiments, ...)
//! * To consume : must use a small set of ingredients (leftovers)
//!
//!
//! Price
//! - Per week : should restrict ingredients cost to a given amount
// Nutritional values enum Status {
// - Per day : according to user profile (man: 2000kcal, woman: 1800kcal) Ok,
// - Per meal : some meals should have higher nutrional values than others Violated,
}
// Ingredients trait Rule {
// - Per week : should use most of a limited set of ingredients (excluding type Key;
// condiments, ...) type Value;
// - To consume : must use a small set of ingredients (leftovers)
//
// Price fn status(&self, state: (Vec<&Self::Key>, Vec<&Self::Value>));
// - Per week : should restrict ingredients cost to a given amount fn update(&self, idx: usize, value: Option<Self::Value>) -> Option<Filter>;
};
struct AllDifferentMeals;
impl Rule for AllDifferentMeals {
type State = Vec<Recipe>;
fn status(&self, _: Self::State) -> Status {
Status::Ok // Always enforced by update rule
}
fn update(&self, _: Self::State) -> Option<Filter> {
// Returns a filter excluding this value from domain.
// so that it is impossible to select the same meal twice.
None
}
}
struct FilterRecipeByMeals; // Essentially work on domain
struct NutritionalByDayAverageReq;
struct NutritionalByMealAverageValues;
struct IngredientsInFridge;
struct IngredientsMustUse;

View File

@@ -2,22 +2,33 @@
//! //!
//! Provides `Variables`, `Domain` structs and `solve_all` function. //! Provides `Variables`, `Domain` structs and `solve_all` function.
use std::fmt; use std::fmt;
use std::hash::Hash;
use std::clone::Clone; use std::clone::Clone;
use std::collections::HashMap; use std::collections::HashMap;
/// An assignments map of variables use crate::constraint::{Status, Constraint};
pub type Variables<'a, V> = HashMap<String, Option<&'a V>>;
/// A solution returned by [`Solver`]
pub type Solution<'a, V, K> = HashMap<K, Option<&'a V>>;
/// An assignments map of variables
type Variables<'a, V> = Vec<Option<&'a V>>;
/// Orders used by solver to update variables
enum Assignment<'a, V> { enum Assignment<'a, V> {
Update(String, &'a V), Update(usize, &'a V),
Clear(String) Clear(usize)
} }
type Domains<'a, V> = HashMap<String, &'a Domain<V>>; /// Collection of references to values owned by a domain.
/// The domain of values that can be assigned to variables pub type DomainValues<'a, V> = Vec<&'a V>;
/// The domain of values that can be assigned to a variable.
/// The values are owned by the instance.
#[derive(Clone)] #[derive(Clone)]
pub struct Domain<V> { pub struct Domain<V> {
pub values: Vec<V> values: Vec<V>
} }
impl<V> Domain<V> { impl<V> Domain<V> {
@@ -25,7 +36,21 @@ impl<V> Domain<V> {
Domain { values } Domain { values }
} }
/// Returns a new domain with the given filter applied to inner values /// Returns references to all values of this instance
///
/// # Examples
///
/// ```
/// # extern crate planner;
/// # use planner::solver::Domain;
/// let domain = Domain::new(vec!["a", "b", "c"]);
/// assert_eq!(domain.all(), vec![&"a", &"b", &"c"]);
/// ```
pub fn all(&self) -> DomainValues<V> {
self.values.iter().collect()
}
/// Returns a collection of references to a filtered
/// subset of this domain.
/// ///
/// # Examples /// # Examples
/// ///
@@ -33,16 +58,45 @@ impl<V> Domain<V> {
/// # extern crate planner; /// # extern crate planner;
/// # use planner::solver::Domain; /// # use planner::solver::Domain;
/// let domain = Domain::new(vec![1,2,3]); /// let domain = Domain::new(vec![1,2,3]);
/// fn even(i: &i32) -> bool { i % 2 == 0 }; /// fn even(i: &i32) -> bool {
/// assert_eq!(&domain.filter(even).values, &vec![2]); /// 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) -> Domain<V> pub fn filter<F>(&self, filter_func: F) -> DomainValues<V>
where V: std::clone::Clone where F: Fn(&V) -> bool
{ {
Domain { self.values
values: self.values.iter().cloned().filter(f).collect(), .iter()
} .filter(|v: &&V| filter_func(*v))
.collect()
} }
/// Wrapper for `find`, returns a optionnal reference
/// to the first found value of this domain.
///
/// # Examples
///
/// ```
/// # extern crate planner;
/// # use planner::solver::Domain;
/// let domain = Domain::new(vec![1,2,3]);
/// fn even(i: &i32) -> bool {
/// *i == 2
/// };
/// assert_eq!(domain.find(even), Some(&2));
/// assert_eq!(domain.find(|i: &i32| i % 2 == 1), Some(&1));
/// assert_eq!(domain.find(|i| *i == 4), None);
/// ```
pub fn find<F>(&self, getter_func: F) -> Option<&V>
where F: Fn(&V) -> bool
{
self.values
.iter()
.find(|v: &&V| getter_func(*v))
}
} }
impl<V: fmt::Debug> fmt::Debug for Domain<V> { impl<V: fmt::Debug> fmt::Debug for Domain<V> {
@@ -51,112 +105,231 @@ impl<V: fmt::Debug> fmt::Debug for Domain<V> {
} }
} }
/// Or we can have a much more complex version of Domain.
pub type Constraint<'a,V> = fn(&Variables<'a,V>) -> bool; /// We want to retrieve a filtered domain for each variable.
/// Filters will be static (filter by category,...) or dynamic
pub struct Problem<'a, V> { /// (inserted by rules updates).
/// The initial assignements map ///
variables: Variables<'a, V>, /// For every variable, we can retrieve its filtered values (values,
/// Each variable has its associated domain /// filtered by all globals, filtered by one local).
domains: Domains<'a,V>, /// Plus, set a dynamic filter that will apply to all other variables.
/// Set of constraints to validate /// Of course, it also affects this variable, but considering that dynamic
constraints: Vec<Constraint<'a,V>>, /// filters are cleared and repopulated on every assign, this side-effect
/// can never occur.
struct SDomain<V, Filter> {
values: Vec<V>,
global_filters: Vec<Filter>, // Globals are dynamic Filters
local_filters: Vec<Filter>, // Locals are static Filters
} }
impl<'a,V> Problem<'a, V> { impl<V, F> SDomain<V, F> {
fn new(values: Vec<V>) -> Self {
Self {
values,
global_filters: Vec::new(),
local_filters: Vec::new(),
}
}
pub fn build() -> ProblemBuilder<'a,V> { /// Returns the current domain values for a variable by index
fn get(&self, idx: usize) -> DomainValues<V> {
self.values
.iter()
.collect()
}
/// Adds a dynamic filter to globals, identified by its setter's id
/// /!\ Previously set filters are overriden, hence dynamic
fn set_global(&mut self, setter: usize, filter: F) {
self.global_filters[setter] = filter;
}
}
//pub type Constraint<'a,V> = fn(&Variables<'a,V>) -> bool;
/// Could be more efficient to just use fixed array of options as variables,
/// using a helper to bind Keys to Index in this array.
/// Domains could be a similar array of DomainValues.
/// It makes sense with an array where indexing is O(1)
pub struct Problem<'p, V, K> {
keys: Vec<K>,
/// The initial assignements
variables: Variables<'p, V>,
/// Each variable has its associated domain
domains: Vec<DomainValues<'p,V>>,
/// Set of constraints to validate
constraints: Vec<Constraint<'p,V,K>>,
}
impl<'p, V: PartialEq, K: Eq + Hash + Clone> Problem<'p, V, K> {
pub fn build() -> ProblemBuilder<'p,V, K> {
ProblemBuilder::new() ProblemBuilder::new()
} }
pub fn from_template() -> Problem<'p, V, K> {
let builder = Self::build();
builder.finish()
}
/// Returns all possible Updates for next assignements, prepended with /// Returns all possible Updates for next assignements, prepended with
/// a Clear to ensure the variable is unset before when leaving the branch. /// a Clear to ensure the variable is unset before when leaving the branch.
fn _assign_next(&self) -> Option<Vec<Assignment<'a,V>>> { fn _push_updates(&self) -> Option<Vec<Assignment<'p,V>>> {
// TODO: should be able to inject a choosing strategy if let Some(idx) = self._next_assign() {
if let Some((key,_)) = self.variables.iter().find(|(_, val)| val.is_none()) { // TODO: Domain will filter possible values for us
let domain = self.domains.get(key).expect("No domain for variable !"); // let values = self.domain.get(idx);
let mut updates = vec![Assignment::Clear(key.clone())]; let domain_values = self.domains
.get(idx)
if domain.values.is_empty() { panic!("No value in domain !"); } .expect("No domain for variable !");
// TODO: should be able to filter domain values (inference, pertinence) // TODO: handle case of empty domain.values
for value in domain.values.iter() { assert!(!domain_values.is_empty());
updates.push(Assignment::Update(key.clone(), value)); // Push a clear assignment first, just before going up the stack.
} let mut updates = vec![Assignment::Clear(idx.clone())];
domain_values.iter().for_each(|value| {
updates.push(
Assignment::Update(idx, *value)
);
});
Some(updates) Some(updates)
} else { // End of assignements } else { // End of assignements
None None
} }
} }
fn _next_assign(&self) -> Option<usize> {
// TODO: should be able to inject a choosing strategy
self.variables.iter()
.enumerate()
.find_map(|(idx, val)| {
if val.is_none() { Some(idx) }
else { None }
})
}
/// Checks that the current assignments doesn't violate any constraint /// Checks that the current assignments doesn't violate any constraint
fn _is_valid(&self) -> bool { fn _is_valid(&self) -> bool {
for validator in self.constraints.iter() { for status in self.constraints.iter().map(|c| c.status()) {
if validator(&self.variables) == false { return false; } if status == &Status::Violated { return false; }
} }
return true; return true;
} }
/// Visit all possible solutions, using a stack. fn _get_key(&self, idx: usize) -> &K {
pub fn solve_all(&mut self) -> Vec<Variables<'a,V>> &self.keys[idx]
where V: Clone + fmt::Debug }
{
let mut solutions: Vec<Variables<V>> = vec![]; fn _get_solution(&self) -> Solution<'p, V, K> {
let mut stack: Vec<Assignment<'a, V>> = vec![]; // Returns the current state wrapped in a Solution type.
stack.append(&mut self._assign_next().unwrap()); self.keys.iter().cloned()
.zip(self.variables.iter().cloned())
.collect()
}
/// Assigns a new value to the given index, then calls update
/// on every constraints.
fn _assign(&mut self, idx: usize, value: Option<&'p V>) {
self.variables[idx] = value;
let var_key = &self.keys[idx];
// TODO: manage dynamic filters on Domain
// let filters: Filter::Chain = self.constraints.iter_mut().map([...]).collect();
// self.domain.set_global(idx, filters);
self.constraints.iter_mut()
.for_each(|c| {
c.update(&var_key, value);
});
}
fn _solve(&mut self, limit: Option<usize>) -> Vec<Solution<'p, V, K>> {
let mut solutions: Vec<Solution<V, K>> = vec![];
let mut stack: Vec<Assignment<'p, V>> = vec![];
if let Some(mut init_updates) = self._push_updates() {
stack.append(&mut init_updates);
} else {
// Solution is complete
panic!("Could not initialize !");
}
loop { loop {
let node = stack.pop(); let node = stack.pop();
// There is no more combination to try out
if node.is_none() { break; }; if node.is_none() { break; };
// Exit early if we have enough solutions
if limit.is_some()
&& solutions.len() == limit.unwrap()
{
break;
};
match node.unwrap() { match node.unwrap() {
Assignment::Update(key, val) => { Assignment::Update(idx, val) => {
// Assign the variable and open new branches, if any. // Assign the variable and open new branches, if any.
*self.variables.get_mut(&key).unwrap() = Some(val); self._assign(idx, Some(val));
// TODO: handle case of empty domain.values if let Some(mut nodes) = self._push_updates() {
if let Some(mut nodes) = self._assign_next() {
stack.append(&mut nodes); stack.append(&mut nodes);
} else { } else {
// Assignements are completed // Assignements are completed
if self._is_valid() { if self._is_valid() {
solutions.push(self.variables.clone()); solutions.push(self._get_solution());
}; };
}; };
}, },
Assignment::Clear(key) => { Assignment::Clear(idx) => {
// We are closing this branch, unset the variable // We are closing this branch, unset the variable
*self.variables.get_mut(&key).unwrap() = None; self._assign(idx, None);
}, },
}; };
}; };
solutions solutions
}
/// Returns all complete solutions, after visiting all possible outcomes using a stack (DFS).
pub fn solve_all(mut self) -> Vec<Solution<'p,V,K>>
where V: fmt::Debug,
K: fmt::Debug,
{
self._solve(None) // No limit
}
pub fn solve_one(mut self) -> Option<Solution<'p,V,K>>
where V: fmt::Debug,
K: fmt::Debug,
{
self._solve(Some(1)).pop()
} }
} }
pub struct ProblemBuilder<'a, V>(Problem<'a, V>); pub struct ProblemBuilder<'p, V, K>(Problem<'p, V, K>);
impl<'a, V> ProblemBuilder<'a, V> { impl<'p, V, K: Eq + Hash + Clone> ProblemBuilder<'p, V, K> {
fn new() -> ProblemBuilder<'a, V> { fn new() -> ProblemBuilder<'p, V, K> {
ProblemBuilder( ProblemBuilder(
Problem{ Problem{
keys: Vec::new(),
variables: Variables::new(), variables: Variables::new(),
domains: HashMap::new(), domains: Vec::new(),
constraints: Vec::new(), constraints: Vec::new(),
}) })
} }
pub fn add_variable<S>(mut self, name: S, domain: &'a Domain<V>, value: Option<&'a V>) -> Self pub fn add_variable(mut self, name: K, static_filter: Vec<&'p V>, initial: Option<&'p V>) -> Self
where S: Into<String>
{ {
let name = name.into(); self.0.keys.push(name);
self.0.variables.insert(name.clone(), value); self.0.variables.push(initial);
self.0.domains.insert(name, domain); self.0.domains.push(static_filter);
self self
} }
pub fn add_constraint(mut self, cons: Constraint<'a,V>) -> Self { pub fn add_constraint(mut self, cons: Constraint<'p,V,K>) -> Self {
self.0.constraints.push(cons); self.0.constraints.push(cons);
self self
} }
pub fn finish(self) -> Problem<'a, V> { pub fn finish(self) -> Problem<'p,V, K> {
self.0 self.0
} }
} }
@@ -168,18 +341,15 @@ mod tests {
fn test_solver_find_pairs() { fn test_solver_find_pairs() {
use super::*; use super::*;
let domain = Domain::new(vec![1,2,3]); let domain = Domain::new(vec![1,2,3]);
let mut problem: Problem<_> = Problem::build() let problem: Problem<_, _> = Problem::build()
.add_variable(String::from("Left"), &domain, None) .add_variable(String::from("Left"), domain.all(), None)
.add_variable(String::from("Right"), &domain, None) .add_variable(String::from("Right"), domain.all(), None)
.add_constraint(|assign: &Variables<i32>| {
assign.get("Left").unwrap() == assign.get("Right").unwrap()
})
.finish(); .finish();
let solutions: Vec<Variables<i32>> = vec![ let solutions: Vec<Solution<i32, _>> = vec![
[("Left".to_string(), Some(&3)), ("Right".to_string(), Some(&3)),].iter().cloned().collect(), [(String::from("Left"), Some(&3)), (String::from("Right"), Some(&3))].iter().cloned().collect(),
[("Left".to_string(), Some(&2)), ("Right".to_string(), Some(&2)),].iter().cloned().collect(), [(String::from("Left"), Some(&2)), (String::from("Right"), Some(&2))].iter().cloned().collect(),
[("Left".to_string(), Some(&1)), ("Right".to_string(), Some(&1)),].iter().cloned().collect(), [(String::from("Left"), Some(&1)), (String::from("Right"), Some(&1))].iter().cloned().collect(),
]; ];
assert_eq!(problem.solve_all(), solutions); assert_eq!(problem.solve_all(), solutions);
@@ -189,16 +359,13 @@ mod tests {
fn test_solver_find_pairs_with_initial() { fn test_solver_find_pairs_with_initial() {
use super::*; use super::*;
let domain = Domain::new(vec![1,2,3]); let domain = Domain::new(vec![1,2,3]);
let mut problem: Problem<_> = Problem::build() let problem: Problem<_, _> = Problem::build()
.add_variable("Left".to_string(), &domain, None) .add_variable("Left".to_string(), domain.all(), None)
.add_variable("Right".to_string(), &domain, Some(&2)) .add_variable("Right".to_string(), domain.all(), Some(&2))
.add_constraint( |assign: &Variables<i32>| {
assign.get("Left").unwrap() == assign.get("Right").unwrap()
})
.finish(); .finish();
let solutions: Vec<Variables<i32>> = vec![ let solutions: Vec<Solution<i32, String>> = vec![
[("Left".to_string(), Some(&2)), ("Right".to_string(), Some(&2)),].iter().cloned().collect(), [(String::from("Left"), Some(&2)), (String::from("Right"), Some(&2))].iter().cloned().collect(),
]; ];
assert_eq!(problem.solve_all(), solutions); assert_eq!(problem.solve_all(), solutions);

79
planner/src/template.rs Normal file
View File

@@ -0,0 +1,79 @@
use super::{Domain, DomainValues, Value};
const DAYS: &[&str] = &[
"Lundi", "Mardi", "Mercredi",
"Jeudi", "Vendredi", "Samedi",
"Dimanche"];
/// An enum to discriminate every meals
#[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<V> {
is_used: bool,
initial: Option<V>,
}
/// 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<Key<'a>> {
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<Value>)
-> Vec<(Key, DomainValues<Value>, Option<&Value>)>
{
use cookbook::recipes::RecipeCategory;
Self::keys()
.into_iter()
.filter(|key| {
match *key {
("Samedi", _) | ("Dimanche", _) => true,
(_, Meal::Breakfast) => false,
(_,_) => true,
}
})
.map(|key| {
//TODO: is key used ? MealOpts.is_used
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 initial = None;
(key, domain.filter(category_filter), initial)
})
.collect()
}
}

View File

@@ -6,7 +6,9 @@ edition = "2018"
[dependencies] [dependencies]
rocket = "0.4.0" rocket = "0.4.0"
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" }
cookbook = { path = "../cookbook/" } cookbook = { path = "../cookbook/" }
planner = { path = "../planner/" }
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"

View File

@@ -1,103 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello Bulma!</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css">
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.22/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div class="hero hero-body">
<h1 class="title">Cook Assistant</h1>
<h2 class="subtitle">Recettes</h2>
</div>
<!-- Details View -->
<section v-if="active_view > -1" class="section has-background-grey-lighter">
<div class="box">
<button @click="closeActiveView" class="button is-pulled-right">X close</button>
<h4 class="title">{{ items[active_view].title }}</h4>
<h6 class="subtitle">{{ categories[items[active_view].category].name }}</h6>
<p><strong>{{ items[active_view].ingredients }}</strong></p>
<button @click="deleteRecipe(active_view + 1)" class="button is-danger is-pulled-right">DELETE !</button>
</div>
</section>
<!-- Category List View -->
<section v-else class="section has-background-grey-lighter">
<div class="container">
<div v-if="active_category == -1" class="columns">
<div v-for="c in categories" :key="c.id" class="column">
<button @click="setActiveCategory(c.id)" class="button is-large is-primary has-text-dark">{{ c.name }}</button>
</div>
</div>
<div v-else class="box">
<button @click="setActiveCategory(-1)" class="button is-pulled-left"><< back</button>
<h2 class="subtitle">{{ categories[active_category].name }}</h2>
<ul>
<li v-for="item in displayed" :key="item.id">
<a href="" @click.prevent="setActiveView(items.findIndex((i) => i.id ==item.id))">{{ item.title }}</a>
</li>
</ul>
</div>
</div>
</section>
</div>
</body>
<script type="text/javascript">
// TODO: Must find a viable use of Recipe.id instead of active_view !
var app = new Vue({
el: '#app',
data () {
return {
categories: [
{id: 0, name: "Petit-déjeuner"},
{id: 1, name: "Entrée"},
{id: 2, name: "Plat principal"},
{id: 3, name: "Dessert"}
],
active_category: -1,
active_view: -1,
items: []
};
},
methods: {
setActiveCategory: function(id) {
this.active_category = id;
},
setActiveView: function(idx) {
this.active_view = idx;
},
closeActiveView: function() {
this.active_view = -1;
},
deleteRecipe: function(id) {
fetch("/api/delete/" + id)
.then((res) => res.json())
.then((data) => console.log("Deleted :" + data))
.catch((err) => console.error(err));
this.closeActiveView();
},
fetchRecipesList: function() {
fetch("/api/list")
.then((res) => res.json())
.then((data) => this.items = data)
.catch(function(err) {
console.error(err);
});
}
},
computed: {
displayed: function() {
return this.items.filter(
rec => rec.category == this.active_category
);
}
},
mounted () {
this.fetchRecipesList()
}
});
</script>
</html>

View File

@@ -3,16 +3,27 @@
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_contrib; #[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive; #[macro_use] extern crate serde_derive;
extern crate rocket_cors;
extern crate cookbook; extern crate cookbook;
extern crate planner;
use std::path::Path; use std::path::{Path, PathBuf};
use rocket::response::{NamedFile, status::NotFound}; use rocket::{
response::{NamedFile, status::NotFound},
http::Method,
};
use rocket_cors::{AllowedHeaders, AllowedOrigins, Error};
#[get("/")] #[get("/")]
fn index() -> Result<NamedFile, NotFound<String>> { fn index() -> Result<NamedFile, NotFound<String>> {
NamedFile::open(&Path::new("./html/index.html")) files(PathBuf::from("index.html"))
.map_err(|_| NotFound(format!("Server error : index not found"))) }
#[get("/<file..>", rank=6)]
fn files(file: PathBuf) -> Result<NamedFile, NotFound<String>> {
let path = Path::new("vue/dist/").join(file);
NamedFile::open(&path)
.map_err(|_| NotFound(format!("Bad path: {:?}", path)))
} }
mod api { mod api {
@@ -25,8 +36,9 @@ mod api {
#[database("cookbook_db")] #[database("cookbook_db")]
pub struct CookbookDbConn(diesel::SqliteConnection); pub struct CookbookDbConn(diesel::SqliteConnection);
#[derive(Serialize)] /// A serializable wrapper for [`cookbook::recipes::Recipe`]
pub struct Recipe { #[derive(Serialize, Deserialize, Debug)]
pub struct RecipeObject {
id: i32, id: i32,
title: String, title: String,
category: i16, category: i16,
@@ -34,39 +46,160 @@ mod api {
preparation: String, preparation: String,
} }
impl Recipe { impl RecipeObject {
fn from(rec: models::Recipe) -> Recipe { fn from(conn: &diesel::SqliteConnection, rec: recipes::Recipe) -> Self {
Recipe { Self {
id: rec.id, id: rec.id,
title: rec.title, title: rec.title,
category: rec.category as i16, category: rec.category as i16,
ingredients: rec.ingredients, ingredients: rec.ingredients
.into_manager(conn)
.display_lines(),
preparation: rec.preparation, preparation: rec.preparation,
} }
} }
} }
/// Retrieves data from database and returns all recipes,
/// transformed into a js friendly [`RecipeObject`].
#[get("/list")] #[get("/list")]
pub fn recipes_list(conn: CookbookDbConn) -> Json<Vec<Recipe>> { pub fn recipes_list(conn: CookbookDbConn) -> Json<Vec<RecipeObject>> {
Json( Json( recipes::load_all(&conn)
recipes::load_all(&conn) .into_iter()
.into_iter() .map(|r| RecipeObject::from(&conn, r))
.map(|r| Recipe::from(r)) .collect() )
.collect()
)
} }
/// Delete a recipe given it's `id`
#[get("/delete/<id>")] #[get("/delete/<id>")]
pub fn delete_recipe(conn: CookbookDbConn, id: i32) -> Json<bool> { pub fn delete_recipe(conn: CookbookDbConn, id: i32) -> Json<bool> {
Json( Json( recipes::delete(&conn, id) )
recipes::delete(&conn, id) }
)
#[derive(Serialize, Deserialize, Debug)]
pub struct TemplateItems {
key: (String, String),
value: Option<RecipeObject>,
}
#[derive(Serialize, Deserialize)]
pub struct TemplateObject {
items: Vec<TemplateItems>
}
#[get("/solver/one")]
pub fn one_solution(conn: CookbookDbConn) -> Json<TemplateObject> {
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 problem = problem.finish();
if let Some(one_result) = problem.solve_one() {
Json(TemplateObject {
items: one_result
.into_iter()
.map(|(k,v)| {
TemplateItems {
key: (format!("{}", k.0), format!("{:?}", k.1)),
value: v.map(|r| RecipeObject::from(&conn, r.clone())),
}})
.collect(),
})
} else {
panic!("No solution at all !");
}
}
#[post("/solver/complete", data="<partial>")]
pub fn complete_problem(conn: CookbookDbConn, partial: Json<Vec<TemplateItems>>) -> Json<TemplateObject> {
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) {
// Let's hack for now
// BUGGY because template does not generate every variables, needs refactoring
// Find variable in partial
let initial_id = partial.iter()
.filter(|slot| slot.value.is_some())
.find_map(|slot| {
//println!("{:?} vs {:?}", slot, var);
if slot.key.0 == var.0
&& slot.key.1 == format!("{:?}",var.1)
{
let id = slot.value.as_ref().unwrap().id;
//println!("found initial : recipe with id {}", id);
Some(id)
} else {
None
}
});
let ini = if let Some(id) = initial_id {
let new_ini = domain.find(|r| {r.id == id});
//println!("Overrided {:?}", new_ini);
new_ini
} else {
ini
};
// If found, override initial value
problem = problem.add_variable(var, dom, ini);
};
let problem = problem.finish();
if let Some(one_result) = problem.solve_one() {
Json(TemplateObject {
items: one_result
.into_iter()
.map(|(k,v)| {
TemplateItems {
key: (format!("{}", k.0), format!("{:?}", k.1)),
value: v.map(|r| RecipeObject::from(&conn, r.clone())),
}})
.collect(),
})
} else {
panic!("No solution at all !");
}
} }
} }
fn main() { fn main() -> Result<(), Error> {
let (allowed_origins, failed_origins) = AllowedOrigins::some(&["http://localhost:8080"]);
assert!(failed_origins.is_empty());
// You can also deserialize this
let cors = rocket_cors::CorsOptions {
allowed_origins: allowed_origins,
allowed_methods: vec![Method::Get].into_iter().map(From::from).collect(),
allowed_headers: AllowedHeaders::some(&["Authorization", "Accept"]),
allow_credentials: true,
..Default::default()
}
.to_cors()?;
rocket::ignite() rocket::ignite()
.attach(api::CookbookDbConn::fairing()) .attach(api::CookbookDbConn::fairing())
.mount("/", routes![index]) .mount("/", routes![index, files])
.mount("/api", routes![api::recipes_list, api::delete_recipe]).launch(); .mount("/api", routes![
api::recipes_list,
api::delete_recipe,
api::one_solution,
api::complete_problem,
])
.attach(cors)
.launch();
Ok(())
} }

29
web/vue/README.md Normal file
View File

@@ -0,0 +1,29 @@
# vue
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your tests
```
npm run test
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
web/vue/babel.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

48
web/vue/package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "vue",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@mdi/font": "^3.4.93",
"bulma": "^0.7.2",
"vue": "^2.5.22"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.4.0",
"@vue/cli-plugin-eslint": "^3.4.0",
"@vue/cli-service": "^3.4.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0",
"vue-template-compiler": "^2.5.21"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

BIN
web/vue/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

17
web/vue/public/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>vue</title>
</head>
<body>
<noscript>
<strong>We're sorry but vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

108
web/vue/src/App.vue Normal file
View File

@@ -0,0 +1,108 @@
<template>
<div id="app">
<Heading msg="Cook-Assistant"/>
<section class="section" id="recipes-view">
<div v-if="status.loading" class="notification is-info">Loading...</div>
<div v-if="status.error" class="notification is-danger">Error: {{ status.error_msg }}</div>
<div class="container">
<keep-alive>
<RecipeDetails v-if="active_view > -1"
v-bind:item="items[active_view]"
v-on:close="closeActiveView"
v-on:add="addToPlanning" />
<RecipeList v-else
v-bind:items="items"
v-on:open-details="setActiveView" />
</keep-alive>
</div>
</section>
<section class="section has-background-grey-lighter" id="planner-view">
<h2 class="subtitle">Planning</h2>
<Planner ref="weekPlanning" />
</section>
</div>
</template>
<script>
import 'bulma/css/bulma.css'
import '@mdi/font/css/materialdesignicons.min.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'
export default {
name: 'app',
components: {
Heading,
RecipeDetails,
RecipeList,
Planner,
},
data () {
return {
status: {
loading: true,
error: false,
error_msg: "",
},
items: [],
// Index of the item
// activated for details view
active_view: -1,
}
},
methods: {
setActiveView: function(idx) {
this.active_view = idx;
},
closeActiveView: function() {
this.active_view = -1;
},
addToPlanning: function(mealKey, id) {
let mealData = this.items.find((recipe) => recipe.id == id);
this.$refs.weekPlanning.setMeal(mealKey, mealData);
},
deleteRecipe: function(id) {
fetch("http://localhost:8000/api/delete/" + id)
.then((res) => res.json())
.then((data) => {
if (data === true) {
this.items.splice(this.active_view, 1);
this.closeActiveView();
console.log("Deleted :" + data);
} else {
console.log("Error deleting");
}})
.catch((err) => console.error(err));
},
fetchRecipesList: function() {
fetch("http://localhost:8000/api/list")
.then((res) => res.json())
.then((data) => {
console.log(data);
this.items = data;
this.status.loading = false;
})
.catch(function(err) {
this.status.loading = false;
this.status.error = true;
this.statue.error_msg = err;
console.error(err);
});
},
initWeekPlanning: function() {
this.$refs.weekPlanning.fetchSolution();
}
},
mounted () {
this.fetchRecipesList();
console.log("MOUNTED !");
}
}
</script>
<style>
</style>

BIN
web/vue/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,27 @@
<template>
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="#">
{{ msg }}
</a>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
</nav>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@@ -0,0 +1,146 @@
<template>
<div class="box">
<button @click="fetchCompletion"
class="button is-primary is-fullwidth"
v-bind:class="{'is-loading': is_loading }">
FetchSolution</button>
<div class="box">
<div class="columns is-mobile is-vcentered is-multiline">
<div v-for="[day, meals] in itemsGroupedByDay"
class="column is-one-quarter-desktop is-half-mobile">
<p class="subtitle"><strong> {{ day }}</strong></p>
<div v-for="meal in meals">
<p v-if="meal.value" class="tags has-addons">
<span class="tag is-info">{{ meal.key[1] }}</span>
<span class="tag is-light">{{ meal.value.title }}</span>
<a @click="unsetMeal(meal.key)"
class="tag is-delete"></a>
</p>
<div v-else class="tag is-warning">Empty</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
const DAYS = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"];
const compareWeekDay = function(entryA, entryB) {
return DAYS.indexOf(entryA[0]) - DAYS.indexOf(entryB[0]);
}
const MEALS = ["Breakfast", "Lunch", "Dinner"];
const compareMealType = function(mealA, mealB) {
return MEALS.indexOf(mealA.key[1]) - MEALS.indexOf(mealB.key[1]);
}
function TemplateSlot(key, value) {
this.key = key;
this.value = value;
}
function Template() {
this.items = [];
var day;
for (day in DAYS) {
var meal;
for (meal in MEALS) {
this.items.push(
new TemplateSlot(
[DAYS[day], MEALS[meal]], //slotKey
null) //value
);
}
}
}
/// Given that Template.items are kept in order, we can
/// use a very simple solution (yields slices of 3, with name)
Template.prototype.groupedByDays = function() {
var i;
var grouped = [];
for (i = 0; i < this.items.length / 3; i++) {
let start = i * 3;
let end = (i + 1) * 3;
let day = this.items[start].key[0];
let slice = this.items.slice(start, end);
console.log(day, slice);
grouped.push([day, slice]);
}
return grouped;
}
Template.prototype.findIndexByKey = function(slotKey) {
console.log("Search index of key: " + slotKey);
var day_idx = DAYS.indexOf(slotKey[0]);
var meal_idx = MEALS.indexOf(slotKey[1]);
if (day_idx == -1 || meal_idx == -1) {
console.error("Index not found");
};
return day_idx * 3 + meal_idx;
}
Template.prototype.updateJson = function(data) {
var i;
for (i in data.items) {
let item = data.items[i];
let idx = this.findIndexByKey(item.key);
this.items[idx].value = item.value;
}
}
export default {
name: 'Planner',
data () {
var template = new Template();
return {
template,
is_loading: false,
};
},
methods: {
fetchSolution: function() {
this.is_loading = true;
fetch("http://localhost:8000/api/solver/one")
.then((res) => {
this.is_loading = false;
return res.json();}
)
.then((data) => this.template.updateJson(data))
.catch((err) => console.error(err));
},
fetchCompletion: function() {
this.is_loading = true;
// TODO: Keep only value's id
let body = JSON.stringify(this.template.items);
fetch("http://localhost:8000/api/solver/complete", {
method: 'POST',
body,
})
.then((res) => {
return res.json();}
)
.then((data) => this.template.updateJson(data))
.catch((err) => console.error(err));
this.is_loading = false;
},
unsetMeal: function(mealKey) {
let idx = this.template.findIndexByKey(mealKey);
// console.log("Unset " + idx);
this.template.items[idx].value = null;
},
setMeal: function(mealKey, mealData) {
let idx = this.template.findIndexByKey(mealKey);
// console.log("Set " + idx);
this.template.items[idx].value = mealData;
}
},
computed: {
itemsGroupedByDay: function() {
return this.template.groupedByDays();
}
}
}
</script>

View File

@@ -0,0 +1,62 @@
<template>
<div class="columns">
<div class="column is-narrow">
<a @click="$emit('close')"
class="button is-large is-outlined-dark is-fullwidth">
<span>Liste</span>
<span class="icon is-large">
<i class="mdi mdi-36px mdi-view-list"></i>
</span>
</a>
<br class="is-hidden-mobile"/>
<SlotSelect v-on:add="addToPlanning" />
</div>
<div class="column">
<h4 class="title">{{ item.title }}</h4>
<h6 class="subtitle">{{ categories[item.category].name }}</h6>
<p><strong>Ingredients</strong></p>
<ul>
<li v-for="ing in item.ingredients.split('\n')">{{ ing }}</li>
</ul>
</div>
<div class="column is-narrow">
<a @click="$emit('delete', item.id)">
<span class="icon is-large has-text-danger">
<i class="mdi mdi-48px mdi-delete-forever"></i>
</span>
</a>
</div>
</div>
</template>
<script>
import _, {categories} from './RecipeList.vue'
import SlotSelect from './planner/SlotSelect.vue'
export default {
name: 'RecipeDetails',
components: {
SlotSelect,
},
props: {
item: {
type: Object,
required: true,
default () {
return {id: 0, title: "", category: 0, ingredients: ""};
},
}
},
data () {
return {
categories: categories,
}
},
methods: {
addToPlanning: function(slotKey) {
this.$emit('add', slotKey, this.item.id);
}
}
}
</script>

View File

@@ -0,0 +1,81 @@
<template>
<div class="container">
<div v-if="active_category == -1" class="columns is-multiline is-mobile">
<div v-for="c in categories" :key="c.id" class="column is-one-quarter-desktop is-half-tablet">
<button @click="setActiveCategory(c.id)"
class="button is-large is-info is-fullwidth button-block">
<span class="icon is-large">
<i class="mdi mdi-24px" v-bind:class="c.icon"></i>
</span>
<br />
<p>{{ c.name }}</p></button>
</div>
</div>
<div v-else class="columns">
<div class="column is-narrow">
<a @click="setActiveCategory(-1)"
class="button is-large is-fullwidth">
<span>Catégories</span>
<span class="icon is-large" >
<i class="mdi mdi-36px mdi-view-grid"></i>
</span>
</a>
</div>
<div class="column">
<h2 class="title">{{ categories[active_category].name }}</h2>
<ul class="content">
<li v-for="item in displayed" :key="item.id" class="has-text-left">
<a href=""
@click.prevent="$emit(
'open-details',
items.findIndex((i) => i.id ==item.id))">
{{ item.title }}</a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export const categories = [
{id: 0, name: "Petit-déjeuner", icon: "mdi-food-croissant"},
{id: 1, name: "Entrée", icon: "mdi-bowl"},
{id: 2, name: "Plat principal", icon: "mdi-food"},
{id: 3, name: "Dessert", icon: "mdi-cupcake"}
];
export default {
name: 'RecipeList',
props: ["items"],
data () {
return {
active_category: -1,
categories,
}
},
methods: {
setActiveCategory: function(id) {
this.active_category = id;
},
},
computed: {
displayed: function() {
return this.items.filter(
rec => rec.category == this.active_category
);
}
},
}
</script>
<style scoped>
.button-block {
min-height: 150px;
}
.li {
list-style: circle;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="box">
<div class="columns">
<div class="column">
<div class="field">
<div class="control is-expanded">
<div class="select is-small is-fullwidth">
<select v-model="selected_day">
<option value="Lundi">Lundi</option>
<option value="Mardi">Mardi</option>
</select>
</div>
</div>
</div>
<div class="field">
<p class="control">
<div class="select is-small">
<select v-model="selected_meal">
<option value="Breakfast">Breakfast</option>
<option value="Lunch">Lunch</option>
</select>
</div>
</p>
</div>
</div>
<div class="column">
<a @click="$emit('add', getSelectedKey)"
class="has-text-success">
<span class="icon is-large">
<i class="mdi mdi-36px mdi-table-plus"></i>
</span>
</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SlotSelect',
data () {
return {
selected_day: "Lundi",
selected_meal: "Breakfast",
}
},
computed: {
getSelectedKey: function() {
return [this.selected_day, this.selected_meal];
}
}
}
</script>

8
web/vue/src/main.js Normal file
View File

@@ -0,0 +1,8 @@
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')