From 5bf64b262eec4933c7acd0bcf0684f5a46307e00 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 25 Jun 2026 13:47:10 +0100 Subject: [PATCH 1/3] feat(rest-api-application): migrate whitelist context to contract-first architecture - Add WhitelistError protocol type in rest-api-protocol - Define WhitelistCommandPort trait in rest-api-application - Implement WhitelistApiService use-case in rest-api-application - Implement TrackerWhitelistAdapter in rest-api-runtime-adapter - Rewire Axum handlers to dispatch through WhitelistApiService - Update issue spec progress --- ...940-1938-si-2-migrate-whitelist-context.md | 29 ++++++----- .../src/v1/context/whitelist/handlers.rs | 14 ++--- .../src/v1/context/whitelist/routes.rs | 10 ++-- .../axum-rest-api-server/src/v1/routes.rs | 11 ++-- .../rest-api-application/src/ports/mod.rs | 1 + .../src/ports/whitelist.rs | 24 +++++++++ .../rest-api-application/src/use_cases/mod.rs | 1 + .../src/use_cases/whitelist.rs | 51 +++++++++++++++++++ .../rest-api-protocol/src/v1/context/mod.rs | 1 + .../src/v1/context/whitelist/mod.rs | 5 ++ .../src/v1/context/whitelist/resources/mod.rs | 2 + .../context/whitelist/resources/whitelist.rs | 27 ++++++++++ .../src/adapters/mod.rs | 1 + .../src/adapters/whitelist.rs | 48 +++++++++++++++++ 14 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 packages/rest-api-application/src/ports/whitelist.rs create mode 100644 packages/rest-api-application/src/use_cases/whitelist.rs create mode 100644 packages/rest-api-protocol/src/v1/context/whitelist/mod.rs create mode 100644 packages/rest-api-protocol/src/v1/context/whitelist/resources/mod.rs create mode 100644 packages/rest-api-protocol/src/v1/context/whitelist/resources/whitelist.rs create mode 100644 packages/rest-api-runtime-adapter/src/adapters/whitelist.rs 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..920fecd7b 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` | | +| 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 | TODO | 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` +- [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` - [ ] Pre-commit checks pass - [ ] 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..68438e1ec --- /dev/null +++ b/packages/rest-api-protocol/src/v1/context/whitelist/resources/whitelist.rs @@ -0,0 +1,27 @@ +//! 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 { + WhitelistError::Database(msg) => write!(f, "database error: {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())) + } +} From a6f08c536078c51c28c7851ed068d0549b579821 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 26 Jun 2026 08:18:43 +0100 Subject: [PATCH 2/3] docs: update SI-2 spec T8 status to DONE after pre-push pass --- docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 920fecd7b..4af9e2e40 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 @@ -111,7 +111,7 @@ This maps naturally to a single port trait with three methods. The `ActionStatus | 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 | TODO | Verify pre-commit and pre-push checks pass | | +| T8 | DONE | Verify pre-commit and pre-push checks pass | | ## Verification / Progress From 8b6d92d58555f59fedc07cf3be7e21413bdf7b5a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 26 Jun 2026 08:58:58 +0100 Subject: [PATCH 3/3] fix(rest-api-protocol): preserve error format in WhitelistError Display Remove the "database error: " prefix from the Display impl to preserve the original error message format (out-of-scope to change). Also check the pre-commit/pre-push verification boxes in the spec. --- docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md | 4 ++-- .../src/v1/context/whitelist/resources/whitelist.rs | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) 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 4af9e2e40..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 @@ -119,8 +119,8 @@ This maps naturally to a single port trait with three methods. The `ActionStatus - [x] `WhitelistApiService` use-case implemented - [x] `TrackerWhitelistAdapter` implemented in `rest-api-runtime-adapter` - [x] Axum handlers dispatch through use-case instead of direct `WhitelistManager` -- [ ] Pre-commit checks pass -- [ ] Pre-push checks pass +- [x] Pre-commit checks pass +- [x] Pre-push checks pass ### Progress Log 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 index 68438e1ec..b33d4da8f 100644 --- a/packages/rest-api-protocol/src/v1/context/whitelist/resources/whitelist.rs +++ b/packages/rest-api-protocol/src/v1/context/whitelist/resources/whitelist.rs @@ -19,7 +19,10 @@ pub enum WhitelistError { impl fmt::Display for WhitelistError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - WhitelistError::Database(msg) => write!(f, "database error: {msg}"), + // 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}"), } } }