use crate::{
authentication::{AuthData, SecurityAddon},
domain::DataDomainError,
};
use once_cell::sync::Lazy;
use regex::Regex;
use routes::{health, ingredient::FormData};
use serde::{Deserialize, Serialize};
use utoipa::{
openapi::{Object, ObjectBuilder},
OpenApi,
};
use uuid::Uuid;
use validator::ValidationError;
pub use domain::{IngCategory, Ingredient};
static RE_UUID_V4: Lazy<Regex> = Lazy::new(|| Regex::new(r"([a-fA-F0-9-]{4,12}){5}$").unwrap());
pub mod configuration;
pub mod startup;
pub mod telemetry;
pub mod routes {
pub mod health;
pub use health::echo;
pub mod ingredient {
pub mod get;
pub mod post;
mod utils;
pub use get::{get_ingredient, search_ingredient, QueryData};
pub use post::{add_ingredient, FormData};
}
pub mod author {
pub mod delete;
pub mod get;
pub mod head;
pub mod patch;
pub mod post;
mod utils;
pub use delete::delete_author;
pub use get::{get_author, search_author};
pub use head::head_author;
pub use patch::patch_author;
pub use post::post_author;
}
pub mod recipe {
pub mod get;
pub mod head;
pub mod patch;
pub mod post;
pub mod utils;
pub use get::get_recipe;
pub use get::search_recipe;
pub use head::head_recipe;
pub use patch::patch_recipe;
pub use post::post_recipe;
pub use utils::{
get_recipe_from_db, register_new_recipe, search_recipe_by_category,
search_recipe_by_name, search_recipe_by_rating,
};
}
pub mod token {
pub mod token_request;
pub use token_request::{req_validation, token_req_get, token_req_post};
}
}
pub mod domain {
pub mod auth;
pub mod author;
mod error;
mod ingredient;
pub mod recipe;
pub mod tag;
pub use auth::ClientId;
pub use author::{Author, AuthorBuilder, SocialProfile};
pub use error::{DataDomainError, ServerError};
pub use ingredient::{IngCategory, Ingredient};
pub use recipe::{QuantityUnit, Recipe, RecipeCategory, RecipeContains, RecipeQuery, StarRate};
pub use tag::Tag;
pub static ID_LENGTH: usize = 8;
}
pub mod utils {
pub mod mailing {
mod mailing_utils;
pub use mailing_utils::*;
}
}
pub mod authentication {
mod token_auth;
use secrecy::SecretString;
use serde::Deserialize;
pub use token_auth::*;
use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
IntoParams, Modify, ToSchema,
};
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
pub struct AuthData {
pub api_key: SecretString,
}
#[allow(dead_code)]
pub struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.as_mut().unwrap(); components.add_security_scheme(
"api_key",
SecurityScheme::ApiKey(ApiKey::Query(ApiKeyValue::with_description(
"api_key",
"API key token to access restricted endpoints.",
))),
)
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct QueryId(Uuid);
impl TryFrom<&str> for QueryId {
type Error = DataDomainError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let id = Uuid::parse_str(value).map_err(|_| DataDomainError::InvalidId)?;
Ok(QueryId(id))
}
}
#[derive(OpenApi)]
#[openapi(
paths(
routes::ingredient::get::get_ingredient,
routes::ingredient::get::search_ingredient,
routes::ingredient::post::add_ingredient,
routes::health::echo,
routes::health::health_check,
routes::author::get::search_author,
routes::author::get::get_author,
routes::author::patch::patch_author,
routes::author::delete::delete_author,
routes::author::head::head_author,
routes::author::post::post_author,
routes::recipe::get::search_recipe,
routes::recipe::get::get_recipe,
routes::recipe::head::head_recipe,
routes::recipe::post::post_recipe,
routes::recipe::patch::patch_recipe,
),
components(
schemas(
Ingredient, IngCategory, FormData, AuthData, health::HealthResponse, health::ServerStatus, domain::Author,
domain::SocialProfile, domain::Tag, domain::Recipe, domain::RecipeCategory, domain::StarRate,
domain::RecipeContains, domain::QuantityUnit
)
),
tags(
(name = "Ingredient", description = "Resources related to the Ingredient management"),
(name = "Maintenance", description = "Resources related to server's status"),
(name = "Author", description = "Resources related to the Author management"),
(name = "Recipe", description = "Resources related to the Recipe management")
),
info(
title = "La Coctelera API",
description = r#"## A REST API for La Coctelera.
La Coctelera is a collaborative open data base to share cocktail recipes. You can find more information about
the project in this [website](https://felipe.nubecita.eu/projects/lacoctelera/).
The project is aiming to develop a front-end web site that would ease the access to the data base to the main
public. However, this part of the project is still under development.
An open REST API is offered to the community, so anyone can implement a client of the data base for a specific
platform. This page shows the OpenAPI docs for the API.
Accessing data from the data base is open with no restrictions (besides author's information marked as
non-public), but in order to add new data, a registered account is needed to avoid spamming the data base.
If you are interested on developing a front-end client, and you aim to access the restricted resources of the
API, please, go to the [token request page](./token/request).
**We hope you'll enjoy using this data base, and let's share our love for cocktails!**
"#,
contact(name = "Felipe Torres González", email = "admin@nubecita.eu")
),
modifiers(&SecurityAddon)
)]
pub struct ApiDoc;
pub fn datetime_object_type() -> Object {
ObjectBuilder::new()
.schema_type(utoipa::openapi::SchemaType::String)
.format(Some(utoipa::openapi::SchemaFormat::Custom(
"YYYY-MM-DDTHH:MM:SS.NNNNNNNNN+HH:MM".to_string(),
)))
.description(Some("A full timestamp with a time offset respect to UTC."))
.example(Some(serde_json::Value::String(String::from(
"2025-09-11T08:58:56.121331664+02:00",
))))
.build()
}
fn validate_id(value: &Uuid) -> Result<(), ValidationError> {
if RE_UUID_V4.is_match(&value.to_string()) {
std::result::Result::Ok(())
} else {
Err(ValidationError::new("1"))
}
}