Skip to content

Commit

Permalink
refactor(api): [#198] Axum API, tag context
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Jun 15, 2023
1 parent cbd70b6 commit b53ce8d
Show file tree
Hide file tree
Showing 11 changed files with 544 additions and 4 deletions.
2 changes: 2 additions & 0 deletions src/web/api/v1/contexts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
//! `Category` | Torrent categories | [`v1`](crate::web::api::v1::contexts::category)
//! `Proxy` | Image proxy cache | [`v1`](crate::web::api::v1::contexts::proxy)
//! `Settings` | Index settings | [`v1`](crate::web::api::v1::contexts::settings)
//! `Tag` | Torrent tags | [`v1`](crate::web::api::v1::contexts::tag)
//! `Torrent` | Indexed torrents | [`v1`](crate::web::api::v1::contexts::torrent)
//! `User` | Users | [`v1`](crate::web::api::v1::contexts::user)
//!
pub mod about;
pub mod category;
pub mod proxy;
pub mod settings;
pub mod tag;
pub mod torrent;
pub mod user;
15 changes: 15 additions & 0 deletions src/web/api/v1/contexts/tag/forms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! API forms for the the [`tag`](crate::web::api::v1::contexts::tag) API
//! context.
use serde::{Deserialize, Serialize};

use crate::models::torrent_tag::TagId;

#[derive(Serialize, Deserialize, Debug)]
pub struct AddTagForm {
pub name: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct DeleteTagForm {
pub tag_id: TagId,
}
82 changes: 82 additions & 0 deletions src/web/api/v1/contexts/tag/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! API handlers for the [`tag`](crate::web::api::v1::contexts::tag) API
//! context.
use std::sync::Arc;

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

use super::forms::{AddTagForm, DeleteTagForm};
use super::responses::{added_tag, deleted_tag};
use crate::common::AppData;
use crate::databases::database;
use crate::errors::ServiceError;
use crate::models::torrent_tag::TorrentTag;
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 tags.
///
/// It returns:
///
/// - `200` response with a json containing the tag list [`Vec<TorrentTag>`](crate::models::torrent_tag::TorrentTag).
/// - Other error status codes if there is a database error.
///
/// Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag)
/// 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<TorrentTag>>>, database::Error> {
match app_data.tag_repository.get_all().await {
Ok(tags) => Ok(Json(responses::OkResponse { data: tags })),
Err(error) => Err(error),
}
}

/// It adds a new tag.
///
/// # Errors
///
/// It returns an error if:
///
/// - The user does not have permissions to create a new tag.
/// - 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(add_tag_form): extract::Json<AddTagForm>,
) -> Result<Json<OkResponse<String>>, ServiceError> {
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

match app_data.tag_service.add_tag(&add_tag_form.name, &user_id).await {
Ok(_) => Ok(added_tag(&add_tag_form.name)),
Err(error) => Err(error),
}
}

/// It deletes a tag.
///
/// # Errors
///
/// It returns an error if:
///
/// - The user does not have permissions to delete tags.
/// - 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(delete_tag_form): extract::Json<DeleteTagForm>,
) -> Result<Json<OkResponse<String>>, ServiceError> {
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;

match app_data.tag_service.delete_tag(&delete_tag_form.tag_id, &user_id).await {
Ok(_) => Ok(deleted_tag(delete_tag_form.tag_id)),
Err(error) => Err(error),
}
}
123 changes: 123 additions & 0 deletions src/web/api/v1/contexts/tag/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! API context: `tag`.
//!
//! This API context is responsible for handling torrent tags.
//!
//! # Endpoints
//!
//! - [Get all tags](#get-all-tags)
//! - [Add a tag](#add-a-tag)
//! - [Delete a tag](#delete-a-tag)
//!
//! **NOTICE**: We don't support multiple languages yet, so the tag is always
//! in English.
//!
//! # Get all tags
//!
//! `GET /v1/tag`
//!
//! Returns all torrent tags.
//!
//! **Example request**
//!
//! ```bash
//! curl "http://127.0.0.1:3000/v1/tags"
//! ```
//!
//! **Example response** `200`
//!
//! ```json
//! {
//! "data": [
//! {
//! "tag_id": 1,
//! "name": "anime"
//! },
//! {
//! "tag_id": 2,
//! "name": "manga"
//! }
//! ]
//! }
//! ```
//! **Resource**
//!
//! Refer to the [`Tag`](crate::databases::database::Tag)
//! struct for more information about the response attributes.
//!
//! # Add a tag
//!
//! `POST /v1/tag`
//!
//! It adds a new tag.
//!
//! **POST params**
//!
//! Name | Type | Description | Required | Example
//! ---|---|---|---|---
//! `name` | `String` | The tag name | Yes | `new tag`
//!
//! **Example request**
//!
//! ```bash
//! curl \
//! --header "Content-Type: application/json" \
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI" \
//! --request POST \
//! --data '{"name":"new tag"}' \
//! http://127.0.0.1:3000/v1/tag
//! ```
//!
//! **Example response** `200`
//!
//! ```json
//! {
//! "data": "new tag"
//! }
//! ```
//!
//! **Resource**
//!
//! Refer to [`OkResponse`](crate::models::response::OkResponse<T>) for more
//! information about the response attributes. The response contains only the
//! name of the newly created tag.
//!
//! # Delete a tag
//!
//! `DELETE /v1/tag`
//!
//! It deletes a tag.
//!
//! **POST params**
//!
//! Name | Type | Description | Required | Example
//! ---|---|---|---|---
//! `tag_id` | `i64` | The internal tag ID | Yes | `1`
//!
//! **Example request**
//!
//! ```bash
//! curl \
//! --header "Content-Type: application/json" \
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI" \
//! --request DELETE \
//! --data '{"tag_id":1}' \
//! http://127.0.0.1:3000/v1/tag
//! ```
//!
//! **Example response** `200`
//!
//! ```json
//! {
//! "data": 1
//! }
//! ```
//!
//! **Resource**
//!
//! Refer to [`OkResponse`](crate::models::response::OkResponse<T>) for more
//! information about the response attributes. The response contains only the
//! name of the deleted tag.
pub mod forms;
pub mod handlers;
pub mod responses;
pub mod routes;
20 changes: 20 additions & 0 deletions src/web/api/v1/contexts/tag/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//! API responses for the [`tag`](crate::web::api::v1::contexts::tag) API
//! context.
use axum::Json;

