mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
283 lines
11 KiB
Rust
283 lines
11 KiB
Rust
// Copyright 2021 Jeremy Wall
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
/// Storage backend for recipes.
|
|
use std::convert::From;
|
|
use std::path::Path;
|
|
|
|
use rusqlite::{params, Connection, DropBehavior, Result as SqliteResult, Transaction};
|
|
|
|
use recipes::{unit::Measure, Ingredient, Recipe, Step};
|
|
|
|
// TODO Model the error domain of our storage layer.
|
|
#[derive(Debug)]
|
|
pub struct StorageError {
|
|
message: String,
|
|
}
|
|
|
|
impl From<rusqlite::Error> for StorageError {
|
|
fn from(e: rusqlite::Error) -> Self {
|
|
match e {
|
|
rusqlite::Error::SqliteFailure(e, msg) => StorageError {
|
|
message: format!("{}: {}", e, msg.unwrap_or_default()),
|
|
},
|
|
rusqlite::Error::SqliteSingleThreadedMode => unimplemented!(),
|
|
rusqlite::Error::FromSqlConversionFailure(_, _, _) => unimplemented!(),
|
|
rusqlite::Error::IntegralValueOutOfRange(_, _) => todo!(),
|
|
rusqlite::Error::Utf8Error(_) => todo!(),
|
|
rusqlite::Error::NulError(_) => todo!(),
|
|
rusqlite::Error::InvalidParameterName(_) => todo!(),
|
|
rusqlite::Error::InvalidPath(_) => todo!(),
|
|
rusqlite::Error::ExecuteReturnedResults => todo!(),
|
|
rusqlite::Error::QueryReturnedNoRows => todo!(),
|
|
rusqlite::Error::InvalidColumnIndex(_) => todo!(),
|
|
rusqlite::Error::InvalidColumnName(_) => todo!(),
|
|
rusqlite::Error::InvalidColumnType(_, _, _) => todo!(),
|
|
rusqlite::Error::StatementChangedRows(_) => todo!(),
|
|
rusqlite::Error::ToSqlConversionFailure(_) => todo!(),
|
|
rusqlite::Error::InvalidQuery => todo!(),
|
|
rusqlite::Error::MultipleStatement => todo!(),
|
|
rusqlite::Error::InvalidParameterCount(_, _) => todo!(),
|
|
_ => todo!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub enum IterResult<Entity> {
|
|
Some(Entity),
|
|
Err(StorageError),
|
|
None,
|
|
}
|
|
|
|
pub trait RecipeStore {
|
|
fn store_recipe(&mut self, e: &mut Recipe) -> Result<(), StorageError>;
|
|
fn fetch_all_recipes(&self) -> Result<Vec<Recipe>, StorageError>;
|
|
fn fetch_recipe_steps(&self, recipe_id: i64) -> Result<Option<Vec<Step>>, StorageError>;
|
|
fn fetch_recipe(&mut self, k: &str) -> Result<Option<Recipe>, StorageError>;
|
|
fn fetch_recipe_ingredients(&self, recipe_id: i64) -> Result<Vec<Ingredient>, StorageError>;
|
|
}
|
|
|
|
pub struct SqliteBackend {
|
|
conn: Connection,
|
|
}
|
|
|
|
impl SqliteBackend {
|
|
pub fn new<P: AsRef<Path>>(path: P) -> SqliteResult<Self> {
|
|
Ok(Self {
|
|
conn: Connection::open(path)?,
|
|
})
|
|
}
|
|
|
|
pub fn new_in_memory() -> SqliteResult<Self> {
|
|
Ok(Self {
|
|
conn: Connection::open_in_memory()?,
|
|
})
|
|
}
|
|
|
|
pub fn get_schema_version(&self) -> SqliteResult<Option<u32>> {
|
|
let mut stmt = self
|
|
.conn
|
|
.prepare("SELECT max(version) from schema_version")?;
|
|
Ok(stmt.query_row([], |r| r.get(0))?)
|
|
}
|
|
|
|
pub fn start_transaction<'a>(&'a mut self) -> SqliteResult<Transaction<'a>> {
|
|
self.conn.transaction().map(|mut tx| {
|
|
tx.set_drop_behavior(DropBehavior::Commit);
|
|
tx
|
|
})
|
|
}
|
|
|
|
pub fn create_schema(&self) -> SqliteResult<()> {
|
|
self.conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY )",
|
|
[],
|
|
)?;
|
|
let version = self.get_schema_version()?;
|
|
if let None = version {
|
|
self.conn
|
|
.execute("INSERT INTO schema_version ( version ) values ( 0 )", [])?;
|
|
} else {
|
|
return Ok(());
|
|
}
|
|
|
|
self.conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS recipes (
|
|
id INTEGER PRIMARY KEY,
|
|
title TEXT UNIQUE NOT NULL,
|
|
desc TEXT NOT NULL
|
|
)",
|
|
[],
|
|
)?;
|
|
self.conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS steps (
|
|
recipe_id INTEGER NOT NULL,
|
|
step_idx INTEGER NOT NULL,
|
|
prep_time INTEGER, -- in seconds
|
|
instructions TEXT NOT NULL,
|
|
FOREIGN KEY(recipe_id) REFERENCES recipes(id)
|
|
CONSTRAINT step_key PRIMARY KEY (recipe_id, step_idx)
|
|
)",
|
|
[],
|
|
)?;
|
|
self.conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS step_ingredients (
|
|
recipe_id INTEGER NOT NULL,
|
|
step_idx INTEGER NOT NULL,
|
|
ingredient_idx INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
amt TEXT NOT NULL,
|
|
category TEXT NOT NULL,
|
|
form TEXT,
|
|
FOREIGN KEY(recipe_id, step_idx) REFERENCES steps(recipe_id, step_idx),
|
|
CONSTRAINT step_ingredients_key PRIMARY KEY (recipe_id, step_idx, name, form)
|
|
)",
|
|
[],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn serialize_step_stmt_rows(
|
|
mut stmt: rusqlite::Statement,
|
|
recipe_id: i64,
|
|
) -> Result<Option<Vec<Step>>, StorageError> {
|
|
if let Ok(step_iter) = stmt.query_map(params![recipe_id], |row| {
|
|
let prep_time: Option<i64> = row.get(2)?;
|
|
let instructions: String = row.get(3)?;
|
|
Ok(Step::new(
|
|
prep_time.map(|i| std::time::Duration::from_secs(i as u64)),
|
|
instructions,
|
|
))
|
|
}) {
|
|
let mut steps = Vec::new();
|
|
for step in step_iter {
|
|
steps.push(step?);
|
|
}
|
|
return Ok(Some(steps));
|
|
}
|
|
return Ok(None);
|
|
}
|
|
|
|
fn map_recipe_row(r: &rusqlite::Row) -> Result<Recipe, rusqlite::Error> {
|
|
let id: i64 = r.get(0)?;
|
|
let title: String = r.get(1)?;
|
|
let desc: String = r.get(2)?;
|
|
Ok(Recipe::new_id(id, title, desc))
|
|
}
|
|
|
|
fn map_ingredient_row(r: &rusqlite::Row) -> Result<Ingredient, rusqlite::Error> {
|
|
let name: String = r.get(3)?;
|
|
let amt: String = r.get(4)?;
|
|
let category = r.get(5)?;
|
|
let form = r.get(6)?;
|
|
Ok(Ingredient::new(
|
|
name,
|
|
form,
|
|
dbg!(Measure::parse(dbg!(&amt)).unwrap()),
|
|
category,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl RecipeStore for SqliteBackend {
|
|
fn fetch_all_recipes(&self) -> Result<Vec<Recipe>, StorageError> {
|
|
let mut stmt = self.conn.prepare("SELECT * FROM recipes")?;
|
|
let recipe_iter = stmt.query_map([], |r| Self::map_recipe_row(r))?;
|
|
let mut recipes = Vec::new();
|
|
for next in recipe_iter {
|
|
recipes.push(next?);
|
|
}
|
|
Ok(recipes)
|
|
}
|
|
|
|
fn store_recipe(&mut self, recipe: &mut Recipe) -> Result<(), StorageError> {
|
|
// If we don't have a transaction already we should start one.
|
|
let tx = self.start_transaction()?;
|
|
if let Some(id) = recipe.id {
|
|
tx.execute(
|
|
"INSERT OR REPLACE INTO recipes (id, title, desc) VALUES (?1, ?2, ?3)",
|
|
params![id, recipe.title, recipe.desc],
|
|
)?;
|
|
} else {
|
|
tx.execute(
|
|
"INSERT INTO recipes (title, desc) VALUES (?1, ?2)",
|
|
params![recipe.title, recipe.desc],
|
|
)?;
|
|
let mut stmt = tx.prepare("select id from recipes where title = ?1")?;
|
|
let id = stmt.query_row(params![recipe.title], |row| Ok(row.get(0)?))?;
|
|
recipe.id = Some(id);
|
|
}
|
|
for (idx, step) in recipe.steps.iter().enumerate() {
|
|
tx.execute("INSERT INTO steps (recipe_id, step_idx, prep_time, instructions) VALUES (?1, ?2, ?3, ?4)",
|
|
params![recipe.id, dbg!(idx), step.prep_time.map(|v| v.as_secs()) , step.instructions])?;
|
|
for (ing_idx, ing) in step.ingredients.iter().enumerate() {
|
|
dbg!(tx.execute(
|
|
"INSERT INTO step_ingredients (recipe_id, step_idx, ingredient_idx, name, amt, category, form) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
|
params![recipe.id, dbg!(idx), ing_idx, ing.name, format!("{}", ing.amt), ing.category, ing.form])?);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn fetch_recipe_steps(&self, recipe_id: i64) -> Result<Option<Vec<Step>>, StorageError> {
|
|
let stmt = self
|
|
.conn
|
|
.prepare("SELECT * from steps WHERE recipe_id = ?1 ORDER BY step_idx")?;
|
|
SqliteBackend::serialize_step_stmt_rows(stmt, recipe_id)
|
|
}
|
|
|
|
fn fetch_recipe(&mut self, key: &str) -> Result<Option<Recipe>, StorageError> {
|
|
let tx = self.start_transaction()?;
|
|
let mut stmt = tx.prepare("SELECT * FROM recipes WHERE title = ?1")?;
|
|
let recipe_iter = stmt.query_map(params![key], |r| Self::map_recipe_row(r))?;
|
|
let mut recipe = recipe_iter
|
|
.filter(|res| res.is_ok()) // TODO(jwall): What about failures here?
|
|
.map(|r| r.unwrap())
|
|
.next();
|
|
if let Some(recipe) = recipe.as_mut() {
|
|
// We know the recipe.id has to exist since we filled it when it came from the database.
|
|
let stmt = tx.prepare("SELECT * FROM steps WHERE recipe_id = ?1 ORDER BY step_idx")?;
|
|
let steps = SqliteBackend::serialize_step_stmt_rows(stmt, recipe.id.unwrap())?;
|
|
let mut stmt = tx.prepare("SELECT * from step_ingredients WHERE recipe_id = ?1 and step_idx = ?2 ORDER BY ingredient_idx")?;
|
|
if let Some(mut steps) = steps {
|
|
for (step_idx, mut step) in steps.drain(0..).enumerate() {
|
|
// TODO(jwall): Fetch the ingredients.
|
|
let ing_iter = stmt.query_map(params![recipe.id, step_idx], |row| {
|
|
Self::map_ingredient_row(row)
|
|
})?;
|
|
for ing in ing_iter {
|
|
step.ingredients.push(ing?);
|
|
}
|
|
recipe.add_step(step);
|
|
}
|
|
}
|
|
}
|
|
return Ok(recipe);
|
|
}
|
|
|
|
fn fetch_recipe_ingredients(&self, recipe_id: i64) -> Result<Vec<Ingredient>, StorageError> {
|
|
let mut stmt = self.conn.prepare(
|
|
"SELECT * FROM step_ingredients WHERE recipe_id = ?1 ORDER BY step_idx, ingredient_idx",
|
|
)?;
|
|
let ing_iter = stmt.query_map(params![recipe_id], |row| Self::map_ingredient_row(row))?;
|
|
let mut ingredients = Vec::new();
|
|
for i in ing_iter {
|
|
ingredients.push(i?);
|
|
}
|
|
Ok(ingredients)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|