Skip to content

Commit

Permalink
Merge torrust#195: Axum API: category context
Browse files Browse the repository at this point in the history
b4a7ea6 refactor(api): [torrust#179] Axum API, category context, delete category (Jose Celano)
f63bf05 refactor(api): [torrust#179] Axum API, category context, add category (Jose Celano)
bb6d9bf refactor(api): [torrust#179] Axum API, category context, get all categories (Jose Celano)
6f9c1a2 refactor: API category context tetests (Jose Celano)

Pull request description:

  API migration to Axum for `category` context.

Top commit has no ACKs.

Tree-SHA512: 01a983f5e2f8c1a547aea5145cb7133b572dbf4569cb84db1b4272b03cdcd283d44ce4b4a1c1a3618c145242bc0e019bb3c02805e4b1cbf6ee558c9be80462cf
  • Loading branch information
josecelano committed Jun 15, 2023
2 parents 03dba5c + b4a7ea6 commit 9a277e8
Show file tree
Hide file tree
Showing 12 changed files with 476 additions and 41 deletions.
9 changes: 9 additions & 0 deletions src/web/api/v1/contexts/category/forms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct AddCategoryForm {
pub name: String,
pub icon: Option<String>,
}

pub type DeleteCategoryForm = AddCategoryForm;
85 changes: 85 additions & 0 deletions src/web/api/v1/contexts/category/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! API handlers for the the [`category`](crate::web::api::v1::contexts::category) API
//! context.
use std::sync::Arc;

use axum::extract::{self, State};
use axum::response::Json;

use super::forms::{AddCategoryForm, DeleteCategoryForm};
use super::responses::{added_category, deleted_category};
use crate::common::AppData;
use crate::databases::database::{self, Category};
use crate::errors::ServiceError;
use crate::web::api::v1::extractors::bearer_token::Extract;
use crate::web::api::v1::responses::{self, OkResponse};

/// It handles the request to get all the categories.
///
/// It returns:
///
/// - `200` response with a json containing the category list [`Vec<Category>`](crate::databases::database::Category).
/// - Other error status codes if there is a database error.
///
/// Refer to the [API endpoint documentation](crate::web::api::v1::contexts::category)
/// for more information about this endpoint.
///
/// # Errors
///
/// It returns an error if there is a database error.
#[allow(clippy::unused_async)]
pub async fn get_all_handler(
State(app_data): State<Arc<AppData>>,
) -> Result<Json<responses::OkResponse<Vec<Category>>>, database::Error> {
match app_data.category_repository.get_all().await {
Ok(categories) => Ok(Json(responses::OkResponse { data: categories })),
Err(error) => Err(error),
}
}

/// It adds a new category.
///
/// # Errors
///
/// It returns an error if:
///
/// - The user does not have permissions to create a new category.
/// - There is a database error.
#[allow(clippy::unused_async)]
pub async fn add_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
extract::Json(category_form): extract::Json<AddCategoryForm>,
) -> Result<Json<OkResponse<String>>, ServiceError> {
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

match app_data.category_service.add_category(&category_form.name, &user_id).await {
Ok(_) => Ok(added_category(&category_form.name)),
Err(error) => Err(error),
}
}

/// It deletes a category.
///
/// # Errors
///
/// It returns an error if:
///
/// - The user does not have permissions to delete category.
/// - There is a database error.
#[allow(clippy::unused_async)]
pub async fn delete_handler(
State(app_data): State<Arc<AppData>>,
Extract(maybe_bearer_token): Extract,
extract::Json(category_form): extract::Json<DeleteCategoryForm>,
) -> Result<Json<OkResponse<String>>, ServiceError> {
// code-review: why do we need to send the whole category object to delete it?
// And we should use the ID instead of the name, because the name could change
// or we could add support for multiple languages.

let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

match app_data.category_service.delete_category(&category_form.name, &user_id).await {
Ok(_) => Ok(deleted_category(&category_form.name)),
Err(error) => Err(error),
}
}
4 changes: 4 additions & 0 deletions src/web/api/v1/contexts/category/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,7 @@
//! Refer to [`OkResponse`](crate::models::response::OkResponse<T>) for more
//! information about the response attributes. The response contains only the
//! name of the deleted category.
pub mod forms;
pub mod handlers;
pub mod responses;
pub mod routes;
19 changes: 19 additions & 0 deletions src/web/api/v1/contexts/category/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//! API responses for the the [`category`](crate::web::api::v1::contexts::category) API
//! context.
use axum::Json;

use crate::web::api::v1::responses::OkResponse;

/// Response after successfully creating a new category.
pub fn added_category(category_name: &str) -> Json<OkResponse<String>> {
Json(OkResponse {
data: category_name.to_string(),
})
}

/// Response after successfully deleting a new category.
pub fn deleted_category(category_name: &str) -> Json<OkResponse<String>> {
Json(OkResponse {
data: category_name.to_string(),
})
}
18 changes: 18 additions & 0 deletions src/web/api/v1/contexts/category/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! API routes for the [`category`](crate::web::api::v1::contexts::category) API context.
//!
//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::category).
use std::sync::Arc;

use axum::routing::{delete, get, post};
use axum::Router;

use super::handlers::{add_handler, delete_handler, get_all_handler};
use crate::common::AppData;

/// Routes for the [`category`](crate::web::api::v1::contexts::category) API context.
pub fn router(app_data: Arc<AppData>) -> Router {
Router::new()
.route("/", get(get_all_handler).with_state(app_data.clone()))
.route("/", post(add_handler).with_state(app_data.clone()))
.route("/", delete(delete_handler).with_state(app_data))
}
13 changes: 7 additions & 6 deletions src/web/api/v1/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@ use std::sync::Arc;
use axum::Router;

//use tower_http::cors::CorsLayer;
use super::contexts::{about, user};
use super::contexts::about;
use super::contexts::{category, user};
use crate::common::AppData;

/// Add all API routes to the router.
#[allow(clippy::needless_pass_by_value)]
pub fn router(app_data: Arc<AppData>) -> Router {
let user_routes = user::routes::router(app_data.clone());
let about_routes = about::routes::router(app_data);
let api_routes = Router::new()
.nest("/user", user::routes::router(app_data.clone()))
.nest("/about", about::routes::router(app_data.clone()))
.nest("/category", category::routes::router(app_data));

let api_routes = Router::new().nest("/user", user_routes).nest("/about", about_routes);
Router::new().nest("/v1", api_routes)

// For development purposes only.
// It allows calling the API on a different port. For example
// API: http://localhost:3000/v1
// Webapp: http://localhost:8080
//Router::new().nest("/v1", api_routes).layer(CorsLayer::permissive())

Router::new().nest("/v1", api_routes)
}
21 changes: 21 additions & 0 deletions tests/common/contexts/category/asserts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use crate::common::asserts::assert_json_ok;
use crate::common::contexts::category::responses::{AddedCategoryResponse, DeletedCategoryResponse};
use crate::common::responses::TextResponse;

pub fn assert_added_category_response(response: &TextResponse, category_name: &str) {
let added_category_response: AddedCategoryResponse = serde_json::from_str(&response.body)
.unwrap_or_else(|_| panic!("response {:#?} should be a AddedCategoryResponse", response.body));

assert_eq!(added_category_response.data, category_name);

assert_json_ok(response);
}

pub fn assert_deleted_category_response(response: &TextResponse, category_name: &str) {
let deleted_category_response: DeletedCategoryResponse = serde_json::from_str(&response.body)
.unwrap_or_else(|_| panic!("response {:#?} should be a DeletedCategoryResponse", response.body));

assert_eq!(deleted_category_response.data, category_name);

assert_json_ok(response);
}
1 change: 1 addition & 0 deletions tests/common/contexts/category/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod asserts;
pub mod fixtures;
pub mod forms;
pub mod responses;
5 changes: 5 additions & 0 deletions tests/common/contexts/category/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ pub struct AddedCategoryResponse {
pub data: String,
}

#[derive(Deserialize)]
pub struct DeletedCategoryResponse {
pub data: String,
}

#[derive(Deserialize, Debug)]
pub struct ListResponse {
pub data: Vec<ListItem>,
Expand Down
Loading

0 comments on commit 9a277e8

Please sign in to comment.