Skip to content
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
35 changes: 18 additions & 17 deletions docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Arc<WhitelistManager>>,
State(whitelist_service): State<Arc<WhitelistApiService>>,
Path(info_hash): Path<InfoHashParam>,
) -> 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),
},
Expand All @@ -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<Arc<WhitelistManager>>,
State(whitelist_service): State<Arc<WhitelistApiService>>,
Path(info_hash): Path<InfoHashParam>,
) -> 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),
},
Expand All @@ -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<Arc<WhitelistManager>>) -> Response {
match whitelist_manager.load_whitelist_from_database().await {
pub async fn reload_whitelist_handler(State(whitelist_service): State<Arc<WhitelistApiService>>) -> Response {
match whitelist_service.reload().await {
Ok(()) => ok_response(),
Err(e) => failed_to_reload_whitelist_response(e),
}
Expand Down
10 changes: 5 additions & 5 deletions packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<WhitelistManager>) -> Router {
pub fn add(prefix: &str, router: Router, whitelist_service: &Arc<WhitelistApiService>) -> 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()),
)
}
11 changes: 6 additions & 5 deletions packages/axum-rest-api-server/src/v1/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -18,11 +20,10 @@ pub fn add(prefix: &str, router: Router, http_api_container: &Arc<TrackerHttpApi
&http_api_container.tracker_core_container.keys_handler.clone(),
);
let router = stats::routes::add(&v1_prefix, router, http_api_container);
let router = whitelist::routes::add(
&v1_prefix,
router,
&http_api_container.tracker_core_container.whitelist_manager,
);

let whitelist_adapter = TrackerWhitelistAdapter::new(&http_api_container.tracker_core_container.whitelist_manager);
let whitelist_service = Arc::new(WhitelistApiService::new(Box::new(whitelist_adapter)));
let router = whitelist::routes::add(&v1_prefix, router, &whitelist_service);

let tracker_adapter =
TrackerTorrentQueryAdapter::new(&http_api_container.tracker_core_container.in_memory_torrent_repository);
Expand Down
1 change: 1 addition & 0 deletions packages/rest-api-application/src/ports/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
//! the tracker-internal implementation. Implementations live in the
//! runtime adapter package.
pub mod torrent;
pub mod whitelist;
24 changes: 24 additions & 0 deletions packages/rest-api-application/src/ports/whitelist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! Port trait for whitelist command operations.
//!
//! Defines the boundary between the application layer and the
//! tracker-internal whitelist implementation. Implementations
//! live in the runtime adapter package.
use async_trait::async_trait;
use torrust_info_hash::InfoHash;
use torrust_tracker_rest_api_protocol::v1::context::whitelist::resources::whitelist::WhitelistError;

/// Port for whitelist command operations.
///
/// All whitelist operations are pure commands with no query/read
/// operations. They return either success or an error.
#[async_trait]
pub trait WhitelistCommandPort: Send + Sync {
/// Adds a torrent to the whitelist.
async fn add_torrent(&self, info_hash: &InfoHash) -> 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>;
}
1 change: 1 addition & 0 deletions packages/rest-api-application/src/use_cases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
//!
//! Each service orchestrates business logic by calling port traits.
pub mod torrent;
pub mod whitelist;
51 changes: 51 additions & 0 deletions packages/rest-api-application/src/use_cases/whitelist.rs
Original file line number Diff line number Diff line change
@@ -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<dyn WhitelistCommandPort>,
}

impl WhitelistApiService {
/// Creates a new service backed by the given port implementation.
#[must_use]
pub fn new(command_port: Box<dyn WhitelistCommandPort>) -> 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
}
}
1 change: 1 addition & 0 deletions packages/rest-api-protocol/src/v1/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
//! live under its `resources/` subdirectory.
pub mod health_check;
pub mod torrent;
pub mod whitelist;
5 changes: 5 additions & 0 deletions packages/rest-api-protocol/src/v1/context/whitelist/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//! Resources for the [`whitelist`](super) context.
pub mod whitelist;
Original file line number Diff line number Diff line change
@@ -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 {}
1 change: 1 addition & 0 deletions packages/rest-api-runtime-adapter/src/adapters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
//! Adapter implementations for REST API port traits.
pub mod torrent;
pub mod whitelist;
48 changes: 48 additions & 0 deletions packages/rest-api-runtime-adapter/src/adapters/whitelist.rs
Original file line number Diff line number Diff line change
@@ -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<WhitelistManager>,
}

impl TrackerWhitelistAdapter {
/// Creates a new adapter wrapping the given whitelist manager.
#[must_use]
pub fn new(whitelist_manager: &Arc<WhitelistManager>) -> 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()))
}
}
Loading