Add GetAllRelaysUseCase (T043) for retrieving all 8 relay states with labels, coordinating controller reads and repository label lookups with comprehensive error handling and logging. Implement ToggleRelayUseCase (T041) for toggling individual relay states with read-before-write pattern, state validation, and label retrieval. Add use_cases module (T044) with trait-based dependency injection for testability, exposing both use cases for presentation layer integration. Comprehensive test coverage includes 7 toggle tests (state transitions, error handling, double-toggle idempotency) and 9 get-all tests (count, ordering, state correctness, label inclusion, error scenarios). Ref: T041 T042 T043 T044 (specs/001-modbus-relay-control/tasks.org)
208 lines
8.0 KiB
Rust
208 lines
8.0 KiB
Rust
//! Toggle relay use case.
|
|
//!
|
|
//! This use case handles toggling a single relay's state from on to off or vice versa.
|
|
//! It coordinates with the relay controller to read the current state, toggle it,
|
|
//! and write the new state back.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::domain::relay::{
|
|
controller::{ControllerError, RelayController},
|
|
entity::Relay,
|
|
repository::{RelayLabelRepository, RepositoryError},
|
|
types::RelayId,
|
|
};
|
|
|
|
/// Error type for toggle relay use case operations.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ToggleRelayError {
|
|
/// Error from the relay controller (connection, timeout, protocol issues).
|
|
#[error("Controller error: {0}")]
|
|
Controller(#[from] ControllerError),
|
|
|
|
/// Error from the label repository.
|
|
#[error("Repository error: {0}")]
|
|
Repository(#[from] RepositoryError),
|
|
}
|
|
|
|
/// Use case for toggling a relay's state.
|
|
///
|
|
/// This use case:
|
|
/// 1. Reads the current state of the specified relay
|
|
/// 2. Toggles the state (On → Off, Off → On)
|
|
/// 3. Writes the new state to the relay
|
|
/// 4. Returns the updated relay entity with its label
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust,ignore
|
|
/// let use_case = ToggleRelayUseCase::new(controller, repository);
|
|
/// let relay = use_case.execute(RelayId::new(1).unwrap()).await?;
|
|
/// // relay.state() is now toggled from its previous value
|
|
/// ```
|
|
pub struct ToggleRelayUseCase {
|
|
controller: Arc<dyn RelayController>,
|
|
repository: Arc<dyn RelayLabelRepository>,
|
|
}
|
|
|
|
impl ToggleRelayUseCase {
|
|
/// Creates a new toggle relay use case.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `controller` - The relay controller for hardware communication
|
|
/// * `repository` - The label repository for relay labels
|
|
#[must_use]
|
|
pub fn new(
|
|
controller: Arc<dyn RelayController>,
|
|
repository: Arc<dyn RelayLabelRepository>,
|
|
) -> Self {
|
|
Self {
|
|
controller,
|
|
repository,
|
|
}
|
|
}
|
|
|
|
/// Executes the toggle relay use case.
|
|
///
|
|
/// Reads the current state, toggles it, writes the new state, and returns
|
|
/// the updated relay entity including its label.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `relay_id` - The ID of the relay to toggle (1-8)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The updated `Relay` entity with the new state.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `ToggleRelayError` if:
|
|
/// - Controller fails to read/write relay state
|
|
/// - Repository fails to retrieve the label
|
|
pub async fn execute(&self, relay_id: RelayId) -> Result<Relay, ToggleRelayError> {
|
|
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), "Toggling relay state");
|
|
let current_state = self.controller.read_relay_state(relay_id).await?;
|
|
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?current_state, "Read current state");
|
|
let new_state = current_state.toggle();
|
|
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?new_state, "New state after toggle");
|
|
self.controller
|
|
.write_relay_state(relay_id, new_state)
|
|
.await?;
|
|
tracing::info!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?new_state, "Successfully toggled relay");
|
|
let label = self.repository.get_label(relay_id).await?;
|
|
Ok(Relay::new(relay_id, new_state, label))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::domain::relay::types::RelayState;
|
|
use crate::infrastructure::modbus::mock_controller::MockRelayController;
|
|
use crate::infrastructure::persistence::label_repository::MockRelayLabelRepository;
|
|
|
|
/// Helper to create a test controller with initialized relays.
|
|
async fn create_test_controller() -> MockRelayController {
|
|
let controller = MockRelayController::new();
|
|
for i in 1..=8 {
|
|
controller
|
|
.write_relay_state(RelayId::new(i).unwrap(), RelayState::Off)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
controller
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_toggles_relay_state_off_to_on() {
|
|
let controller = Arc::new(create_test_controller().await);
|
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
|
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
|
|
let relay_id = RelayId::new(1).unwrap();
|
|
let initial_state = controller.read_relay_state(relay_id).await.unwrap();
|
|
assert_eq!(initial_state, RelayState::Off);
|
|
let result = use_case.execute(relay_id).await.unwrap();
|
|
assert_eq!(result.state(), RelayState::On);
|
|
assert_eq!(result.id(), relay_id);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_toggles_relay_state_on_to_off() {
|
|
let controller = Arc::new(create_test_controller().await);
|
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
|
let relay_id = RelayId::new(1).unwrap();
|
|
controller
|
|
.write_relay_state(relay_id, RelayState::On)
|
|
.await
|
|
.unwrap();
|
|
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
|
|
let result = use_case.execute(relay_id).await.unwrap();
|
|
assert_eq!(result.state(), RelayState::Off);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_returns_error_if_controller_fails() {
|
|
let controller = Arc::new(MockRelayController::new().with_timeout_simulation());
|
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
|
let use_case = ToggleRelayUseCase::new(controller, repository);
|
|
let relay_id = RelayId::new(1).unwrap();
|
|
let result = use_case.execute(relay_id).await;
|
|
assert!(result.is_err());
|
|
assert!(matches!(
|
|
result.unwrap_err(),
|
|
ToggleRelayError::Controller(_)
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_updates_state_in_controller() {
|
|
let controller = Arc::new(create_test_controller().await);
|
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
|
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
|
|
let relay_id = RelayId::new(1).unwrap();
|
|
use_case.execute(relay_id).await.unwrap();
|
|
let state_in_controller = controller.read_relay_state(relay_id).await.unwrap();
|
|
assert_eq!(state_in_controller, RelayState::On);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_returns_relay_with_label() {
|
|
let controller = Arc::new(create_test_controller().await);
|
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
|
let relay_id = RelayId::new(1).unwrap();
|
|
let label = crate::domain::relay::types::RelayLabel::new("Test Label".to_string()).unwrap();
|
|
repository
|
|
.save_label(relay_id, label.clone())
|
|
.await
|
|
.unwrap();
|
|
let use_case = ToggleRelayUseCase::new(controller, repository);
|
|
let result = use_case.execute(relay_id).await.unwrap();
|
|
assert_eq!(result.label(), Some(label));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_returns_relay_without_label_when_none_set() {
|
|
let controller = Arc::new(create_test_controller().await);
|
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
|
let use_case = ToggleRelayUseCase::new(controller, repository);
|
|
let relay_id = RelayId::new(1).unwrap();
|
|
let result = use_case.execute(relay_id).await.unwrap();
|
|
assert_eq!(result.label(), None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_double_toggle_returns_to_original_state() {
|
|
let controller = Arc::new(create_test_controller().await);
|
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
|
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
|
|
let relay_id = RelayId::new(1).unwrap();
|
|
let initial_state = controller.read_relay_state(relay_id).await.unwrap();
|
|
assert_eq!(initial_state, RelayState::Off);
|
|
use_case.execute(relay_id).await.unwrap();
|
|
let result = use_case.execute(relay_id).await.unwrap();
|
|
assert_eq!(result.state(), RelayState::Off);
|
|
}
|
|
}
|