use actix_web::{HttpRequest, HttpResponse, Responder, Result, get, web}; use log::info; use percent_encoding::percent_decode_str; use crate::{ app::App, storage::sql::{ product::{Product, ProductId, ProductIdStub}, product_amount::ProductAmount, product_parent::{ProductParent, ProductParentId, ProductParentIdStub}, }, }; /// A String, that is not url-decoded on parse. struct UrlEncodedString(String); impl UrlEncodedString { /// Percent de-encode a given string fn percent_decode(&self) -> Result { percent_decode_str(self.0.replace('+', "%20").as_str()) .decode_utf8() .map(|s| s.to_string()) .inspect(|s| info!("Decoded `{}` as `{s}`", self.0)) } fn from_str(inner: &str) -> Self { Self(inner.to_owned()) } } /// Get Product by id #[utoipa::path( responses( (status = OK, description = "Product found from database", body = Product), (status = NOT_FOUND, description = "Product not found in database"), (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) ), params( ("id" = ProductId, description = "Product id" ), ) )] #[get("/product/by-id/{id}")] pub(crate) async fn product_by_id( app: web::Data, id: web::Path, ) -> Result { let id = id.into_inner(); match Product::from_id(&app, id.into()).await? { Some(product) => Ok(HttpResponse::Ok().json(product)), None => Ok(HttpResponse::NotFound().finish()), } } /// Get Product by name #[utoipa::path( responses( (status = OK, description = "Product found from database", body = Product), (status = NOT_FOUND, description = "Product not found in database"), (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) ), params( ("name" = String, description = "Name of the product" ), ) )] #[get("/product/by-name/{name}")] pub(crate) async fn product_by_name( app: web::Data, req: HttpRequest, name: web::Path, ) -> Result { drop(name); let name = UrlEncodedString::from_str( req.path() .strip_prefix("/product/by-name/") .expect("Will always exists"), ); let name = name.percent_decode()?; match Product::from_name(&app, name).await? { Some(product) => Ok(HttpResponse::Ok().json(product)), None => Ok(HttpResponse::NotFound().finish()), } } /// Get Product suggestion by name #[utoipa::path( responses( (status = OK, description = "Product suggestions found from database", body = Vec), (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) ), params( ("name" = String, description = "Partial name of a product" ), ) )] #[get("/product/by-part-name/{name}")] pub(crate) async fn product_suggestion_by_name( app: web::Data, req: HttpRequest, name: web::Path, ) -> Result { drop(name); let name = UrlEncodedString::from_str( req.path() .strip_prefix("/product/by-part-name/") .expect("Will always exists"), ); let name = &name.percent_decode()?; let all = Product::get_all(&app).await?; let matching = all .into_iter() .filter(|product| product.name.starts_with(name.as_str())) .collect::>(); Ok(HttpResponse::Ok().json(matching)) } /// Return all registered products #[utoipa::path( responses( (status = OK, description = "All products found", body = Vec), (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) ), )] #[get("/products_registered/")] pub(crate) async fn products_registered(app: web::Data) -> Result { 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), (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) ), )] #[get("/products_in_storage/")] pub(crate) async fn products_in_storage(app: web::Data) -> Result { 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), (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, id: web::Path, ) -> Result { 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> { let mut all = Product::get_all(app) .await? .into_iter() .filter(|prod| prod.parent.is_some_and(|val| val == parent.id)) .collect::>(); 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), (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, id: web::Path, ) -> Result { 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::>(); Ok(HttpResponse::Ok().json(all)) } else { Ok(HttpResponse::NotFound().finish()) } }