diff options
Diffstat (limited to 'crates/rocie-server/src/storage/sql')
| -rw-r--r-- | crates/rocie-server/src/storage/sql/config.rs | 7 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/get/config/mod.rs | 41 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/get/mod.rs | 3 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/get/unit/mod.rs | 40 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/insert/config/mod.rs | 80 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/insert/mod.rs | 1 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/mod.rs | 1 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/recipe.rs | 100 |
8 files changed, 254 insertions, 19 deletions
diff --git a/crates/rocie-server/src/storage/sql/config.rs b/crates/rocie-server/src/storage/sql/config.rs new file mode 100644 index 0000000..d62859c --- /dev/null +++ b/crates/rocie-server/src/storage/sql/config.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(ToSchema, Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Config { + pub(crate) should_use_defaults: bool, +} diff --git a/crates/rocie-server/src/storage/sql/get/config/mod.rs b/crates/rocie-server/src/storage/sql/get/config/mod.rs new file mode 100644 index 0000000..eb8be86 --- /dev/null +++ b/crates/rocie-server/src/storage/sql/get/config/mod.rs @@ -0,0 +1,41 @@ +use crate::{app::App, storage::sql::config::Config}; + +use sqlx::query; + +impl Config { + pub(crate) async fn get(app: &App) -> Result<Self, get::Error> { + let record = query!( + " + SELECT use_defaults + FROM config + WHERE id = 0 +" + ) + .fetch_one(&app.db) + .await?; + + let should_use_defaults = if record.use_defaults == 1 { + true + } else if record.use_defaults == 0 { + false + } else { + unreachable!("Should not be possible, sqlite's CHECK prevents it") + }; + + Ok(Self { + should_use_defaults, + }) + } +} + +pub(crate) mod get { + use actix_web::ResponseError; + + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute the sql query")] + SqlError(#[from] sqlx::Error), + } + + impl ResponseError for Error {} +} diff --git a/crates/rocie-server/src/storage/sql/get/mod.rs b/crates/rocie-server/src/storage/sql/get/mod.rs index a6ee0e1..e3520da 100644 --- a/crates/rocie-server/src/storage/sql/get/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/mod.rs @@ -1,9 +1,10 @@ pub(crate) mod barcode; +pub(crate) mod config; pub(crate) mod product; pub(crate) mod product_amount; pub(crate) mod product_parent; -pub(crate) mod recipe_parent; pub(crate) mod recipe; +pub(crate) mod recipe_parent; pub(crate) mod unit; pub(crate) mod unit_property; pub(crate) mod user; diff --git a/crates/rocie-server/src/storage/sql/get/unit/mod.rs b/crates/rocie-server/src/storage/sql/get/unit/mod.rs index 6f5d297..2c85970 100644 --- a/crates/rocie-server/src/storage/sql/get/unit/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/unit/mod.rs @@ -57,6 +57,34 @@ impl Unit { Ok(None) } } + + pub(crate) async fn from_name(app: &App, name: &str) -> Result<Option<Self>, from_name::Error> { + let record = query!( + " + SELECT id, full_name_singular, unit_property, full_name_plural, short_name, description + FROM units + WHERE full_name_singular = ? OR full_name_plural = ? OR short_name = ? +", + name, + name, + name + ) + .fetch_optional(&app.db) + .await?; + + if let Some(record) = record { + Ok(Some(Self { + id: UnitId::from_db(&record.id), + unit_property: UnitPropertyId::from_db(&record.unit_property), + full_name_singular: record.full_name_singular, + full_name_plural: record.full_name_plural, + short_name: record.short_name, + description: record.description, + })) + } else { + Ok(None) + } + } } pub(crate) mod get_all { @@ -82,3 +110,15 @@ pub(crate) mod from_id { impl ResponseError for Error {} } + +pub(crate) mod from_name { + use actix_web::ResponseError; + + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute the sql query")] + SqlError(#[from] sqlx::Error), + } + + impl ResponseError for Error {} +} diff --git a/crates/rocie-server/src/storage/sql/insert/config/mod.rs b/crates/rocie-server/src/storage/sql/insert/config/mod.rs new file mode 100644 index 0000000..8c81feb --- /dev/null +++ b/crates/rocie-server/src/storage/sql/insert/config/mod.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; +use sqlx::query; + +use crate::storage::sql::{ + config::Config, + insert::{Operations, Transactionable}, +}; + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) enum Operation { + UseDefault { value: bool, old_value: bool }, +} + +impl Transactionable for Operation { + type ApplyError = apply::Error; + type UndoError = undo::Error; + + async fn apply(self, txn: &mut sqlx::SqliteConnection) -> Result<(), apply::Error> { + match self { + Operation::UseDefault { value, .. } => { + query!( + " + UPDATE config + SET use_defaults = ? + WHERE id = 0 +", + value, + ) + .execute(txn) + .await?; + } + } + Ok(()) + } + + async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> { + match self { + Operation::UseDefault { old_value, .. } => { + query!( + " + UPDATE config + SET use_defaults = ? + WHERE id = 0 +", + old_value, + ) + .execute(txn) + .await?; + } + } + Ok(()) + } +} + +pub(crate) mod undo { + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute undo sql statments: {0}")] + SqlError(#[from] sqlx::Error), + } +} +pub(crate) mod apply { + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute apply sql statments: {0}")] + SqlError(#[from] sqlx::Error), + } +} + +impl Config { + pub(crate) fn set_use_default(&mut self, new_value: bool, ops: &mut Operations<Operation>) { + if self.should_use_defaults != new_value { + ops.push(Operation::UseDefault { + value: new_value, + old_value: self.should_use_defaults, + }); + self.should_use_defaults = new_value; + } + } +} diff --git a/crates/rocie-server/src/storage/sql/insert/mod.rs b/crates/rocie-server/src/storage/sql/insert/mod.rs index b92a88c..c106b2b 100644 --- a/crates/rocie-server/src/storage/sql/insert/mod.rs +++ b/crates/rocie-server/src/storage/sql/insert/mod.rs @@ -8,6 +8,7 @@ use serde::{Serialize, de::DeserializeOwned}; use sqlx::{SqliteConnection, query}; pub(crate) mod barcode; +pub(crate) mod config; pub(crate) mod product; pub(crate) mod product_parent; pub(crate) mod recipe; diff --git a/crates/rocie-server/src/storage/sql/mod.rs b/crates/rocie-server/src/storage/sql/mod.rs index c37e68f..f1d7cb1 100644 --- a/crates/rocie-server/src/storage/sql/mod.rs +++ b/crates/rocie-server/src/storage/sql/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod insert; // Types pub(crate) mod barcode; +pub(crate) mod config; pub(crate) mod product; pub(crate) mod product_amount; pub(crate) mod product_parent; diff --git a/crates/rocie-server/src/storage/sql/recipe.rs b/crates/rocie-server/src/storage/sql/recipe.rs index 1fc3b56..7347b4b 100644 --- a/crates/rocie-server/src/storage/sql/recipe.rs +++ b/crates/rocie-server/src/storage/sql/recipe.rs @@ -1,5 +1,6 @@ #![expect(clippy::unused_async)] +use log::error; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -10,7 +11,7 @@ use crate::{ product::{Product, ProductId}, product_amount::ProductAmount, recipe_parent::RecipeParentId, - unit::UnitAmount, + unit::{Unit, UnitAmount}, }, }; @@ -97,7 +98,6 @@ pub(crate) struct Section { /// Each type of content inside a section #[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Clone)] -#[serde(tag = "type", content = "value", rename_all = "camelCase")] pub(crate) enum Content { /// A step Step(Step), @@ -124,7 +124,6 @@ pub(crate) struct Step { /// Except for [`Item::Text`], the value is the index where the item is located /// in it's corresponding [`Vec`] in the [`Recipe`]. #[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Clone)] -#[serde(tag = "type", rename_all = "camelCase")] pub(crate) enum Item { /// Just plain text Text { @@ -205,7 +204,8 @@ pub(crate) struct Timer { pub(crate) name: Option<String>, /// Time quantity - pub(crate) quantity: UnitAmount, + #[schema(nullable = false)] + pub(crate) quantity: Option<UnitAmount>, } #[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Clone)] @@ -238,12 +238,18 @@ pub(crate) struct Metadata { } pub(crate) mod conversion { - use crate::storage::sql::get::product::from_id; + use crate::storage::sql::get::{product::from_id, unit::from_name}; #[derive(thiserror::Error, Debug)] pub(crate) enum Error { #[error("Failed to get a product by id: `{0}`")] ProductAccess(#[from] from_id::Error), + + #[error("The quantity of a cookware has an unit: `{0}`")] + CookwareQtyHasUnit(cooklang::Quantity), + + #[error("Failed to get a unit by name: `{0}`")] + UnitLookup(#[from] from_name::Error), } } @@ -258,8 +264,8 @@ pub(crate) struct NameAndUrl { impl NameAndUrl { async fn from(value: cooklang::metadata::NameAndUrl) -> Result<Self, conversion::Error> { Ok(Self { - name: value.name().map(|v| v.to_owned()), - url: value.url().map(|v| v.to_owned()), + name: value.name().map(ToOwned::to_owned), + url: value.url().map(ToOwned::to_owned), }) } } @@ -274,7 +280,7 @@ impl CooklangRecipe { sections: for_in!(value.sections, |s| Section::from(s).await), ingredients: for_in!(value.ingredients, |i| Ingredient::from(app, i).await), cookware: for_in!(value.cookware, |c| Cookware::from(c).await), - timers: for_in!(value.timers, |t| Timer::from(t).await), + timers: for_in!(value.timers, |t| Timer::from(app, t).await), }) } } @@ -353,16 +359,22 @@ impl Ingredient { async fn from(app: &App, value: cooklang::Ingredient) -> Result<Self, conversion::Error> { if value.name.starts_with('/') { Ok(Self::RecipeReference { id: todo!() }) - } else if let Some(product) = Product::from_name(&app, value.name.as_str()).await? { + } else if let Some(product) = Product::from_name(app, value.name.as_str()).await? { Ok(Self::RegisteredProduct { id: product.id, alias: value.alias, - quantity: None, + quantity: { + let unit_amount = quantity_to_unit_amount(app, value.quantity).await?; + unit_amount.map(|ua| ProductAmount { + product_id: product.id, + amount: ua, + }) + }, }) } else { Ok(Self::NotRegisteredProduct { name: value.name, - quantity: None, + quantity: quantity_to_unit_amount(app, value.quantity).await?, }) } } @@ -372,20 +384,72 @@ impl Cookware { Ok(Self { name: value.name, alias: value.alias, - quantity: value.quantity.map(|q| match q.value() { - cooklang::Value::Number(number) => number.value() as usize, - cooklang::Value::Range { start, end } => todo!(), - cooklang::Value::Text(_) => todo!(), - }), + quantity: if let Some(qty) = value.quantity { + if qty.unit().is_some() { + return Err(conversion::Error::CookwareQtyHasUnit(qty)); + } + + let var_name = match qty.value() { + cooklang::Value::Number(number) => number.value() as usize, + cooklang::Value::Range { .. } | cooklang::Value::Text(_) => todo!(), + }; + + Some(var_name) + } else { + None + }, note: value.note, }) } } impl Timer { - async fn from(value: cooklang::Timer) -> Result<Self, conversion::Error> { + async fn from(app: &App, value: cooklang::Timer) -> Result<Self, conversion::Error> { Ok(Self { name: value.name, - quantity: todo!(), + quantity: quantity_to_unit_amount(app, value.quantity).await?, }) } } + +async fn quantity_to_unit_amount( + app: &App, + qty: Option<cooklang::Quantity>, +) -> Result<Option<UnitAmount>, conversion::Error> { + if let Some(qty) = qty { + let amount: f64 = match qty.value() { + cooklang::Value::Number(number) => number.value(), + cooklang::Value::Range { start, end } => { + // Let's just assume that more is better than less? + // TODO: This should be mapped correctly <2026-03-15> + + end.value() + } + cooklang::Value::Text(_) => { + // TODO: Is there maybe a better way to deal with, non-parsable quantities? <2026-03-15> + return Ok(None); + } + }; + + let unit = if let Some(unit_name) = qty.unit() { + let unit = Unit::from_name(app, unit_name).await?; + + if unit.is_none() { + error!( + "Failed to transfer the quantity for a recipe amount, as the unit is not yet known: {unit_name}" + ); + } + + unit + } else { + return Ok(None); + }; + + Ok(unit.map(|unit| UnitAmount { + // TODO: We need to convert the unit to the one that can fit the full f64 value as u32. <2026-03-15> + value: amount as u32, + unit: unit.id, + })) + } else { + Ok(None) + } +} |