use crate::models::torrent_tag::TagId;
use crate::web::api::v1::responses::OkResponse;

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

/// Response after successfully deleting a tag.
pub fn deleted_tag(tag_id: TagId) -> Json<OkResponse<String>> {
Json(OkResponse {
data: tag_id.to_string(),
})
}
24 changes: 24 additions & 0 deletions src/web/api/v1/contexts/tag/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! API routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
//!
//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag).
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;

// code-review: should we use `tags` also for single resources?

/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
pub fn router_for_single_resources(app_data: Arc<AppData>) -> Router {
Router::new()
.route("/", post(add_handler).with_state(app_data.clone()))
.route("/", delete(delete_handler).with_state(app_data))
}

/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
pub fn router_for_multiple_resources(app_data: Arc<AppData>) -> Router {
Router::new().route("/", get(get_all_handler).with_state(app_data))
}
11 changes: 8 additions & 3 deletions src/web/api/v1/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ use std::sync::Arc;
use axum::routing::get;
use axum::Router;

//use tower_http::cors::CorsLayer;
use super::contexts::about;
use super::contexts::about::handlers::about_page_handler;
//use tower_http::cors::CorsLayer;
use super::contexts::{about, tag};
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 {
// code-review: should we use plural for the resource prefix: `users`, `categories`, `tags`?
// See: https://stackoverflow.com/questions/6845772/should-i-use-singular-or-plural-name-convention-for-rest-resources

let v1_api_routes = Router::new()
.route("/", get(about_page_handler).with_state(app_data.clone()))
.nest("/user", user::routes::router(app_data.clone()))
.nest("/about", about::routes::router(app_data.clone()))
.nest("/category", category::routes::router(app_data.clone()));
.nest("/category", category::routes::router(app_data.clone()))
.nest("/tag", tag::routes::router_for_single_resources(app_data.clone()))
.nest("/tags", tag::routes::router_for_multiple_resources(app_data.clone()));

Router::new()
.route("/", get(about_page_handler).with_state(app_data))
Expand Down
23 changes: 23 additions & 0 deletions tests/common/contexts/tag/asserts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use torrust_index_backend::models::torrent_tag::TagId;

use crate::common::asserts::assert_json_ok;
use crate::common::contexts::tag::responses::{AddedTagResponse, DeletedTagResponse};
use crate::common::responses::TextResponse;

pub fn assert_added_tag_response(response: &TextResponse, tag_name: &str) {
let added_tag_response: AddedTagResponse = serde_json::from_str(&response.body)
.unwrap_or_else(|_| panic!("response {:#?} should be a AddedTagResponse", response.body));

assert_eq!(added_tag_response.data, tag_name);

assert_json_ok(response);
}

pub fn assert_deleted_tag_response(response: &TextResponse, tag_id: TagId) {
let deleted_tag_response: DeletedTagResponse = serde_json::from_str(&response.body)
.unwrap_or_else(|_| panic!("response {:#?} should be a DeletedTagResponse", response.body));

assert_eq!(deleted_tag_response.data, tag_id);

assert_json_ok(response);
}
1 change: 1 addition & 0 deletions tests/common/contexts/tag/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;
16 changes: 15 additions & 1 deletion tests/common/contexts/tag/responses.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
use serde::Deserialize;

// code-review: we should always include a API resource in the `data`attribute.
//
// ```
// pub struct DeletedTagResponse {
// pub data: DeletedTag,
// }
//
// pub struct DeletedTag {
// pub tag_id: i64,
// }
// ```
//
// This way the API client knows what's the meaning of the `data` attribute.

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

#[derive(Deserialize)]
pub struct DeletedTagResponse {
pub data: i64, // tag_id
pub data: i64,
}

#[derive(Deserialize, Debug)]
Expand Down
Loading

0 comments on commit b53ce8d

Please sign in to comment.