lacoctelera/routes/ingredient/
get.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// Copyright 2024 Felipe Torres González
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::{
    domain::{DataDomainError, Ingredient},
    routes::ingredient::utils::{check_ingredient, get_ingredient_from_db},
};
use actix_web::{
    get,
    web::{Data, Path, Query},
    HttpResponse,
};
use serde::Deserialize;
use sqlx::MySqlPool;
use std::error::Error;
use tracing::{debug, error, info, instrument};
use utoipa::IntoParams;
use uuid::Uuid;

/// `Struct` QueryData models the expected fields for a query string.
///
/// # Description
///
/// Using a `Struct` rather than a simple `String` as the received data for the Query will leverage
/// the internal parsing logic of the framework. This way, the endpoint handler would only receive
/// valid data, since wrong data is rejected and the request is answered with a code 400 by the
/// framework.
#[derive(Deserialize, IntoParams)]
pub struct QueryData {
    pub name: String,
}

/// GET for the API's /ingredient endpoint.
#[utoipa::path(
    get,
    path = "/ingredient",
    tag = "Ingredient",
    params(
        QueryData
    ),
    responses(
        (
            status = 200,
            description = "The query was successfully executed",
            body = [Ingredient]
        ),
        (
            status = 400,
            description = "Error found in the given query",
        ),
    )
)]
#[instrument(
    skip(pool, req),
    fields(
        ingredient_name = %req.name,
    )
)]
#[get("")]
pub async fn search_ingredient(
    pool: Data<MySqlPool>,
    req: Query<QueryData>,
) -> Result<HttpResponse, Box<dyn Error>> {
    // First, validate the given form as a correct name for the instantiation of an Ingredient.
    let query_ingredient = match Ingredient::parse(None, &req.name, "other", None) {
        Ok(ingredient) => {
            info!(
                "Received search request for an ingredient identified by: '{}'",
                ingredient.name()
            );
            ingredient
        }
        Err(e) => return Ok(HttpResponse::BadRequest().body(format!("{}", e))),
    };

    // Issue a query to the DB to search for ingredients using the given name.
    let ingredients = match check_ingredient(&pool, query_ingredient).await {
        Ok(ingredients) => {
            if !ingredients.is_empty() {
                let mut ing_list = String::new();
                ingredients
                    .iter()
                    .for_each(|i| ing_list.push_str(&format!("{{ {:#?} }},", i)));
                info!("Ingredients found: {}", ingredients.len());
                debug!("Ingredients found: {:#?}.", ing_list);
            } else {
                info!("No ingredients found.");
            }

            ingredients
        }
        Err(_) => Vec::new(),
    };

    Ok(HttpResponse::Ok().json(ingredients))
}

#[utoipa::path(
    get,
    context_path = "/ingredient/",
    tag = "Ingredient",
    responses(
        (
            status = 200,
            description = "The given ID matches an ingredient entry in the DB.",
            body = Ingredient,
            headers(
                ("Content-Length"),
                ("Content-Type"),
                ("Date"),
                ("Vary", description = "Origin,Access-Control-Request-Method,Access-Control-Request-Headers")
            ),
        ),
        (
            status = 404,
            description = "The given ingredient'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(
                ("Cache-Control", description = "Cache control is set to *no-cache*."),
                ("Access-Control-Allow-Origin"),
                ("Retry-After", description = "Amount of time between requests (seconds).")
            )
        )
    )
)]
#[instrument(
    skip(pool, req),
    fields(
        ingredient_id = %req.0,
    )
)]
#[get("{id}")]
pub async fn get_ingredient(
    req: Path<(String,)>,
    pool: Data<MySqlPool>,
) -> Result<HttpResponse, Box<dyn Error>> {
    let id = match Uuid::parse_str(&req.0) {
        Ok(id) => id,
        Err(e) => {
            error!("{e}");
            return Err(Box::new(DataDomainError::InvalidId));
        }
    };

    match get_ingredient_from_db(&pool, &id).await? {
        Some(ingredient) => Ok(HttpResponse::Ok().json(ingredient)),
        None => Ok(HttpResponse::NotFound().finish()),
    }
}