Implementing Mixer and Recipes
Adding tests!
I have found it useful to start writing unit tests. The Rust Book has a good chapter on unit testing in rust. For now I am going to write tests inline in the modules using the following template:
#[cfg(test)]
mod tests {
// in
use dispenser::TestDispenser;
use mixer::Mixer;
#[test]
fn test_it_works() {
}
}
There are a few other methods such as using the tests
folder, but this is the
easiest for me to work with at the moment. Another interesting option is to add
doc-test code. Which allows example code in your documentation to be tested
and validated.
Implementing the mixer struct
The Mixers
are essentially a HashMap
of String
's mapping to Dispensers.
Because the Dispensers are a trait object they must be Box<Dispenser>
s. The
plan is to implement the following:
- [x]
clear
- [x]
add
- [x]
remove
- [x]
get
- [ ]
mix
(make_recipe
in the previous recipe). - [ ]
status
Setting Up Recipes
Diesel with SQLite
I want the recipes to be installed in a database, for this section I am going
to use diesel-rs and sqlite3 (which
is great for embedded systems, but not webapps). This will require moving to
nightly builds of rust (which is easy with rustup
):
$ rustup install nightly
$ rustup default nightly
Also changing the reccomended Cargo.toml addition a little:
diesel = "0.9.0"
diesel_codegen = { version = "0.9.0", features = ["sqlite"] }
dotenv = "0.8.0"
Make the database
First lets make the database and setup dotenv
for diesel:
$ echo DATABASE_URL=$PWD/rusty_nail.db > .env
$ diesel setup
Then write the up.sql
and down.sql
for the migration:
up.sql:
CREATE TABLE recipes (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name VARCHAR NOT NULL,
description TEXT NOT NULL
);
CREATE TABLE ingredients (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name VARCHAR NOT NULL,
description TEXT NOT NULL,
available BOOLEAN NOT NULL
);
CREATE TABLE recipe_ingredients (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ,
ingredient_id INTEGER NOT NULL REFERENCES ingredient(id) ,
amount REAL NOT NULL
);
So we will have 3 tables in our database:
recipes
: Which is what we are really interested in and contains a representation of each individual recipe.ingredients
: Which contains a representation of the available ingredients (which can be mapped to dispensers).recipe_ingredients
: Which will be used to create a list of ingredients for a recipe and will beJOIN
ed withingredients
.
So basically to get a list of ingredients for a recipe and run the migration
$ diesel migration run
Other Notes
- Use
use diesel::sqlite::SqliteConnection;
instead ofuse diesel::pg::PgConnection;
get_result()
is not supported for Sqlite insertions so:diesel::insert(&new_recipe).into(recipes::table) .get_result(conn) .expect("Error saving new recipe")
From the demo does not work properly Instead you can use.
diesel::insert(&new_recipe).into(recipe::table)
.execute(conn)
.expect("Error saving new recipe")
Which returns usize
instead of a Recipe.
The right query for stitching ingredients to recipes
What we want to be able to do is for a given recipe look up the Ingredient
and RecipeIngredient
for all ingredients in a given recipe creating a vector
of all relevant Ingrdient data ( not including primary and foreign keys):
+-------------+ +------------------+ +-------------+
| Recipe | | RecipeIngredient | | Ingredient |
+-------------+ +------------------+ +-------------+
| id |<---+ | id | +------>| id |
| name | +------| recipe_id | | | name |
| description | | ingredient_id |--------+ | description |
| | | amount | | available |
+-------------+ +------------------+ +-------------+
To do this what we need to do is:
- Perform a query to find the Recipe we are interested in.
- Join the recipe_ingredients and ingrdients table using an inner join
- Filter the join on
recipe_ingredients::recipe_id
to just get those belonging to our current recipe. - create a
select
statement to discard unneeded content.
We can then create a Vec<String, String, i32, f32>
which can then correspond to
a Queryable
struct we can create which is a fusion of RecipeIngredient and
Ingredient in the following example called
... This may
change to DispenserInstruction
or may become the public
representation of Ingredient
. What would be ideal is to generate a
representation of a Recipe
which had the following structure definition:
struct MixerIngredient {
name: String,
description: String,
amount: f32
}
struct MixerRecipe {
name: String,
description: String,
ingredients: Vec<MixerIngredient>
}
Here is an example query program:
#![feature(proc_macro)]
extern crate rusty_nail;
#[macro_use]
extern crate diesel_codegen;
#[macro_use]
extern crate diesel;
use self::rusty_nail::*;
use self::rusty_nail::recipe::*;
use self::rusty_nail::recipe::models::*;
use self::rusty_nail::recipe::schema::recipes::dsl as recipes;
use self::rusty_nail::recipe::schema::recipe_ingredients::dsl as recipe_ingredients;
use self::rusty_nail::recipe::schema::ingredients::dsl as ingredients;
use self::diesel::prelude::*;
#[derive(Debug, Queryable)]
struct MixerIngredient {
pub name: String,
pub description: String,
pub available: bool,
pub amount: f32,
}
fn main() {
let connection = establish_connection();
let results: Vec<Recipe> = recipes::recipes.load(&connection)
.expect("Error loading recipes");
println!("Displaying {} recipes", results.len());
for recipe in results {
println!("{:?}", recipe);
let ings: Vec<MixerIngredient> =
ingredients::ingredients.inner_join(recipe_ingredients::recipe_ingredients)
.filter(recipe_ingredients::recipe_id.eq(recipe.id))
.select((ingredients::name,
ingredients::description,
ingredients::available,
recipe_ingredients::amount))
.load(&connection)
.unwrap();
for ingredient in ings {
println!("\t- {:?}", ingredient);
}
}
}
Which can become:
#![feature(proc_macro)]
extern crate rusty_nail;
#[macro_use]
extern crate diesel_codegen;
#[macro_use]
extern crate diesel;
use self::rusty_nail::recipe::*;
use self::rusty_nail::recipe::models::*;
use self::rusty_nail::recipe::schema::recipes::dsl as recipes;
use self::rusty_nail::recipe::schema::recipe_ingredients::dsl as recipe_ingredients;
use self::rusty_nail::recipe::schema::ingredients::dsl as ingredients;
use self::diesel::prelude::*;
#[derive(Debug, Queryable)]
struct MixerIngredient {
pub id: i32,
pub name: String,
pub description: String,
pub available: bool,
pub amount: f32,
}
#[derive(Debug)]
struct MixerRecipe {
pub id: i32,
pub name: String,
pub description: String,
pub ingredients: Vec<MixerIngredient>,
}
fn main() {
let connection = establish_connection();
let results: Vec<Recipe> = recipes::recipes.load(&connection)
.expect("Error loading recipes");
println!("Displaying {} recipes", results.len());
for recipe in results {
let test: MixerRecipe = MixerRecipe {
id: recipe.id,
name: recipe.name,
description: recipe.description,
ingredients: ingredients::ingredients.inner_join(recipe_ingredients::recipe_ingredients)
.filter(recipe_ingredients::recipe_id.eq(recipe.id))
.select((ingredients::id, ingredients::name,
ingredients::description,
ingredients::available,
recipe_ingredients::amount))
.load(&connection)
.unwrap(),
};
println!("{:?}", test);
}
}
Now the next big step is searching for ingredients which are availables