feat: wire relay API with dependency injection
- split settings module into per-struct files
- add DatabaseSettings with default in-memory SQLite path
- implement RelayApi struct with GET /relays and POST
/relays/{id}/toggle
- wire create_relay_controller and create_label_repository into
Application::build() with mock/real selection via cfg!(test) || CI
- register RelayApi in OpenApiService alongside existing APIs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#+title: Implementation Tasks: Modbus Relay Control System
|
||||
#+author: Lucien Cartier-Tilet
|
||||
#+email: lucien@phundrak.com
|
||||
#+startup: content align hideblocks
|
||||
#+options: ^:nil
|
||||
#+LATEX_CLASS_OPTIONS: [a4paper,10pt]
|
||||
#+LATEX_HEADER: \makeatletter \@ifpackageloaded{geometry}{\geometry{margin=2cm}}{\usepackage[margin=2cm]{geometry}} \makeatother
|
||||
@@ -586,7 +587,7 @@ CLOSED: [2026-01-22 jeu. 00:02]
|
||||
|
||||
--------------
|
||||
|
||||
** STARTED Phase 4: US1 - Monitor & Toggle Relay States (MVP) (2 days) [2/5]
|
||||
** STARTED Phase 4: US1 - Monitor & Toggle Relay States (MVP) (2 days) [3/5]
|
||||
- State "STARTED" from "TODO" [2026-01-23 ven. 20:20]
|
||||
*Goal*: View current state of all 8 relays + toggle individual relay on/off
|
||||
|
||||
@@ -616,9 +617,9 @@ CLOSED: [2026-01-23 ven. 20:42]
|
||||
- *File*: =src/application/use_cases/get_all_relays.rs=
|
||||
- *Complexity*: Low | *Uncertainty*: Low
|
||||
|
||||
*** DONE Presentation Layer (Backend API) [2/2]
|
||||
CLOSED: [2026-03-01 dim. 11:07]
|
||||
- State "DONE" from "STARTED" [2026-03-01 dim. 11:07]
|
||||
*** DONE Presentation Layer (Backend API) [3/3]
|
||||
CLOSED: [2026-05-14 jeu. 18:43]
|
||||
- State "DONE" from "TODO" [2026-05-14 jeu. 18:43]
|
||||
- State "STARTED" from "TODO" [2026-01-23 ven. 20:42]
|
||||
- [X] *T045* [US1] [TDD] Define =RelayDto= in presentation layer
|
||||
- Fields: =id= (=u8=), =state= ("on"/"off"), =label= (=Option=)
|
||||
@@ -630,15 +631,94 @@ CLOSED: [2026-03-01 dim. 11:07]
|
||||
- Implement =poem::error::ResponseError=
|
||||
- *File*: =src/presentation/error.rs=
|
||||
- *Complexity*: Low | *Uncertainty*: Low
|
||||
- [X] *T047* [US1] [TDD] Create =RelayApi= struct with dependency injection
|
||||
- Create =RelayApi= struct that holds dependencies:
|
||||
- =relay_controller: Arc<dyn RelayController>=
|
||||
- =label_repository: Arc<dyn RelayLabelRepository>=
|
||||
- Implement constructor: =RelayApi::new(controller, repository) -> Self=
|
||||
- Add =#[derive(Clone)]= to allow sharing across poem-openapi
|
||||
- *File*: =src/presentation/api/relay_api.rs= or =src/route/relay.rs=
|
||||
- *Complexity*: Low | *Uncertainty*: Low
|
||||
|
||||
*TDD Checklist*:
|
||||
|
||||
- [ ] Test: =RelayApi::new()= creates instance with provided dependencies
|
||||
- [ ] Test: =RelayApi= can be cloned (required for poem-openapi)
|
||||
- [ ] Test: Constructor stores both controller and repository
|
||||
|
||||
*Pseudocode*:
|
||||
|
||||
#+begin_src rust
|
||||
use std::sync::Arc;
|
||||
use crate::domain::relay::{
|
||||
controller::RelayController,
|
||||
repository::RelayLabelRepository,
|
||||
};
|
||||
|
||||
/// API handler for relay control endpoints.
|
||||
///
|
||||
/// This struct holds the dependencies needed for relay operations
|
||||
/// and implements the poem-openapi handlers.
|
||||
#[derive(Clone)]
|
||||
pub struct RelayApi {
|
||||
relay_controller: Arc<dyn RelayController>,
|
||||
label_repository: Arc<dyn RelayLabelRepository>,
|
||||
}
|
||||
|
||||
impl RelayApi {
|
||||
/// Creates a new RelayApi with the provided dependencies.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `relay_controller` - Controller for reading/writing relay states
|
||||
/// * `label_repository` - Repository for managing relay labels
|
||||
pub fn new(
|
||||
relay_controller: Arc<dyn RelayController>,
|
||||
label_repository: Arc<dyn RelayLabelRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
relay_controller,
|
||||
label_repository,
|
||||
}
|
||||
}
|
||||
}6 lerolero 7
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::infrastructure::modbus::MockRelayController;
|
||||
use crate::infrastructure::persistence::MockLabelRepository;
|
||||
|
||||
#[test]
|
||||
fn test_relay_api_new_creates_instance() {
|
||||
// GIVEN: Mock dependencies
|
||||
let controller = Arc::new(MockRelayController::new());
|
||||
let repository = Arc::new(MockLabelRepository::new());
|
||||
|
||||
// WHEN: Creating RelayApi
|
||||
let api = RelayApi::new(controller.clone(), repository.clone());
|
||||
|
||||
// THEN: Instance is created successfully
|
||||
// Verify by checking that we can clone it (required for poem-openapi)
|
||||
let _cloned_api = api.clone();
|
||||
}
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Note*: After this task, T048-T051 will add endpoint methods to this struct.
|
||||
|
||||
--------------
|
||||
|
||||
*** TODO T039: Dependency Injection Setup (DECOMPOSED) [0/8]
|
||||
*** DONE T039: Dependency Injection Setup (DECOMPOSED) [8/8]
|
||||
CLOSED: [2026-05-14 jeu. 20:09]
|
||||
- State "DONE" from "STARTED" [2026-05-14 jeu. 20:09]
|
||||
- State "STARTED" from "TODO" [2026-03-06 ven. 22:11]
|
||||
- Complexity :: High → Broken into 4 sub-tasks
|
||||
- Uncertainty :: Medium
|
||||
- Rationale :: Graceful degradation (FR-023), conditional mock/real controller
|
||||
- Prerequisites :: T047 (RelayApi struct) must be complete before T039c
|
||||
|
||||
- [ ] *T039a* [US1] [TDD] Create =ModbusRelayController= factory with retry and fallback
|
||||
- [X] *T039a* [US1] [TDD] Create =ModbusRelayController= factory with retry and fallback
|
||||
|
||||
- Factory function: ~create_relay_controller(settings, use_mock) => Arc~
|
||||
- Retry 3 times with 2s backoff on connection failure
|
||||
@@ -694,13 +774,12 @@ CLOSED: [2026-03-01 dim. 11:07]
|
||||
|
||||
*TDD Checklist*:
|
||||
|
||||
- [ ] Test: use_mock=true returns =MockRelayController= immediately
|
||||
- [ ] Test: Successful connection returns =ModbusRelayController=
|
||||
- [ ] Test: Connection failure after 3 retries returns =MockRelayController=
|
||||
- [ ] Test: Retry delays are 2 seconds between attempts
|
||||
- [ ] Test: Logs appropriate messages for each connection attempt
|
||||
|
||||
- [ ] *T039b* [US4] [TDD] Create =RelayLabelRepositor=y factory
|
||||
- [X] Test: ~use_mock=true~ returns =MockRelayController= immediately
|
||||
- [X] Test: Successful connection returns =ModbusRelayController=
|
||||
- [X] Test: Connection failure after 3 retries returns =MockRelayController=
|
||||
- [X] Test: Retry delays are 2 seconds between attempts
|
||||
- [X] Test: Logs appropriate messages for each connection attempt
|
||||
- [X] *T039b* [US4] [TDD] Create =RelayLabelRepository= factory
|
||||
|
||||
- Factory function: ~create_label_repository(db_path, use_mock) => Arc~
|
||||
- If use_mock: return =MockLabelRepository=
|
||||
@@ -727,17 +806,19 @@ CLOSED: [2026-03-01 dim. 11:07]
|
||||
|
||||
*TDD Checklist*:
|
||||
|
||||
- [ ] Test: use_mock=true returns =MockLabelRepository=
|
||||
- [ ] Test: use_mock=false returns =SQLiteLabelRepository=
|
||||
- [ ] Test: Invalid =db_path= returns =RepositoryError=
|
||||
|
||||
- [ ] *T039c* [US1] [TDD] Wire dependencies in =Application::build()=
|
||||
- [X] Test: use_mock=true returns =MockLabelRepository=
|
||||
- [X] Test: use_mock=false returns =SQLiteLabelRepository=
|
||||
- [X] Test: Invalid =db_path= returns =RepositoryError=
|
||||
- [X] *T039c* [US1] [TDD] Wire dependencies in =Application::build()=
|
||||
|
||||
- *Prerequisites*: T047 must be complete (RelayApi struct created)
|
||||
- Determine test mode: ~cfg!(test) || env::var("CI").is_ok()~
|
||||
- Call =create_relay_controller()= and =create_label_repository()=
|
||||
- Pass dependencies to =RelayApi::new()=
|
||||
- Create =RelayApi= instance with dependencies (requires T047)
|
||||
- Pass =RelayApi= to OpenAPI service
|
||||
- *File*: =src/startup.rs=
|
||||
- *Complexity*: Medium | *Uncertainty*: Low
|
||||
- *Note*: Tests for T039c have been written (they currently pass trivially)
|
||||
|
||||
*Pseudocode*:
|
||||
|
||||
@@ -772,12 +853,10 @@ CLOSED: [2026-03-01 dim. 11:07]
|
||||
|
||||
*TDD Checklist*:
|
||||
|
||||
- [ ] Test: =Application::build()= succeeds in test mode
|
||||
- [ ] Test: =Application::build()= creates correct mock dependencies when CI=true
|
||||
- [ ] Test: =Application::build()= creates real dependencies when not in test mode
|
||||
|
||||
- [ ] *T039d* [US1] [TDD] Register =RelayApi= in route aggregator
|
||||
|
||||
- [X] Test: =Application::build()= succeeds in test mode
|
||||
- [X] Test: =Application::build()= creates correct mock dependencies when CI=true
|
||||
- [X] Test: =Application::build()= creates real dependencies when not in test mode
|
||||
- [X] *T039d* [US1] [TDD] Register =RelayApi= in route aggregator
|
||||
- Add =RelayApi= to OpenAPI service
|
||||
- Tag: "Relays"
|
||||
- *File*: =src/startup.rs=
|
||||
@@ -785,28 +864,28 @@ CLOSED: [2026-03-01 dim. 11:07]
|
||||
|
||||
*TDD Checklist*:
|
||||
|
||||
- [ ] Test: OpenAPI spec includes =/api/relays= endpoints
|
||||
- [ ] Test: Swagger UI renders =Relays= tag
|
||||
- [X] Test: OpenAPI spec includes =/api/relays= endpoints
|
||||
- [X] Test: Swagger UI renders =Relays= tag
|
||||
|
||||
--------------
|
||||
|
||||
- [ ] *T048* [US1] [TDD] Write contract tests for =GET /api/relays=
|
||||
- [X] *T048* [US1] [TDD] Write contract tests for =GET /api/relays=
|
||||
- Test: Returns 200 with array of 8 =RelayDto=
|
||||
- Test: Each relay has id 1-8, state, and optional label
|
||||
- *File*: =tests/contract/test_relay_api.rs=
|
||||
- *Complexity*: Low | *Uncertainty*: Low
|
||||
- [ ] *T049* [US1] [TDD] Implement =GET /api/relays= endpoint
|
||||
- [X] *T049* [US1] [TDD] Implement =GET /api/relays= endpoint
|
||||
- ~#[oai(path = "/relays", method = "get")]~
|
||||
- Call =GetAllRelaysUseCase=, map to =RelayDto=
|
||||
- *File*: =src/presentation/api/relay_api.rs=
|
||||
- *Complexity*: Low | *Uncertainty*: Low
|
||||
- [ ] *T050* [US1] [TDD] Write contract tests for =POST /api/relays/{id}/toggle=
|
||||
- [X] *T050* [US1] [TDD] Write contract tests for =POST /api/relays/{id}/toggle=
|
||||
- Test: Returns 200 with updated =RelayDto=
|
||||
- Test: Returns 404 for id < 1 or id > 8
|
||||
- Test: State actually changes in controller
|
||||
- *File*: =tests/contract/test_relay_api.rs=
|
||||
- *Complexity*: Low | *Uncertainty*: Low
|
||||
- [ ] *T051* [US1] [TDD] Implement =POST /api/relays/{id}/toggle= endpoint
|
||||
- [X] *T051* [US1] [TDD] Implement =POST /api/relays/{id}/toggle= endpoint
|
||||
- ~#[oai(path = "/relays/:id/toggle", method = "post")]~
|
||||
- Parse id, call =ToggleRelayUseCase=, return updated state
|
||||
- *File*: =src/presentation/api/relay_api.rs=
|
||||
|
||||
Reference in New Issue
Block a user