Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Axum API: tag context #200

Merged
merged 1 commit into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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