diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-11-28 16:30:02 +0100 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-11-28 16:30:02 +0100 |
| commit | a62ab5c6dacaddb67931d7ac160bc7faaa707737 (patch) | |
| tree | a35fa3540fbb89f575ab1ea72f9b23ace399e01c /crates | |
| parent | chore(crates/rocie-client): Re-generate (diff) | |
| download | server-a62ab5c6dacaddb67931d7ac160bc7faaa707737.zip | |
feat(crates/rocie-server): Get closer to feature parity between rocie and grocy
Diffstat (limited to 'crates')
39 files changed, 1344 insertions, 78 deletions
diff --git a/crates/rocie-server/Cargo.toml b/crates/rocie-server/Cargo.toml index 9f09b3c..51df09c 100644 --- a/crates/rocie-server/Cargo.toml +++ b/crates/rocie-server/Cargo.toml @@ -32,6 +32,8 @@ tokio.workspace = true [dependencies] actix-cors = "0.7.1" +actix-identity = "0.9.0" +actix-session = { version = "0.11.0", features = ["cookie-session"] } actix-web = "4.11.0" chrono = "0.4.41" clap = { version = "4.5.45", features = ["derive", "env"] } diff --git a/crates/rocie-server/src/api/get/mod.rs b/crates/rocie-server/src/api/get/mod.rs index 1c32105..487b55c 100644 --- a/crates/rocie-server/src/api/get/mod.rs +++ b/crates/rocie-server/src/api/get/mod.rs @@ -2,6 +2,8 @@ use actix_web::web; pub(crate) mod inventory; pub(crate) mod product; +pub(crate) mod product_parent; +pub(crate) mod recipe; pub(crate) mod unit; pub(crate) mod unit_property; @@ -9,8 +11,17 @@ pub(crate) fn register_paths(cfg: &mut web::ServiceConfig) { cfg.service(product::product_by_id) .service(product::product_by_name) .service(product::product_suggestion_by_name) - .service(product::products) + .service(product::products_registered) + .service(product::products_in_storage) + .service(product::products_by_product_parent_id_indirect) + .service(product::products_by_product_parent_id_direct) + .service(product_parent::product_parents) + .service(product_parent::product_parents_toplevel) + .service(product_parent::product_parents_under) + .service(recipe::recipe_by_id) + .service(recipe::recipes) .service(unit::units) + .service(unit::units_by_property_id) .service(unit::unit_by_id) .service(unit_property::unit_properties) .service(unit_property::unit_property_by_id) diff --git a/crates/rocie-server/src/api/get/product.rs b/crates/rocie-server/src/api/get/product.rs index 55e5d91..4216f9b 100644 --- a/crates/rocie-server/src/api/get/product.rs +++ b/crates/rocie-server/src/api/get/product.rs @@ -4,7 +4,11 @@ use percent_encoding::percent_decode_str; use crate::{ app::App, - storage::sql::product::{Product, ProductId, ProductIdStub}, + storage::sql::{ + product::{Product, ProductId, ProductIdStub}, + product_amount::ProductAmount, + product_parent::{ProductParent, ProductParentId, ProductParentIdStub}, + }, }; /// A String, that is not url-decoded on parse. @@ -65,7 +69,7 @@ pub(crate) async fn product_by_name( req: HttpRequest, name: web::Path<String>, ) -> Result<impl Responder> { - let _name = name; + drop(name); let name = UrlEncodedString::from_str( req.path() @@ -96,7 +100,7 @@ pub(crate) async fn product_suggestion_by_name( req: HttpRequest, name: web::Path<String>, ) -> Result<impl Responder> { - let _name = name; + drop(name); let name = UrlEncodedString::from_str( req.path() @@ -118,13 +122,116 @@ pub(crate) async fn product_suggestion_by_name( /// Return all registered products #[utoipa::path( responses( - (status = OK, description = "All products founds", body = Vec<Product>), + (status = OK, description = "All products found", body = Vec<Product>), (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) ), )] -#[get("/products/")] -pub(crate) async fn products(app: web::Data<App>) -> Result<impl Responder> { +#[get("/products_registered/")] +pub(crate) async fn products_registered(app: web::Data<App>) -> Result<impl Responder> { let all = Product::get_all(&app).await?; Ok(HttpResponse::Ok().json(all)) } + +/// Return all products, which non-null amount in storage +#[utoipa::path( + responses( + (status = OK, description = "All products found", body = Vec<Product>), + (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) + ), +)] +#[get("/products_in_storage/")] +pub(crate) async fn products_in_storage(app: web::Data<App>) -> Result<impl Responder> { + let all = Product::get_all(&app).await?; + + let mut output_products = Vec::with_capacity(all.len()); + for product in all { + let amount = ProductAmount::from_id(&app, product.id).await?; + + if amount.is_some_and(|amount| amount.amount.value > 0) { + output_products.push(product); + } + } + + Ok(HttpResponse::Ok().json(output_products)) +} + +/// Get Products by it's product parent id +/// +/// This will also return all products below this product parent id +#[utoipa::path( + responses( + (status = OK, description = "Products found from database", body = Vec<Product>), + (status = NOT_FOUND, description = "Product parent id not found in database"), + (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) + ), + params( + ("id" = ProductParentId, description = "Product parent id" ), + ) +)] +#[get("/product/by-product-parent-id-indirect/{id}")] +pub(crate) async fn products_by_product_parent_id_indirect( + app: web::Data<App>, + id: web::Path<ProductParentIdStub>, +) -> Result<impl Responder> { + let id = id.into_inner(); + + if let Some(parent) = ProductParent::from_id(&app, id.into()).await? { + async fn collect_products(app: &App, parent: ProductParent) -> Result<Vec<Product>> { + let mut all = Product::get_all(app) + .await? + .into_iter() + .filter(|prod| prod.parent.is_some_and(|val| val == parent.id)) + .collect::<Vec<_>>(); + + if let Some(child) = ProductParent::get_all(app) + .await? + .into_iter() + .find(|pp| pp.parent.is_some_and(|id| id == parent.id)) + { + all.extend(Box::pin(collect_products(app, child)).await?); + } + + Ok(all) + } + + let all = collect_products(&app, parent).await?; + + Ok(HttpResponse::Ok().json(all)) + } else { + Ok(HttpResponse::NotFound().finish()) + } +} + +/// Get Products by it's product parent id +/// +/// This will only return products directly associated with this product parent id +#[utoipa::path( + responses( + (status = OK, description = "Products found from database", body = Vec<Product>), + (status = NOT_FOUND, description = "Product parent id not found in database"), + (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) + ), + params( + ("id" = ProductParentId, description = "Product parent id" ), + ) +)] +#[get("/product/by-product-parent-id-direct/{id}")] +pub(crate) async fn products_by_product_parent_id_direct( + app: web::Data<App>, + id: web::Path<ProductParentIdStub>, +) -> Result<impl Responder> { + let id = id.into_inner(); + + if let Some(parent) = ProductParent::from_id(&app, id.into()).await? { + let all = Product::get_all(&app) + .await? + .into_iter() + .filter(|prod| prod.parent.is_some_and(|val| val == parent.id)) + .collect::<Vec<_>>(); + + Ok(HttpResponse::Ok().json(all)) + } else { + Ok(HttpResponse::NotFound().finish()) + } +} diff --git a/crates/rocie-server/src/api/get/product_parent.rs b/crates/rocie-server/src/api/get/product_parent.rs new file mode 100644 index 0000000..a62397c --- /dev/null +++ b/crates/rocie-server/src/api/get/product_parent.rs @@ -0,0 +1,64 @@ +use actix_web::{HttpResponse, Responder, error::Result, get, web}; + +use crate::{ + app::App, + storage::sql::product_parent::{ProductParent, ProductParentId, ProductParentIdStub}, +}; + +/// Return all registered product parents +#[utoipa::path( + responses( + (status = OK, description = "All parents found", body = Vec<ProductParent>), + (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) + ), +)] +#[get("/product_parents/")] +pub(crate) async fn product_parents(app: web::Data<App>) -> Result<impl Responder> { + let all: Vec<ProductParent> = ProductParent::get_all(&app).await?; + + Ok(HttpResponse::Ok().json(all)) +} + +/// Return all registered product parents, that have no parents themselves +#[utoipa::path( + responses( + (status = OK, description = "All parents found", body = Vec<ProductParent>), + (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) + ), +)] +#[get("/product_parents_toplevel/")] +pub(crate) async fn product_parents_toplevel(app: web::Data<App>) -> Result<impl Responder> { + let all: Vec<ProductParent> = ProductParent::get_all(&app) + .await? + .into_iter() + .filter(|parent| parent.parent.is_none()) + .collect(); + + Ok(HttpResponse::Ok().json(all)) +} + +/// Return all parents, that have this parent as parent +#[utoipa::path( + responses( + (status = OK, description = "All parents found", body = Vec<ProductParent>), + (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) + ), + params( + ("id" = ProductParentId, description = "Product parent id" ), + ), +)] +#[get("/product_parents_under/{id}")] +pub(crate) async fn product_parents_under( + app: web::Data<App>, + id: web::Path<ProductParentIdStub>, +) -> Result<impl Responder> { + let id = id.into_inner().into(); + + let all: Vec<_> = ProductParent::get_all(&app) + .await? + .into_iter() + .filter(|parent| parent.parent.is_some_and(|found| found == id)) + .collect(); + + Ok(HttpResponse::Ok().json(all)) +} diff --git a/crates/rocie-server/src/api/get/recipe.rs b/crates/rocie-server/src/api/get/recipe.rs new file mode 100644 index 0000000..70bab39 --- /dev/null +++ b/crates/rocie-server/src/api/get/recipe.rs @@ -0,0 +1,66 @@ +use actix_web::{HttpResponse, Responder, error::Result, get, web}; + +use crate::{ + app::App, + storage::sql::recipe::{Recipe, RecipeId, RecipeIdStub}, +}; + +/// Get an recipe by it's id. +#[utoipa::path( + responses( + ( + status = OK, + description = "Recipe found in database and fetched", + body = Recipe, + ), + ( + status = NOT_FOUND, + description = "Recipe not found in database" + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) + ), + params( + ( + "id" = RecipeId, + description = "Recipe id" + ), + ) +)] +#[get("/recipe/by-id/{id}")] +pub(crate) async fn recipe_by_id( + app: web::Data<App>, + id: web::Path<RecipeIdStub>, +) -> Result<impl Responder> { + let id = id.into_inner(); + + match Recipe::from_id(&app, id.into()).await? { + Some(recipe) => Ok(HttpResponse::Ok().json(recipe)), + None => Ok(HttpResponse::NotFound().finish()), + } +} + +/// Get all added recipes +#[utoipa::path( + responses( + ( + status = OK, + description = "All recipes found in database and fetched", + body = Recipe, + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) + ), +)] +#[get("/recipe/all")] +pub(crate) async fn recipes(app: web::Data<App>) -> Result<impl Responder> { + let all = Recipe::get_all(&app).await?; + + Ok(HttpResponse::Ok().json(all)) +} diff --git a/crates/rocie-server/src/api/get/unit.rs b/crates/rocie-server/src/api/get/unit.rs index 73aa626..caafaa3 100644 --- a/crates/rocie-server/src/api/get/unit.rs +++ b/crates/rocie-server/src/api/get/unit.rs @@ -1,6 +1,12 @@ -use actix_web::{get, web, HttpResponse, Responder, Result}; +use actix_web::{HttpResponse, Responder, Result, get, web}; -use crate::{app::App, storage::sql::unit::{Unit, UnitId, UnitIdStub}}; +use crate::{ + app::App, + storage::sql::{ + unit::{Unit, UnitId, UnitIdStub}, + unit_property::{UnitPropertyId, UnitPropertyIdStub}, + }, +}; /// Return all registered units #[utoipa::path( @@ -24,6 +30,42 @@ pub(crate) async fn units(app: web::Data<App>) -> Result<impl Responder> { Ok(HttpResponse::Ok().json(all)) } +/// Return all registered units for a specific unit property +#[utoipa::path( + responses( + ( + status = OK, + description = "All units founds", + body = Vec<Unit> + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) + ), + params( + ( + "id" = UnitPropertyId, + description = "Unit property id" + ), + ) +)] +#[get("/units-by-property/{id}")] +pub(crate) async fn units_by_property_id( + app: web::Data<App>, + id: web::Path<UnitPropertyIdStub>, +) -> Result<impl Responder> { + let id = id.into_inner(); + let all = Unit::get_all(&app) + .await? + .into_iter() + .filter(|unit| unit.unit_property == id.into()) + .collect::<Vec<_>>(); + + Ok(HttpResponse::Ok().json(all)) +} + /// Get Unit by id #[utoipa::path( responses( diff --git a/crates/rocie-server/src/api/set/mod.rs b/crates/rocie-server/src/api/set/mod.rs index 5dd0219..a6037b9 100644 --- a/crates/rocie-server/src/api/set/mod.rs +++ b/crates/rocie-server/src/api/set/mod.rs @@ -2,12 +2,16 @@ use actix_web::web; pub(crate) mod barcode; pub(crate) mod product; +pub(crate) mod product_parent; +pub(crate) mod recipe; pub(crate) mod unit; pub(crate) mod unit_property; pub(crate) fn register_paths(cfg: &mut web::ServiceConfig) { cfg.service(product::register_product) .service(product::associate_barcode) + .service(product_parent::register_product_parent) + .service(recipe::add_recipe) .service(unit::register_unit) .service(unit_property::register_unit_property) .service(barcode::consume_barcode) diff --git a/crates/rocie-server/src/api/set/product.rs b/crates/rocie-server/src/api/set/product.rs index 7372d23..74a92d2 100644 --- a/crates/rocie-server/src/api/set/product.rs +++ b/crates/rocie-server/src/api/set/product.rs @@ -8,6 +8,7 @@ use crate::{ barcode::Barcode, insert::Operations, product::{Product, ProductId, ProductIdStub}, + product_parent::ProductParentId, unit::Unit, unit_property::UnitPropertyId, }, @@ -22,10 +23,12 @@ struct ProductStub { unit_property: UnitPropertyId, /// A description. + #[schema(nullable = false)] description: Option<String>, /// A parent of this product, otherwise the parent will be the root of the parent tree. - parent: Option<ProductId>, + #[schema(nullable = false)] + parent: Option<ProductParentId>, } /// Register a product @@ -54,7 +57,7 @@ pub(crate) async fn register_product( let product = Product::register( product_stub.name.clone(), product_stub.description.clone(), - product_stub.parent, + product_stub.parent.into(), product_stub.unit_property, &mut ops, ); diff --git a/crates/rocie-server/src/api/set/product_parent.rs b/crates/rocie-server/src/api/set/product_parent.rs new file mode 100644 index 0000000..f917207 --- /dev/null +++ b/crates/rocie-server/src/api/set/product_parent.rs @@ -0,0 +1,60 @@ +use actix_web::{HttpResponse, Responder, Result, post, web}; +use serde::Deserialize; +use utoipa::ToSchema; + +use crate::{ + app::App, + storage::sql::{ + insert::Operations, + product_parent::{ProductParent, ProductParentId}, + }, +}; + +#[derive(Deserialize, ToSchema)] +struct ProductParentStub { + /// The name of the product parent + name: String, + + /// A description. + #[schema(nullable = false)] + description: Option<String>, + + /// A parent of this product parent, otherwise the parent will be the root of the parent tree. + #[schema(nullable = false)] + parent: Option<ProductParentId>, +} + +/// Register a product parent +#[utoipa::path( + responses( + ( + status = 200, + description = "Product parent successfully registered in database", + body = ProductParentId, + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String, + ) + ), + request_body = ProductParentStub, +)] +#[post("/product_parent/new")] +pub(crate) async fn register_product_parent( + app: web::Data<App>, + product_stub: web::Json<ProductParentStub>, +) -> Result<impl Responder> { + let mut ops = Operations::new("register product parent"); + + let product = ProductParent::register( + product_stub.name.clone(), + product_stub.description.clone(), + product_stub.parent.into(), + &mut ops, + ); + + ops.apply(&app).await?; + + Ok(HttpResponse::Ok().json(product.id)) +} diff --git a/crates/rocie-server/src/api/set/recipe.rs b/crates/rocie-server/src/api/set/recipe.rs new file mode 100644 index 0000000..bb5be37 --- /dev/null +++ b/crates/rocie-server/src/api/set/recipe.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +use actix_web::{HttpResponse, Responder, error::Result, post, web}; +use serde::Deserialize; +use utoipa::ToSchema; + +use crate::{ + app::App, + storage::sql::{ + insert::Operations, + recipe::{Recipe, RecipeId}, + }, +}; + +#[derive(Deserialize, ToSchema)] +struct RecipeStub { + /// The path the recipe should have + #[schema(value_type = String)] + path: PathBuf, + + /// The content of this recipe, in cooklang format + content: String, +} + +/// Register a product parent +#[utoipa::path( + responses( + ( + status = 200, + description = "Product parent successfully registered in database", + body = RecipeId, + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String, + ) + ), + request_body = RecipeStub, +)] +#[post("/recipe/new")] +pub(crate) async fn add_recipe( + app: web::Data<App>, + stub: web::Json<RecipeStub>, +) -> Result<impl Responder> { + let stub = stub.into_inner(); + let mut ops = Operations::new("add recipe parent"); + + let recipe = Recipe::new(stub.path, stub.content, &mut ops); + + ops.apply(&app).await?; + + Ok(HttpResponse::Ok().json(recipe.id)) +} diff --git a/crates/rocie-server/src/api/set/unit.rs b/crates/rocie-server/src/api/set/unit.rs index 5f39c8f..1671918 100644 --- a/crates/rocie-server/src/api/set/unit.rs +++ b/crates/rocie-server/src/api/set/unit.rs @@ -17,6 +17,8 @@ struct UnitStub { full_name_singular: String, short_name: String, unit_property: UnitPropertyId, + + #[schema(nullable = false)] description: Option<String>, } diff --git a/crates/rocie-server/src/api/set/unit_property.rs b/crates/rocie-server/src/api/set/unit_property.rs index 9915e84..ca2960f 100644 --- a/crates/rocie-server/src/api/set/unit_property.rs +++ b/crates/rocie-server/src/api/set/unit_property.rs @@ -12,7 +12,11 @@ use crate::{ #[derive(Deserialize, ToSchema)] struct UnitPropertyStub { + /// The name of the unit property. name: String, + + /// An optional description of the unit property. + #[schema(nullable = false)] description: Option<String>, } diff --git a/crates/rocie-server/src/main.rs b/crates/rocie-server/src/main.rs index 2329b0b..dc5be0b 100644 --- a/crates/rocie-server/src/main.rs +++ b/crates/rocie-server/src/main.rs @@ -1,5 +1,10 @@ use actix_cors::Cors; -use actix_web::{App, HttpServer, middleware::Logger, web::Data}; +use actix_web::{ + App, HttpServer, + cookie::{Key, SameSite}, + middleware::Logger, + web::Data, +}; use clap::Parser; use utoipa::OpenApi; @@ -10,6 +15,9 @@ mod app; mod cli; mod storage; +use actix_identity::IdentityMiddleware; +use actix_session::{SessionMiddleware, storage::CookieSessionStore}; + #[actix_web::main] #[expect( clippy::needless_for_each, @@ -22,14 +30,25 @@ async fn main() -> Result<(), std::io::Error> { api::get::product::product_by_id, api::get::product::product_by_name, api::get::product::product_suggestion_by_name, - api::get::product::products, + api::get::product::products_registered, + api::get::product::products_in_storage, + api::get::product::products_by_product_parent_id_indirect, + api::get::product::products_by_product_parent_id_direct, + api::get::product_parent::product_parents, + api::get::product_parent::product_parents_toplevel, + api::get::product_parent::product_parents_under, + api::get::recipe::recipe_by_id, + api::get::recipe::recipes, api::get::unit::units, + api::get::unit::units_by_property_id, api::get::unit::unit_by_id, api::get::unit_property::unit_property_by_id, api::get::unit_property::unit_properties, api::get::inventory::amount_by_id, api::set::product::register_product, api::set::product::associate_barcode, + api::set::product_parent::register_product_parent, + api::set::recipe::add_recipe, api::set::unit::register_unit, api::set::unit_property::register_unit_property, api::set::barcode::buy_barcode, @@ -45,6 +64,11 @@ async fn main() -> Result<(), std::io::Error> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + // When using `Key::generate()` it is important to initialize outside of the + // `HttpServer::new` closure. When deployed the secret key should be read from a + // configuration file or environment variables. + let secret_key = Key::generate(); + let args = CliArgs::parse(); match args.command { @@ -67,6 +91,18 @@ async fn main() -> Result<(), std::io::Error> { .wrap(Logger::new( r#"%a "%r" -> %s %b ("%{Referer}i" "%{User-Agent}i" %T s)"#, )) + // Install the identity framework before middle-ware (as actix is filo). + .wrap(IdentityMiddleware::default()) + .wrap( + SessionMiddleware::builder( + CookieSessionStore::default(), + secret_key.clone(), + ) + .cookie_secure(true) + .cookie_http_only(true) + .cookie_same_site(SameSite::Strict) + .build(), + ) .app_data(Data::clone(&data)) .configure(api::get::register_paths) .configure(api::set::register_paths) 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 7f08738..664f40f 100644 --- a/crates/rocie-server/src/storage/migrate/sql/0->1.sql +++ b/crates/rocie-server/src/storage/migrate/sql/0->1.sql @@ -22,6 +22,8 @@ CREATE TABLE parents ( parent TEXT DEFAULT NULL CHECK ( id IS NOT parent ), + name TEXT UNIQUE NOT NULL, + description TEXT, FOREIGN KEY(parent) REFERENCES parents(id) ) STRICT; @@ -97,7 +99,7 @@ END; CREATE TABLE units ( id TEXT UNIQUE NOT NULL PRIMARY KEY, - unit_property TEXT UNIQUE NOT NULL, + unit_property TEXT NOT NULL, full_name_singular TEXT UNIQUE NOT NULL, full_name_plural TEXT UNIQUE NOT NULL, short_name TEXT UNIQUE NOT NULL, @@ -111,6 +113,12 @@ CREATE TABLE unit_properties ( description TEXT ) STRICT; +CREATE TABLE recipies ( + id TEXT UNIQUE NOT NULL PRIMARY KEY, + path TEXT UNIQUE NOT NULL, + content TEXT NOT NULL +) STRICT; + -- Encodes unit conversions: -- {factor} {from_unit} = 1 {to_unit} -- E.g.: 1000 g = 1 kg diff --git a/crates/rocie-server/src/storage/sql/get/mod.rs b/crates/rocie-server/src/storage/sql/get/mod.rs index 62047b8..1fb54b0 100644 --- a/crates/rocie-server/src/storage/sql/get/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/mod.rs @@ -1,5 +1,7 @@ pub(crate) mod product; +pub(crate) mod product_parent; pub(crate) mod product_amount; pub(crate) mod unit; pub(crate) mod unit_property; pub(crate) mod barcode; +pub(crate) mod recipe; diff --git a/crates/rocie-server/src/storage/sql/get/product/mod.rs b/crates/rocie-server/src/storage/sql/get/product/mod.rs index 0df51d8..915da81 100644 --- a/crates/rocie-server/src/storage/sql/get/product/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/product/mod.rs @@ -3,6 +3,7 @@ use crate::{ storage::sql::{ barcode::{Barcode, BarcodeId}, product::{Product, ProductId}, + product_parent::ProductParentId, unit::{UnitAmount, UnitId}, unit_property::UnitPropertyId, }, @@ -10,11 +11,46 @@ use crate::{ use sqlx::query; +macro_rules! product_from_record { + ($app:ident, $record:ident) => {{ + let barcodes = query!( + " + SELECT id, amount, unit + FROM barcodes + WHERE product_id = ? + ", + $record.id, + ) + .fetch_all(&$app.db) + .await?; + + Self { + id: ProductId::from_db(&$record.id), + unit_property: UnitPropertyId::from_db(&$record.unit_property), + name: $record.name, + description: $record.description, + parent: $record + .parent + .map(|parent| ProductParentId::from_db(&parent)), + associated_bar_codes: barcodes + .into_iter() + .map(|record| Barcode { + id: BarcodeId::from_db(record.id), + amount: UnitAmount { + value: u32::try_from(record.amount).expect("Should be strictly positve"), + unit: UnitId::from_db(&record.unit), + }, + }) + .collect(), + } + }}; +} + impl Product { pub(crate) async fn from_id(app: &App, id: ProductId) -> Result<Option<Self>, from_id::Error> { let record = query!( " - SELECT name, unit_property, description, parent + SELECT name, id, unit_property, description, parent FROM products WHERE id = ? ", @@ -24,13 +60,7 @@ impl Product { .await?; if let Some(record) = record { - Ok(Some(Self { - id, - unit_property: UnitPropertyId::from_db(&record.unit_property), - name: record.name, - description: record.description, - associated_bar_codes: vec![], // todo - })) + Ok(Some(product_from_record!(app, record))) } else { Ok(None) } @@ -49,13 +79,7 @@ impl Product { .await?; if let Some(record) = record { - Ok(Some(Self { - id: ProductId::from_db(&record.id), - unit_property: UnitPropertyId::from_db(&record.unit_property), - name, - description: record.description, - associated_bar_codes: vec![], // todo - })) + Ok(Some(product_from_record!(app, record))) } else { Ok(None) } @@ -73,34 +97,9 @@ impl Product { let mut all = Vec::with_capacity(records.len()); for record in records { - let barcodes = query!( - " - SELECT id, amount, unit - FROM barcodes - WHERE product_id = ? -", - record.id, - ) - .fetch_all(&app.db) - .await?; - - all.push(Self { - id: ProductId::from_db(&record.id), - unit_property: UnitPropertyId::from_db(&record.unit_property), - name: record.name, - description: record.description, - associated_bar_codes: barcodes - .into_iter() - .map(|record| Barcode { - id: BarcodeId::from_db(record.id), - amount: UnitAmount { - value: u32::try_from(record.amount) - .expect("Should be strictly positve"), - unit: UnitId::from_db(&record.unit), - }, - }) - .collect(), - }); + let product = product_from_record!(app, record); + + all.push(product); } Ok(all) diff --git a/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs b/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs new file mode 100644 index 0000000..5b85b62 --- /dev/null +++ b/crates/rocie-server/src/storage/sql/get/product_parent/mod.rs @@ -0,0 +1,87 @@ +use crate::{ + app::App, + storage::sql::product_parent::{ProductParent, ProductParentId}, +}; + +use sqlx::query; + +impl ProductParent { + pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>, get_all::Error> { + let records = query!( + " + SELECT id, parent, name, description + FROM parents +" + ) + .fetch_all(&app.db) + .await?; + + let mut all = Vec::with_capacity(records.len()); + for record in records { + let parent = ProductParent { + id: ProductParentId::from_db(&record.id), + parent: record + .parent + .map(|parent| ProductParentId::from_db(&parent)), + name: record.name, + description: record.description, + }; + + all.push(parent); + } + + Ok(all) + } + + pub(crate) async fn from_id( + app: &App, + id: ProductParentId, + ) -> Result<Option<Self>, from_id::Error> { + let record = query!( + " + SELECT parent, name, description + FROM parents + WHERE id = ? +", + id + ) + .fetch_optional(&app.db) + .await?; + + match record { + Some(record) => Ok(Some(ProductParent { + id, + parent: record + .parent + .map(|parent| ProductParentId::from_db(&parent)), + name: record.name, + description: record.description, + })), + None => Ok(None), + } + } +} + +pub(crate) mod from_id { + 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 {} +} + +pub(crate) mod get_all { + 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/recipe/mod.rs b/crates/rocie-server/src/storage/sql/get/recipe/mod.rs new file mode 100644 index 0000000..9d6dc79 --- /dev/null +++ b/crates/rocie-server/src/storage/sql/get/recipe/mod.rs @@ -0,0 +1,78 @@ +use crate::{ + app::App, + storage::sql::recipe::{Recipe, RecipeId}, +}; + +use sqlx::query; + +impl Recipe { + pub(crate) async fn from_id(app: &App, id: RecipeId) -> Result<Option<Self>, from_id::Error> { + let record = query!( + " + SELECT content, path + FROM recipies + WHERE id = ? +", + id + ) + .fetch_optional(&app.db) + .await?; + + if let Some(record) = record { + Ok(Some(Self { + id, + path: record + .path + .parse() + .expect("Was a path before, should still be one"), + content: record.content, + })) + } else { + Ok(None) + } + } + + pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>, get_all::Error> { + let records = query!( + " + SELECT id, content, path + FROM recipies +", + ) + .fetch_all(&app.db) + .await?; + + Ok(records + .into_iter() + .map(|record| Self { + id: RecipeId::from_db(&record.id), + path: record.path.parse().expect("Is still valid"), + content: record.content, + }) + .collect()) + } +} + +pub(crate) mod from_id { + 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 {} +} + +pub(crate) mod get_all { + 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/mod.rs b/crates/rocie-server/src/storage/sql/insert/mod.rs index 3b2d702..8a15385 100644 --- a/crates/rocie-server/src/storage/sql/insert/mod.rs +++ b/crates/rocie-server/src/storage/sql/insert/mod.rs @@ -9,8 +9,10 @@ use sqlx::{SqliteConnection, query}; pub(crate) mod barcode; pub(crate) mod product; +pub(crate) mod product_parent; pub(crate) mod unit; pub(crate) mod unit_property; +pub(crate) mod recipe; pub(crate) trait Transactionable: Sized + std::fmt::Debug + Serialize + DeserializeOwned diff --git a/crates/rocie-server/src/storage/sql/insert/product/mod.rs b/crates/rocie-server/src/storage/sql/insert/product/mod.rs index d762e9b..455eb4f 100644 --- a/crates/rocie-server/src/storage/sql/insert/product/mod.rs +++ b/crates/rocie-server/src/storage/sql/insert/product/mod.rs @@ -6,6 +6,7 @@ use crate::storage::sql::{ barcode::Barcode, insert::{Operations, Transactionable}, product::{Product, ProductId}, + product_parent::ProductParentId, unit_property::UnitPropertyId, }; @@ -15,7 +16,7 @@ pub(crate) enum Operation { id: ProductId, name: String, description: Option<String>, - parent: Option<ProductId>, + parent: Option<ProductParentId>, unit_property: UnitPropertyId, }, AssociateBarcode { @@ -138,7 +139,7 @@ impl Product { pub(crate) fn register( name: String, description: Option<String>, - parent: Option<ProductId>, + parent: Option<ProductParentId>, unit_property: UnitPropertyId, ops: &mut Operations<Operation>, ) -> Self { @@ -157,6 +158,7 @@ impl Product { name, description, unit_property, + parent, associated_bar_codes: vec![], } } diff --git a/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs b/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs new file mode 100644 index 0000000..644778f --- /dev/null +++ b/crates/rocie-server/src/storage/sql/insert/product_parent/mod.rs @@ -0,0 +1,113 @@ +use serde::{Deserialize, Serialize}; +use sqlx::query; +use uuid::Uuid; + +use crate::storage::sql::{ + insert::{Operations, Transactionable}, + product_parent::{ProductParent, ProductParentId}, +}; + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) enum Operation { + RegisterProductParent { + id: ProductParentId, + name: String, + description: Option<String>, + parent: Option<ProductParentId>, + }, +} + +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::RegisterProductParent { + id, + name, + description, + parent, + } => { + query!( + " + INSERT INTO parents (id, name, description, parent) + VALUES (?,?,?,?) +", + id, + name, + description, + parent + ) + .execute(txn) + .await?; + } + } + Ok(()) + } + + async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> { + match self { + Operation::RegisterProductParent { + id, + name, + description, + parent, + } => { + query!( + " + DELETE FROM products + WHERE id = ? AND name = ? AND description = ? AND parent = ?; +", + id, + name, + description, + parent, + ) + .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 ProductParent { + pub(crate) fn register( + name: String, + description: Option<String>, + parent: Option<ProductParentId>, + ops: &mut Operations<Operation>, + ) -> Self { + let id = ProductParentId::from(Uuid::new_v4()); + + ops.push(Operation::RegisterProductParent { + id, + name: name.clone(), + description: description.clone(), + parent, + }); + + Self { + id, + name, + description, + parent, + } + } +} diff --git a/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs b/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs new file mode 100644 index 0000000..b223bfe --- /dev/null +++ b/crates/rocie-server/src/storage/sql/insert/recipe/mod.rs @@ -0,0 +1,95 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use sqlx::query; +use uuid::Uuid; + +use crate::storage::sql::{ + insert::{Operations, Transactionable}, + recipe::{Recipe, RecipeId}, +}; + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) enum Operation { + New { + id: RecipeId, + path: PathBuf, + 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, path, content } => { + let path = path.display().to_string(); + + query!( + " + INSERT INTO recipies (id, path, content) + VALUES (?, ?, ?) +", + id, + path, + content, + ) + .execute(txn) + .await?; + } + } + Ok(()) + } + + async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> { + match self { + Operation::New { id, path, content } => { + let path = path.display().to_string(); + + query!( + " + DELETE FROM recipies + WHERE id = ? AND path = ? AND content = ? +", + id, + path, + 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), + } +} + +impl Recipe { + pub(crate) fn new(path: PathBuf, content: String, ops: &mut Operations<Operation>) -> Self { + let id = RecipeId::from(Uuid::new_v4()); + + ops.push(Operation::New { + id, + path: path.clone(), + content: content.clone(), + }); + + Self { id, path, content } + } +} diff --git a/crates/rocie-server/src/storage/sql/mod.rs b/crates/rocie-server/src/storage/sql/mod.rs index a44fbad..dd46eab 100644 --- a/crates/rocie-server/src/storage/sql/mod.rs +++ b/crates/rocie-server/src/storage/sql/mod.rs @@ -5,14 +5,26 @@ pub(crate) mod insert; pub(crate) mod barcode; pub(crate) mod product; pub(crate) mod product_amount; +pub(crate) mod product_parent; +pub(crate) mod recipe; pub(crate) mod unit; pub(crate) mod unit_property; macro_rules! mk_id { ($name:ident and $stub_name:ident) => { - mk_id!($name and $stub_name with uuid::Uuid, "uuid::Uuid"); + mk_id!( + $name and $stub_name, + with uuid::Uuid, "uuid::Uuid", + to_string {|val: &uuid::Uuid| val.to_string()}, + copy Copy + ); }; - ($name:ident and $stub_name:ident with $inner:path, $inner_string:literal $($args:meta)*) => { + ( + $name:ident and $stub_name:ident, + with $inner:path $(=> $($args:meta)* )?, $inner_string:literal, + to_string $to_string:expr, + $(copy $copy:path)? + ) => { #[derive( serde::Deserialize, serde::Serialize, @@ -20,17 +32,28 @@ macro_rules! mk_id { Default, utoipa::ToSchema, Clone, - Copy, PartialEq, Eq, PartialOrd, Ord, + $($copy,)? )] pub(crate) struct $name { + $( + $( + #[$args] + )* + )? value: $inner, } - #[derive(Deserialize, Serialize, Debug, Clone, Copy)] + #[derive( + Deserialize, + Serialize, + Debug, + Clone, + $($copy,)? + )] #[serde(from = $inner_string)] pub(crate) struct $stub_name { value: $inner, @@ -55,7 +78,7 @@ macro_rules! mk_id { impl std::fmt::Display for $name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.value) + write!(f, "{}", $to_string(&self.value)) } } @@ -83,7 +106,7 @@ macro_rules! mk_id { &self, buf: &mut <DB as sqlx::Database>::ArgumentBuffer<'q>, ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> { - let inner = self.value.to_string(); + let inner = $to_string(&self.value); sqlx::Encode::<DB>::encode_by_ref(&inner, buf) } } diff --git a/crates/rocie-server/src/storage/sql/product.rs b/crates/rocie-server/src/storage/sql/product.rs index 1c5a7d8..8d2c951 100644 --- a/crates/rocie-server/src/storage/sql/product.rs +++ b/crates/rocie-server/src/storage/sql/product.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::storage::sql::{barcode::Barcode, mk_id, unit_property::UnitPropertyId}; +use crate::storage::sql::{barcode::Barcode, mk_id, product_parent::ProductParentId, unit_property::UnitPropertyId}; /// The base of rocie. /// @@ -12,6 +12,12 @@ pub(crate) struct Product { /// The id of the product. pub(crate) id: ProductId, + /// The parent this product has. + /// + /// This is effectively it's anchor in the product DAG. + #[schema(nullable = false)] + pub(crate) parent: Option<ProductParentId>, + /// The property this product is measured in. /// /// (This is probably always either Mass, Volume or Quantity). @@ -23,6 +29,7 @@ pub(crate) struct Product { pub(crate) name: String, /// An optional description of this product. + #[schema(nullable = false)] pub(super) description: Option<String>, /// Which barcodes are associated with this product. diff --git a/crates/rocie-server/src/storage/sql/product_parent.rs b/crates/rocie-server/src/storage/sql/product_parent.rs new file mode 100644 index 0000000..f689024 --- /dev/null +++ b/crates/rocie-server/src/storage/sql/product_parent.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::storage::sql::mk_id; + +/// The grouping system for products. +/// +/// Every Product can have a related parent, and every parent can have a parent themselves. +/// As such, the products list constructs a DAG. +#[derive(Clone, ToSchema, Serialize, Deserialize)] +pub(crate) struct ProductParent { + /// The id of the product parent. + pub(crate) id: ProductParentId, + + /// The optional id of the parent of this product parent. + /// + /// This must not form a cycle. + #[schema(nullable = false)] + pub(crate) parent: Option<ProductParentId>, + + /// The name of the product parent. + /// + /// This should be globally unique, to make searching easier for the user. + pub(crate) name: String, + + /// An optional description of this product parent. + #[schema(nullable = false)] + pub(super) description: Option<String>, +} + +mk_id!(ProductParentId and ProductParentIdStub); diff --git a/crates/rocie-server/src/storage/sql/recipe.rs b/crates/rocie-server/src/storage/sql/recipe.rs new file mode 100644 index 0000000..835d98b --- /dev/null +++ b/crates/rocie-server/src/storage/sql/recipe.rs @@ -0,0 +1,18 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::storage::sql::mk_id; + +#[derive(ToSchema, Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Recipe { + pub(crate) id: RecipeId, + + #[schema(value_type = String)] + pub(crate) path: PathBuf, + + pub(crate) content: String, +} + +mk_id!(RecipeId and RecipeIdStub); diff --git a/crates/rocie-server/src/storage/sql/unit.rs b/crates/rocie-server/src/storage/sql/unit.rs index d16e783..8bbfe60 100644 --- a/crates/rocie-server/src/storage/sql/unit.rs +++ b/crates/rocie-server/src/storage/sql/unit.rs @@ -29,6 +29,7 @@ pub(crate) struct Unit { pub(crate) short_name: String, /// Description of this unit. + #[schema(nullable = false)] pub(crate) description: Option<String>, /// Which property is described by this unit. diff --git a/crates/rocie-server/src/storage/sql/unit_property.rs b/crates/rocie-server/src/storage/sql/unit_property.rs index 9da2d2e..adb4767 100644 --- a/crates/rocie-server/src/storage/sql/unit_property.rs +++ b/crates/rocie-server/src/storage/sql/unit_property.rs @@ -17,6 +17,7 @@ pub(crate) struct UnitProperty { pub(crate) units: Vec<UnitId>, /// An description of this property. + #[schema(nullable = false)] pub(crate) description: Option<String>, } diff --git a/crates/rocie-server/tests/_testenv/log.rs b/crates/rocie-server/tests/_testenv/log.rs index 9a07e78..837ccdd 100644 --- a/crates/rocie-server/tests/_testenv/log.rs +++ b/crates/rocie-server/tests/_testenv/log.rs @@ -48,8 +48,10 @@ macro_rules! request { }}; (@format, $fn:ident, $($arg:expr),*) => {{ + #[allow(unused_imports)] use std::fmt::Write; + #[allow(unused_mut)] let mut base = String::new(); $( write!(base, "{:?}, ", $arg) diff --git a/crates/rocie-server/tests/product_parents/mod.rs b/crates/rocie-server/tests/product_parents/mod.rs new file mode 100644 index 0000000..46ec0ca --- /dev/null +++ b/crates/rocie-server/tests/product_parents/mod.rs @@ -0,0 +1,2 @@ +mod register; +mod query; diff --git a/crates/rocie-server/tests/product_parents/query.rs b/crates/rocie-server/tests/product_parents/query.rs new file mode 100644 index 0000000..6d16ca3 --- /dev/null +++ b/crates/rocie-server/tests/product_parents/query.rs @@ -0,0 +1,144 @@ +use rocie_client::{ + apis::{ + api_get_product_api::{products_by_product_parent_id_direct, products_by_product_parent_id_indirect}, + api_get_product_parent_api::{product_parents_toplevel, product_parents_under}, + api_set_product_api::register_product, + api_set_product_parent_api::register_product_parent, + api_set_unit_property_api::register_unit_property, + }, + models::{ProductParentStub, ProductStub, UnitPropertyStub}, +}; + +use crate::{ + _testenv::init::function_name, + testenv::{TestEnv, log::request}, +}; + +#[tokio::test] +async fn test_product_parent_query() { + let env = TestEnv::new(function_name!()); + + let parent_dairy = request!( + env, + register_product_parent(ProductParentStub { + description: Some("Dairy replacment products".to_owned()), + name: "Dairy replacements".to_owned(), + parent: None, + }) + ); + request!( + env, + register_product_parent(ProductParentStub { + description: Some("Dairy replacment products".to_owned()), + name: "Wheat products".to_owned(), + parent: None, + }) + ); + + request!( + env, + register_product_parent(ProductParentStub { + description: Some("Milk replacment products".to_owned()), + name: "Milk replacements".to_owned(), + parent: Some(parent_dairy), + }) + ); + + assert_eq!( + request!(env, product_parents_toplevel()) + .into_iter() + .map(|parent| parent.name) + .collect::<Vec<_>>(), + vec!["Dairy replacements".to_owned(), "Wheat products".to_owned(),] + ); + + assert_eq!( + request!(env, product_parents_under(parent_dairy)) + .into_iter() + .map(|parent| parent.name) + .collect::<Vec<_>>(), + vec!["Milk replacements".to_owned()] + ); +} + +#[tokio::test] +async fn test_product_parent_query_product() { + let env = TestEnv::new(function_name!()); + + let parent_dairy = request!( + env, + register_product_parent(ProductParentStub { + description: Some("Dairy replacment products".to_owned()), + name: "Dairy replacements".to_owned(), + parent: None, + }) + ); + let parent_dairy_milk = request!( + env, + register_product_parent(ProductParentStub { + description: Some("Dairy replacment products".to_owned()), + name: "milk products".to_owned(), + parent: Some(parent_dairy), + }) + ); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: None, + name: "Volume".to_owned() + }) + ); + + request!( + env, + register_product(ProductStub { + description: None, + name: "Soy milk".to_owned(), + parent: Some(parent_dairy_milk), + unit_property, + }) + ); + + request!( + env, + register_product(ProductStub { + description: None, + name: "Cheese".to_owned(), + parent: Some(parent_dairy), + unit_property, + }) + ); + + assert_eq!( + request!(env, products_by_product_parent_id_indirect(parent_dairy_milk)) + .into_iter() + .map(|product| product.name) + .collect::<Vec<_>>(), + vec!["Soy milk".to_owned()], + ); + + assert_eq!( + request!(env, products_by_product_parent_id_direct(parent_dairy_milk)) + .into_iter() + .map(|product| product.name) + .collect::<Vec<_>>(), + vec!["Soy milk".to_owned()], + ); + + assert_eq!( + request!(env, products_by_product_parent_id_indirect(parent_dairy)) + .into_iter() + .map(|product| product.name) + .collect::<Vec<_>>(), + vec!["Cheese".to_owned(), "Soy milk".to_owned(),], + ); + + assert_eq!( + request!(env, products_by_product_parent_id_direct(parent_dairy)) + .into_iter() + .map(|product| product.name) + .collect::<Vec<_>>(), + vec!["Cheese".to_owned()], + ); +} diff --git a/crates/rocie-server/tests/product_parents/register.rs b/crates/rocie-server/tests/product_parents/register.rs new file mode 100644 index 0000000..c84ffea --- /dev/null +++ b/crates/rocie-server/tests/product_parents/register.rs @@ -0,0 +1,84 @@ +use rocie_client::{ + apis::{ + api_get_product_api::product_by_id, api_set_product_api::register_product, + api_set_product_parent_api::register_product_parent, + api_set_unit_property_api::register_unit_property, + }, + models::{ProductParentStub, ProductStub, UnitPropertyStub}, +}; + +use crate::{ + _testenv::init::function_name, + testenv::{TestEnv, log::request}, +}; + +#[tokio::test] +async fn test_product_parent_register_roundtrip() { + let env = TestEnv::new(function_name!()); + + let parent_dairy = request!( + env, + register_product_parent(ProductParentStub { + description: Some("Dairy replacment products".to_owned()), + name: "Dairy replacements".to_owned(), + parent: None, + }) + ); + let parent_dairy_milk = request!( + env, + register_product_parent(ProductParentStub { + description: Some("Milk replacment products".to_owned()), + name: "Milk replacements".to_owned(), + parent: Some(parent_dairy), + }) + ); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: Some("The total volume of a product".to_owned()), + name: "Volume".to_owned() + }) + ); + + let product_soy_milk = request!( + env, + register_product(ProductStub { + description: Some("A soy based alternative to milk".to_owned()), + name: "Soy drink".to_owned(), + parent: Some(parent_dairy_milk), + unit_property, + }) + ); + let product_oat_milk = request!( + env, + register_product(ProductStub { + description: Some("A oat based alternative to milk".to_owned()), + name: "Oat drink".to_owned(), + parent: Some(parent_dairy_milk), + unit_property, + }) + ); + + let product_vegan_cheese = request!( + env, + register_product(ProductStub { + description: None, + name: "Vegan cheese".to_owned(), + parent: Some(parent_dairy), + unit_property, + }) + ); + + for product in [product_soy_milk, product_oat_milk] { + let product = request!(env, product_by_id(product)); + + assert_eq!(product.parent, Some(parent_dairy_milk)); + } + + { + let product = request!(env, product_by_id(product_vegan_cheese)); + + assert_eq!(product.parent, Some(parent_dairy)); + } +} diff --git a/crates/rocie-server/tests/products/mod.rs b/crates/rocie-server/tests/products/mod.rs index 886e5b7..ee139f0 100644 --- a/crates/rocie-server/tests/products/mod.rs +++ b/crates/rocie-server/tests/products/mod.rs @@ -20,7 +20,7 @@ async fn create_product(env: &TestEnv, unit_property: UnitPropertyId, name: &str request!( env, register_product(ProductStub { - description: Some(None), + description: None, name: name.to_owned(), parent: None, unit_property @@ -31,7 +31,7 @@ async fn create_unit(env: &TestEnv, name: &str, unit_property: UnitPropertyId) - request!( env, register_unit(UnitStub { - description: Some(None), + description: None, full_name_plural: name.to_owned(), full_name_singular: name.to_owned(), short_name: name.to_owned(), diff --git a/crates/rocie-server/tests/products/register.rs b/crates/rocie-server/tests/products/register.rs index 4284bd1..bae7bc7 100644 --- a/crates/rocie-server/tests/products/register.rs +++ b/crates/rocie-server/tests/products/register.rs @@ -18,7 +18,7 @@ async fn test_product_register_roundtrip() { let unit_property = request!( env, register_unit_property(UnitPropertyStub { - description: Some(Some("The total mass of a product".to_owned())), + description: Some("The total mass of a product".to_owned()), name: "Mass".to_owned() }) ); @@ -26,7 +26,7 @@ async fn test_product_register_roundtrip() { let id = request!( env, register_product(ProductStub { - description: Some(Some("A soy based alternative to milk".to_owned())), + description: Some("A soy based alternative to milk".to_owned()), name: "Soy drink".to_owned(), parent: None, unit_property, @@ -38,11 +38,8 @@ async fn test_product_register_roundtrip() { assert_eq!(&product.name, "Soy drink"); assert_eq!( product.description, - Some(Some("A soy based alternative to milk".to_owned())) + Some("A soy based alternative to milk".to_owned()) ); assert_eq!(product.id, id); - // assert_eq!( - // product.parent, - // None, - // ); + assert_eq!(product.parent, None); } diff --git a/crates/rocie-server/tests/recipies/mod.rs b/crates/rocie-server/tests/recipies/mod.rs new file mode 100644 index 0000000..e8aa3c2 --- /dev/null +++ b/crates/rocie-server/tests/recipies/mod.rs @@ -0,0 +1,24 @@ +use rocie_client::{ + apis::{api_get_recipe_api::recipe_by_id, api_set_recipe_api::add_recipe}, + models::RecipeStub, +}; + +use crate::testenv::{TestEnv, init::function_name, log::request}; + +#[tokio::test] +async fn test_recipe_roundtrip() { + let env = TestEnv::new(function_name!()); + + let recipe_id = request!( + env, + add_recipe(RecipeStub { + path: "/asia/curry".to_owned(), + content: "just make the curry".to_owned(), + }) + ); + + let output = request!(env, recipe_by_id(recipe_id)); + + assert_eq!(output.path, "/asia/curry".to_owned()); + assert_eq!(output.content, "just make the curry".to_owned()); +} diff --git a/crates/rocie-server/tests/tests.rs b/crates/rocie-server/tests/tests.rs index cf34156..3759042 100644 --- a/crates/rocie-server/tests/tests.rs +++ b/crates/rocie-server/tests/tests.rs @@ -4,4 +4,6 @@ mod _testenv; pub(crate) use _testenv as testenv; mod products; +mod product_parents; mod units; +mod recipies; diff --git a/crates/rocie-server/tests/units/fetch.rs b/crates/rocie-server/tests/units/fetch.rs new file mode 100644 index 0000000..b0bfffb --- /dev/null +++ b/crates/rocie-server/tests/units/fetch.rs @@ -0,0 +1,88 @@ +use rocie_client::{ + apis::{ + api_get_unit_api::units_by_property_id, api_set_unit_api::register_unit, + api_set_unit_property_api::register_unit_property, + }, + models::{UnitPropertyStub, UnitStub}, +}; + +use crate::testenv::{TestEnv, init::function_name, log::request}; + +#[tokio::test] +async fn test_units_fetch_by_property_id() { + let env = TestEnv::new(function_name!()); + + let unit_property = request!( + env, + register_unit_property(UnitPropertyStub { + description: Some("The total mass of a product".to_owned()), + name: "Mass".to_owned() + }) + ); + request!( + env, + register_unit(UnitStub { + description: Some("Fancy new unit".to_owned()), + full_name_plural: "Grams".to_owned(), + full_name_singular: "Gram".to_owned(), + short_name: "g".to_owned(), + unit_property, + }) + ); + request!( + env, + register_unit(UnitStub { + description: Some("Better new unit (we should make it SI)".to_owned()), + full_name_plural: "Kilograms".to_owned(), + full_name_singular: "Kilogram".to_owned(), + short_name: "kg".to_owned(), + unit_property, + }) + ); + + let unit_property2 = request!( + env, + register_unit_property(UnitPropertyStub { + description: Some("The total volume of a product".to_owned()), + name: "Volume".to_owned() + }) + ); + request!( + env, + register_unit(UnitStub { + description: Some("Fancy new unit".to_owned()), + full_name_plural: "Liter".to_owned(), + full_name_singular: "Liter".to_owned(), + short_name: "l".to_owned(), + unit_property: unit_property2, + }) + ); + request!( + env, + register_unit(UnitStub { + description: Some("Better new unit (we should make it SI)".to_owned()), + full_name_plural: "Mililiters".to_owned(), + full_name_singular: "Mililiter".to_owned(), + short_name: "ml".to_owned(), + unit_property: unit_property2, + }) + ); + + let units = request!(env, units_by_property_id(unit_property)); + let other_units = request!(env, units_by_property_id(unit_property2)); + + assert_eq!( + units + .iter() + .map(|unit| unit.short_name.clone()) + .collect::<Vec<_>>(), + vec!["g".to_owned(), "kg".to_owned(),] + ); + assert_eq!( + other_units + .iter() + .map(|unit| unit.short_name.clone()) + .collect::<Vec<_>>(), + vec!["l".to_owned(), "ml".to_owned(),] + ); +} diff --git a/crates/rocie-server/tests/units/mod.rs b/crates/rocie-server/tests/units/mod.rs index 5518167..0481d71 100644 --- a/crates/rocie-server/tests/units/mod.rs +++ b/crates/rocie-server/tests/units/mod.rs @@ -1 +1,2 @@ mod register; +mod fetch; diff --git a/crates/rocie-server/tests/units/register.rs b/crates/rocie-server/tests/units/register.rs index 5367b55..8181c25 100644 --- a/crates/rocie-server/tests/units/register.rs +++ b/crates/rocie-server/tests/units/register.rs @@ -18,7 +18,7 @@ async fn test_unit_register_roundtrip() { let unit_property = request!( env, register_unit_property(UnitPropertyStub { - description: Some(Some("The total mass of a product".to_owned())), + description: Some("The total mass of a product".to_owned()), name: "Mass".to_owned() }) ); @@ -26,7 +26,7 @@ async fn test_unit_register_roundtrip() { let id = request!( env, register_unit(UnitStub { - description: Some(Some("Fancy new unit".to_owned())), + description: Some("Fancy new unit".to_owned()), full_name_plural: "Grams".to_owned(), full_name_singular: "Gram".to_owned(), short_name: "g".to_owned(), @@ -39,7 +39,7 @@ async fn test_unit_register_roundtrip() { assert_eq!(&unit.short_name, "g"); assert_eq!(&unit.full_name_plural, "Grams"); assert_eq!(&unit.full_name_singular, "Gram"); - assert_eq!(unit.description, Some(Some("Fancy new unit".to_owned()))); + assert_eq!(unit.description, Some("Fancy new unit".to_owned())); assert_eq!(unit.id, id); } |
