feat(domain): implement Relay aggregate and RelayLabel newtype
Implemented the Relay aggregate as the primary domain entity for relay control operations. Added RelayLabel newtype for validated human-readable relay labels. Relay aggregate features: - Construction with id, state, and optional label - State control methods: toggle(), turn_on(), turn_off() - Accessor methods: id(), state(), label() - All methods use const where possible for compile-time optimization RelayLabel newtype features: - Validation: non-empty, max 50 characters - Smart constructor with Result-based error handling - Default implementation: "Unlabeled" - Transparent representation for zero-cost abstraction Additional changes: - Made RelayId derive Copy for ergonomic value semantics - All public APIs include documentation and #[must_use] attributes TDD phase: GREEN - Tests pass for Relay aggregate (T021 tests now pass) Ref: T022, T024 (specs/001-modbus-relay-control/tasks.md)
This commit is contained in:
@@ -1,11 +1,65 @@
|
||||
//! Relay entity representing a relay aggregate in the domain model.
|
||||
|
||||
use super::types::{RelayId, RelayState};
|
||||
use super::types::{RelayId, RelayLabel, RelayState};
|
||||
|
||||
/// Relay aggregate representing a physical relay device.
|
||||
///
|
||||
/// Encapsulates the relay's identity, current state, and optional human-readable label.
|
||||
/// This is the primary domain entity for relay control operations.
|
||||
pub struct Relay {
|
||||
id: RelayId,
|
||||
state: RelayState,
|
||||
label: Option<RelayLabel>,
|
||||
}
|
||||
|
||||
impl Relay {
|
||||
/// Creates a new relay with the specified ID, state, and optional label.
|
||||
#[must_use]
|
||||
pub const fn new(id: RelayId, state: RelayState, label: Option<RelayLabel>) -> Self {
|
||||
Self { id, state, label }
|
||||
}
|
||||
|
||||
/// Toggles the relay state between On and Off.
|
||||
pub const fn toggle(&mut self) {
|
||||
match self.state {
|
||||
RelayState::On => self.turn_off(),
|
||||
RelayState::Off => self.turn_on(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the relay state to On.
|
||||
pub const fn turn_on(&mut self) {
|
||||
self.state = RelayState::On;
|
||||
}
|
||||
|
||||
/// Sets the relay state to Off.
|
||||
pub const fn turn_off(&mut self) {
|
||||
self.state = RelayState::Off;
|
||||
}
|
||||
|
||||
/// Returns the relay's unique identifier.
|
||||
#[must_use]
|
||||
pub const fn id(&self) -> RelayId {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Returns the current state of the relay.
|
||||
#[must_use]
|
||||
pub const fn state(&self) -> RelayState {
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Returns a copy of the relay's label, if present.
|
||||
#[must_use]
|
||||
pub fn label(&self) -> Option<RelayLabel> {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::relay::controler::ControllerError;
|
||||
|
||||
#[test]
|
||||
fn test_relay_new_creates_relay() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
mod relayid;
|
||||
mod relaystate;
|
||||
mod relaylabel;
|
||||
|
||||
pub use relayid::RelayId;
|
||||
pub use relaystate::RelayState;
|
||||
pub use relaylabel::RelayLabel;
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::domain::relay::controler::ControllerError;
|
||||
/// Uses the newtype pattern to provide type safety and prevent mixing relay IDs
|
||||
/// with other numeric values. Valid values range from 0-255, corresponding to
|
||||
/// individual relay channels in the Modbus controller.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct RelayId(u8);
|
||||
|
||||
|
||||
47
backend/src/domain/relay/types/relaylabel.rs
Normal file
47
backend/src/domain/relay/types/relaylabel.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// Human-readable label for a relay.
|
||||
///
|
||||
/// Labels must be non-empty and no longer than 50 characters.
|
||||
/// Uses the newtype pattern to provide type safety and validation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct RelayLabel(String);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RelayLabelError {
|
||||
#[error("Label cannot be empty")]
|
||||
Empty,
|
||||
#[error("Label exceeds maximum length of 50 characters: {0}")]
|
||||
TooLong(usize)
|
||||
}
|
||||
|
||||
impl RelayLabel {
|
||||
/// Creates a new relay label with validation.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `RelayLabelError::Empty` if the label is an empty string.
|
||||
/// Returns `RelayLabelError::TooLong` if the label exceeds 50 characters.
|
||||
pub fn new(value: String) -> Result<Self, RelayLabelError> {
|
||||
if value.is_empty() {
|
||||
Err(RelayLabelError::Empty)
|
||||
} else if value.len() > 50 {
|
||||
Err(RelayLabelError::TooLong(value.len()))
|
||||
} else {
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the label as a string slice.
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RelayLabel {
|
||||
fn default() -> Self {
|
||||
Self(String::from("Unlabeled"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user