lacoctelera/
startup.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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// 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/.

//! Module that includes helper functions to start the **La Coctelera** application.

use crate::{
    configuration::{DataBaseSettings, Settings},
    routes::{self, health},
    ApiDoc,
};
use actix_cors::Cors;
use actix_files as fs;
use actix_web::{dev::Server, http, web, App, HttpServer};
use mailjet_client::{MailjetClient, MailjetClientBuilder};
use secrecy::ExposeSecret;
use sqlx::{mysql::MySqlPoolOptions, MySqlPool};
use std::net::TcpListener;
use tracing_actix_web::TracingLogger;
use utoipa::{openapi, OpenApi};
use utoipa_swagger_ui::SwaggerUi;

pub struct Application {
    port: u16,
    server: Server,
}

impl Application {
    pub async fn build(configuration: Settings) -> Result<Self, anyhow::Error> {
        // Create a connection pool to handle connections to the DB.
        let connection_pool = get_connection_pool(&configuration.database)
            .await
            .expect("Failed to connect to MariaDB.");

        let address = format!(
            "{}:{}",
            configuration.application.host, configuration.application.port
        );
        let listener = TcpListener::bind(address)?;
        let port = listener.local_addr().unwrap().port();
        let max_workers = configuration.application.max_workers;

        let mut mail_client = MailjetClientBuilder::new(
            configuration.email_client.api_user,
            configuration.email_client.api_key,
        )
        .with_api_version(&configuration.email_client.target_api)
        .with_email_name("La Coctelera")
        .with_email_address(configuration.email_client.admin_address.expose_secret())
        .with_https_enforcing(true)
        .build()?;

        if configuration.email_client.sandbox_mode.unwrap_or_default() {
            mail_client.enable_sandbox_mode();
        }

        let server = run(
            listener,
            connection_pool,
            configuration.application.base_url,
            max_workers,
            mail_client,
        )
        .await?;

        Ok(Self { port, server })
    }

    pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
        self.server.await
    }

    pub fn port(&self) -> u16 {
        self.port
    }
}

pub async fn run(
    listener: TcpListener,
    db_pool: MySqlPool,
    base_url: String,
    max_workers: u16,
    mail_client: MailjetClient,
) -> Result<Server, anyhow::Error> {
    let db_pool = web::Data::new(db_pool);
    let mail_client = web::Data::new(mail_client);

    let server = HttpServer::new(move || {
        let cors_ingredient = Cors::default()
            .allow_any_origin()
            .allowed_methods(vec!["GET", "POST"])
            .allowed_header(http::header::CONTENT_TYPE)
            .max_age(3600);

        let cors_author = Cors::default()
            .allow_any_origin()
            .allowed_methods(vec!["GET", "POST", "PATCH", "DELETE", "HEAD"])
            .allowed_header(http::header::CONTENT_TYPE)
            .max_age(86400);

        let cors_recipe = Cors::default()
            .allow_any_origin()
            .allowed_methods(vec!["GET", "POST", "PATCH", "DELETE", "HEAD"])
            .allowed_header(http::header::CONTENT_TYPE)
            .max_age(3600);

        let relative_url = &format!(
            "{base_url}/v{}",
            env!("CARGO_PKG_VERSION").split(".").collect::<Vec<&str>>()[0]
        );
        let mut api_doc = ApiDoc::openapi();
        api_doc.servers = Some(Vec::from([openapi::Server::new(relative_url)]));
        let mut external_docs = openapi::ExternalDocs::new(
            "https://felipet.github.io/lacoctelera_backend/lacoctelera/",
        );
        external_docs.description = Some(String::from("Code documentation of the API (Rust docs)"));
        api_doc.external_docs = Some(external_docs);

        App::new()
            .wrap(TracingLogger::default())
            .service(
                web::scope(relative_url)
                    .service(routes::echo)
                    .service(health::options_echo)
                    .service(health::health_check)
                    .service(health::options_health)
                    .service(
                        web::scope("/ingredient")
                            .wrap(cors_ingredient)
                            .service(routes::ingredient::search_ingredient)
                            .service(routes::ingredient::get_ingredient)
                            .service(routes::ingredient::add_ingredient),
                    )
                    .service(
                        web::scope("/author")
                            .wrap(cors_author)
                            .service(routes::author::search_author)
                            .service(routes::author::patch_author)
                            .service(routes::author::head_author)
                            .service(routes::author::post_author)
                            .service(routes::author::get_author)
                            .service(routes::author::delete_author),
                    )
                    .service(
                        web::scope("/recipe")
                            .wrap(cors_recipe)
                            .service(routes::recipe::get_recipe)
                            .service(routes::recipe::search_recipe)
                            .service(routes::recipe::head_recipe)
                            .service(routes::recipe::post_recipe),
                    )
                    .service(fs::Files::new("/static", "./static/resources").show_files_listing())
                    .service(
                        web::scope("/token")
                            .service(routes::token::token_req_get)
                            .service(routes::token::token_req_post)
                            .service(routes::token::req_validation),
                    )
                    .service(SwaggerUi::new("/{_:.*}").url("api-docs/openapi.json", api_doc)),
            )
            .app_data(db_pool.clone())
            .app_data(mail_client.clone())
    })
    .workers(max_workers as usize)
    .listen(listener)?
    .run();

    Ok(server)
}

pub async fn get_connection_pool(
    configuration: &DataBaseSettings,
) -> Result<MySqlPool, sqlx::Error> {
    MySqlPoolOptions::new()
        .max_connections(configuration.max_connections as u32)
        .idle_timeout(configuration.idle_timeout())
        .connect_with(configuration.build_db_conn_with_db())
        .await
}