feat(infrastructure): implement SQLite repository for relay labels

Add complete SQLite implementation of RelayLabelRepository trait with
all CRUD operations (get_label, save_label, delete_label, get_all_labels).

Key changes:
- Create infrastructure entities module with RelayLabelRecord struct
- Implement TryFrom traits for converting between database records and domain types
- Add From<sqlx::Error> and From<RelayLabelError> for RepositoryError
- Write comprehensive functional tests covering all repository operations
- Verify proper handling of edge cases (empty results, overwrites, max length)

TDD phase: GREEN - All repository trait tests now passing with SQLite implementation

Ref: T036 (specs/001-modbus-relay-control/tasks.md)
This commit is contained in:
2026-01-22 00:03:48 +01:00
parent 6d0a2bdb9e
commit 29ebe015fd
9 changed files with 661 additions and 8 deletions
@@ -0,0 +1,33 @@
//! Infrastructure entities for database persistence.
//!
//! This module defines entities that directly map to database tables,
//! providing a clear separation between the persistence layer and the
//! domain layer. These entities represent raw database records without
//! domain validation or business logic.
//!
//! # Conversion Pattern
//!
//! Infrastructure entities implement `TryFrom` traits to convert between
//! database records and domain types:
//!
//! ```rust
//! # use sta::domain::relay::types::{RelayId, RelayLabel};
//! # use sta::infrastructure::persistence::entities::relay_label_record::RelayLabelRecord;
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Database Record -> Domain Types
//! // ... from database
//! let record: RelayLabelRecord = RelayLabelRecord { relay_id: 2, label: "label".to_string() };
//! let (relay_id, relay_label): (RelayId, RelayLabel) = record.try_into()?;
//!
//! // Domain Types -> Database Record
//! let domain_record= RelayLabelRecord::new(relay_id, &relay_label);
//! # Ok(())
//! # }
//! ```
/// Database entity for relay labels.
///
/// This module contains the `RelayLabelRecord` struct which represents
/// a single row in the `RelayLabels` database table, along with conversion
/// traits to and from domain types.
pub mod relay_label_record;
@@ -0,0 +1,62 @@
use crate::domain::relay::{
controller::ControllerError,
repository::RepositoryError,
types::{RelayId, RelayLabel, RelayLabelError},
};
/// Database record representing a relay label.
///
/// This struct directly maps to the `RelayLabels` table in the
/// database. It represents the raw data as stored in the database,
/// without domain validation or business logic.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct RelayLabelRecord {
/// The relay ID (1-8) as stored in the database
pub relay_id: i64,
/// The label text as stored in the database
pub label: String,
}
impl RelayLabelRecord {
/// Creates a new `RecordLabelRecord` from domain types.
#[must_use]
pub fn new(relay_id: RelayId, label: &RelayLabel) -> Self {
Self {
relay_id: i64::from(relay_id.as_u8()),
label: label.as_str().to_string(),
}
}
}
impl TryFrom<RelayLabelRecord> for RelayId {
type Error = ControllerError;
fn try_from(value: RelayLabelRecord) -> Result<Self, Self::Error> {
let value = u8::try_from(value.relay_id).map_err(|e| {
Self::Error::InvalidInput(format!("Got value {} from database: {e}", value.relay_id))
})?;
Self::new(value)
}
}
impl TryFrom<RelayLabelRecord> for RelayLabel {
type Error = RelayLabelError;
fn try_from(value: RelayLabelRecord) -> Result<Self, Self::Error> {
Self::new(value.label)
}
}
impl TryFrom<RelayLabelRecord> for (RelayId, RelayLabel) {
type Error = RepositoryError;
fn try_from(value: RelayLabelRecord) -> Result<Self, Self::Error> {
let record_id: RelayId = value
.clone()
.try_into()
.map_err(|e: ControllerError| RepositoryError::DatabaseError(e.to_string()))?;
let label: RelayLabel = RelayLabel::new(value.label)
.map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
Ok((record_id, label))
}
}