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}, }; /// 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 { let _name = 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 { let _name = 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 founds", body = Vec), (status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String) ), )] #[get("/products/")] pub(crate) async fn products(app: web::Data) -> Result { let all = Product::get_all(&app).await?; Ok(HttpResponse::Ok().json(all)) }