2026-01-01 14:10:13 +01:00
|
|
|
//! Backend API server for `StA` (Smart Temperature & Appliance Control)
|
|
|
|
|
//!
|
|
|
|
|
//! `StA` is a web-based Modbus relay control system that provides `RESTful` API access
|
|
|
|
|
//! to 8-channel relay devices. The system eliminates the need for specialized Modbus
|
|
|
|
|
//! software, enabling browser-based relay control for automation and remote management.
|
|
|
|
|
//!
|
|
|
|
|
//! # Architecture
|
|
|
|
|
//!
|
|
|
|
|
//! This crate follows **Hexagonal Architecture** (Clean Architecture) with strict
|
|
|
|
|
//! layer separation and inward-pointing dependencies:
|
|
|
|
|
//!
|
|
|
|
|
//! - **[`domain`]**: Pure business logic with no external dependencies (relay entities, value objects)
|
|
|
|
|
//! - **[`application`]**: Use cases and orchestration logic (relay control, label management)
|
|
|
|
|
//! - **[`infrastructure`]**: External integrations (Modbus TCP, `SQLite` persistence)
|
|
|
|
|
//! - **[`presentation`]**: API contracts and DTOs (not yet used - see [`route`] for current API)
|
|
|
|
|
//!
|
|
|
|
|
//! Traditional modules (will be migrated to hexagonal layers):
|
|
|
|
|
//! - **[`route`]**: HTTP API endpoints (will move to `presentation`)
|
|
|
|
|
//! - **[`middleware`]**: Custom middleware (rate limiting, CORS)
|
|
|
|
|
//! - **[`settings`]**: Configuration management from YAML + env vars
|
|
|
|
|
//! - **[`startup`]**: Application builder and server configuration
|
|
|
|
|
//! - **[`telemetry`]**: Logging and tracing setup
|
|
|
|
|
//!
|
|
|
|
|
//! # Current Features
|
|
|
|
|
//!
|
2025-12-21 18:19:21 +01:00
|
|
|
//! - Health check endpoints
|
|
|
|
|
//! - Application metadata endpoints
|
2026-01-01 14:10:13 +01:00
|
|
|
//! - Rate limiting middleware
|
|
|
|
|
//! - CORS support
|
|
|
|
|
//! - `OpenAPI` documentation
|
|
|
|
|
//!
|
|
|
|
|
//! # Planned Features (001-modbus-relay-control)
|
|
|
|
|
//!
|
|
|
|
|
//! - Modbus RTU over TCP communication with 8-channel relay devices
|
|
|
|
|
//! - Real-time relay status monitoring
|
|
|
|
|
//! - Individual relay control (on/off toggle)
|
|
|
|
|
//! - Bulk relay operations (all on, all off)
|
|
|
|
|
//! - Persistent relay labels (`SQLite` with `SQLx`)
|
|
|
|
|
//! - Device health monitoring
|
|
|
|
|
//!
|
|
|
|
|
//! See `specs/001-modbus-relay-control/` for detailed specification.
|
2025-12-21 18:19:21 +01:00
|
|
|
|
|
|
|
|
#![deny(clippy::all)]
|
|
|
|
|
#![deny(clippy::pedantic)]
|
|
|
|
|
#![deny(clippy::nursery)]
|
|
|
|
|
#![warn(missing_docs)]
|
|
|
|
|
#![allow(clippy::unused_async)]
|
|
|
|
|
|
|
|
|
|
/// Custom middleware implementations
|
|
|
|
|
pub mod middleware;
|
|
|
|
|
/// API route handlers and endpoints
|
|
|
|
|
pub mod route;
|
|
|
|
|
/// Application configuration settings
|
|
|
|
|
pub mod settings;
|
|
|
|
|
/// Application startup and server configuration
|
|
|
|
|
pub mod startup;
|
|
|
|
|
/// Logging and tracing setup
|
|
|
|
|
pub mod telemetry;
|
|
|
|
|
|
2026-01-01 14:10:13 +01:00
|
|
|
/// Domain layer - Pure business logic with no external dependencies
|
|
|
|
|
///
|
|
|
|
|
/// Contains domain entities, value objects, and business rules for the relay
|
|
|
|
|
/// control system. This layer has no dependencies on frameworks or infrastructure.
|
|
|
|
|
///
|
|
|
|
|
/// See `specs/constitution.md` for hexagonal architecture principles.
|
|
|
|
|
pub mod domain;
|
|
|
|
|
|
|
|
|
|
/// Application layer - Use cases and orchestration logic
|
|
|
|
|
///
|
|
|
|
|
/// Coordinates domain entities to implement business use cases such as relay
|
|
|
|
|
/// control, label management, and device health monitoring.
|
|
|
|
|
pub mod application;
|
|
|
|
|
|
|
|
|
|
/// Infrastructure layer - External integrations and adapters
|
|
|
|
|
///
|
|
|
|
|
/// Implements interfaces defined in domain/application layers for external
|
|
|
|
|
/// systems: Modbus TCP communication, SQLite persistence, HTTP clients.
|
|
|
|
|
pub mod infrastructure;
|
|
|
|
|
|
|
|
|
|
/// Presentation layer - API contracts and DTOs
|
|
|
|
|
///
|
|
|
|
|
/// Defines data transfer objects and API request/response types. Currently
|
|
|
|
|
/// unused - API handlers are in [`route`] module (legacy structure).
|
|
|
|
|
pub mod presentation;
|
|
|
|
|
|
2025-12-21 18:19:21 +01:00
|
|
|
type MaybeListener = Option<poem::listener::TcpListener<String>>;
|
|
|
|
|
|
|
|
|
|
fn prepare(listener: MaybeListener) -> startup::Application {
|
|
|
|
|
dotenvy::dotenv().ok();
|
|
|
|
|
let settings = settings::Settings::new().expect("Failed to read settings");
|
|
|
|
|
if !cfg!(test) {
|
|
|
|
|
let subscriber = telemetry::get_subscriber(settings.debug);
|
|
|
|
|
telemetry::init_subscriber(subscriber);
|
|
|
|
|
}
|
|
|
|
|
tracing::event!(
|
|
|
|
|
target: "backend",
|
|
|
|
|
tracing::Level::DEBUG,
|
|
|
|
|
"Using these settings: {:?}",
|
|
|
|
|
settings
|
|
|
|
|
);
|
|
|
|
|
let application = startup::Application::build(settings, listener);
|
|
|
|
|
tracing::event!(
|
|
|
|
|
target: "backend",
|
|
|
|
|
tracing::Level::INFO,
|
|
|
|
|
"Listening on http://{}:{}/",
|
|
|
|
|
application.host(),
|
|
|
|
|
application.port()
|
|
|
|
|
);
|
|
|
|
|
tracing::event!(
|
|
|
|
|
target: "backend",
|
|
|
|
|
tracing::Level::INFO,
|
|
|
|
|
"Documentation available at http://{}:{}/",
|
|
|
|
|
application.host(),
|
|
|
|
|
application.port()
|
|
|
|
|
);
|
|
|
|
|
application
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Runs the application with the specified TCP listener.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns a `std::io::Error` if the server fails to start or encounters
|
|
|
|
|
/// an I/O error during runtime (e.g., port already in use, network issues).
|
|
|
|
|
#[cfg(not(tarpaulin_include))]
|
|
|
|
|
pub async fn run(listener: MaybeListener) -> Result<(), std::io::Error> {
|
|
|
|
|
let application = prepare(listener);
|
|
|
|
|
application.make_app().run().await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
fn make_random_tcp_listener() -> poem::listener::TcpListener<String> {
|
|
|
|
|
let tcp_listener =
|
|
|
|
|
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener");
|
|
|
|
|
let port = tcp_listener.local_addr().unwrap().port();
|
|
|
|
|
poem::listener::TcpListener::bind(format!("127.0.0.1:{port}"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
fn get_test_app() -> startup::App {
|
|
|
|
|
let tcp_listener = make_random_tcp_listener();
|
|
|
|
|
prepare(Some(tcp_listener)).make_app().into()
|
|
|
|
|
}
|