diff --git a/docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md b/docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md index 0849ba469..bcc4db4d9 100644 --- a/docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md +++ b/docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md @@ -104,26 +104,27 @@ This maps naturally to a single port trait with three methods. The `ActionStatus | ID | Status | Task | Notes | | --- | ------ | ---------------------------------------------------------------------------------- | ----------------------------------------- | -| T1 | TODO | Add `WhitelistCommandPort` to `rest-api-application/src/ports/` | Three methods matching current operations | -| T2 | TODO | Add `WhitelistApiService` to `rest-api-application/src/use_cases/` | Calls port trait, maps errors | -| T3 | TODO | Add domain→protocol error mapping for whitelist errors | Leverage existing `ActionStatus` | -| T4 | TODO | Implement `TrackerWhitelistAdapter` in `rest-api-runtime-adapter/src/adapters/` | Wraps `WhitelistManager` | -| T5 | TODO | Add conversion functions to `rest-api-runtime-adapter/src/conversion.rs` if needed | | -| T6 | TODO | Update Axum handlers to use `WhitelistApiService` | | -| T7 | TODO | Update Axum state to inject `TrackerWhitelistAdapter` | | -| T8 | TODO | Verify pre-commit and pre-push checks pass | | +| T1 | DONE | Add `WhitelistCommandPort` to `rest-api-application/src/ports/` | Three methods matching current operations | +| T2 | DONE | Add `WhitelistApiService` to `rest-api-application/src/use_cases/` | Calls port trait, maps errors | +| T3 | DONE | Add domain→protocol error mapping for whitelist errors | `WhitelistError` in protocol package | +| T4 | DONE | Implement `TrackerWhitelistAdapter` in `rest-api-runtime-adapter/src/adapters/` | Wraps `WhitelistManager` | +| T5 | DONE | Add conversion functions to `rest-api-runtime-adapter/src/conversion.rs` if needed | Not needed — adapter maps inline | +| T6 | DONE | Update Axum handlers to use `WhitelistApiService` | | +| T7 | DONE | Update Axum state to inject `TrackerWhitelistAdapter` | In `v1/routes.rs` | +| T8 | DONE | Verify pre-commit and pre-push checks pass | | ## Verification / Progress -- [ ] `WhitelistCommandPort` trait defined in `rest-api-application` -- [ ] `WhitelistApiService` use-case implemented -- [ ] `TrackerWhitelistAdapter` implemented in `rest-api-runtime-adapter` -- [ ] Axum handlers dispatch through use-case instead of direct `WhitelistManager` -- [ ] Pre-commit checks pass -- [ ] Pre-push checks pass +- [x] `WhitelistCommandPort` trait defined in `rest-api-application` +- [x] `WhitelistApiService` use-case implemented +- [x] `TrackerWhitelistAdapter` implemented in `rest-api-runtime-adapter` +- [x] Axum handlers dispatch through use-case instead of direct `WhitelistManager` +- [x] Pre-commit checks pass +- [x] Pre-push checks pass ### Progress Log -| Date | Event | -| ---------- | ------------------ | -| 2026-06-24 | Draft spec created | +| Date | Event | +| ---------- | --------------------------------------------------------- | +| 2026-06-24 | Draft spec created | +| 2026-06-25 | Whitelist context migrated to contract-first architecture | diff --git a/packages/axum-rest-api-server/src/v1/context/whitelist/handlers.rs b/packages/axum-rest-api-server/src/v1/context/whitelist/handlers.rs index 449984da6..571bee86c 100644 --- a/packages/axum-rest-api-server/src/v1/context/whitelist/handlers.rs +++ b/packages/axum-rest-api-server/src/v1/context/whitelist/handlers.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use axum::extract::{Path, State}; use axum::response::Response; use torrust_info_hash::InfoHash; -use torrust_tracker_core::whitelist::manager::WhitelistManager; +use torrust_tracker_rest_api_application::use_cases::whitelist::WhitelistApiService; use super::responses::{ failed_to_reload_whitelist_response, failed_to_remove_torrent_from_whitelist_response, failed_to_whitelist_torrent_response, @@ -24,12 +24,12 @@ use crate::v1::responses::{invalid_info_hash_param_response, ok_response}; /// Refer to the [API endpoint documentation](crate::v1::context::whitelist#add-a-torrent-to-the-whitelist) /// for more information about this endpoint. pub async fn add_torrent_to_whitelist_handler( - State(whitelist_manager): State>, + State(whitelist_service): State>, Path(info_hash): Path, ) -> Response { match InfoHash::from_str(&info_hash.0) { Err(_) => invalid_info_hash_param_response(&info_hash.0), - Ok(info_hash) => match whitelist_manager.add_torrent_to_whitelist(&info_hash).await { + Ok(info_hash) => match whitelist_service.add_torrent(&info_hash).await { Ok(()) => ok_response(), Err(e) => failed_to_whitelist_torrent_response(e), }, @@ -47,12 +47,12 @@ pub async fn add_torrent_to_whitelist_handler( /// Refer to the [API endpoint documentation](crate::v1::context::whitelist#remove-a-torrent-from-the-whitelist) /// for more information about this endpoint. pub async fn remove_torrent_from_whitelist_handler( - State(whitelist_manager): State>, + State(whitelist_service): State>, Path(info_hash): Path, ) -> Response { match InfoHash::from_str(&info_hash.0) { Err(_) => invalid_info_hash_param_response(&info_hash.0), - Ok(info_hash) => match whitelist_manager.remove_torrent_from_whitelist(&info_hash).await { + Ok(info_hash) => match whitelist_service.remove_torrent(&info_hash).await { Ok(()) => ok_response(), Err(e) => failed_to_remove_torrent_from_whitelist_response(e), }, @@ -69,8 +69,8 @@ pub async fn remove_torrent_from_whitelist_handler( /// /// Refer to the [API endpoint documentation](crate::v1::context::whitelist#reload-the-whitelist) /// for more information about this endpoint. -pub async fn reload_whitelist_handler(State(whitelist_manager): State>) -> Response { - match whitelist_manager.load_whitelist_from_database().await { +pub async fn reload_whitelist_handler(State(whitelist_service): State>) -> Response { + match whitelist_service.reload().await { Ok(()) => ok_response(), Err(e) => failed_to_reload_whitelist_response(e), } diff --git a/packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs b/packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs index 98cffad8b..d4728b1df 100644 --- a/packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs +++ b/packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs @@ -9,27 +9,27 @@ use std::sync::Arc; use axum::Router; use axum::routing::{delete, get, post}; -use torrust_tracker_core::whitelist::manager::WhitelistManager; +use torrust_tracker_rest_api_application::use_cases::whitelist::WhitelistApiService; use super::handlers::{add_torrent_to_whitelist_handler, reload_whitelist_handler, remove_torrent_from_whitelist_handler}; /// It adds the routes to the router for the [`whitelist`](crate::v1::context::whitelist) API context. -pub fn add(prefix: &str, router: Router, whitelist_manager: &Arc) -> Router { +pub fn add(prefix: &str, router: Router, whitelist_service: &Arc) -> Router { let prefix = format!("{prefix}/whitelist"); router // Whitelisted torrents .route( &format!("{prefix}/{{info_hash}}"), - post(add_torrent_to_whitelist_handler).with_state(whitelist_manager.clone()), + post(add_torrent_to_whitelist_handler).with_state(whitelist_service.clone()), ) .route( &format!("{prefix}/{{info_hash}}"), - delete(remove_torrent_from_whitelist_handler).with_state(whitelist_manager.clone()), + delete(remove_torrent_from_whitelist_handler).with_state(whitelist_service.clone()), ) // Whitelist commands .route( &format!("{prefix}/reload"), - get(reload_whitelist_handler).with_state(whitelist_manager.clone()), + get(reload_whitelist_handler).with_state(whitelist_service.clone()), ) } diff --git a/packages/axum-rest-api-server/src/v1/routes.rs b/packages/axum-rest-api-server/src/v1/routes.rs index e14c11829..809d86d2c 100644 --- a/packages/axum-rest-api-server/src/v1/routes.rs +++ b/packages/axum-rest-api-server/src/v1/routes.rs @@ -3,8 +3,10 @@ use std::sync::Arc; use axum::Router; use torrust_tracker_rest_api_application::use_cases::torrent::TorrentApiService; +use torrust_tracker_rest_api_application::use_cases::whitelist::WhitelistApiService; use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; use torrust_tracker_rest_api_runtime_adapter::adapters::torrent::TrackerTorrentQueryAdapter; +use torrust_tracker_rest_api_runtime_adapter::adapters::whitelist::TrackerWhitelistAdapter; use super::context::{auth_key, stats, torrent, whitelist}; @@ -18,11 +20,10 @@ pub fn add(prefix: &str, router: Router, http_api_container: &Arc Result<(), WhitelistError>; + + /// Removes a torrent from the whitelist. + async fn remove_torrent(&self, info_hash: &InfoHash) -> Result<(), WhitelistError>; + + /// Reloads the whitelist from the database into memory. + async fn reload(&self) -> Result<(), WhitelistError>; +} diff --git a/packages/rest-api-application/src/use_cases/mod.rs b/packages/rest-api-application/src/use_cases/mod.rs index 3be261574..73bdf84bc 100644 --- a/packages/rest-api-application/src/use_cases/mod.rs +++ b/packages/rest-api-application/src/use_cases/mod.rs @@ -2,3 +2,4 @@ //! //! Each service orchestrates business logic by calling port traits. pub mod torrent; +pub mod whitelist; diff --git a/packages/rest-api-application/src/use_cases/whitelist.rs b/packages/rest-api-application/src/use_cases/whitelist.rs new file mode 100644 index 000000000..05c0b6e6d --- /dev/null +++ b/packages/rest-api-application/src/use_cases/whitelist.rs @@ -0,0 +1,51 @@ +//! Use-case service for whitelist API operations. +//! +//! Orchestrates calls to the [`WhitelistCommandPort`] and adds business logic +//! such as validation, error mapping, or caching as needed. +use torrust_info_hash::InfoHash; +use torrust_tracker_rest_api_protocol::v1::context::whitelist::resources::whitelist::WhitelistError; + +use crate::ports::whitelist::WhitelistCommandPort; + +/// Use-case service for whitelist-related API operations. +/// +/// Delegates to a [`WhitelistCommandPort`] implementation (tracker adapter) +/// and maps domain errors to protocol error types. +pub struct WhitelistApiService { + command_port: Box, +} + +impl WhitelistApiService { + /// Creates a new service backed by the given port implementation. + #[must_use] + pub fn new(command_port: Box) -> Self { + Self { command_port } + } + + /// Adds a torrent to the whitelist. + /// + /// # Errors + /// + /// Returns a [`WhitelistError`] if the database operation fails. + pub async fn add_torrent(&self, info_hash: &InfoHash) -> Result<(), WhitelistError> { + self.command_port.add_torrent(info_hash).await + } + + /// Removes a torrent from the whitelist. + /// + /// # Errors + /// + /// Returns a [`WhitelistError`] if the database operation fails. + pub async fn remove_torrent(&self, info_hash: &InfoHash) -> Result<(), WhitelistError> { + self.command_port.remove_torrent(info_hash).await + } + + /// Reloads the whitelist from the database. + /// + /// # Errors + /// + /// Returns a [`WhitelistError`] if the database operation fails. + pub async fn reload(&self) -> Result<(), WhitelistError> { + self.command_port.reload().await + } +} diff --git a/packages/rest-api-protocol/src/v1/context/mod.rs b/packages/rest-api-protocol/src/v1/context/mod.rs index 40e1954bb..a542e5ebc 100644 --- a/packages/rest-api-protocol/src/v1/context/mod.rs +++ b/packages/rest-api-protocol/src/v1/context/mod.rs @@ -4,3 +4,4 @@ //! live under its `resources/` subdirectory. pub mod health_check; pub mod torrent; +pub mod whitelist; diff --git a/packages/rest-api-protocol/src/v1/context/whitelist/mod.rs b/packages/rest-api-protocol/src/v1/context/whitelist/mod.rs new file mode 100644 index 000000000..5c27ea74f --- /dev/null +++ b/packages/rest-api-protocol/src/v1/context/whitelist/mod.rs @@ -0,0 +1,5 @@ +//! Whitelist context — `/api/v1/whitelist` endpoints. +//! +//! Refer to the [`axum-rest-api-server`] counterpart at +//! `v1::context::whitelist` for the HTTP routing and handler layer. +pub mod resources; diff --git a/packages/rest-api-protocol/src/v1/context/whitelist/resources/mod.rs b/packages/rest-api-protocol/src/v1/context/whitelist/resources/mod.rs new file mode 100644 index 000000000..742dcb6dd --- /dev/null +++ b/packages/rest-api-protocol/src/v1/context/whitelist/resources/mod.rs @@ -0,0 +1,2 @@ +//! Resources for the [`whitelist`](super) context. +pub mod whitelist; diff --git a/packages/rest-api-protocol/src/v1/context/whitelist/resources/whitelist.rs b/packages/rest-api-protocol/src/v1/context/whitelist/resources/whitelist.rs new file mode 100644 index 000000000..b33d4da8f --- /dev/null +++ b/packages/rest-api-protocol/src/v1/context/whitelist/resources/whitelist.rs @@ -0,0 +1,30 @@ +//! API resources for the whitelist context. +//! +//! Most whitelist responses reuse the [`ActionStatus`] enum from +//! `rest-api-protocol::v1::responses`. This module defines the specific +//! error type for whitelist command failures. +use std::fmt; + +/// Errors that can occur during whitelist operations. +/// +/// This type is used in the port trait's return type so that +/// the application layer and Axum handlers can handle errors +/// without depending on `tracker-core` database error types. +#[derive(Debug)] +pub enum WhitelistError { + /// A database error occurred during the whitelist operation. + Database(String), +} + +impl fmt::Display for WhitelistError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // Forward the inner message as-is to preserve the original + // error response format from the previous direct-to-WhitelistManager + // wiring (the Axum handlers format via `{e}`). + WhitelistError::Database(msg) => write!(f, "{msg}"), + } + } +} + +impl std::error::Error for WhitelistError {} diff --git a/packages/rest-api-runtime-adapter/src/adapters/mod.rs b/packages/rest-api-runtime-adapter/src/adapters/mod.rs index c527f3834..ad46e8f45 100644 --- a/packages/rest-api-runtime-adapter/src/adapters/mod.rs +++ b/packages/rest-api-runtime-adapter/src/adapters/mod.rs @@ -1,2 +1,3 @@ //! Adapter implementations for REST API port traits. pub mod torrent; +pub mod whitelist; diff --git a/packages/rest-api-runtime-adapter/src/adapters/whitelist.rs b/packages/rest-api-runtime-adapter/src/adapters/whitelist.rs new file mode 100644 index 000000000..a354572f9 --- /dev/null +++ b/packages/rest-api-runtime-adapter/src/adapters/whitelist.rs @@ -0,0 +1,48 @@ +//! Tracker-specific implementation of [`WhitelistCommandPort`]. +use std::sync::Arc; + +use async_trait::async_trait; +use torrust_info_hash::InfoHash; +use torrust_tracker_core::whitelist::manager::WhitelistManager; +use torrust_tracker_rest_api_application::ports::whitelist::WhitelistCommandPort; +use torrust_tracker_rest_api_protocol::v1::context::whitelist::resources::whitelist::WhitelistError; + +/// Adapter that wraps [`WhitelistManager`] and implements the +/// [`WhitelistCommandPort`] trait. +pub struct TrackerWhitelistAdapter { + whitelist_manager: Arc, +} + +impl TrackerWhitelistAdapter { + /// Creates a new adapter wrapping the given whitelist manager. + #[must_use] + pub fn new(whitelist_manager: &Arc) -> Self { + Self { + whitelist_manager: whitelist_manager.clone(), + } + } +} + +#[async_trait] +impl WhitelistCommandPort for TrackerWhitelistAdapter { + async fn add_torrent(&self, info_hash: &InfoHash) -> Result<(), WhitelistError> { + self.whitelist_manager + .add_torrent_to_whitelist(info_hash) + .await + .map_err(|e| WhitelistError::Database(e.to_string())) + } + + async fn remove_torrent(&self, info_hash: &InfoHash) -> Result<(), WhitelistError> { + self.whitelist_manager + .remove_torrent_from_whitelist(info_hash) + .await + .map_err(|e| WhitelistError::Database(e.to_string())) + } + + async fn reload(&self) -> Result<(), WhitelistError> { + self.whitelist_manager + .load_whitelist_from_database() + .await + .map_err(|e| WhitelistError::Database(e.to_string())) + } +}