aboutsummaryrefslogtreecommitdiffstats
path: root/crates/rocie-server/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/rocie-server/src/api/set/no_auth/user.rs43
-rw-r--r--crates/rocie-server/src/app.rs10
-rw-r--r--crates/rocie-server/src/storage/migrate/defaults.rs99
-rw-r--r--crates/rocie-server/src/storage/migrate/mod.rs72
-rw-r--r--crates/rocie-server/src/storage/migrate/sql/0->1.sql7
-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
13 files changed, 464 insertions, 40 deletions
diff --git a/crates/rocie-server/src/api/set/no_auth/user.rs b/crates/rocie-server/src/api/set/no_auth/user.rs
index 7ca865c..69758a7 100644
--- a/crates/rocie-server/src/api/set/no_auth/user.rs
+++ b/crates/rocie-server/src/api/set/no_auth/user.rs
@@ -6,9 +6,13 @@ use utoipa::ToSchema;
use crate::{
api::set::auth::user::UserStub,
app::App,
- storage::sql::{
- insert::Operations,
- user::{PasswordHash, User, UserId},
+ storage::{
+ migrate::migrate_db,
+ sql::{
+ config::Config,
+ insert::Operations,
+ user::{PasswordHash, User, UserId},
+ },
},
};
@@ -84,6 +88,14 @@ async fn logout(user: Identity) -> impl Responder {
HttpResponse::Ok()
}
+#[derive(ToSchema, Deserialize)]
+struct ProvisionInfo {
+ user: UserStub,
+
+ /// Whether we should apply the default configuration.
+ use_defaults: bool,
+}
+
/// Provision this instance.
///
/// This only works, if no users exist yet.
@@ -104,24 +116,39 @@ async fn logout(user: Identity) -> impl Responder {
body = String
)
),
- request_body = UserStub,
+ request_body = ProvisionInfo,
)]
#[post("/provision")]
async fn provision(
request: HttpRequest,
app: web::Data<App>,
- new_user: web::Json<UserStub>,
+ info: web::Json<ProvisionInfo>,
) -> Result<impl Responder> {
if User::get_all(&app).await?.is_empty() {
- let user = new_user.into_inner();
+ let info = info.into_inner();
let mut ops = Operations::new("register user (during provisioning)");
- let password_hash = PasswordHash::from_password(&user.password);
- let user = User::register(user.name, password_hash, user.description, &mut ops);
+ let password_hash = PasswordHash::from_password(&info.user.password);
+ let user = User::register(
+ info.user.name,
+ password_hash,
+ info.user.description,
+ &mut ops,
+ );
ops.apply(&app).await?;
+ if info.use_defaults {
+ let mut ops =
+ Operations::new("Set should use defaults on config (during provisioning)");
+ let mut config = Config::get(&app).await?;
+ config.set_use_default(true, &mut ops);
+ ops.apply(&app).await?;
+
+ migrate_db(&app).await?;
+ }
+
Identity::login(&request.extensions(), user.id.to_string())?;
Ok(HttpResponse::Ok().json(user.id))
diff --git a/crates/rocie-server/src/app.rs b/crates/rocie-server/src/app.rs
index bb27470..59eed28 100644
--- a/crates/rocie-server/src/app.rs
+++ b/crates/rocie-server/src/app.rs
@@ -1,12 +1,13 @@
-use std::path::PathBuf;
+use std::{cell::OnceCell, path::PathBuf, sync::OnceLock};
use sqlx::{SqlitePool, sqlite::SqliteConnectOptions};
-use crate::storage::migrate::migrate_db;
+use crate::storage::migrate::{DbVersion, migrate_db};
#[derive(Clone)]
pub(crate) struct App {
pub(crate) db: SqlitePool,
+ pub(crate) db_version_at_start: OnceLock<DbVersion>,
}
impl App {
@@ -25,7 +26,10 @@ impl App {
})?
};
- let me = Self { db };
+ let me = Self {
+ db,
+ db_version_at_start: OnceLock::new(),
+ };
migrate_db(&me).await?;
diff --git a/crates/rocie-server/src/storage/migrate/defaults.rs b/crates/rocie-server/src/storage/migrate/defaults.rs
new file mode 100644
index 0000000..3a2019c
--- /dev/null
+++ b/crates/rocie-server/src/storage/migrate/defaults.rs
@@ -0,0 +1,99 @@
+use crate::{
+ app::App,
+ storage::sql::{insert::Operations, unit::Unit, unit_property::UnitProperty},
+};
+
+#[expect(clippy::unnecessary_wraps, reason = "The API expects an Option<_>")]
+fn so(input: &'static str) -> Option<String> {
+ Some(s(input))
+}
+fn s(input: &'static str) -> String {
+ String::from(input)
+}
+
+macro_rules! register {
+ (
+ $app:ident # $unit_prop_description:literal, $unit_prop_name:literal
+ $(
+ -> $unit_full_name_singular:ident, $unit_full_name_plural:ident, $unit_short_name:ident
+ )*
+ ) => {
+ let mut ops = Operations::new(concat!(
+ "create",
+ $unit_prop_name,
+ "unit property (during provisioning)"
+ ));
+ let unit_property = UnitProperty::register(
+ s($unit_prop_name),
+ so($unit_prop_description),
+ &mut ops,
+ )
+ .id;
+ ops.apply(&$app).await?;
+
+ let mut ops = Operations::new(concat!(
+ "create default units for",
+ $unit_prop_name,
+ "property (during provisioning)"
+ ));
+
+ $(
+ Unit::register(
+ s(stringify!($unit_full_name_singular)),
+ s(stringify!($unit_full_name_plural)),
+ s(stringify!($unit_short_name)),
+ None,
+ unit_property,
+ &mut ops,
+ );
+ )*
+
+ ops.apply(&$app).await?;
+ };
+}
+
+pub(super) async fn add_defaults_0_to_1(app: &App) -> Result<(), apply_defaults::Error> {
+ register!(
+ app # "Time mesurement units", "Time"
+ -> second, seconds, s
+ -> minute, minutes, min
+ -> hour, hours, h
+ );
+
+ register!(
+ app # "Mass (weight) mesurement units", "Mass"
+ -> milligram, milligrams, mg
+ -> gram, grams, g
+ -> kilogram, kilograms, kg
+ );
+
+ register!(
+ app # "Volume mesurement units", "Volume"
+ -> milliliter, millilters, ml
+ -> deciliter, deciliters, dl
+ -> liter, liters, l
+
+ // English
+ -> tablespoon, tablespoons, tbsp
+ -> teaspoon, teaspoons, tsp
+
+ // Swedish
+ -> tesked, teskedar, tsk
+ -> matsked, matskedar, msk
+ );
+
+ Ok(())
+}
+
+pub(crate) mod apply_defaults {
+ use crate::storage::sql::insert::{self, apply};
+
+ #[derive(thiserror::Error, Debug)]
+ pub(crate) enum Error {
+ #[error("Failed to add new default unit-property the database: {0}")]
+ AddUnitProperty(#[from] apply::Error<insert::unit_property::Operation>),
+
+ #[error("Failed to add new default unit the database: {0}")]
+ AddUnit(#[from] apply::Error<insert::unit::Operation>),
+ }
+}
diff --git a/crates/rocie-server/src/storage/migrate/mod.rs b/crates/rocie-server/src/storage/migrate/mod.rs
index ae0732b..5c81580 100644
--- a/crates/rocie-server/src/storage/migrate/mod.rs
+++ b/crates/rocie-server/src/storage/migrate/mod.rs
@@ -7,10 +7,15 @@ use chrono::TimeDelta;
use log::{debug, info};
use sqlx::{Sqlite, SqlitePool, Transaction, query};
-use crate::app::App;
+use crate::{
+ app::App,
+ storage::sql::{config::Config, get::config},
+};
+
+mod defaults;
macro_rules! make_upgrade {
- ($app:expr, $old_version:expr, $new_version:expr, $sql_name:expr) => {
+ ($app:expr, $old_version:expr, $new_version:expr, $sql_name:expr) => {{
let mut tx = $app
.db
.begin()
@@ -57,8 +62,8 @@ macro_rules! make_upgrade {
new_version: $new_version,
})?;
- Ok(())
- };
+ Ok::<_, update::Error>(())
+ }};
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
@@ -135,11 +140,12 @@ impl DbVersion {
///
/// Each update is atomic, so if this function fails you are still guaranteed to have a
/// database at version `get_version`.
- #[allow(clippy::too_many_lines)]
async fn update(self, app: &App) -> Result<(), update::Error> {
match self {
Self::Empty => {
- make_upgrade! {app, Self::Empty, Self::One, "./sql/0->1.sql"}
+ make_upgrade! {app, Self::Empty, Self::One, "./sql/0->1.sql"}?;
+
+ Ok(())
}
// This is the current_version
@@ -158,7 +164,10 @@ impl Display for DbVersion {
}
}
pub(crate) mod update {
- use crate::storage::migrate::{DbVersion, db_version_set, get_db_version};
+ use crate::storage::{
+ migrate::{DbVersion, db_version_set, defaults, get_db_version},
+ sql::get::config,
+ };
#[derive(thiserror::Error, Debug)]
pub(crate) enum Error {
@@ -182,6 +191,11 @@ pub(crate) mod update {
#[error("Failed to commit the update transaction: {0}")]
TxnCommit(sqlx::Error),
+ #[error("Failed to access the rocie config: {0}")]
+ ConfigGet(#[from] config::get::Error),
+ #[error("Failed to add defaults to the database: {0}")]
+ AddDefaults(#[from] defaults::apply_defaults::Error),
+
#[error("Failed to perform the next chained update (to ver {new_version}): {err}")]
NextUpdate {
err: Box<Self>,
@@ -197,6 +211,12 @@ pub(crate) mod db_version_parse {
}
}
+async fn should_use_defaults(app: &App) -> Result<bool, config::get::Error> {
+ let config = Config::get(app).await?;
+
+ Ok(config.should_use_defaults)
+}
+
/// Returns the current data as UNIX time stamp.
pub(crate) fn get_current_date() -> i64 {
let start = SystemTime::now();
@@ -238,7 +258,7 @@ pub(crate) async fn get_version_db(pool: &SqlitePool) -> Result<DbVersion, get_d
)
.fetch_optional(pool)
.await
- .map_err(|err| get_db_version::Error::VersionTableExistance(err))?;
+ .map_err(get_db_version::Error::VersionTableExistance)?;
if let Some(output) = query {
assert_eq!(output.result, 1);
@@ -261,7 +281,7 @@ pub(crate) async fn get_version_db(pool: &SqlitePool) -> Result<DbVersion, get_d
)
.fetch_one(pool)
.await
- .map_err(|err| get_db_version::Error::VersionNumberFetch(err))?;
+ .map_err(get_db_version::Error::VersionNumberFetch)?;
Ok(DbVersion::from_db(
current_version.number,
@@ -288,6 +308,26 @@ pub(crate) mod get_db_version {
pub(crate) async fn migrate_db(app: &App) -> Result<(), migrate_db::Error> {
let current_version = get_version(app).await?;
+ if app.db_version_at_start.get().is_none() {
+ app.db_version_at_start
+ .set(current_version)
+ .expect("the cell to be unititialized, we checked");
+ }
+
+ if app.db_version_at_start.get() == Some(&DbVersion::Empty) {
+ // We cannot run this code in the normal update function, because for the empty db, there
+ // is no way to know if we want defaults.
+ //
+ // So we need to add the defaults later, which is achieved by another call to `migrate_db`
+ // in provision.
+ //
+ // That is kinda hacky, but I don't see a way around it.
+ // For defaults added in version Two and onward, the default mechanism should just work.
+ if should_use_defaults(app).await? {
+ defaults::add_defaults_0_to_1(app).await?;
+ }
+ }
+
if current_version == CURRENT_VERSION {
return Ok(());
}
@@ -300,7 +340,12 @@ pub(crate) async fn migrate_db(app: &App) -> Result<(), migrate_db::Error> {
}
pub(crate) mod migrate_db {
- use crate::storage::migrate::{get_db_version, update};
+ use actix_web::ResponseError;
+
+ use crate::storage::{
+ migrate::{defaults, get_db_version, update},
+ sql::get::config,
+ };
#[derive(thiserror::Error, Debug)]
pub(crate) enum Error {
@@ -309,5 +354,12 @@ pub(crate) mod migrate_db {
#[error("Failed to update the database: {0}")]
Upadate(#[from] update::Error),
+
+ #[error("Failed to access the rocie config: {0}")]
+ ConfigGet(#[from] config::get::Error),
+ #[error("Failed to add defaults to the database: {0}")]
+ AddDefaults(#[from] defaults::apply_defaults::Error),
}
+
+ impl ResponseError for Error {}
}
diff --git a/crates/rocie-server/src/storage/migrate/sql/0->1.sql b/crates/rocie-server/src/storage/migrate/sql/0->1.sql
index ba44c68..8f99322 100644
--- a/crates/rocie-server/src/storage/migrate/sql/0->1.sql
+++ b/crates/rocie-server/src/storage/migrate/sql/0->1.sql
@@ -178,3 +178,10 @@ CREATE TABLE txn_log (
timestamp INTEGER NOT NULL,
operation TEXT NOT NULL
) STRICT;
+
+CREATE TABLE config (
+ -- Make it impossible to insert more than one value here.
+ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 0),
+ use_defaults INTEGER NOT NULL CHECK (use_defaults = 1 OR use_defaults = 0)
+) STRICT;
+INSERT INTO config (id, use_defaults) VALUES (0, 0);
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)
+ }
+}