aboutsummaryrefslogtreecommitdiffstats
path: root/crates/rocie-server/src/storage/sql
diff options
context:
space:
mode:
Diffstat (limited to 'crates/rocie-server/src/storage/sql')
-rw-r--r--crates/rocie-server/src/storage/sql/config.rs7
-rw-r--r--crates/rocie-server/src/storage/sql/get/config/mod.rs41
-rw-r--r--crates/rocie-server/src/storage/sql/get/mod.rs3
-rw-r--r--crates/rocie-server/src/storage/sql/get/unit/mod.rs40
-rw-r--r--crates/rocie-server/src/storage/sql/insert/config/mod.rs80
-rw-r--r--crates/rocie-server/src/storage/sql/insert/mod.rs1
-rw-r--r--crates/rocie-server/src/storage/sql/mod.rs1
-rw-r--r--crates/rocie-server/src/storage/sql/recipe.rs100
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)
+ }
+}