Skip to content

Commit b53ce8d

Browse files
committed
refactor(api): [#198] Axum API, tag context
1 parent cbd70b6 commit b53ce8d

11 files changed

Lines changed: 544 additions & 4 deletions

File tree

src/web/api/v1/contexts/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
//! `Category` | Torrent categories | [`v1`](crate::web::api::v1::contexts::category)
77
//! `Proxy` | Image proxy cache | [`v1`](crate::web::api::v1::contexts::proxy)
88
//! `Settings` | Index settings | [`v1`](crate::web::api::v1::contexts::settings)
9+
//! `Tag` | Torrent tags | [`v1`](crate::web::api::v1::contexts::tag)
910
//! `Torrent` | Indexed torrents | [`v1`](crate::web::api::v1::contexts::torrent)
1011
//! `User` | Users | [`v1`](crate::web::api::v1::contexts::user)
1112
//!
1213
pub mod about;
1314
pub mod category;
1415
pub mod proxy;
1516
pub mod settings;
17+
pub mod tag;
1618
pub mod torrent;
1719
pub mod user;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! API forms for the the [`tag`](crate::web::api::v1::contexts::tag) API
2+
//! context.
3+
use serde::{Deserialize, Serialize};
4+
5+
use crate::models::torrent_tag::TagId;
6+
7+
#[derive(Serialize, Deserialize, Debug)]
8+
pub struct AddTagForm {
9+
pub name: String,
10+
}
11+
12+
#[derive(Serialize, Deserialize, Debug)]
13+
pub struct DeleteTagForm {
14+
pub tag_id: TagId,
15+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! API handlers for the [`tag`](crate::web::api::v1::contexts::tag) API
2+
//! context.
3+
use std::sync::Arc;
4+
5+
use axum::extract::{self, State};
6+
use axum::response::Json;
7+
8+
use super::forms::{AddTagForm, DeleteTagForm};
9+
use super::responses::{added_tag, deleted_tag};
10+
use crate::common::AppData;
11+
use crate::databases::database;
12+
use crate::errors::ServiceError;
13+
use crate::models::torrent_tag::TorrentTag;
14+
use crate::web::api::v1::extractors::bearer_token::Extract;
15+
use crate::web::api::v1::responses::{self, OkResponse};
16+
17+
/// It handles the request to get all the tags.
18+
///
19+
/// It returns:
20+
///
21+
/// - `200` response with a json containing the tag list [`Vec<TorrentTag>`](crate::models::torrent_tag::TorrentTag).
22+
/// - Other error status codes if there is a database error.
23+
///
24+
/// Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag)
25+
/// for more information about this endpoint.
26+
///
27+
/// # Errors
28+
///
29+
/// It returns an error if there is a database error.
30+
#[allow(clippy::unused_async)]
31+
pub async fn get_all_handler(
32+
State(app_data): State<Arc<AppData>>,
33+
) -> Result<Json<responses::OkResponse<Vec<TorrentTag>>>, database::Error> {
34+
match app_data.tag_repository.get_all().await {
35+
Ok(tags) => Ok(Json(responses::OkResponse { data: tags })),
36+
Err(error) => Err(error),
37+
}
38+
}
39+
40+
/// It adds a new tag.
41+
///
42+
/// # Errors
43+
///
44+
/// It returns an error if:
45+
///
46+
/// - The user does not have permissions to create a new tag.
47+
/// - There is a database error.
48+
#[allow(clippy::unused_async)]
49+
pub async fn add_handler(
50+
State(app_data): State<Arc<AppData>>,
51+
Extract(maybe_bearer_token): Extract,
52+
extract::Json(add_tag_form): extract::Json<AddTagForm>,
53+
) -> Result<Json<OkResponse<String>>, ServiceError> {
54+
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;
55+
56+
match app_data.tag_service.add_tag(&add_tag_form.name, &user_id).await {
57+
Ok(_) => Ok(added_tag(&add_tag_form.name)),
58+
Err(error) => Err(error),
59+
}
60+
}
61+
62+
/// It deletes a tag.
63+
///
64+
/// # Errors
65+
///
66+
/// It returns an error if:
67+
///
68+
/// - The user does not have permissions to delete tags.
69+
/// - There is a database error.
70+
#[allow(clippy::unused_async)]
71+
pub async fn delete_handler(
72+
State(app_data): State<Arc<AppData>>,
73+
Extract(maybe_bearer_token): Extract,
74+
extract::Json(delete_tag_form): extract::Json<DeleteTagForm>,
75+
) -> Result<Json<OkResponse<String>>, ServiceError> {
76+
let user_id = app_data.auth.get_user_id_from_bearer_token(&maybe_bearer_token).await?;
77+
78+
match app_data.tag_service.delete_tag(&delete_tag_form.tag_id, &user_id).await {
79+
Ok(_) => Ok(deleted_tag(delete_tag_form.tag_id)),
80+
Err(error) => Err(error),
81+
}
82+
}

src/web/api/v1/contexts/tag/mod.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//! API context: `tag`.
2+
//!
3+
//! This API context is responsible for handling torrent tags.
4+
//!
5+
//! # Endpoints
6+
//!
7+
//! - [Get all tags](#get-all-tags)
8+
//! - [Add a tag](#add-a-tag)
9+
//! - [Delete a tag](#delete-a-tag)
10+
//!
11+
//! **NOTICE**: We don't support multiple languages yet, so the tag is always
12+
//! in English.
13+
//!
14+
//! # Get all tags
15+
//!
16+
//! `GET /v1/tag`
17+
//!
18+
//! Returns all torrent tags.
19+
//!
20+
//! **Example request**
21+
//!
22+
//! ```bash
23+
//! curl "http://127.0.0.1:3000/v1/tags"
24+
//! ```
25+
//!
26+
//! **Example response** `200`
27+
//!
28+
//! ```json
29+
//! {
30+
//! "data": [
31+
//! {
32+
//! "tag_id": 1,
33+
//! "name": "anime"
34+
//! },
35+
//! {
36+
//! "tag_id": 2,
37+
//! "name": "manga"
38+
//! }
39+
//! ]
40+
//! }
41+
//! ```
42+
//! **Resource**
43+
//!
44+
//! Refer to the [`Tag`](crate::databases::database::Tag)
45+
//! struct for more information about the response attributes.
46+
//!
47+
//! # Add a tag
48+
//!
49+
//! `POST /v1/tag`
50+
//!
51+
//! It adds a new tag.
52+
//!
53+
//! **POST params**
54+
//!
55+
//! Name | Type | Description | Required | Example
56+
//! ---|---|---|---|---
57+
//! `name` | `String` | The tag name | Yes | `new tag`
58+
//!
59+
//! **Example request**
60+
//!
61+
//! ```bash
62+
//! curl \
63+
//! --header "Content-Type: application/json" \
64+
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI" \
65+
//! --request POST \
66+
//! --data '{"name":"new tag"}' \
67+
//! http://127.0.0.1:3000/v1/tag
68+
//! ```
69+
//!
70+
//! **Example response** `200`
71+
//!
72+
//! ```json
73+
//! {
74+
//! "data": "new tag"
75+
//! }
76+
//! ```
77+
//!
78+
//! **Resource**
79+
//!
80+
//! Refer to [`OkResponse`](crate::models::response::OkResponse<T>) for more
81+
//! information about the response attributes. The response contains only the
82+
//! name of the newly created tag.
83+
//!
84+
//! # Delete a tag
85+
//!
86+
//! `DELETE /v1/tag`
87+
//!
88+
//! It deletes a tag.
89+
//!
90+
//! **POST params**
91+
//!
92+
//! Name | Type | Description | Required | Example
93+
//! ---|---|---|---|---
94+
//! `tag_id` | `i64` | The internal tag ID | Yes | `1`
95+
//!
96+
//! **Example request**
97+
//!
98+
//! ```bash
99+
//! curl \
100+
//! --header "Content-Type: application/json" \
101+
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI" \
102+
//! --request DELETE \
103+
//! --data '{"tag_id":1}' \
104+
//! http://127.0.0.1:3000/v1/tag
105+
//! ```
106+
//!
107+
//! **Example response** `200`
108+
//!
109+
//! ```json
110+
//! {
111+
//! "data": 1
112+
//! }
113+
//! ```
114+
//!
115+
//! **Resource**
116+
//!
117+
//! Refer to [`OkResponse`](crate::models::response::OkResponse<T>) for more
118+
//! information about the response attributes. The response contains only the
119+
//! name of the deleted tag.
120+
pub mod forms;
121+
pub mod handlers;
122+
pub mod responses;
123+
pub mod routes;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//! API responses for the [`tag`](crate::web::api::v1::contexts::tag) API
2+
//! context.
3+
use axum::Json;
4+
5+
use crate::models::torrent_tag::TagId;
6+
use crate::web::api::v1::responses::OkResponse;
7+
8+
/// Response after successfully creating a new tag.
9+
pub fn added_tag(tag_name: &str) -> Json<OkResponse<String>> {
10+
Json(OkResponse {
11+
data: tag_name.to_string(),
12+
})
13+
}
14+
15+
/// Response after successfully deleting a tag.
16+
pub fn deleted_tag(tag_id: TagId) -> Json<OkResponse<String>> {
17+
Json(OkResponse {
18+
data: tag_id.to_string(),
19+
})
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! API routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
2+
//!
3+
//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::tag).
4+
use std::sync::Arc;
5+
6+
use axum::routing::{delete, get, post};
7+
use axum::Router;
8+
9+
use super::handlers::{add_handler, delete_handler, get_all_handler};
10+
use crate::common::AppData;
11+
12+
// code-review: should we use `tags` also for single resources?
13+
14+
/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
15+
pub fn router_for_single_resources(app_data: Arc<AppData>) -> Router {
16+
Router::new()
17+
.route("/", post(add_handler).with_state(app_data.clone()))
18+
.route("/", delete(delete_handler).with_state(app_data))
19+
}
20+
21+
/// Routes for the [`tag`](crate::web::api::v1::contexts::tag) API context.
22+
pub fn router_for_multiple_resources(app_data: Arc<AppData>) -> Router {
23+
Router::new().route("/", get(get_all_handler).with_state(app_data))
24+
}

src/web/api/v1/routes.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,25 @@ use std::sync::Arc;
44
use axum::routing::get;
55
use axum::Router;
66

7-
//use tower_http::cors::CorsLayer;
8-
use super::contexts::about;
97
use super::contexts::about::handlers::about_page_handler;
8+
//use tower_http::cors::CorsLayer;
9+
use super::contexts::{about, tag};
1010
use super::contexts::{category, user};
1111
use crate::common::AppData;
1212

1313
/// Add all API routes to the router.
1414
#[allow(clippy::needless_pass_by_value)]
1515
pub fn router(app_data: Arc<AppData>) -> Router {
16+
// code-review: should we use plural for the resource prefix: `users`, `categories`, `tags`?
17+
// See: https://stackoverflow.com/questions/6845772/should-i-use-singular-or-plural-name-convention-for-rest-resources
18+
1619
let v1_api_routes = Router::new()
1720
.route("/", get(about_page_handler).with_state(app_data.clone()))
1821
.nest("/user", user::routes::router(app_data.clone()))
1922
.nest("/about", about::routes::router(app_data.clone()))
20-
.nest("/category", category::routes::router(app_data.clone()));
23+
.nest("/category", category::routes::router(app_data.clone()))
24+
.nest("/tag", tag::routes::router_for_single_resources(app_data.clone()))
25+
.nest("/tags", tag::routes::router_for_multiple_resources(app_data.clone()));
2126

2227
Router::new()
2328
.route("/", get(about_page_handler).with_state(app_data))
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use torrust_index_backend::models::torrent_tag::TagId;
2+
3+
use crate::common::asserts::assert_json_ok;
4+
use crate::common::contexts::tag::responses::{AddedTagResponse, DeletedTagResponse};
5+
use crate::common::responses::TextResponse;
6+
7+
pub fn assert_added_tag_response(response: &TextResponse, tag_name: &str) {
8+
let added_tag_response: AddedTagResponse = serde_json::from_str(&response.body)
9+
.unwrap_or_else(|_| panic!("response {:#?} should be a AddedTagResponse", response.body));
10+
11+
assert_eq!(added_tag_response.data, tag_name);
12+
13+
assert_json_ok(response);
14+
}
15+
16+
pub fn assert_deleted_tag_response(response: &TextResponse, tag_id: TagId) {
17+
let deleted_tag_response: DeletedTagResponse = serde_json::from_str(&response.body)
18+
.unwrap_or_else(|_| panic!("response {:#?} should be a DeletedTagResponse", response.body));
19+
20+
assert_eq!(deleted_tag_response.data, tag_id);
21+
22+
assert_json_ok(response);
23+
}

tests/common/contexts/tag/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod asserts;
12
pub mod fixtures;
23
pub mod forms;
34
pub mod responses;

tests/common/contexts/tag/responses.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
use serde::Deserialize;
22

3+
// code-review: we should always include a API resource in the `data`attribute.
4+
//
5+
// ```
6+
// pub struct DeletedTagResponse {
7+
// pub data: DeletedTag,
8+
// }
9+
//
10+
// pub struct DeletedTag {
11+
// pub tag_id: i64,
12+
// }
13+
// ```
14+
//
15+
// This way the API client knows what's the meaning of the `data` attribute.
16+
317
#[derive(Deserialize)]
418
pub struct AddedTagResponse {
519
pub data: String,
620
}
721

822
#[derive(Deserialize)]
923
pub struct DeletedTagResponse {
10-
pub data: i64, // tag_id
24+
pub data: i64,
1125
}
1226

1327
#[derive(Deserialize, Debug)]

0 commit comments

Comments
 (0)