lacoctelera/routes/recipe/
get.rsuse crate::{
domain::{DataDomainError, RecipeQuery},
routes::recipe::{
get_recipe_from_db, search_recipe_by_category, search_recipe_by_name,
search_recipe_by_rating,
},
};
use actix_web::{
get,
web::{Data, Path, Query},
HttpResponse,
};
use sqlx::MySqlPool;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt::Display;
use tracing::{info, instrument};
use uuid::Uuid;
#[utoipa::path(
get,
path = "/recipe",
tag = "Recipe",
params(RecipeQuery),
responses(
(
status = 200,
description = "The query was executed successfully and produced some matches.",
body = [Recipe],
headers(
("Access-Control-Allow-Origin"),
("Content-Type"),
("Cache-Control"),
)
),
(
status = 404,
description = "The query was executed successfully but didn't produce any match.",
headers(
("Content-Length"),
("Date"),
("Vary", description = "Origin,Access-Control-Request-Method,Access-Control-Request-Headers")
),
),
(
status = 429,
description = "Too many requests",
headers(
("Access-Control-Allow-Origin"),
("Retry-After"),
)
),
)
)]
#[get("")]
pub async fn search_recipe(
req: Query<RecipeQuery>,
pool: Data<MySqlPool>,
) -> Result<HttpResponse, Box<dyn Error>> {
let search_type: SearchType = (&req.0).try_into().expect("Wrong query");
info!("Recipe search ({search_type}) using: {{{}}}", req.0);
let recipe_ids = match search_type {
SearchType::ByName => {
let search_token = match req.0.name {
Some(name) => name,
None => return Err(Box::new(DataDomainError::InvalidSearch)),
};
search_recipe_by_name(&pool, &search_token).await?
}
SearchType::ByCategory => {
let search_token = match req.0.category {
Some(category) => category,
None => return Err(Box::new(DataDomainError::InvalidSearch)),
};
search_recipe_by_category(&pool, search_token).await?
}
SearchType::ByRating => {
let search_token = match req.0.rating {
Some(rating) => rating,
None => return Err(Box::new(DataDomainError::InvalidSearch)),
};
search_recipe_by_rating(&pool, search_token).await?
}
SearchType::ByTags => return Ok(HttpResponse::NotImplemented().finish()),
SearchType::Intersection => return Ok(HttpResponse::NotImplemented().finish()),
};
let mut recipes = Vec::new();
for id in recipe_ids.iter() {
recipes.push(get_recipe_from_db(&pool, id).await?)
}
if recipes.is_empty() {
Ok(HttpResponse::Ok().json(recipes))
} else {
Ok(HttpResponse::NotFound().finish())
}
}
#[utoipa::path(
get,
context_path = "/recipe/",
tag = "Recipe",
responses(
(
status = 200,
description = "The recipe identified by the given ID was found in the DB",
body = Recipe,
headers(
("Content-Length"),
("Content-Type"),
("Date"),
("Vary", description = "Origin,Access-Control-Request-Method,Access-Control-Request-Headers")
),
),
(
status = 404,
description = "The given recipe's ID was not found in the DB.",
headers(
("Content-Length"),
("Date"),
("Vary", description = "Origin,Access-Control-Request-Method,Access-Control-Request-Headers")
),
),
(
status = 429,
description = "Too many requests",
headers(
("Access-Control-Allow-Origin"),
("Retry-After"),
)
),
)
)]
#[instrument(skip(pool))]
#[get("{id}")]
pub async fn get_recipe(
pool: Data<MySqlPool>,
path: Path<(String,)>,
) -> Result<HttpResponse, Box<dyn Error>> {
let recipe_id = Uuid::parse_str(&path.0).map_err(|_| DataDomainError::InvalidId)?;
let recipe = get_recipe_from_db(&pool, &recipe_id).await?;
match recipe {
Some(recipe) => Ok(HttpResponse::Ok().json(recipe)),
None => Ok(HttpResponse::NotFound().finish()),
}
}
#[derive(Debug, Clone)]
enum SearchType {
ByName,
ByTags,
ByRating,
ByCategory,
Intersection,
}
impl Display for SearchType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ss = match self {
SearchType::ByName => "ByName",
SearchType::ByTags => "ByTags",
SearchType::ByRating => "ByRating",
SearchType::ByCategory => "ByCategory",
SearchType::Intersection => "Intersection",
};
write!(f, "{ss}")
}
}
fn multiple_choices(query: &RecipeQuery) -> bool {
if (query.name.is_some()
&& (query.tags.is_some() || query.rating.is_some() || query.category.is_some()))
|| (query.tags.is_some() && (query.rating.is_some() || query.category.is_some()))
|| (query.rating.is_some() && query.category.is_some())
{
return true;
}
false
}
impl TryFrom<&RecipeQuery> for SearchType {
type Error = String;
fn try_from(query: &RecipeQuery) -> std::result::Result<Self, Self::Error> {
if multiple_choices(query) {
Ok(SearchType::Intersection)
} else if query.name.is_some() {
Ok(SearchType::ByName)
} else if query.tags.is_some() {
Ok(SearchType::ByTags)
} else if query.rating.is_some() {
Ok(SearchType::ByRating)
} else if query.category.is_some() {
Ok(SearchType::ByCategory)
} else {
Err("Invalid conversion".to_string())
}
}
}