// rocie - An enterprise grocery management system // // Copyright (C) 2026 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Rocie. // // You should have received a copy of the License along with this program. // If not, see . use cooklang::{Converter, CooklangParser, Extensions}; use serde::{Deserialize, Serialize}; use sqlx::query; use uuid::Uuid; use crate::{ app::App, storage::sql::{ insert::{Operations, Transactionable}, recipe::{CooklangRecipe, Recipe, RecipeId}, recipe_parent::RecipeParentId, }, }; #[derive(Debug, Deserialize, Serialize)] pub(crate) enum Operation { New { id: RecipeId, name: String, parent: Option, content: String, }, } 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::New { id, name, parent, content, } => { query!( " INSERT INTO recipies (id, name, parent, content) VALUES (?, ?, ?, ?) ", id, name, parent, content, ) .execute(txn) .await?; } } Ok(()) } async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> { match self { Operation::New { id, name, parent, content, } => { query!( " DELETE FROM recipies WHERE id = ? AND name = ? AND parent = ? AND content = ? ", id, name, parent, content ) .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), } } pub(crate) mod new { use actix_web::ResponseError; use crate::storage::sql::recipe::conversion; #[derive(thiserror::Error, Debug)] pub(crate) enum Error { #[error("Failed to parse the recipe contents as cooklang: `{0}`")] RecipeParse(#[from] cooklang::error::SourceReport), #[error("Failed to convert the cooklang recipe to our struct: `{0}`")] RecipeConvert(#[from] conversion::Error), } impl ResponseError for Error {} } impl Recipe { pub(crate) async fn new( app: &App, name: String, parent: Option, content: String, ops: &mut Operations, ) -> Result { let id = RecipeId::from(Uuid::new_v4()); let parser = CooklangParser::new(Extensions::empty(), Converter::bundled()); // TODO: Somehow return the warnings <2026-01-31> let (recipe, _warnings) = parser.parse(&content).into_result()?; ops.push(Operation::New { id, content, name: name.clone(), parent, }); Ok(Self { id, name, parent, content: CooklangRecipe::from(app, recipe).await?, }) } }