diff --git a/cookbook/db.sqlite3 b/cookbook/db.sqlite3 index dd3a8c0..fdbaf37 100644 Binary files a/cookbook/db.sqlite3 and b/cookbook/db.sqlite3 differ diff --git a/cookbook/migrations/2019-01-27-201901_create_recipes/up.sql b/cookbook/migrations/2019-01-27-201901_create_recipes/up.sql index 709cf18..38a021a 100644 --- a/cookbook/migrations/2019-01-27-201901_create_recipes/up.sql +++ b/cookbook/migrations/2019-01-27-201901_create_recipes/up.sql @@ -2,7 +2,7 @@ CREATE TABLE recipes ( id INTEGER PRIMARY KEY NOT NULL, title VARCHAR NOT NULL, - category INTEGER NOT NULL, + category SMALLINT NOT NULL, ingredients TEXT NOT NULL, preparation TEXT NOT NULL ) diff --git a/cookbook/src/bin/show_recipes.rs b/cookbook/src/bin/show_recipes.rs index f4ddc44..f92dae1 100644 --- a/cookbook/src/bin/show_recipes.rs +++ b/cookbook/src/bin/show_recipes.rs @@ -3,7 +3,22 @@ extern crate diesel; use self::cookbook::*; use self::models::*; -use self::diesel::prelude::*; + +use diesel::sqlite::SqliteConnection; +use cookbook::models::fields::IngredientList; + +struct IngredientListManager<'a>(&'a SqliteConnection, IngredientList); + +impl<'a> IngredientListManager<'a> { + fn display_lines(&self) -> String { + let mut repr = String::new(); + for id in self.1.iter() { + let ingdt = ingredients::get(self.0, id).expect("Database integrity error"); + repr.push_str(&format!("{}\n", ingdt.alias)); + } + repr + } +} fn main() { @@ -13,6 +28,6 @@ fn main() { for rec in result { println!("*************\n{}\n({:?})", rec.title, rec.category); println!("-------------\n"); - println!("{}", rec.ingredients); + println!("{}", rec.ingredients.as_string()); } } diff --git a/cookbook/src/bin/write_recipe.rs b/cookbook/src/bin/write_recipe.rs index 518ecc0..694a062 100644 --- a/cookbook/src/bin/write_recipe.rs +++ b/cookbook/src/bin/write_recipe.rs @@ -1,16 +1,16 @@ extern crate cookbook; extern crate diesel; -use std::io::{Read, stdin}; +use std::io::{stdin}; use diesel::SqliteConnection; use self::cookbook::*; -use self::models::{NewRecipe, fields::RecipeCategory}; +use self::models::{NewRecipe, fields::RecipeCategory, fields::IngredientList}; struct CreateRecipe<'a> { connection: &'a SqliteConnection, title: &'a str, category: Option, - ingredients: String, + ingredients: IngredientList, } impl<'a> CreateRecipe<'a> { @@ -19,7 +19,7 @@ impl<'a> CreateRecipe<'a> { connection: conn, title: "New recipe", category: None, - ingredients: String::new(), + ingredients: IngredientList::new(), } } @@ -35,23 +35,23 @@ impl<'a> CreateRecipe<'a> { use crate::ingredients::*; // Check it exists or create - if let Some(_ingdt) = find(self.connection, &name) { - println!("="); - } else { - create(self.connection, &name); - println!("+{}", &name); + match get_id_or_create(self.connection, &name) { + Ok(id) => { + println!("Got id {}", id); + self.ingredients.push(id); + }, + Err(_) => println!("Error adding ingredient") } - - self.ingredients.push_str(&name); } /// Builds a NewRecipe instance from current data and insert it. fn insert(self) { - let new_recipe = NewRecipe::new( - self.title, - self.category.unwrap_or(RecipeCategory::Breakfast), - &self.ingredients, - ""); + let new_recipe = NewRecipe { + title: self.title, + category: self.category.unwrap_or(RecipeCategory::Breakfast), + ingredients: self.ingredients, + preparation: "" + }; match new_recipe.insert(self.connection) { Ok(new) => println!("Added {}", new.title), Err(e) => println!("Error: {}", e), diff --git a/cookbook/src/lib.rs b/cookbook/src/lib.rs index adc341e..1079a44 100644 --- a/cookbook/src/lib.rs +++ b/cookbook/src/lib.rs @@ -40,34 +40,52 @@ pub mod recipes { } } - pub mod ingredients { - use crate::models::{Ingredient, NewIngredient}; + use crate::models::{Ingredient, NewIngredient, fields::IngredientId}; use super::{SqliteConnection, schema}; use super::diesel::prelude::*; - pub fn find(conn: &SqliteConnection, name: &str) -> Option { - use self::schema::ingredients::dsl::*; - - let results = ingredients.filter(alias.like(name)) - .limit(1) - .load::(conn) - .expect("Error finding ingredient"); - - if !results.is_empty() { - Some(results.into_iter().nth(0).unwrap()) + /// 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 { + if let Some(ingdt) = find(conn, name) { + return Ok(ingdt.id); } else { - None + return create(conn, name); } } - pub fn create(conn: &SqliteConnection, name: &str) -> usize { - use self::schema::ingredients; + pub fn get(conn: &SqliteConnection, ingdt_id: &IngredientId) -> Result { + use self::schema::ingredients::dsl::*; - diesel::insert_into(ingredients::table) + ingredients.filter(id.eq(ingdt_id)) + .first::(conn) + .map_err(|e| format!("Could not retrieve : {}", e)) + } + + fn find(conn: &SqliteConnection, name: &str) -> Option { + use self::schema::ingredients::dsl::*; + + match ingredients.filter(alias.like(name)) + .first(conn) + { + Ok(ingdt) => Some(ingdt), + Err(_) => None, + } + } + + fn create(conn: &SqliteConnection, name: &str) -> Result { + use self::schema::ingredients::dsl::*; + + let _ = diesel::insert_into(ingredients) .values(&NewIngredient { alias: name }) .execute(conn) - .expect("Error inserting ingredient") + .map_err(|e| format!("Error inserting ingredient ! {:?}", e))?; + let inserted = ingredients + .order(id.desc()) + .first::(conn) + .map_err(|e| format!("No ingredient at all ! {:?}", e))?; + Ok(inserted.id) } } diff --git a/cookbook/src/models.rs b/cookbook/src/models.rs index 0dcd5ea..34f3d6b 100644 --- a/cookbook/src/models.rs +++ b/cookbook/src/models.rs @@ -57,8 +57,7 @@ pub mod fields { } impl FromSql for RecipeCategory - where - i16: FromSql + where i16: FromSql { fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result { let v = i16::from_sql(bytes)?; @@ -68,13 +67,70 @@ pub mod fields { } impl ToSql for RecipeCategory - where - i16: ToSql{ + where i16: ToSql + { fn to_sql(&self, out: &mut Output) -> serialize::Result { i16::to_sql(&(*self as i16), out) } } + pub type IngredientId = i32; + + #[derive(Debug, Clone, FromSqlRow, AsExpression)] + #[sql_type = "Text"] + pub struct IngredientList(Vec); + + /// 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::>() + .join(" ") + } + } + + impl std::ops::Deref for IngredientList { + type Target = Vec; + + 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 FromSql for IngredientList + where String: FromSql + { + fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result { + let data = String::from_sql(bytes)?; + Ok( + IngredientList( + data.split(" ") + .map(|i| i.parse::().unwrap()) + .collect() + )) + } + } + + impl ToSql for IngredientList + where String: ToSql + { + fn to_sql(&self, out: &mut Output) -> serialize::Result { + String::to_sql(&self.as_string(), out) + } + } + } @@ -84,7 +140,7 @@ pub struct Recipe { pub id: i32, pub title: String, pub category: fields::RecipeCategory, - pub ingredients: String, + pub ingredients: fields::IngredientList, pub preparation: String, } @@ -93,20 +149,11 @@ pub struct Recipe { pub struct NewRecipe<'a> { pub title: &'a str, pub category: fields::RecipeCategory, - pub ingredients: &'a str, + pub ingredients: fields::IngredientList, pub preparation: &'a str, } 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 { diesel::insert_into(recipes::table) .values(&self) diff --git a/planner/src/bin/weekly.rs b/planner/src/bin/weekly.rs index 7ee4e34..79a5045 100644 --- a/planner/src/bin/weekly.rs +++ b/planner/src/bin/weekly.rs @@ -14,6 +14,7 @@ use self::planner::solver::{Variables, Domain, Problem}; type Day = String; const DAYS: &[&str] = &["Lundi", "Mardi", "Mercredi"]; +#[allow(dead_code)] enum Meals { Breakfast(Day), Lunch(Day), @@ -41,8 +42,9 @@ fn generate_variables(domain: &Domain) -> Vec<(String, &Domain, Option< } 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") + let id = 0; + assign.get("Lundi_Lunch").unwrap().unwrap().ingredients.contains(&id) + && !assign.get("Mardi_Lunch").unwrap().unwrap().ingredients.contains(&id) } diff --git a/planner/src/solver.rs b/planner/src/solver.rs index 6dc0b9a..5cd8319 100644 --- a/planner/src/solver.rs +++ b/planner/src/solver.rs @@ -13,6 +13,7 @@ enum Assignment<'a, V> { Clear(String) } + type Domains<'a, V> = HashMap>; /// The domain of values that can be assigned to variables #[derive(Clone)] @@ -71,10 +72,11 @@ impl<'a,V> Problem<'a, V> { /// Returns all possible Updates for next assignements, prepended with /// a Clear to ensure the variable is unset before when leaving the branch. - fn _assign_next(&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 = self.domains.get(key).expect("No domain for variable !"); + // Push a clear assignment first, just before going up the stack. let mut updates = vec![Assignment::Clear(key.clone())]; if domain.values.is_empty() { panic!("No value in domain !"); } @@ -96,13 +98,13 @@ impl<'a,V> Problem<'a, V> { return true; } - /// Visit all possible solutions, using a stack. + /// Visit all possible solutions, using a stack (DFS). pub fn solve_all(&mut self) -> Vec> where V: Clone + fmt::Debug { let mut solutions: Vec> = vec![]; let mut stack: Vec> = vec![]; - stack.append(&mut self._assign_next().unwrap()); + stack.append(&mut self._push_updates().unwrap()); loop { let node = stack.pop(); if node.is_none() { break; }; @@ -111,7 +113,7 @@ impl<'a,V> Problem<'a, V> { // Assign the variable and open new branches, if any. *self.variables.get_mut(&key).unwrap() = Some(val); // TODO: handle case of empty domain.values - if let Some(mut nodes) = self._assign_next() { + if let Some(mut nodes) = self._push_updates() { stack.append(&mut nodes); } else { // Assignements are completed diff --git a/web/html/index.html b/web/html/index.html index f939890..ee2d32e 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -20,7 +20,7 @@

{{ items[active_view].title }}

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

{{ items[active_view].ingredients }}

- + @@ -75,9 +75,15 @@ deleteRecipe: function(id) { fetch("/api/delete/" + id) .then((res) => res.json()) - .then((data) => console.log("Deleted :" + data)) + .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)); - this.closeActiveView(); }, fetchRecipesList: function() { fetch("/api/list") diff --git a/web/src/main.rs b/web/src/main.rs index f532e43..302f46e 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -40,7 +40,7 @@ mod api { id: rec.id, title: rec.title, category: rec.category as i16, - ingredients: rec.ingredients, + ingredients: rec.ingredients.as_string(), preparation: rec.preparation, } }