diff --git a/Cargo.lock b/Cargo.lock index 39a6af9a..8d27689b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,9 +133,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -225,9 +225,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block" @@ -362,7 +362,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block", "core-foundation 0.10.1", "core-graphics-types", @@ -377,7 +377,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "controller-events-receiver" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "tokio", @@ -389,7 +389,7 @@ dependencies = [ [[package]] name = "controller-events-sse-receiver" -version = "0.1.0" +version = "0.2.0" dependencies = [ "askama", "axum", @@ -455,7 +455,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "libc", ] @@ -1083,9 +1083,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if 1.0.4", "futures-util", @@ -1107,9 +1107,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libloading" @@ -1165,9 +1165,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "mdns-sd" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451927183d65d600e52b4e877a1251e051576f84fa01e5b4a50b450dfaaa537c" +checksum = "c2bb8ce26633738d98ffcef71ec58bff967c6675be50229823c2835f6316e67e" dependencies = [ "fastrand", "flume 0.11.1", @@ -1458,7 +1458,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "os-ip-camera" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "image", @@ -1475,7 +1475,7 @@ dependencies = [ [[package]] name = "os-light" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "serde", @@ -1538,7 +1538,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", @@ -1594,7 +1594,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -1696,7 +1696,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -1964,9 +1964,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -2016,7 +2016,7 @@ dependencies = [ [[package]] name = "tosca" -version = "0.1.1" +version = "0.2.0" dependencies = [ "hashbrown", "indexmap", @@ -2027,7 +2027,7 @@ dependencies = [ [[package]] name = "tosca-controller" -version = "0.1.1" +version = "0.2.0" dependencies = [ "bytes", "flume 0.12.0", @@ -2050,7 +2050,7 @@ dependencies = [ [[package]] name = "tosca-drivers" -version = "0.1.1" +version = "0.2.0" dependencies = [ "embedded-hal 1.0.0", "embedded-hal-async", @@ -2060,7 +2060,7 @@ dependencies = [ [[package]] name = "tosca-os" -version = "0.1.1" +version = "0.2.0" dependencies = [ "axum", "futures-core", @@ -2095,7 +2095,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -2245,9 +2245,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -2258,9 +2258,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -2268,9 +2268,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2278,9 +2278,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -2291,9 +2291,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -2313,9 +2313,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index cb5f350a..78761ea9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["crates/*", "examples/*"] exclude = ["crates/tosca-esp32c3"] [workspace.package] -version = "0.1.1" +version = "0.2.0" authors = ["Michele Valsesia "] description = "A versatile, customizable, and secure IoT framework." edition = "2024" @@ -18,10 +18,10 @@ rust-version = "1.90" readme = "README.md" [workspace.dependencies] -tosca = { version = "0.1.1", path = "crates/tosca", default-features = false } -tosca-os = { version = "0.1.1", path = "crates/tosca-os", default-features = false } -tosca-drivers = { version = "0.1.1", path = "crates/tosca-drivers", default-features = false } -tosca-controller = { version = "0.1.1", path = "crates/tosca-controller" } +tosca = { version = "0.2.0", path = "crates/tosca", default-features = false } +tosca-os = { version = "0.2.0", path = "crates/tosca-os", default-features = false } +tosca-drivers = { version = "0.2.0", path = "crates/tosca-drivers", default-features = false } +tosca-controller = { version = "0.2.0", path = "crates/tosca-controller" } hashbrown = { version = "0.17", default-features = false, features = ["default-hasher"] } indexmap = { version = "2.12", default-features = false } diff --git a/crates/tosca-controller/src/controller.rs b/crates/tosca-controller/src/controller.rs index cc5ac449..0fdb8a4c 100644 --- a/crates/tosca-controller/src/controller.rs +++ b/crates/tosca-controller/src/controller.rs @@ -50,7 +50,7 @@ impl RequestSender<'_> { /// and affect the returned response as well. pub async fn send_with_parameters( &self, - parameters: &ParametersValues<'_>, + parameters: &ParametersValues, ) -> Result { if self.request.parameters_data.is_empty() { warn!("The request does not have input parameters."); @@ -349,9 +349,9 @@ mod tests { use crate::policy::Policy; use crate::response::Response; - use crate::device::tests::{create_light, create_unknown}; + use crate::device::tests::{create_custom_device, create_light}; use crate::discovery::tests::configure_discovery; - use crate::tests::{Brightness, check_function_with_device}; + use crate::tests::{Brightness, light_with_toggle, run_one_device}; use super::{Controller, DeviceSender, RequestSender, sender_error}; @@ -374,7 +374,7 @@ mod tests { #[test] fn controller_from_devices() { - let devices = Devices::from_devices(vec![create_light(), create_unknown()]); + let devices = Devices::from_devices(vec![create_light(), create_custom_device()]); let controller = Controller::from_devices(configure_discovery(), devices); @@ -382,7 +382,7 @@ mod tests { controller, Controller { discovery: configure_discovery(), - devices: Devices::from_devices(vec![create_light(), create_unknown()]), + devices: Devices::from_devices(vec![create_light(), create_custom_device()]), privacy_policy: Policy::init(), } ); @@ -398,7 +398,7 @@ mod tests { async fn check_ok_response_with_parameters( device_sender: &DeviceSender<'_>, route: &str, - parameters: &ParametersValues<'_>, + parameters: &ParametersValues, ) { check_ok_response(device_sender, route, async move |request_sender| { request_sender.send_with_parameters(parameters).await @@ -447,7 +447,7 @@ mod tests { >( device_sender: &DeviceSender<'_>, route: &str, - parameters: &ParametersValues<'_>, + parameters: &ParametersValues, value: T, ) { check_serial_response( @@ -573,7 +573,7 @@ mod tests { } #[inline] - async fn run_controller_function(name: &str, function: F) + async fn controller_test(name: &str, function: F) where F: FnOnce() -> Fut, Fut: Future, @@ -584,9 +584,14 @@ mod tests { name ); } else { - check_function_with_device(|| async { - function().await; - }) + run_one_device( + |close_rx| async { + light_with_toggle(close_rx).await; + }, + || async { + function().await; + }, + ) .await; } } @@ -594,7 +599,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[serial] async fn test_without_policy_controller() { - run_controller_function("controller_without_policy", || async { + controller_test("controller_without_policy", || async { controller_without_policy().await; }) .await; @@ -603,7 +608,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[serial] async fn test_with_policy_controller() { - run_controller_function("controller_with_policy", || async { + controller_test("controller_with_policy", || async { controller_with_policy().await; }) .await; diff --git a/crates/tosca-controller/src/device.rs b/crates/tosca-controller/src/device.rs index 00f352bd..313dfe56 100644 --- a/crates/tosca-controller/src/device.rs +++ b/crates/tosca-controller/src/device.rs @@ -6,7 +6,7 @@ use serde::Serialize; use tokio::sync::broadcast::{self, Receiver}; use tokio::task::JoinHandle; -use tosca::device::{DeviceEnvironment, DeviceKindId}; +use tosca::device::{DeviceEnvironment, DeviceSchemeOwned}; use tosca::events::{Events as ToscaEvents, EventsDescription}; use tosca::route::RouteConfigs; @@ -84,13 +84,13 @@ impl NetworkInformation { } } -/// Device description. +/// Device metadata. /// /// All properties defining a device. #[derive(Debug, PartialEq, Serialize)] -pub struct Description { - /// Device kind. - pub kind: DeviceKindId, +pub struct Metadata { + /// Device scheme. + pub scheme: DeviceSchemeOwned, /// Device environment. pub environment: DeviceEnvironment, /// Device main route. @@ -100,16 +100,16 @@ pub struct Description { pub description: Option, } -impl Description { - /// Creates a [`Description`]. +impl Metadata { + /// Creates a [`Metadata`]. #[must_use] pub const fn new( - kind: DeviceKindId, + scheme: DeviceSchemeOwned, environment: DeviceEnvironment, main_route: String, ) -> Self { Self { - kind, + scheme, environment, main_route, #[cfg(feature = "metadata")] @@ -132,8 +132,8 @@ impl Description { pub struct Device { // Information needed to contact a device in a network. network_info: NetworkInformation, - // All data needed to describe a device. - description: Description, + // All metadata needed to describe a device. + metadata: Metadata, // All device requests. requests: HashMap, // All device events. @@ -149,13 +149,13 @@ pub struct Device { impl PartialEq for Device { fn eq(&self, other: &Self) -> bool { self.network_info == other.network_info - && self.description == other.description + && self.metadata == other.metadata && self.requests == other.requests } } impl Device { - /// Creates a [`Device`] from [`NetworkInformation`], [`Description`], + /// Creates a [`Device`] from [`NetworkInformation`], [`Metadata`], /// and [`RouteConfigs`]. /// /// This method can be useful when creating a device from data stored @@ -163,14 +163,14 @@ impl Device { #[must_use] pub fn new( network_info: NetworkInformation, - description: Description, + metadata: Metadata, route_configs: RouteConfigs, ) -> Self { let requests = create_requests( route_configs, &network_info.last_reachable_address, - &description.main_route, - description.environment, + &metadata.main_route, + metadata.environment, ); // TODO: Check if the last reachable address works or it is better to @@ -179,7 +179,7 @@ impl Device { Self { network_info, - description, + metadata, requests, events: None, event_handle: None, @@ -192,10 +192,10 @@ impl Device { &self.network_info } - /// Returns an immutable reference to [`Description`]. + /// Returns an immutable reference to [`Metadata`]. #[must_use] - pub const fn description(&self) -> &Description { - &self.description + pub const fn metadata(&self) -> &Metadata { + &self.metadata } /// Returns an immutable reference to [`EventsDescription`]. @@ -300,13 +300,13 @@ impl Device { pub(crate) const fn init( network_info: NetworkInformation, - description: Description, + metadata: Metadata, requests: HashMap, events: Option, ) -> Self { Self { network_info, - description, + metadata, requests, events, event_handle: None, @@ -407,12 +407,14 @@ impl Devices { pub(crate) mod tests { use std::collections::{HashMap, HashSet}; - use tosca::device::{DeviceEnvironment, DeviceKindId}; + use tosca::device::{ + DeviceEnvironment, DeviceScheme, DeviceSchemeOwned, schemes::LIGHT_SCHEME, + }; use tosca::hazards::{Hazard, Hazards}; use tosca::parameters::Parameters; use tosca::route::{Route, RouteConfigs}; - use super::{Description, Device, Devices, NetworkInformation, build_device_address}; + use super::{Device, Devices, Metadata, NetworkInformation, build_device_address}; fn create_network_info(address: &str, port: u16) -> NetworkInformation { let ip_address = address.parse().unwrap(); @@ -437,21 +439,25 @@ pub(crate) mod tests { .ethernet_mac([0x06, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE]) } - fn create_description( - device_kind: DeviceKindId, + fn define_device_metadata( + device_scheme: DeviceScheme, main_route: &str, #[cfg(feature = "metadata")] description: Option<&str>, - ) -> Description { - let desc = Description::new(device_kind, DeviceEnvironment::Os, main_route.into()); + ) -> Metadata { + let meta = Metadata::new( + DeviceSchemeOwned::from(device_scheme), + DeviceEnvironment::Os, + main_route.into(), + ); #[cfg(feature = "metadata")] - let desc = desc.description(description.map(std::convert::Into::into)); - desc + let meta = meta.description(description.map(std::convert::Into::into)); + meta } pub(crate) fn create_light() -> Device { let network_info = create_network_info("192.168.1.174", 5000); - let description = create_description( - DeviceKindId::new("Light"), + let metadata = define_device_metadata( + LIGHT_SCHEME, "light/", #[cfg(feature = "metadata")] Some("A smart light"), @@ -479,13 +485,13 @@ pub(crate) mod tests { .insert(light_off_route.serialize_data()) .insert(toggle_route.serialize_data()); - Device::new(network_info, description, route_configs) + Device::new(network_info, metadata, route_configs) } - pub(crate) fn create_unknown() -> Device { + pub(crate) fn create_custom_device() -> Device { let network_info = create_network_info("192.168.1.176", 5500); - let description = create_description( - DeviceKindId::new("Unknown"), + let description = define_device_metadata( + DeviceScheme::base_custom_scheme("Thermostat"), "ip-camera/", #[cfg(feature = "metadata")] None, @@ -518,7 +524,7 @@ pub(crate) mod tests { #[test] fn check_devices() { - let devices_vector = vec![create_light(), create_unknown()]; + let devices_vector = vec![create_light(), create_custom_device()]; let devices_from_vector = Devices::from_devices(devices_vector); @@ -528,7 +534,7 @@ pub(crate) mod tests { assert!(devices.is_empty()); devices.add(create_light()); - devices.add(create_unknown()); + devices.add(create_custom_device()); // Compare devices created with two different methods. assert_eq!(devices_from_vector, devices); @@ -543,6 +549,6 @@ pub(crate) mod tests { assert_eq!(devices.get(1000), None); // Get a reference to a device. The order is important. - assert_eq!(devices.get(1), Some(&create_unknown())); + assert_eq!(devices.get(1), Some(&create_custom_device())); } } diff --git a/crates/tosca-controller/src/discovery.rs b/crates/tosca-controller/src/discovery.rs index c9c50ee9..cbc17a91 100644 --- a/crates/tosca-controller/src/discovery.rs +++ b/crates/tosca-controller/src/discovery.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::net::IpAddr; use std::time::Duration; -use tosca::device::DeviceDescription; +use tosca::device::{DeviceDescription, DeviceKind}; use flume::RecvTimeoutError; @@ -12,7 +12,7 @@ use tokio::time::sleep; use tracing::{info, warn}; -use crate::device::{Description, Device, Devices, NetworkInformation, build_device_address}; +use crate::device::{Device, Devices, Metadata, NetworkInformation, build_device_address}; use crate::error::Error; use crate::events::Events; use crate::request::create_requests; @@ -61,6 +61,7 @@ pub struct Discovery { disable_ipv6: bool, disable_ip: Option, disable_network_interface: Option<&'static str>, + strict_mode: bool, } impl Discovery { @@ -76,6 +77,7 @@ impl Discovery { disable_ipv6: false, disable_ip: None, disable_network_interface: None, + strict_mode: false, } } @@ -137,11 +139,25 @@ impl Discovery { self } + /// Enables strict mode on the controller. + /// + /// In strict mode, only devices with a predefined + /// [`tosca::device::DeviceScheme`] are accepted. + /// + /// When this mode is active, **all** devices with a custom structure + /// are discarded, keeping in memory only devices that adhere to a + /// determined [`tosca::device::DeviceScheme`]. + #[must_use] + pub const fn strict_mode(mut self) -> Self { + self.strict_mode = true; + self + } + pub(crate) async fn discover(&self) -> Result { // Discover devices. let discovery_info = self.discover_devices().await?; - Self::obtain_devices_data(discovery_info).await + Self::obtain_devices_data(discovery_info, self.strict_mode).await } async fn discover_devices(&self) -> Result, Error> { @@ -223,6 +239,7 @@ impl Discovery { async fn obtain_devices_data( discovery_service: Vec, + strict_mode: bool, ) -> Result { // Devices collection. let mut devices = Devices::new(); @@ -263,6 +280,15 @@ impl Discovery { continue; } + if let DeviceKind::Custom(kind) = device_desc.data.scheme.kind() + && strict_mode + { + warn!( + "Strict mode is enabled and the device is a custom `{kind}`, therefore discarding it" + ); + continue; + } + let requests = create_requests( device_desc.route_configs, &complete_address, @@ -270,8 +296,8 @@ impl Discovery { device_desc.data.environment, ); - let description = Description::new( - device_desc.data.kind, + let description = Metadata::new( + device_desc.data.scheme, device_desc.data.environment, device_desc.main_route.into_owned(), ); @@ -363,7 +389,8 @@ pub(crate) mod tests { use serial_test::serial; use crate::tests::{ - DOMAIN, check_function_with_device, check_function_with_two_devices, compare_device_data, + DOMAIN, analyze_light_data, custom_device, light_with_toggle, light_without_toggle, + run_one_device, run_two_devices, }; use super::Discovery; @@ -375,20 +402,28 @@ pub(crate) mod tests { .disable_network_interface("docker0") } - async fn discovery_comparison(devices_len: usize) { - let devices = configure_discovery().discover().await.unwrap(); + #[inline] + async fn run_discovery(strict_mode: bool, number_of_devices: usize) { + let devices = if strict_mode { + configure_discovery().strict_mode() + } else { + configure_discovery() + } + .discover() + .await + .unwrap(); // Count devices. - assert_eq!(devices.len(), devices_len); + assert_eq!(devices.len(), number_of_devices); - // Iterate over devices and compare data. + // Analyze light data. for device in devices { - compare_device_data(&device); + analyze_light_data(&device); } } #[inline] - async fn run_discovery_function(name: &str, function: F) + async fn discovery_test(name: &str, function: F) where F: FnOnce() -> Fut, Fut: Future, @@ -406,10 +441,15 @@ pub(crate) mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[serial] async fn test_single_device_discovery() { - run_discovery_function("discovery_with_single_device", || async { - check_function_with_device(|| async { - discovery_comparison(1).await; - }) + discovery_test("discovery_with_single_device", || async { + run_one_device( + |close_rx| async { + light_with_toggle(close_rx).await; + }, + || async { + run_discovery(false, 1).await; + }, + ) .await; }) .await; @@ -418,10 +458,38 @@ pub(crate) mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[serial] async fn test_more_devices_discovery() { - run_discovery_function("discovery_with_more_devices", || async { - check_function_with_two_devices(|| async { - discovery_comparison(2).await; - }) + discovery_test("discovery_with_more_devices", || async { + run_two_devices( + |close_rx| async { + light_with_toggle(close_rx).await; + }, + |close_rx| async { + light_without_toggle(close_rx).await; + }, + || async { + run_discovery(false, 2).await; + }, + ) + .await; + }) + .await; + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 3)] + #[serial] + async fn test_more_devices_strict_discovery() { + discovery_test("strict_discovery_with_more_devices", || async { + run_two_devices( + |close_rx| async { + light_with_toggle(close_rx).await; + }, + |close_rx| async { + custom_device(close_rx).await; + }, + || async { + run_discovery(true, 1).await; + }, + ) .await; }) .await; diff --git a/crates/tosca-controller/src/request.rs b/crates/tosca-controller/src/request.rs index 011d42c7..a8ff2da9 100644 --- a/crates/tosca-controller/src/request.rs +++ b/crates/tosca-controller/src/request.rs @@ -36,7 +36,7 @@ fn slash_start_end(s: &str) -> &str { } fn compare_values_with_params_data( - parameter_values: &ParametersValues<'_>, + parameter_values: &ParametersValues, parameters_data: &ParametersData, ) -> Result<(), Error> { for (name, parameter_value) in parameter_values { @@ -239,7 +239,7 @@ impl Request { pub(crate) async fn create_response( &self, - parameters: &ParametersValues<'_>, + parameters: &ParametersValues, ) -> Result { let request_data = self.create_request(parameters)?; self.parameters_send(request_data).await @@ -319,7 +319,7 @@ impl Request { RequestData::new(request, parameters) } - fn create_request(&self, parameters: &ParametersValues<'_>) -> Result { + fn create_request(&self, parameters: &ParametersValues) -> Result { // Compare parameters values with parameters data. compare_values_with_params_data(parameters, &self.parameters_data)?; @@ -349,7 +349,7 @@ impl Request { let mut params = HashMap::new(); for (name, parameter_kind) in &self.parameters_data { let _ = params.insert( - name.clone(), + name.to_string(), format!("{}", ParameterValue::from_parameter_kind(parameter_kind)), ); } @@ -358,7 +358,7 @@ impl Request { // Axum parameters: hello/{{1}}/{{2}} // hello/0.5/1 - fn axum_get(&self, parameters: &ParametersValues<'_>) -> String { + fn axum_get(&self, parameters: &ParametersValues) -> String { let mut route = String::from(&self.route); for (name, parameter_kind) in &self.parameters_data { let value = if let Some(value) = parameters.get(name) { @@ -376,7 +376,7 @@ impl Request { route } - fn create_params(&self, parameters: &ParametersValues<'_>) -> HashMap { + fn create_params(&self, parameters: &ParametersValues) -> HashMap { let mut params = HashMap::new(); for (name, parameter_kind) in &self.parameters_data { let (name, value) = if let Some(value) = parameters.get(name) { @@ -387,7 +387,7 @@ impl Request { format!("{}", ParameterValue::from_parameter_kind(parameter_kind)), ) }; - let _ = params.insert(name.clone(), value); + let _ = params.insert(name.to_string(), value); } params } diff --git a/crates/tosca-controller/src/tests/mod.rs b/crates/tosca-controller/src/tests/mod.rs index ea537da8..28420c04 100644 --- a/crates/tosca-controller/src/tests/mod.rs +++ b/crates/tosca-controller/src/tests/mod.rs @@ -1,16 +1,16 @@ use std::net::Ipv4Addr; use std::time::Duration; -use tosca::device::{DeviceEnvironment, DeviceKindId}; +use tosca::device::{DeviceEnvironment, DeviceScheme, DeviceSchemeOwned, schemes::LIGHT_SCHEME}; use tosca::hazards::{Hazard, Hazards}; use tosca::parameters::{ParameterKind, Parameters, ParametersData}; use tosca::response::ResponseKind; -use tosca::route::{LightOffRoute, LightOnRoute, RestKind, Route}; +use tosca::route::{RestKind, Route}; -use tosca_os::devices::light::Light; +use tosca_os::device::Device as ToscaOsDevice; use tosca_os::extract::Path; use tosca_os::responses::error::ErrorResponse; -use tosca_os::responses::ok::{OkResponse, mandatory_ok_stateless}; +use tosca_os::responses::ok::{OkResponse, ok_stateless}; use tosca_os::responses::serial::{SerialResponse, serial_stateless}; use tosca_os::server::Server; use tosca_os::service::ServiceConfig; @@ -57,21 +57,19 @@ async fn light( close_rx: tokio::sync::oneshot::Receiver<()>, ) { // Turn light on `PUT` route. - let light_on_route = LightOnRoute::put("On") + let light_on_route = Route::put("On", "/on") .description("Turn light on.") .with_hazard(Hazard::ElectricEnergyConsumption); // Turn light off `PUT` route. - let light_off_route = LightOffRoute::put("Off") + let light_off_route = Route::put("Off", "/off") .description("Turn light off.") .with_hazard(Hazard::LogEnergyConsumption); // A light device which is going to be run on the server. - let light = Light::new() - // This method is mandatory, if not called, a compiler error is raised. - .turn_light_on(light_on_route, mandatory_ok_stateless(turn_light_on)) - // This method is mandatory, if not called, a compiler error is raised. - .turn_light_off(light_off_route, mandatory_ok_stateless(turn_light_off)); + let light = ToscaOsDevice::new(LIGHT_SCHEME) + .route(ok_stateless(light_on_route, turn_light_on)) + .route(ok_stateless(light_off_route, turn_light_off)); let light = if with_toggle { // Toggle `PUT` route. @@ -87,7 +85,6 @@ async fn light( light .main_route(FIRST_DEVICE_ROUTE) .route(serial_stateless(toggle_route, toggle)) - .unwrap() } else { light.main_route(SECOND_DEVICE_ROUTE) }; @@ -98,7 +95,7 @@ async fn light( ); // Run a discovery service and the device on the server. - Server::new(light.build()) + Server::new(light.build().expect("Failed to validate device data")) .address(Ipv4Addr::UNSPECIFIED) .port(port) .well_known_service(id) @@ -111,19 +108,71 @@ async fn light( .expect("Error in running a device server."); } +#[inline] pub(crate) async fn light_with_toggle(close_rx: tokio::sync::oneshot::Receiver<()>) { light(PORT_ONE, "light-with-toggle", true, close_rx).await; } +#[inline] pub(crate) async fn light_without_toggle(close_rx: tokio::sync::oneshot::Receiver<()>) { light(PORT_TWO, "light-without-toggle", false, close_rx).await; } +async fn turn_thermostat_on() -> Result { + println!("Thermostat on"); + Ok(OkResponse::ok()) +} + +async fn turn_thermostat_off() -> Result { + println!("Thermostat off"); + Ok(OkResponse::ok()) +} + +pub(crate) async fn custom_device(close_rx: tokio::sync::oneshot::Receiver<()>) { + // Turn thermostat on `PUT` route. + let thermostat_on_route = Route::put("On", "/on") + .description("Turn thermostat on.") + .with_hazard(Hazard::ElectricEnergyConsumption); + + // Turn thermostat off `PUT` route. + let thermostat_off_route = Route::put("Off", "/off") + .description("Turn thermostat off.") + .with_hazard(Hazard::LogEnergyConsumption); + + // Create base scheme for custom thermostat. + let scheme = DeviceScheme::base_custom_scheme("Thermostat"); + + // A custom thermostat device which is going to be run on the server. + let light = ToscaOsDevice::new(scheme) + .route(ok_stateless(thermostat_on_route, turn_thermostat_on)) + .route(ok_stateless(thermostat_off_route, turn_thermostat_off)) + .main_route("/thermostat"); + + info!("Inside the custom thermostat device with port {PORT_TWO}"); + + // Run a discovery service and the device on the server. + Server::new(light.build().expect("Failed to validate device data")) + .address(Ipv4Addr::UNSPECIFIED) + .port(PORT_TWO) + .well_known_service("thermostat") + .discovery_service( + ServiceConfig::mdns_sd("thermostat") + .hostname("tosca") + .domain(DOMAIN), + ) + .with_graceful_shutdown(async move { + _ = close_rx.await; + }) + .run() + .await + .expect("Error in running a device server."); +} + fn build_route(device: &Device, route: &str) -> String { format!( "{}{}{}", device.network_info().last_reachable_address, - device.description().main_route, + device.metadata().main_route, route ) } @@ -156,13 +205,13 @@ fn check_request( // Device addresses are not considered in the comparisons, because they // depend on the machine this test is being run on. -pub(crate) fn compare_device_data(device: &Device) { +pub(crate) fn analyze_light_data(device: &Device) { // Check port. assert!(device.network_info().port == PORT_ONE || device.network_info().port == PORT_TWO); - // Check scheme. - let scheme = device.network_info().properties.get("scheme"); - assert!(scheme.is_some_and(|scheme| scheme == "http")); + // Check protocol scheme. + let protocol_scheme = device.network_info().properties.get("scheme"); + assert!(protocol_scheme.is_some_and(|protocol_scheme| protocol_scheme == "http")); // Check path. let path = device.network_info().properties.get("path"); @@ -173,22 +222,24 @@ pub(crate) fn compare_device_data(device: &Device) { // Check device main route. assert!( - device.description().main_route == FIRST_DEVICE_ROUTE - || device.description().main_route == SECOND_DEVICE_ROUTE + device.metadata().main_route == FIRST_DEVICE_ROUTE + || device.metadata().main_route == SECOND_DEVICE_ROUTE ); // Check device information. - assert_eq!(device.description().kind, DeviceKindId::new("Light")); - assert_eq!(device.description().environment, DeviceEnvironment::Os); + assert_eq!( + device.metadata().scheme, + DeviceSchemeOwned::from(LIGHT_SCHEME) + ); + assert_eq!(device.metadata().environment, DeviceEnvironment::Os); // Check requests number. assert!( - device.description().main_route == FIRST_DEVICE_ROUTE && device.requests_count() == 3 - || device.description().main_route == SECOND_DEVICE_ROUTE - && device.requests_count() == 2 + device.metadata().main_route == FIRST_DEVICE_ROUTE && device.requests_count() == 3 + || device.metadata().main_route == SECOND_DEVICE_ROUTE && device.requests_count() == 2 ); - if device.description().main_route == FIRST_DEVICE_ROUTE { + if device.metadata().main_route == FIRST_DEVICE_ROUTE { let parameters_data = ParametersData::new().insert( "brightness".into(), ParameterKind::RangeU64 { @@ -238,32 +289,38 @@ pub(crate) fn compare_device_data(device: &Device) { ); } -pub(crate) async fn check_function_with_device(function: F) +pub(crate) async fn run_one_device(device: D, task: F) where + D: Fn(tokio::sync::oneshot::Receiver<()>) -> Fut + Send + 'static, + Fut: Future + Send + 'static, F: AsyncFnOnce(), { let _ = tracing_subscriber::fmt().try_init(); - let (close_tx, close_rx) = tokio::sync::oneshot::channel(); + let (device_tx, device_rx) = tokio::sync::oneshot::channel(); // Run a device task. - let device_handle = tokio::spawn(async { light_with_toggle(close_rx).await }); + let device_handle = tokio::spawn(device(device_rx)); // Wait for device task to be configured. tokio::time::sleep(Duration::from_millis(100)).await; - // Run function. - function().await; + // Run task. + task().await; // Shutdown device server. - _ = close_tx.send(()); + _ = device_tx.send(()); // Wait for device server to gracefully shutdown. _ = device_handle.await; } -pub(crate) async fn check_function_with_two_devices(function: F) +pub(crate) async fn run_two_devices(device1: D1, device2: D2, task: F) where + D1: Fn(tokio::sync::oneshot::Receiver<()>) -> Fut1 + Send + 'static, + D2: Fn(tokio::sync::oneshot::Receiver<()>) -> Fut2 + Send + 'static, + Fut1: Future + Send + 'static, + Fut2: Future + Send + 'static, F: AsyncFnOnce(), { let _ = tracing_subscriber::fmt().try_init(); @@ -272,19 +329,19 @@ where let (device2_tx, device2_rx) = tokio::sync::oneshot::channel(); // Run first device task. - let device1_handle = tokio::spawn(async { light_without_toggle(device1_rx).await }); + let device1_handle = tokio::spawn(device1(device1_rx)); // Wait for first device task to be configured. tokio::time::sleep(Duration::from_millis(100)).await; // Run second device task. - let device2_handle = tokio::spawn(async { light_with_toggle(device2_rx).await }); + let device2_handle = tokio::spawn(device2(device2_rx)); // Wait for second device task to be configured. tokio::time::sleep(Duration::from_millis(100)).await; - // Run function. - function().await; + // Run task. + task().await; // Shutdown first device server. _ = device1_tx.send(()); diff --git a/crates/tosca-esp32c3/Cargo.lock b/crates/tosca-esp32c3/Cargo.lock index e1a6e049..cfd89124 100644 --- a/crates/tosca-esp32c3/Cargo.lock +++ b/crates/tosca-esp32c3/Cargo.lock @@ -983,6 +983,12 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" dependencies = [ "foldhash", ] @@ -1032,7 +1038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -1557,18 +1563,17 @@ dependencies = [ [[package]] name = "tosca" -version = "0.1.1" +version = "0.2.0" dependencies = [ - "hashbrown", + "hashbrown 0.17.0", "indexmap", "log", "serde", - "serde_json", ] [[package]] name = "tosca-esp32c3" -version = "0.1.1" +version = "0.2.0" dependencies = [ "critical-section", "edge-http", diff --git a/crates/tosca-esp32c3/Cargo.toml b/crates/tosca-esp32c3/Cargo.toml index 924096f5..bfe356ef 100644 --- a/crates/tosca-esp32c3/Cargo.toml +++ b/crates/tosca-esp32c3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tosca-esp32c3" -version = "0.1.1" +version = "0.2.0" edition = "2024" authors = ["Michele Valsesia "] description = "A library for building Tosca firmware that runs on ESP32-C3 boards." @@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] # Tosca workspace crate -tosca = { path = "../tosca", version = "0.1.0", default-features = false } +tosca = { path = "../tosca", version = "0.2.0", default-features = false } # Multithread critical section critical-section = "1.2.0" diff --git a/crates/tosca-esp32c3/examples/light-with-events/Cargo.toml b/crates/tosca-esp32c3/examples/light-with-events/Cargo.toml index d6b77b04..da4ad843 100644 --- a/crates/tosca-esp32c3/examples/light-with-events/Cargo.toml +++ b/crates/tosca-esp32c3/examples/light-with-events/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "light-with-events" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["Michele Valsesia "] description = """ @@ -12,8 +12,8 @@ publish = false [dependencies] # Tosca crates -tosca = { path = "../../../tosca", version = "0.1.0", default-features = false } -tosca-esp32c3 = { path = "../../", version = "0.1.0" } +tosca = { path = "../../../tosca", version = "0.2.0", default-features = false } +tosca-esp32c3 = { path = "../../", version = "0.2.0" } # Embassy framework embassy-executor = { version = "0.9.1", features = ["log"] } diff --git a/crates/tosca-esp32c3/examples/light-with-events/src/main.rs b/crates/tosca-esp32c3/examples/light-with-events/src/main.rs index 256a0a51..ff1930bc 100644 --- a/crates/tosca-esp32c3/examples/light-with-events/src/main.rs +++ b/crates/tosca-esp32c3/examples/light-with-events/src/main.rs @@ -13,8 +13,9 @@ extern crate alloc; use core::sync::atomic::{AtomicBool, Ordering}; +use tosca::device::schemes::LIGHT_SCHEME; use tosca::parameters::Parameters; -use tosca::route::{LightOffRoute, LightOnRoute, Route}; +use tosca::route::Route; use esp_hal::Config; use esp_hal::clock::CpuClock; @@ -31,7 +32,7 @@ use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal} use embassy_time::Timer; use tosca_esp32c3::{ - devices::light::Light, + device::Device, events::{EventsConfig, EventsManager, broker::BrokerData, interrupt::Notifier}, mdns::Mdns, net::NetworkStack, @@ -278,13 +279,13 @@ async fn main(spawner: Spawner) { .spawn(press_button(button)) .expect("Impossible to spawn the task to press the button task"); - let device = Light::new(&interfaces.ap) - .turn_light_on_stateless_serial( - LightOnRoute::put("On").description("Turn light on."), + let device = Device::new(&interfaces.ap, LIGHT_SCHEME) + .stateless_serial_route( + Route::put("On", "/on").description("Turn light on."), turn_light_on, ) - .turn_light_off_stateless_serial( - LightOffRoute::put("Off") + .stateless_serial_route( + Route::put("Off", "/off") .description("Turn light off.") .with_parameters(Parameters::new().u8("test-value", 42)), turn_light_off, @@ -293,7 +294,8 @@ async fn main(spawner: Spawner) { Route::get("Toggle", "/toggle").description("Toggle the light on and off."), toggle, ) - .build(); + .build() + .expect("Failed to validate device data"); let events_config = EventsConfig::new( spawner, diff --git a/crates/tosca-esp32c3/examples/light-with-state/Cargo.toml b/crates/tosca-esp32c3/examples/light-with-state/Cargo.toml index 206988bc..e94c68b3 100644 --- a/crates/tosca-esp32c3/examples/light-with-state/Cargo.toml +++ b/crates/tosca-esp32c3/examples/light-with-state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "light-with-state" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["Michele Valsesia "] description = "A simple stateful light for an ESP32-C3 board." @@ -9,8 +9,8 @@ publish = false [dependencies] # Tosca crates -tosca = { path = "../../../tosca", version = "0.1.0", default-features = false } -tosca-esp32c3 = { path = "../../", version = "0.1.0" } +tosca = { path = "../../../tosca", version = "0.2.0", default-features = false } +tosca-esp32c3 = { path = "../../", version = "0.2.0" } # Embassy framework embassy-executor = { version = "0.9.1", features = ["log"] } diff --git a/crates/tosca-esp32c3/examples/light-with-state/src/main.rs b/crates/tosca-esp32c3/examples/light-with-state/src/main.rs index 517e8589..6a79a921 100644 --- a/crates/tosca-esp32c3/examples/light-with-state/src/main.rs +++ b/crates/tosca-esp32c3/examples/light-with-state/src/main.rs @@ -13,10 +13,10 @@ extern crate alloc; use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use tosca::device::DeviceMetrics; +use tosca::device::{DeviceMetrics, schemes::LIGHT_SCHEME}; use tosca::energy::Energy; use tosca::parameters::Parameters; -use tosca::route::{LightOffRoute, LightOnRoute, Route}; +use tosca::route::Route; use esp_hal::Config; use esp_hal::clock::CpuClock; @@ -32,7 +32,7 @@ use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal} use embassy_time::Timer; use tosca_esp32c3::{ - devices::light::Light, + device::Device, mdns::Mdns, net::NetworkStack, parameters::ParametersPayloads, @@ -305,13 +305,13 @@ async fn main(spawner: Spawner) { .expect("Impossible to spawn the task to change the led"); let request_counter = RequestCounter(mk_static!(AtomicU32, AtomicU32::new(0))); - let device = Light::with_state(&interfaces.ap, request_counter) - .turn_light_on_stateless_serial( - LightOnRoute::put("On").description("Turn light on."), + let device = Device::with_state(&interfaces.ap, LIGHT_SCHEME, request_counter) + .stateless_serial_route( + Route::put("On", "/on").description("Turn light on."), |_| async move { turn_light_on().await }, ) - .turn_light_off_stateless_serial( - LightOffRoute::put("Off") + .stateless_serial_route( + Route::put("Off", "/off") .description("Turn light off.") .with_parameters(Parameters::new().u8("test-value", 42)), |mut parameters| async move { @@ -340,7 +340,8 @@ async fn main(spawner: Spawner) { ))) }, ) - .build(); + .build() + .expect("Failed to validate device data"); #[allow(clippy::large_futures)] Server::::new(device, Mdns::new(rng)) diff --git a/crates/tosca-esp32c3/examples/light/Cargo.toml b/crates/tosca-esp32c3/examples/light/Cargo.toml index 93e99863..55ac509c 100644 --- a/crates/tosca-esp32c3/examples/light/Cargo.toml +++ b/crates/tosca-esp32c3/examples/light/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "light" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["Michele Valsesia "] description = "A simple stateless light for an ESP32-C3 board." @@ -9,8 +9,8 @@ publish = false [dependencies] # Tosca crates -tosca = { path = "../../../tosca", version = "0.1.0", default-features = false } -tosca-esp32c3 = { path = "../../", version = "0.1.0" } +tosca = { path = "../../../tosca", version = "0.2.0", default-features = false } +tosca-esp32c3 = { path = "../../", version = "0.2.0" } # Embassy framework embassy-executor = { version = "0.9.1", features = ["log"] } diff --git a/crates/tosca-esp32c3/examples/light/src/main.rs b/crates/tosca-esp32c3/examples/light/src/main.rs index 027f95a9..b5ed1240 100644 --- a/crates/tosca-esp32c3/examples/light/src/main.rs +++ b/crates/tosca-esp32c3/examples/light/src/main.rs @@ -13,10 +13,10 @@ extern crate alloc; use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use tosca::device::DeviceMetrics; +use tosca::device::{DeviceMetrics, schemes::LIGHT_SCHEME}; use tosca::energy::Energy; use tosca::parameters::Parameters; -use tosca::route::{LightOffRoute, LightOnRoute, Route}; +use tosca::route::Route; use esp_hal::Config; use esp_hal::clock::CpuClock; @@ -32,7 +32,7 @@ use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal} use embassy_time::Timer; use tosca_esp32c3::{ - devices::light::Light, + device::Device, mdns::Mdns, net::NetworkStack, parameters::ParametersPayloads, @@ -296,13 +296,13 @@ async fn main(spawner: Spawner) { .spawn(change_led(led)) .expect("Impossible to spawn the task to change the led"); - let device = Light::new(&interfaces.ap) - .turn_light_on_stateless_serial( - LightOnRoute::put("On").description("Turn light on."), + let device = Device::new(&interfaces.ap, LIGHT_SCHEME) + .stateless_serial_route( + Route::put("On", "/on").description("Turn light on."), turn_light_on, ) - .turn_light_off_stateless_serial( - LightOffRoute::put("Off") + .stateless_serial_route( + Route::put("Off", "/off") .description("Turn light off.") .with_parameters(Parameters::new().u8("test-value", 42)), turn_light_off, @@ -330,7 +330,8 @@ async fn main(spawner: Spawner) { ))) }, ) - .build(); + .build() + .expect("Failed to validate device data"); #[allow(clippy::large_futures)] Server::::new(device, Mdns::new(rng)) diff --git a/crates/tosca-esp32c3/src/device.rs b/crates/tosca-esp32c3/src/device.rs index b27cfa68..4c6be220 100644 --- a/crates/tosca-esp32c3/src/device.rs +++ b/crates/tosca-esp32c3/src/device.rs @@ -1,13 +1,28 @@ +use alloc::borrow::Cow; +use alloc::boxed::Box; +use alloc::format; use alloc::vec::Vec; -use tosca::device::DeviceDescription; +use tosca::device::{DeviceDescription, DeviceEnvironment, DeviceScheme}; use tosca::events::EventsDescription; -use tosca::route::RouteConfigs; +use tosca::response::ResponseKind; +use tosca::route::{Route, RouteConfigs}; -use crate::response::Response; -use crate::server::{FuncIndex, Functions}; +use esp_radio::wifi::WifiDevice; + +use log::error; + +use crate::error::{Error, ErrorKind}; +use crate::parameters::ParametersPayloads; +use crate::response::{ErrorResponse, InfoResponse, OkResponse, Response, SerialResponse}; +use crate::server::{ + FuncIndex, FuncType, Functions, InfoFn, InfoStateFn, OkFn, OkStateFn, SerialFn, SerialStateFn, +}; use crate::state::{State, ValueFromRef}; +// Default main route. +const MAIN_ROUTE: &str = "/device"; + /// A generic `tosca` device. /// /// A [`Device`] can only be passed to a [`crate::server::Server`]. @@ -16,52 +31,276 @@ where S: ValueFromRef + Send + Sync + 'static, { pub(crate) wifi_mac: [u8; 6], - pub(crate) state: State, pub(crate) description: DeviceDescription, pub(crate) main_route: &'static str, pub(crate) routes_functions: Functions, pub(crate) index_array: Vec, + pub(crate) state: State, +} + +impl Device<()> { + /// Creates a [`Device`] without a state. + #[must_use] + #[inline] + pub fn new(wifi_interface: &WifiDevice<'_>, scheme: DeviceScheme) -> Self { + Self::with_state(wifi_interface, scheme, ()) + } } impl Device where S: ValueFromRef + Send + Sync + 'static, { + /// Creates a [`Device`] with the given state. + #[must_use] #[inline] - pub(crate) fn new( - wifi_mac: [u8; 6], - state: State, - description: DeviceDescription, - main_route: &'static str, - routes_functions: Functions, - index_array: Vec, - ) -> Self { + pub fn with_state(wifi_interface: &WifiDevice<'_>, scheme: DeviceScheme, state: S) -> Self { + Self::init(wifi_interface, scheme, state) + } + + /// Sets the main route. + #[must_use] + pub const fn main_route(mut self, main_route: &'static str) -> Self { + self.main_route = main_route; + self + } + + /// Sets the device description. + #[must_use] + #[inline] + pub fn description(mut self, description: &'static str) -> Self { + self.description.data.description = Some(Cow::Borrowed(description)); + self + } + + /// Adds a [`Route`] with a stateless handler that returns an [`OkResponse`] + /// on success and an [`ErrorResponse`] on failure. + #[must_use] + pub fn stateless_ok_route(self, route: Route, func: F) -> Self + where + F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + Sync + 'static, + { + self.route_func_manager(route, ResponseKind::Ok, move |mut func_manager| { + let func: OkFn = Box::new(move |parameters_values| Box::pin(func(parameters_values))); + func_manager.routes_functions.0.push(func); + func_manager.index_array.push(FuncIndex::new( + FuncType::OkStateless, + func_manager.routes_functions.0.len() - 1, + )); + func_manager + }) + } + + /// Adds a [`Route`] with a stateful handler that returns an [`OkResponse`] + /// on success and an [`ErrorResponse`] on failure. + #[must_use] + pub fn stateful_ok_route(self, route: Route, func: F) -> Self + where + F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + Sync + 'static, + { + self.route_func_manager(route, ResponseKind::Ok, move |mut func_manager| { + let func: OkStateFn = + Box::new(move |state, parameters_values| Box::pin(func(state, parameters_values))); + func_manager.routes_functions.1.push(func); + func_manager.index_array.push(FuncIndex::new( + FuncType::OkStateful, + func_manager.routes_functions.1.len() - 1, + )); + func_manager + }) + } + + /// Adds a [`Route`] with a stateless handler that returns a + /// [`SerialResponse`] on success and an [`ErrorResponse`] on failure. + #[must_use] + pub fn stateless_serial_route(self, route: Route, func: F) -> Self + where + F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + Sync + 'static, + { + self.route_func_manager(route, ResponseKind::Serial, move |mut func_manager| { + let func: SerialFn = + Box::new(move |parameters_values| Box::pin(func(parameters_values))); + func_manager.routes_functions.2.push(func); + func_manager.index_array.push(FuncIndex::new( + FuncType::SerialStateless, + func_manager.routes_functions.2.len() - 1, + )); + func_manager + }) + } + + /// Adds a [`Route`] with a stateful handler that returns a + /// [`SerialResponse`] on success and an [`ErrorResponse`] on failure. + #[must_use] + pub fn stateful_serial_route(self, route: Route, func: F) -> Self + where + F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + Sync + 'static, + { + self.route_func_manager(route, ResponseKind::Serial, move |mut func_manager| { + let func: SerialStateFn = + Box::new(move |state, parameters_values| Box::pin(func(state, parameters_values))); + func_manager.routes_functions.3.push(func); + func_manager.index_array.push(FuncIndex::new( + FuncType::SerialStateful, + func_manager.routes_functions.3.len() - 1, + )); + func_manager + }) + } + + /// Adds a [`Route`] with a stateless handler that returns an + /// [`InfoResponse`] on success and an [`ErrorResponse`] on failure. + #[must_use] + pub fn stateless_info_route(self, route: Route, func: F) -> Self + where + F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + Sync + 'static, + { + self.route_func_manager(route, ResponseKind::Info, move |mut func_manager| { + let func: InfoFn = Box::new(move |parameters_values| Box::pin(func(parameters_values))); + func_manager.routes_functions.4.push(func); + func_manager.index_array.push(FuncIndex::new( + FuncType::InfoStateless, + func_manager.routes_functions.4.len() - 1, + )); + func_manager + }) + } + + /// Adds a [`Route`] with a stateful handler that returns an + /// [`InfoResponse`] on success and an [`ErrorResponse`] on failure. + #[must_use] + pub fn stateful_info_route(self, route: Route, func: F) -> Self + where + F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + Sync + 'static, + { + self.route_func_manager(route, ResponseKind::Info, move |mut func_manager| { + let func: InfoStateFn = + Box::new(move |state, parameters_values| Box::pin(func(state, parameters_values))); + func_manager.routes_functions.5.push(func); + func_manager.index_array.push(FuncIndex::new( + FuncType::InfoStateful, + func_manager.routes_functions.5.len() - 1, + )); + func_manager + }) + } + + /// Builds a [`DeviceVerified`]. + /// + /// # Errors + /// + /// - Returns an error if mandatory routes are missing or invalid. + #[inline] + pub fn build(mut self) -> crate::error::Result> { + if let Some(missing_route) = + self.description + .data + .scheme + .mandatory_routes() + .iter() + .find(|&mandatory_route| { + !self + .description + .route_configs + .iter() + .any(|route| route.data.path == *mandatory_route) + }) + { + let message = format!("The mandatory route `{missing_route}` is missing"); + error!("{message}"); + return Err(Error::new(ErrorKind::MandatoryRoutes, message)); + } + + self.description.data.wifi_mac = Some(self.wifi_mac); + Ok(DeviceVerified(self)) + } + + fn route_func_manager( + mut self, + route: Route, + response_kind: ResponseKind, + add_async_function: F, + ) -> Self + where + F: FnOnce(Self) -> Self, + { + let route_config = route + .remove_prohibited_hazards(self.description.data.scheme.allowed_hazards()) + .serialize_data() + .change_response_kind(response_kind); + + if self.description.route_configs.contains(&route_config) { + error!( + "The route with prefix `{}` already exists!", + route_config.data.path + ); + return self; + } + + self.description.route_configs.add(route_config); + + add_async_function(self) + } + + #[inline] + fn init(wifi_interface: &WifiDevice<'_>, device_scheme: DeviceScheme, state: S) -> Self { + let wifi_mac = wifi_interface.mac_address(); + + let description = DeviceDescription::new(device_scheme, MAIN_ROUTE, RouteConfigs::new()) + .environment(DeviceEnvironment::Embedded); + Self { wifi_mac, - state, description, - main_route, - routes_functions, - index_array, + main_route: MAIN_ROUTE, + routes_functions: ( + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ), + index_array: Vec::new(), + state: State(state), } } +} +/// A device with fully validated data. +/// +/// [`DeviceVerified`] is a transient type, intended solely for passing +/// validated device data to [`crate::server::Server`]. +pub struct DeviceVerified(pub(crate) Device) +where + S: ValueFromRef + Send + Sync + 'static; + +impl DeviceVerified +where + S: ValueFromRef + Send + Sync + 'static, +{ #[inline] pub(crate) fn events_description(mut self, events_description: EventsDescription) -> Self { - self.description = self.description.events_description(events_description); + self.0.description = self.0.description.events_description(events_description); self } #[inline] pub(crate) fn into_internal(mut self) -> InternalDevice { - self.description.data.wifi_mac = Some(self.wifi_mac); + self.0.description.data.wifi_mac = Some(self.0.wifi_mac); InternalDevice { - state: self.state, - main_route: self.main_route, - main_route_response: Response::json(&self.description), - routes_functions: self.routes_functions, - index_array: self.index_array, - route_configs: self.description.route_configs, + state: self.0.state, + main_route: self.0.main_route, + main_route_response: Response::json(&self.0.description), + routes_functions: self.0.routes_functions, + index_array: self.0.index_array, + route_configs: self.0.description.route_configs, } } } diff --git a/crates/tosca-esp32c3/src/devices/light.rs b/crates/tosca-esp32c3/src/devices/light.rs deleted file mode 100644 index 229faed4..00000000 --- a/crates/tosca-esp32c3/src/devices/light.rs +++ /dev/null @@ -1,412 +0,0 @@ -use alloc::borrow::Cow; -use alloc::boxed::Box; -use alloc::vec::Vec; - -use tosca::device::{DeviceDescription, DeviceKind, DeviceKindId}; -use tosca::hazards::Hazard; -use tosca::response::ResponseKind; -use tosca::route::{Route, RouteConfigs}; - -use esp_radio::wifi::WifiDevice; - -use log::error; - -use crate::device::Device; -use crate::parameters::ParametersPayloads; -use crate::response::{ErrorResponse, InfoResponse, OkResponse, SerialResponse}; -use crate::server::{ - FuncIndex, FuncType, Functions, InfoFn, InfoStateFn, OkFn, OkStateFn, SerialFn, SerialStateFn, -}; -use crate::state::{State, ValueFromRef}; - -// Default main route. -const MAIN_ROUTE: &str = "/light"; - -// Allowed hazards. -const ALLOWED_HAZARDS: &[Hazard] = &[Hazard::FireHazard, Hazard::ElectricEnergyConsumption]; - -/// A `light` device. -/// -/// Its methods guide in the definition of a correct light. -/// -/// The initial placeholder for constructing a [`CompleteLight`]. -pub struct Light(CompleteLight) -where - S: ValueFromRef + Send + Sync + 'static; - -impl Light<()> { - /// Creates a [`Light`] without a [`State`]. - #[must_use] - #[inline] - pub fn new(wifi_interface: &WifiDevice<'_>) -> Self { - Self(CompleteLight::with_state(wifi_interface, ())) - } -} - -impl Light -where - S: ValueFromRef + Send + Sync + 'static, -{ - /// Creates a [`Light`] with a [`State`]. - #[inline] - pub fn with_state(wifi_interface: &WifiDevice<'_>, state: S) -> Self { - Self(CompleteLight::with_state(wifi_interface, state)) - } - - /// Turns on a light using a stateless handler, returning an [`OkResponse`] - /// on success and an [`ErrorResponse`] on failure. - #[must_use] - #[inline] - pub fn turn_light_on_stateless_ok( - self, - route: tosca::route::LightOnRoute, - func: F, - ) -> LightOnRoute - where - F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - LightOnRoute(self.0.stateless_ok_route(route.into_route(), func)) - } - - /// Turns on a light using a stateful handler, returning an [`OkResponse`] - /// on success and an [`ErrorResponse`] on failure. - #[must_use] - #[inline] - pub fn turn_light_on_stateful_ok( - self, - route: tosca::route::LightOnRoute, - func: F, - ) -> LightOnRoute - where - F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - LightOnRoute(self.0.stateful_ok_route(route.into_route(), func)) - } - - /// Turns on a light using a stateless handler, returning a - /// [`SerialResponse`] on success and an [`ErrorResponse`] on failure. - #[must_use] - #[inline] - pub fn turn_light_on_stateless_serial( - self, - route: tosca::route::LightOnRoute, - func: F, - ) -> LightOnRoute - where - F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - LightOnRoute(self.0.stateless_serial_route(route.into_route(), func)) - } - - /// Turns on a light using a stateful handler, returning a - /// [`SerialResponse`] on success and an [`ErrorResponse`] on failure. - #[must_use] - #[inline] - pub fn turn_light_on_stateful_serial( - self, - route: tosca::route::LightOnRoute, - func: F, - ) -> LightOnRoute - where - F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - LightOnRoute(self.0.stateful_serial_route(route.into_route(), func)) - } -} - -/// A `light` placeholder that includes only the route for turning the light on. -/// -/// All methods return a [`CompleteLight`]. -pub struct LightOnRoute(CompleteLight) -where - S: ValueFromRef + Send + Sync + 'static; - -impl LightOnRoute -where - S: ValueFromRef + Send + Sync + 'static, -{ - /// Turns off a light using a stateless handler, returning an [`OkResponse`] - /// on success and an [`ErrorResponse`] on failure. - #[must_use] - #[inline] - pub fn turn_light_off_stateless_ok( - self, - route: tosca::route::LightOffRoute, - func: F, - ) -> CompleteLight - where - F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.0.stateless_ok_route(route.into_route(), func) - } - - /// Turns off a light using a stateful handler, returning an [`OkResponse`] - /// on success and an [`ErrorResponse`] on failure. - #[must_use] - #[inline] - pub fn turn_light_off_stateful_ok( - self, - route: tosca::route::LightOffRoute, - func: F, - ) -> CompleteLight - where - F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.0.stateful_ok_route(route.into_route(), func) - } - - /// Turns off a light using a stateless handler, returning a - /// [`SerialResponse`] on success and an [`ErrorResponse`] on failure. - #[must_use] - #[inline] - pub fn turn_light_off_stateless_serial( - self, - route: tosca::route::LightOffRoute, - func: F, - ) -> CompleteLight - where - F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.0.stateless_serial_route(route.into_route(), func) - } - - /// Turns off a light using a stateful handler, returning a - /// [`SerialResponse`] on success and an [`ErrorResponse`] on failure. - #[must_use] - #[inline] - pub fn turn_light_off_stateful_serial( - self, - route: tosca::route::LightOffRoute, - func: F, - ) -> CompleteLight - where - F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.0.stateful_serial_route(route.into_route(), func) - } -} - -/// A `light` device with methods to turn the light on and off. -pub struct CompleteLight -where - S: ValueFromRef + Send + Sync + 'static, -{ - wifi_mac: [u8; 6], - main_route: &'static str, - state: State, - routes_functions: Functions, - device_data: DeviceDescription, - index_array: Vec, -} - -impl CompleteLight -where - S: ValueFromRef + Send + Sync + 'static, -{ - /// Sets the main route. - #[must_use] - #[inline] - pub fn main_route(mut self, main_route: &'static str) -> Self { - self.main_route = main_route; - self.device_data.main_route = Cow::Borrowed(main_route); - self - } - - /// Adds a [`Route`] with a stateless handler that returns an [`OkResponse`] - /// on success and an [`ErrorResponse`] on failure. - #[must_use] - pub fn stateless_ok_route(self, route: Route, func: F) -> Self - where - F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.route_func_manager(route, ResponseKind::Ok, move |mut func_manager| { - let func: OkFn = Box::new(move |parameters_values| Box::pin(func(parameters_values))); - func_manager.routes_functions.0.push(func); - func_manager.index_array.push(FuncIndex::new( - FuncType::OkStateless, - func_manager.routes_functions.0.len() - 1, - )); - func_manager - }) - } - - /// Adds a [`Route`] with a stateful handler that returns an [`OkResponse`] - /// on success and an [`ErrorResponse`] on failure. - #[must_use] - pub fn stateful_ok_route(self, route: Route, func: F) -> Self - where - F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.route_func_manager(route, ResponseKind::Ok, move |mut func_manager| { - let func: OkStateFn = - Box::new(move |state, parameters_values| Box::pin(func(state, parameters_values))); - func_manager.routes_functions.1.push(func); - func_manager.index_array.push(FuncIndex::new( - FuncType::OkStateful, - func_manager.routes_functions.1.len() - 1, - )); - func_manager - }) - } - - /// Adds a [`Route`] with a stateless handler that returns a - /// [`SerialResponse`] on success and an [`ErrorResponse`] on failure. - #[must_use] - pub fn stateless_serial_route(self, route: Route, func: F) -> Self - where - F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.route_func_manager(route, ResponseKind::Serial, move |mut func_manager| { - let func: SerialFn = - Box::new(move |parameters_values| Box::pin(func(parameters_values))); - func_manager.routes_functions.2.push(func); - func_manager.index_array.push(FuncIndex::new( - FuncType::SerialStateless, - func_manager.routes_functions.2.len() - 1, - )); - func_manager - }) - } - - /// Adds a [`Route`] with a stateful handler that returns a - /// [`SerialResponse`] on success and an [`ErrorResponse`] on failure. - #[must_use] - pub fn stateful_serial_route(self, route: Route, func: F) -> Self - where - F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.route_func_manager(route, ResponseKind::Serial, move |mut func_manager| { - let func: SerialStateFn = - Box::new(move |state, parameters_values| Box::pin(func(state, parameters_values))); - func_manager.routes_functions.3.push(func); - func_manager.index_array.push(FuncIndex::new( - FuncType::SerialStateful, - func_manager.routes_functions.3.len() - 1, - )); - func_manager - }) - } - - /// Adds a [`Route`] with a stateless handler that returns an - /// [`InfoResponse`] on success and an [`ErrorResponse`] on failure. - #[must_use] - pub fn stateless_info_route(self, route: Route, func: F) -> Self - where - F: Fn(ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.route_func_manager(route, ResponseKind::Info, move |mut func_manager| { - let func: InfoFn = Box::new(move |parameters_values| Box::pin(func(parameters_values))); - func_manager.routes_functions.4.push(func); - func_manager.index_array.push(FuncIndex::new( - FuncType::InfoStateless, - func_manager.routes_functions.4.len() - 1, - )); - func_manager - }) - } - - /// Adds a [`Route`] with a stateful handler that returns an - /// [`InfoResponse`] on success and an [`ErrorResponse`] on failure. - #[must_use] - pub fn stateful_info_route(self, route: Route, func: F) -> Self - where - F: Fn(State, ParametersPayloads) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + Sync + 'static, - { - self.route_func_manager(route, ResponseKind::Info, move |mut func_manager| { - let func: InfoStateFn = - Box::new(move |state, parameters_values| Box::pin(func(state, parameters_values))); - func_manager.routes_functions.5.push(func); - func_manager.index_array.push(FuncIndex::new( - FuncType::InfoStateful, - func_manager.routes_functions.5.len() - 1, - )); - func_manager - }) - } - - /// Builds a [`Device`]. - /// - /// **This method consumes the light.** - #[must_use] - #[inline] - pub fn build(self) -> Device { - Device::new( - self.wifi_mac, - self.state, - self.device_data, - self.main_route, - self.routes_functions, - self.index_array, - ) - } - - fn route_func_manager( - mut self, - route: Route, - response_kind: ResponseKind, - add_async_function: F, - ) -> Self - where - F: FnOnce(Self) -> Self, - { - let route_config = route - .remove_prohibited_hazards(ALLOWED_HAZARDS) - .serialize_data() - .change_response_kind(response_kind); - - if self.device_data.route_configs.contains(&route_config) { - error!( - "The route with prefix `{}` already exists!", - route_config.data.path - ); - return self; - } - - self.device_data.route_configs.add(route_config); - - add_async_function(self) - } - - #[inline] - fn with_state(wifi_interface: &WifiDevice<'_>, state: S) -> Self { - let wifi_mac = wifi_interface.mac_address(); - - let device_data = DeviceDescription::new( - DeviceKindId::from(&DeviceKind::Light), - MAIN_ROUTE, - RouteConfigs::new(), - 2, - ) - .text_description("A light device."); - - Self { - wifi_mac, - main_route: MAIN_ROUTE, - state: State(state), - routes_functions: ( - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - ), - device_data, - index_array: Vec::new(), - } - } -} diff --git a/crates/tosca-esp32c3/src/devices/mod.rs b/crates/tosca-esp32c3/src/devices/mod.rs deleted file mode 100644 index 95452f15..00000000 --- a/crates/tosca-esp32c3/src/devices/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// A `light` device. -pub mod light; diff --git a/crates/tosca-esp32c3/src/error.rs b/crates/tosca-esp32c3/src/error.rs index e7d1ff4d..445fd6cb 100644 --- a/crates/tosca-esp32c3/src/error.rs +++ b/crates/tosca-esp32c3/src/error.rs @@ -1,3 +1,5 @@ +use alloc::borrow::Cow; + /// All possible error kinds. #[derive(Copy, Clone)] pub enum ErrorKind { @@ -5,6 +7,8 @@ pub enum ErrorKind { EmptyEventsManager, /// `DNS` error. Dns, + /// Mandatory routes are missing or invalid. + MandatoryRoutes, /// `mDNS` error. MDns, /// `MQTT` error. @@ -28,6 +32,7 @@ impl ErrorKind { match self { Self::EmptyEventsManager => "Empty events manager", Self::Dns => "DNS", + Self::MandatoryRoutes => "Mandatory Routes", Self::MDns => "mDNS", Self::Mqtt => "MQTT", Self::Server => "Server", @@ -55,7 +60,7 @@ impl core::fmt::Display for ErrorKind { /// A library error. pub struct Error { kind: ErrorKind, - info: &'static str, + info: Cow<'static, str>, } impl core::fmt::Debug for Error { @@ -71,8 +76,11 @@ impl core::fmt::Display for Error { } impl Error { - pub(crate) fn new(kind: ErrorKind, info: &'static str) -> Self { - Self { kind, info } + pub(crate) fn new(kind: ErrorKind, info: impl Into>) -> Self { + Self { + kind, + info: info.into(), + } } fn error(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { diff --git a/crates/tosca-esp32c3/src/events/mod.rs b/crates/tosca-esp32c3/src/events/mod.rs index 89c919ed..284b4ea5 100644 --- a/crates/tosca-esp32c3/src/events/mod.rs +++ b/crates/tosca-esp32c3/src/events/mod.rs @@ -28,7 +28,7 @@ use tosca::events::{ BrokerData as ToscaBrokerData, Event, Events, EventsDescription, PeriodicEvent, Topic, }; -use crate::device::Device; +use crate::device::DeviceVerified; use crate::error::{Error, ErrorKind}; use crate::state::ValueFromRef; use crate::wifi::WIFI_RECONNECT_DELAY; @@ -104,7 +104,7 @@ where stack: Stack<'static>, broker: BrokerData, topic: Topic, - device: Device, + device: DeviceVerified, } impl EventsConfig @@ -119,7 +119,7 @@ where stack: Stack<'static>, broker: BrokerData, topic_prefix: &str, - device: Device, + device: DeviceVerified, ) -> Self { Self { spawner, @@ -128,7 +128,7 @@ where topic: TopicBuilder::new() .prefix(topic_prefix) .suffix("events") - .mac(device.wifi_mac) + .mac(device.0.wifi_mac) .build(), device, } @@ -1038,7 +1038,7 @@ where /// Runs the task that transmits events over the network. /// - /// Returns a [`Device`] updated with [`EventsDescription`] data. + /// Returns a [`DeviceVerified`] updated with [`EventsDescription`] data. /// /// # Errors /// @@ -1047,7 +1047,7 @@ where /// - The broker domain cannot be resolved via a `DNS` query. /// - The task responsible for network transmission cannot interact with /// the scheduler or the network. - pub async fn run_network_task(self) -> Result, Error> { + pub async fn run_network_task(self) -> Result, Error> { if self.events.is_empty() { return Err(Error::new( ErrorKind::EmptyEventsManager, diff --git a/crates/tosca-esp32c3/src/lib.rs b/crates/tosca-esp32c3/src/lib.rs index 456fc4ae..caf7e743 100644 --- a/crates/tosca-esp32c3/src/lib.rs +++ b/crates/tosca-esp32c3/src/lib.rs @@ -44,9 +44,6 @@ extern crate alloc; -/// All supported device types. -pub mod devices; - /// General device definition along with its methods. pub mod device; /// Error management. diff --git a/crates/tosca-esp32c3/src/parameters.rs b/crates/tosca-esp32c3/src/parameters.rs index f46c0ae0..1978ed94 100644 --- a/crates/tosca-esp32c3/src/parameters.rs +++ b/crates/tosca-esp32c3/src/parameters.rs @@ -1,5 +1,6 @@ use alloc::borrow::Cow; use alloc::format; +use alloc::string::String; use tosca::parameters::{ ParameterKind, ParameterPayload, ParameterValue, ParametersPayloads as ToscaParametersPayloads, @@ -169,19 +170,19 @@ impl F64Payload { /// A payload consisting of a sequence of characters. pub struct CharsSequencePayload<'a> { /// Value. - pub value: Cow<'a, str>, + pub value: String, /// Default value. pub default: Cow<'a, str>, } impl<'a> CharsSequencePayload<'a> { - const fn new(value: Cow<'a, str>, default: Cow<'a, str>) -> Self { + const fn new(value: String, default: Cow<'a, str>) -> Self { Self { value, default } } } /// A container for storing route parameter payloads. -pub struct ParametersPayloads(pub(crate) ToscaParametersPayloads<'static>); +pub struct ParametersPayloads(pub(crate) ToscaParametersPayloads); impl ParametersPayloads { /// Retrieves the [`BoolPayload`] associated with the given parameter name. diff --git a/crates/tosca-esp32c3/src/server.rs b/crates/tosca-esp32c3/src/server.rs index 478c1e3e..c23fbc0b 100644 --- a/crates/tosca-esp32c3/src/server.rs +++ b/crates/tosca-esp32c3/src/server.rs @@ -2,11 +2,10 @@ use core::fmt::{Debug, Display}; use core::net::SocketAddr; use core::pin::Pin; -use alloc::borrow::Cow; use alloc::boxed::Box; use alloc::format; use alloc::str::SplitTerminator; -use alloc::string::ToString; +use alloc::string::{String, ToString}; use alloc::vec::Vec; use tosca::parameters::{ @@ -28,7 +27,7 @@ use embedded_io_async::{Read, Write}; use log::{error, info}; -use crate::device::{Device, InternalDevice}; +use crate::device::{DeviceVerified, InternalDevice}; use crate::error::Error; use crate::mdns::Mdns; use crate::net::get_ip; @@ -221,9 +220,9 @@ impl, mdns: Mdns) -> Self { + pub fn new(device: DeviceVerified, mdns: Mdns) -> Self { Self { port: DEFAULT_SERVER_PORT, handler: ServerHandler::new(device.into_internal()), @@ -387,7 +386,7 @@ struct RouteInfo { } impl RouteInfo { - const fn new(index: usize, parameters_payloads: ToscaParametersPayloads<'static>) -> Self { + const fn new(index: usize, parameters_payloads: ToscaParametersPayloads) -> Self { Self { index, parameters_payloads: ParametersPayloads(parameters_payloads), @@ -520,7 +519,7 @@ where fn parse_get_parameters( route_config: &RouteConfig, mut route_iter: SplitTerminator<'_, char>, - ) -> Result, Response> { + ) -> Result { // Create parameters payloads. let mut parameters_payloads = ToscaParametersPayloads::new(); @@ -533,7 +532,7 @@ where .parameters .iter() .skip(index) - .map(|parameter| parameter.0.as_str()) + .map(|parameter| parameter.0.as_ref()) .collect::>() )) })?; @@ -542,7 +541,7 @@ where let parameter_value = Self::parse_parameter_value(parameter_value, parameter.1)?; parameters_payloads.add( - parameter.0.clone().into(), + String::from(parameter.0.as_ref()), ParameterPayload::new(parameter.1.clone(), parameter_value), ); } @@ -557,7 +556,7 @@ where route_config: &RouteConfig, headers: &Headers<'_, N>, body: &mut Body<'_, T>, - ) -> Result, Response> { + ) -> Result { info!("Headers: {headers:?}"); let content_length = headers @@ -592,15 +591,13 @@ where error_response_with_error("Error reading the request bytes", &format!("{e:?}")) })?; - let route_parameters = serde_json::from_slice::>( - &bytes[0..content_length], - ) - .map_err(|e| { - error_response_with_error( - "Failed to convert bytes into a sequence of parameters", - &format!("{e}"), - ) - })?; + let route_parameters = + serde_json::from_slice::(&bytes[0..content_length]).map_err(|e| { + error_response_with_error( + "Failed to convert bytes into a sequence of parameters", + &format!("{e}"), + ) + })?; info!("Route parameters: {route_parameters:?}"); @@ -657,9 +654,9 @@ where ParameterKind::F64 { .. } | ParameterKind::RangeF64 { .. } => { Self::into_value::(parameter_value, "f64", ParameterValue::F64) } - ParameterKind::CharsSequence { .. } => Ok(ParameterValue::CharsSequence(Cow::Owned( - parameter_value.to_string(), - ))), + ParameterKind::CharsSequence { .. } => { + Ok(ParameterValue::CharsSequence(parameter_value.to_string())) + } } } diff --git a/crates/tosca-os/src/device.rs b/crates/tosca-os/src/device.rs index 2c277eff..a14f5dfa 100644 --- a/crates/tosca-os/src/device.rs +++ b/crates/tosca-os/src/device.rs @@ -1,19 +1,20 @@ -use tosca::device::{ - DeviceDescription, DeviceEnvironment, DeviceKind, DeviceKindId, DeviceKindTrait, -}; +use std::borrow::Cow; + +use tosca::device::{DeviceDescription, DeviceEnvironment, DeviceScheme}; use tosca::route::{RouteConfig, RouteConfigs}; use axum::Router; -use tracing::{info, warn}; +use tracing::{error, info}; +use crate::error::{Error, ErrorKind, Result}; use crate::mac::get_mac_addresses; use crate::responses::BaseResponse; // Default main route. const MAIN_ROUTE: &str = "/device"; -/// A generic `tosca` device. +/// A `tosca` device. /// /// A [`Device`] can only be passed to a [`crate::server::Server`]. #[derive(Debug)] @@ -28,21 +29,15 @@ where // Device router. router: Router, // Device state. - pub(crate) state: S, -} - -impl Default for Device<()> { - fn default() -> Self { - Self::new() - } + state: S, } impl Device<()> { /// Creates a [`Device`] without a state. #[must_use] #[inline] - pub fn new() -> Self { - Self::with_state(()) + pub fn new(scheme: DeviceScheme) -> Self { + Self::with_state(scheme, ()) } } @@ -53,8 +48,8 @@ where /// Creates a [`Device`] with the given state. #[must_use] #[inline] - pub fn with_state(state: S) -> Self { - Self::init(&DeviceKind::Unknown, state) + pub fn with_state(scheme: DeviceScheme, state: S) -> Self { + Self::init(scheme, state) } /// Sets the main route. @@ -64,25 +59,79 @@ where self } + /// Sets the device description. + #[must_use] + #[inline] + pub fn description(mut self, description: &'static str) -> Self { + self.description.data.description = Some(Cow::Borrowed(description)); + self + } + /// Adds a route to [`Device`]. #[must_use] #[inline] pub fn route(self, route: impl FnOnce(S) -> BaseResponse) -> Self { let base_response = route(self.state.clone()); - self.response_data(base_response.finalize()) + let response = base_response.finalize(self.description.data.scheme.allowed_hazards()); + self.response_data(response) } /// Adds an informative route to [`Device`]. #[must_use] pub fn info_route(self, device_info_route: impl FnOnce(S, ()) -> BaseResponse) -> Self { let base_response = device_info_route(self.state.clone(), ()); - self.response_data(base_response.finalize()) + let response = base_response.finalize(self.description.data.scheme.allowed_hazards()); + self.response_data(response) + } + + /// Builds a [`DeviceVerified`]. + /// + /// # Errors + /// + /// - Returns an error if no **Wi-Fi** or **Ethernet** MAC address is + /// available for the device ID. + /// - Returns an error if mandatory routes are missing or invalid. + pub fn build(mut self) -> Result { + let (wifi_mac, ethernet_mac) = get_mac_addresses(); + if wifi_mac.is_none() && ethernet_mac.is_none() { + let message = "No Wi-Fi or Ethernet MAC address is available for the device ID"; + error!(message); + return Err(Error::new(ErrorKind::NoIdFound, message)); + } + + if let Some(missing_route) = + self.description + .data + .scheme + .mandatory_routes() + .iter() + .find(|&mandatory_route| { + !self + .description + .route_configs + .iter() + .any(|route| route.data.path == *mandatory_route) + }) + { + let message = format!("The mandatory route `{missing_route}` is missing"); + error!(message); + return Err(Error::new(ErrorKind::MandatoryRoutes, message)); + } + + self.description.data.wifi_mac = wifi_mac; + self.description.data.ethernet_mac = ethernet_mac; + self.description.main_route = self.main_route.into(); + + Ok(DeviceVerified { + description: self.description, + main_route: self.main_route, + router: self.router, + }) } - pub(crate) fn init(kind: &K, state: S) -> Self { - let description = - DeviceDescription::new(DeviceKindId::from(kind), MAIN_ROUTE, RouteConfigs::new(), 0) - .environment(DeviceEnvironment::Os); + pub(crate) fn init(scheme: DeviceScheme, state: S) -> Self { + let description = DeviceDescription::new(scheme, MAIN_ROUTE, RouteConfigs::new()) + .environment(DeviceEnvironment::Os); Self { description, main_route: MAIN_ROUTE, @@ -96,28 +145,25 @@ where self.description.route_configs.add(data.0); self } +} - pub(crate) fn mandatory_response_data(mut self, responses: I) -> Self - where - I: IntoIterator, - { - let mut mandatory_routes = RouteConfigs::new(); - for response in responses { - self.router = self.router.merge(response.1); - self.description.mandatory_routes += 1; - mandatory_routes.add(response.0); - } - - self.description.route_configs = mandatory_routes.merge(self.description.route_configs); - self - } - - pub(crate) fn finalize(mut self) -> (&'static str, DeviceDescription, Router) { - let (wifi_mac, ethernet_mac) = get_mac_addresses(); - if wifi_mac.is_none() && ethernet_mac.is_none() { - warn!("Unable to retrieve any Wi-Fi or Ethernet MAC address."); - } +/// A device with fully validated data. +/// +/// [`DeviceVerified`] is a transient type, intended solely for passing +/// validated device data to [`crate::server::Server`]. +#[derive(Debug)] +pub struct DeviceVerified { + // Device description. + description: DeviceDescription, + // Device main route. + main_route: &'static str, + // Device router. + router: Router, +} +impl DeviceVerified { + #[inline] + pub(crate) fn finalize(self) -> (&'static str, DeviceDescription, Router) { for route in &self.description.route_configs { info!( "Device route: [{}, \"{}{}\"]", @@ -125,10 +171,6 @@ where ); } - self.description.data.wifi_mac = wifi_mac; - self.description.data.ethernet_mac = ethernet_mac; - self.description.main_route = self.main_route.into(); - (self.main_route, self.description, self.router) } } @@ -139,7 +181,8 @@ mod tests { use core::ops::{Deref, DerefMut}; - use tosca::device::DeviceMetrics; + use tosca::device::schemes::LIGHT_SCHEME; + use tosca::device::{DeviceMetrics, DeviceScheme}; use tosca::energy::Energy; use tosca::route::Route; @@ -149,11 +192,13 @@ mod tests { use tracing::error; + use crate::error::{Error, ErrorKind, Result}; + use crate::responses::error::ErrorResponse; use crate::responses::info::{InfoResponse, info_stateful}; use crate::responses::serial::{SerialResponse, serial_stateful, serial_stateless}; - use super::Device; + use super::{Device, DeviceVerified}; #[derive(Clone)] struct DeviceState @@ -248,7 +293,7 @@ mod tests { async fn serial_response_with_state( State(_state): State>, Json(inputs): Json, - ) -> Result, ErrorResponse> { + ) -> std::result::Result, ErrorResponse> { Ok(SerialResponse::new(DeviceResponse { parameter: inputs.parameter, })) @@ -257,7 +302,7 @@ mod tests { async fn serial_response_with_substate1( State(_state): State, Json(inputs): Json, - ) -> Result, ErrorResponse> { + ) -> std::result::Result, ErrorResponse> { Ok(SerialResponse::new(DeviceResponse { parameter: inputs.parameter, })) @@ -265,7 +310,7 @@ mod tests { async fn info_response_with_substate2( State(state): State, - ) -> Result { + ) -> std::result::Result { // Retrieve the internal state. let mut device_info = state.lock().map_err(|e| { ErrorResponse::internal_with_error("Failed to obtain state lock", &e.to_string()) @@ -279,55 +324,113 @@ mod tests { async fn serial_response_without_state( Json(inputs): Json, - ) -> Result, ErrorResponse> { + ) -> std::result::Result, ErrorResponse> { Ok(SerialResponse::new(DeviceResponse { parameter: inputs.parameter, })) } struct AllRoutes { - with_state_route: Route, - without_state_route: Route, + route1: Route, + route2: Route, + } + + #[inline] + fn light_routes() -> AllRoutes { + AllRoutes { + route1: Route::put("Light on", "/on").description("Turn light on."), + + route2: Route::post("Light off", "/off").description("Turn light off"), + } + } + + #[inline] + fn custom_routes() -> AllRoutes { + AllRoutes { + route1: Route::put("Switch on", "/switch-on").description("Switch something on."), + + route2: Route::post("Switch off", "/switch-off").description("Switch something off"), + } } #[inline] - fn create_routes() -> AllRoutes { + fn state_routes() -> AllRoutes { AllRoutes { - with_state_route: Route::put("State response", "/state-response") + route1: Route::put("State response", "/state-response") .description("Run response with state."), - without_state_route: Route::post("No state route", "/no-state-route") + route2: Route::post("No state route", "/no-state-route") .description("Run response without state."), } } + #[inline] + fn create_device_for_routes(routes: AllRoutes, scheme: DeviceScheme) -> Result { + Device::new(scheme) + .route(serial_stateless( + routes.route1, + serial_response_without_state, + )) + .route(serial_stateless( + routes.route2, + serial_response_without_state, + )) + .build() + } + + #[test] + fn test_light_device() { + assert!(create_device_for_routes(light_routes(), LIGHT_SCHEME).is_ok()); + } + #[test] - fn with_state() { - let routes = create_routes(); + fn test_light_device_with_wrong_routes() { + let device_verified = create_device_for_routes(custom_routes(), LIGHT_SCHEME); + assert!(device_verified.map_or_else( + |e| e + == Error::new( + ErrorKind::MandatoryRoutes, + "The mandatory route `/on` is missing" + ), + |_| false + )); + } + + #[test] + fn test_custom_device() { + assert!( + create_device_for_routes(custom_routes(), DeviceScheme::base_custom_scheme("Light")) + .is_ok() + ); + } + #[test] + fn test_with_state() { + let routes = state_routes(); + let scheme = DeviceScheme::base_custom_scheme("Light"); let state = DeviceState::empty(); - let _ = Device::with_state(state) - .route(serial_stateful( - routes.with_state_route, - serial_response_with_state, - )) + let device_verified = Device::with_state(scheme, state) + .route(serial_stateful(routes.route1, serial_response_with_state)) .route(serial_stateless( - routes.without_state_route, + routes.route2, serial_response_without_state, - )); + )) + .build(); + + assert!(device_verified.is_ok()); } #[test] - fn with_substates() { - let routes = create_routes(); - + fn test_with_substates() { + let routes = state_routes(); + let scheme = DeviceScheme::base_custom_scheme("Light"); let state = DeviceState::new(SubState {}) .add_device_info(DeviceMetrics::with_energy(Energy::empty())); - let _ = Device::with_state(state) + let device_verified = Device::with_state(scheme, state) .route(serial_stateful( - routes.with_state_route, + routes.route1, serial_response_with_substate1, )) .info_route(info_stateful( @@ -336,18 +439,19 @@ mod tests { info_response_with_substate2, )) .route(serial_stateless( - routes.without_state_route, + routes.route2, serial_response_without_state, - )); + )) + .build(); + + assert!(device_verified.is_ok()); } #[test] - fn without_state() { - let routes = create_routes(); - - let _ = Device::new().route(serial_stateless( - routes.without_state_route, - serial_response_without_state, - )); + fn test_without_state() { + assert!( + create_device_for_routes(state_routes(), DeviceScheme::base_custom_scheme("Light")) + .is_ok() + ); } } diff --git a/crates/tosca-os/src/devices/light.rs b/crates/tosca-os/src/devices/light.rs deleted file mode 100644 index 87a1d597..00000000 --- a/crates/tosca-os/src/devices/light.rs +++ /dev/null @@ -1,350 +0,0 @@ -use axum::Router; - -use tosca::device::DeviceKind; -use tosca::hazards::Hazard; -use tosca::route::{LightOffRoute, LightOnRoute, Route, RouteConfig}; - -use crate::device::Device; -use crate::error::Result; -use crate::responses::{BaseResponse, MandatoryResponse}; - -// Default main route. -const MAIN_ROUTE: &str = "/light"; - -// Allowed hazards. -const ALLOWED_HAZARDS: &[Hazard] = &[ - Hazard::FireHazard, - Hazard::ElectricEnergyConsumption, - Hazard::LogEnergyConsumption, -]; - -/// A `light` device. -/// -/// Its methods guide in the definition of a correct light. -/// -/// The default main route is **/light**. -pub struct Light -where - S: Clone + Send + Sync + 'static, -{ - // Internal device. - device: Device, - // Turn light on. - turn_light_on: MandatoryResponse, - // Turn light off. - turn_light_off: MandatoryResponse, -} - -impl Default for Light { - fn default() -> Self { - Self::new() - } -} - -impl Light { - /// Creates a [`Light`] without a state. - #[must_use] - #[inline] - pub fn new() -> Self { - Self::with_state(()) - } -} - -impl Light -where - S: Clone + Send + Sync + 'static, -{ - /// Creates a [`Light`] with a state. - #[inline] - pub fn with_state(state: S) -> Self { - let device = Device::init(&DeviceKind::Light, state).main_route(MAIN_ROUTE); - - Self { - device, - turn_light_on: MandatoryResponse::empty(), - turn_light_off: MandatoryResponse::empty(), - } - } - - /// Turns on the light. - /// - /// **Calling this method is required, or a compilation error will occur.** - pub fn turn_light_on( - self, - route: LightOnRoute, - turn_light_on: impl FnOnce(Route, S) -> MandatoryResponse, - ) -> Light { - let turn_light_on = turn_light_on(route.into_route(), self.device.state.clone()); - - Light { - device: self.device, - turn_light_on: MandatoryResponse::init(turn_light_on.base_response), - turn_light_off: self.turn_light_off, - } - } -} - -impl Light -where - S: Clone + Send + Sync + 'static, -{ - /// Turns off the light. - /// - /// **Calling this method is required, or a compilation error will occur.** - pub fn turn_light_off( - self, - route: LightOffRoute, - turn_light_off: impl FnOnce(Route, S) -> MandatoryResponse, - ) -> Light { - let turn_light_off = turn_light_off(route.into_route(), self.device.state.clone()); - - Light { - device: self.device, - turn_light_on: self.turn_light_on, - turn_light_off: MandatoryResponse::init(turn_light_off.base_response), - } - } -} - -impl Light -where - S: Clone + Send + Sync + 'static, -{ - /// Sets the main route. - #[must_use] - #[inline] - pub fn main_route(mut self, main_route: &'static str) -> Self { - self.device = self.device.main_route(main_route); - self - } - - /// Adds a route to [`Light`]. - /// - /// # Errors - /// - /// Returns an error if the route contains hazards not allowed for - /// [`Light`]. - pub fn route(mut self, light_route: impl FnOnce(S) -> BaseResponse) -> Result { - let base_response = light_route(self.device.state.clone()); - - self.device = self - .device - .response_data(Self::check_allowed_hazards(base_response)); - - Ok(self) - } - - /// Adds an informative route to [`Light`]. - #[must_use] - pub fn info_route(mut self, light_info_route: impl FnOnce(S, ()) -> BaseResponse) -> Self { - let base_response = light_info_route(self.device.state.clone(), ()); - - self.device = self - .device - .response_data(Self::check_allowed_hazards(base_response)); - - self - } - - /// Builds a [`Device`]. - /// - /// **This method consumes the light.** - pub fn build(self) -> Device { - self.device.mandatory_response_data([ - Self::check_allowed_hazards(self.turn_light_on.base_response), - Self::check_allowed_hazards(self.turn_light_off.base_response), - ]) - } - - fn check_allowed_hazards(base_response: BaseResponse) -> (RouteConfig, Router) { - base_response.finalize_with_hazards(ALLOWED_HAZARDS) - } -} - -#[cfg(test)] -mod tests { - use tosca::hazards::Hazard; - use tosca::parameters::Parameters; - use tosca::route::Route; - - use axum::extract::{Json, State}; - - use serde::{Deserialize, Serialize}; - - use crate::devices::light::{LightOffRoute, LightOnRoute}; - use crate::responses::error::ErrorResponse; - use crate::responses::ok::{ - OkResponse, mandatory_ok_stateful, mandatory_ok_stateless, ok_stateful, ok_stateless, - }; - use crate::responses::serial::{ - SerialResponse, mandatory_serial_stateful, mandatory_serial_stateless, serial_stateful, - serial_stateless, - }; - - use super::Light; - - #[derive(Clone)] - struct LightState; - - #[derive(Deserialize)] - struct Inputs { - brightness: f64, - #[serde(alias = "save-energy")] - save_energy: bool, - } - - #[derive(Serialize, Deserialize)] - struct LightOnResponse { - brightness: f64, - #[serde(rename = "save-energy")] - save_energy: bool, - } - - async fn turn_light_on( - State(_state): State, - Json(inputs): Json, - ) -> Result, ErrorResponse> { - Ok(SerialResponse::new(LightOnResponse { - brightness: inputs.brightness, - save_energy: inputs.save_energy, - })) - } - - async fn turn_light_on_stateless( - Json(inputs): Json, - ) -> Result, ErrorResponse> { - Ok(SerialResponse::new(LightOnResponse { - brightness: inputs.brightness, - save_energy: inputs.save_energy, - })) - } - - async fn turn_light_off(State(_state): State) -> Result { - Ok(OkResponse::ok()) - } - - async fn turn_light_off_stateless() -> Result { - Ok(OkResponse::ok()) - } - - async fn toggle(State(_state): State) -> Result { - Ok(OkResponse::ok()) - } - - async fn toggle_stateless() -> Result { - Ok(OkResponse::ok()) - } - - struct Routes { - light_on: LightOnRoute, - light_on_post: Route, - light_off: LightOffRoute, - toggle: Route, - } - - #[inline] - fn create_routes() -> Routes { - Routes { - light_on: LightOnRoute::put("On") - .description("Turn light on.") - .with_hazard(Hazard::ElectricEnergyConsumption) - .with_parameters( - Parameters::new() - .rangef64("brightness", (0., 20., 0.1)) - .bool("save-energy", false), - ), - - light_on_post: Route::post("On Post", "/on-post") - .description("Turn light on.") - .with_hazard(Hazard::ElectricEnergyConsumption) - .with_parameters( - Parameters::new() - .rangef64("brightness", (0., 20., 0.1)) - .bool("save-energy", false), - ), - - light_off: LightOffRoute::put("Off").description("Turn light off."), - - toggle: Route::put("Toggle", "/toggle") - .description("Toggle a light.") - .with_hazard(Hazard::ElectricEnergyConsumption), - } - } - - #[test] - fn complete_with_state() { - let routes = create_routes(); - - let _ = Light::with_state(LightState {}) - .turn_light_on(routes.light_on, mandatory_serial_stateful(turn_light_on)) - .turn_light_off(routes.light_off, mandatory_ok_stateful(turn_light_off)) - .route(serial_stateful(routes.light_on_post, turn_light_on)) - .unwrap() - .route(ok_stateful(routes.toggle, toggle)) - .unwrap() - .build(); - } - - #[test] - fn without_response_with_state() { - let routes = create_routes(); - - let _ = Light::with_state(LightState {}) - .turn_light_on(routes.light_on, mandatory_serial_stateful(turn_light_on)) - .turn_light_off(routes.light_off, mandatory_ok_stateful(turn_light_off)) - .build(); - } - - #[test] - fn stateless_response_with_state() { - let routes = create_routes(); - - let _ = Light::with_state(LightState {}) - .turn_light_on(routes.light_on, mandatory_serial_stateful(turn_light_on)) - .turn_light_off(routes.light_off, mandatory_ok_stateful(turn_light_off)) - .route(serial_stateful(routes.light_on_post, turn_light_on)) - .unwrap() - .route(ok_stateless(routes.toggle, toggle_stateless)) - .unwrap() - .build(); - } - - #[test] - fn complete_without_state() { - let routes = create_routes(); - - let _ = Light::new() - .turn_light_on( - routes.light_on, - mandatory_serial_stateless(turn_light_on_stateless), - ) - .turn_light_off( - routes.light_off, - mandatory_ok_stateless(turn_light_off_stateless), - ) - .route(serial_stateless( - routes.light_on_post, - turn_light_on_stateless, - )) - .unwrap() - .route(ok_stateless(routes.toggle, toggle_stateless)) - .unwrap() - .build(); - } - - #[test] - fn without_response_and_state() { - let routes = create_routes(); - - let _ = Light::new() - .turn_light_on( - routes.light_on, - mandatory_serial_stateless(turn_light_on_stateless), - ) - .turn_light_off( - routes.light_off, - mandatory_ok_stateless(turn_light_off_stateless), - ) - .build(); - } -} diff --git a/crates/tosca-os/src/devices/mod.rs b/crates/tosca-os/src/devices/mod.rs deleted file mode 100644 index 95452f15..00000000 --- a/crates/tosca-os/src/devices/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// A `light` device. -pub mod light; diff --git a/crates/tosca-os/src/error.rs b/crates/tosca-os/src/error.rs index da67f8ba..138d74ac 100644 --- a/crates/tosca-os/src/error.rs +++ b/crates/tosca-os/src/error.rs @@ -2,21 +2,28 @@ use std::borrow::Cow; /// All possible error kinds. #[derive(Debug, Copy, Clone)] +#[cfg_attr(test, derive(PartialEq))] pub enum ErrorKind { - /// Errors encountered while configuring the discovery service. - Service, + /// Failed to find a device ID. + NoIdFound, /// Not found address. NotFoundAddress, + /// Mandatory routes are missing or invalid. + MandatoryRoutes, /// Errors encountered while serializing or deserializing a file. Serialization, + /// Errors encountered while configuring the discovery service. + Service, } impl ErrorKind { pub(crate) const fn description(self) -> &'static str { match self { - Self::Service => "Service", + Self::NoIdFound => "No Device ID Found", Self::NotFoundAddress => "Not Found Address", + Self::MandatoryRoutes => "Mandatory Routes", Self::Serialization => "Serialization", + Self::Service => "Service", } } } @@ -28,6 +35,7 @@ impl std::fmt::Display for ErrorKind { } /// A library error. +#[cfg_attr(test, derive(PartialEq))] pub struct Error { kind: ErrorKind, description: Cow<'static, str>, diff --git a/crates/tosca-os/src/lib.rs b/crates/tosca-os/src/lib.rs index 50cd7e45..52664cb8 100644 --- a/crates/tosca-os/src/lib.rs +++ b/crates/tosca-os/src/lib.rs @@ -34,10 +34,7 @@ //! //! An `std` environment is required to obtain full crate functionality. -/// All supported device types. -pub mod devices; - -/// General device definition along with its methods. +/// A device definition along with its methods. pub mod device; /// Error management. pub mod error; diff --git a/crates/tosca-os/src/responses/mod.rs b/crates/tosca-os/src/responses/mod.rs index ec7b6222..0d6a97f8 100644 --- a/crates/tosca-os/src/responses/mod.rs +++ b/crates/tosca-os/src/responses/mod.rs @@ -124,59 +124,16 @@ impl BaseResponse { } } - pub(crate) fn finalize_with_hazards(self, allowed_hazards: &[Hazard]) -> (RouteConfig, Router) { - ( + pub(crate) fn finalize(self, allowed_hazards: &[Hazard]) -> (RouteConfig, Router) { + let route = if allowed_hazards.is_empty() { + self.route.serialize_data() + } else { self.route .remove_prohibited_hazards(allowed_hazards) .serialize_data() - .change_response_kind(self.response_kind), - self.router, - ) - } - - pub(crate) fn finalize(self) -> (RouteConfig, Router) { - ( - self.route - .serialize_data() - .change_response_kind(self.response_kind), - self.router, - ) - } -} - -/// A mandatory [`BaseResponse`]. -/// -/// This structure acts as a wrapper for a [`BaseResponse`], making it -/// mandatory. -pub struct MandatoryResponse { - pub(crate) base_response: BaseResponse, -} - -impl MandatoryResponse { - pub(crate) fn empty() -> Self { - Self { - base_response: BaseResponse { - router: Router::new(), - route: Route::get("", ""), - response_kind: ResponseKind::Ok, - }, - } - } - - pub(super) const fn new(base_response: BaseResponse) -> Self { - Self { base_response } - } -} - -impl MandatoryResponse { - /// Returns a reference to the internal [`BaseResponse`]. - #[must_use] - pub const fn base_response_as_ref(&self) -> &BaseResponse { - &self.base_response - } + }; - pub(crate) const fn init(base_response: BaseResponse) -> Self { - Self { base_response } + (route.change_response_kind(self.response_kind), self.router) } } diff --git a/crates/tosca-os/src/responses/ok.rs b/crates/tosca-os/src/responses/ok.rs index 5b08d3f5..3e3b936c 100644 --- a/crates/tosca-os/src/responses/ok.rs +++ b/crates/tosca-os/src/responses/ok.rs @@ -12,7 +12,7 @@ use axum::{ use serde::Serialize; -use super::{BaseResponse, MandatoryResponse, error::ErrorResponse}; +use super::{BaseResponse, error::ErrorResponse}; /// A response which transmits a concise JSON message over the network to notify /// a controller that an operation completed successfully. @@ -60,26 +60,6 @@ macro_rules! impl_ok_type_name { } super::all_the_tuples!(impl_ok_type_name); -/// Creates a stateful [`MandatoryResponse`] from an [`OkResponse`]. -#[inline] -pub fn mandatory_ok_stateful( - handler: H, -) -> impl FnOnce(Route, S) -> MandatoryResponse -where - H: Handler + private::OkTypeName, - T: 'static, - S: Clone + Send + Sync + 'static, -{ - move |route: Route, state: S| { - MandatoryResponse::new(BaseResponse::stateful( - route, - ResponseKind::Ok, - handler, - state, - )) - } -} - /// Creates a stateful [`BaseResponse`] from an [`OkResponse`]. #[inline] pub fn ok_stateful(route: Route, handler: H) -> impl FnOnce(S) -> BaseResponse @@ -91,21 +71,6 @@ where move |state: S| BaseResponse::stateful(route, ResponseKind::Ok, handler, state) } -/// Creates a stateless [`MandatoryResponse`] from an [`OkResponse`]. -#[inline] -pub fn mandatory_ok_stateless( - handler: H, -) -> impl FnOnce(Route, S) -> MandatoryResponse -where - H: Handler + private::OkTypeName, - T: 'static, - S: Clone + Send + Sync + 'static, -{ - move |route: Route, _state: S| { - MandatoryResponse::new(BaseResponse::stateless(route, ResponseKind::Ok, handler)) - } -} - /// Creates a stateless [`BaseResponse`] from an [`OkResponse`]. #[inline] pub fn ok_stateless(route: Route, handler: H) -> impl FnOnce(S) -> BaseResponse diff --git a/crates/tosca-os/src/responses/serial.rs b/crates/tosca-os/src/responses/serial.rs index 9a71bab5..038ca931 100644 --- a/crates/tosca-os/src/responses/serial.rs +++ b/crates/tosca-os/src/responses/serial.rs @@ -12,7 +12,7 @@ use axum::{ use serde::Serialize; -use super::{BaseResponse, MandatoryResponse, error::ErrorResponse}; +use super::{BaseResponse, error::ErrorResponse}; /// A response which transmits a JSON message over the network containing /// the data produced during a device operation. @@ -62,26 +62,6 @@ macro_rules! impl_serial_type_name { super::all_the_tuples!(impl_serial_type_name); -/// Creates a stateful [`MandatoryResponse`] from a [`SerialResponse`]. -#[inline] -pub fn mandatory_serial_stateful( - handler: H, -) -> impl FnOnce(Route, S) -> MandatoryResponse -where - H: Handler + private::SerialTypeName, - T: 'static, - S: Clone + Send + Sync + 'static, -{ - move |route: Route, state: S| { - MandatoryResponse::new(BaseResponse::stateful( - route, - ResponseKind::Serial, - handler, - state, - )) - } -} - /// Creates a stateful [`BaseResponse`] from a [`SerialResponse`]. #[inline] pub fn serial_stateful(route: Route, handler: H) -> impl FnOnce(S) -> BaseResponse @@ -93,25 +73,6 @@ where move |state: S| BaseResponse::stateful(route, ResponseKind::Serial, handler, state) } -/// Creates a stateless [`MandatoryResponse`] from a [`SerialResponse`]. -#[inline] -pub fn mandatory_serial_stateless( - handler: H, -) -> impl FnOnce(Route, S) -> MandatoryResponse -where - H: Handler + private::SerialTypeName, - T: 'static, - S: Clone + Send + Sync + 'static, -{ - move |route: Route, _state: S| { - MandatoryResponse::new(BaseResponse::stateless( - route, - ResponseKind::Serial, - handler, - )) - } -} - /// Creates a stateless [`BaseResponse`] from a [`SerialResponse`]. #[inline] pub fn serial_stateless(route: Route, handler: H) -> impl FnOnce(S) -> BaseResponse diff --git a/crates/tosca-os/src/responses/stream.rs b/crates/tosca-os/src/responses/stream.rs index 139eaafa..d488ddfb 100644 --- a/crates/tosca-os/src/responses/stream.rs +++ b/crates/tosca-os/src/responses/stream.rs @@ -16,7 +16,7 @@ use futures_core::TryStream; use tokio::io::AsyncRead; use tokio_util::io::ReaderStream; -use super::{BaseResponse, MandatoryResponse, error::ErrorResponse}; +use super::{BaseResponse, error::ErrorResponse}; /// A response that transmits a stream of data as a sequence of bytes /// over the network. @@ -105,26 +105,6 @@ macro_rules! impl_empty_type_name { } super::all_the_tuples!(impl_empty_type_name); -/// Creates a stateful [`MandatoryResponse`] from a [`StreamResponse`]. -#[inline] -pub fn mandatory_stream_stateful( - handler: H, -) -> impl FnOnce(Route, S) -> MandatoryResponse -where - H: Handler + private::StreamTypeName, - T: 'static, - S: Clone + Send + Sync + 'static, -{ - move |route: Route, state: S| { - MandatoryResponse::new(BaseResponse::stateful( - route, - ResponseKind::Stream, - handler, - state, - )) - } -} - /// Creates a stateful [`BaseResponse`] from a [`StreamResponse`]. #[inline] pub fn stream_stateful(route: Route, handler: H) -> impl FnOnce(S) -> BaseResponse @@ -136,25 +116,6 @@ where move |state: S| BaseResponse::stateful(route, ResponseKind::Stream, handler, state) } -/// Creates a stateless [`MandatoryResponse`] from a [`StreamResponse`]. -#[inline] -pub fn mandatory_stream_stateless( - handler: H, -) -> impl FnOnce(Route, S) -> MandatoryResponse -where - H: Handler + private::StreamTypeName, - T: 'static, - S: Clone + Send + Sync + 'static, -{ - move |route: Route, _state: S| { - MandatoryResponse::new(BaseResponse::stateless( - route, - ResponseKind::Stream, - handler, - )) - } -} - /// Creates a stateless [`BaseResponse`] from a [`StreamResponse`]. #[inline] pub fn stream_stateless(route: Route, handler: H) -> impl FnOnce(S) -> BaseResponse diff --git a/crates/tosca-os/src/server.rs b/crates/tosca-os/src/server.rs index 7046fe44..1bd1d48b 100644 --- a/crates/tosca-os/src/server.rs +++ b/crates/tosca-os/src/server.rs @@ -5,7 +5,7 @@ use axum::{Router, response::Redirect}; use tracing::info; -use crate::device::Device; +use crate::device::DeviceVerified; use crate::error::Result; use crate::services::{Service, ServiceConfig}; @@ -29,10 +29,7 @@ const DEFAULT_SCHEME: &str = "http"; const DEFAULT_WELL_KNOWN_SERVICE: &str = "tosca"; #[derive(Debug)] -struct ServerData<'a, S> -where - S: Clone + Send + Sync + 'static, -{ +struct ServerData<'a> { // HTTP address. http_address: Ipv4Addr, // Server port. @@ -44,24 +41,19 @@ where // Service configurator. service_config: Option>, // Device. - device: Device, + device: DeviceVerified, } /// A server running indefinitely on the firmware. #[derive(Debug)] -pub struct Server<'a, S = ()> -where - S: Clone + Send + Sync + 'static, -{ - data: ServerData<'a, S>, +pub struct Server<'a> { + data: ServerData<'a>, } -impl<'a, S> Server<'a, S> -where - S: Clone + Send + Sync + 'static, -{ - /// Creates a [`Server`] from the given [`Device`]. - pub const fn new(device: Device) -> Self { +impl<'a> Server<'a> { + /// Creates a [`Server`] from the given [`DeviceVerified`]. + #[must_use] + pub const fn new(device: DeviceVerified) -> Self { Self { data: ServerData { http_address: DEFAULT_HTTP_ADDRESS, @@ -116,7 +108,7 @@ where /// the server. #[must_use] #[inline] - pub fn with_graceful_shutdown(self, signal: F) -> GracefulShutdownServer<'a, S, F> + pub fn with_graceful_shutdown(self, signal: F) -> GracefulShutdownServer<'a, F> where F: Future + Send + 'static, { @@ -143,19 +135,15 @@ where /// Aside from the graceful shutdown functionality, it behaves the same as /// [`Server`]. #[derive(Debug)] -pub struct GracefulShutdownServer<'a, S, F> -where - S: Clone + Send + Sync + 'static, -{ +pub struct GracefulShutdownServer<'a, F> { // Server data. - data: ServerData<'a, S>, + data: ServerData<'a>, // Graceful shutdown signal. signal: F, } -impl GracefulShutdownServer<'_, S, F> +impl GracefulShutdownServer<'_, F> where - S: Clone + Send + Sync + 'static, F: Future + Send + 'static, { /// Runs the server with graceful shutdown. diff --git a/crates/tosca/src/device.rs b/crates/tosca/src/device.rs index d8aa54b1..5eec47c4 100644 --- a/crates/tosca/src/device.rs +++ b/crates/tosca/src/device.rs @@ -1,106 +1,200 @@ +use alloc::borrow::Cow; + use serde::Serialize; use crate::economy::Economy; use crate::energy::Energy; use crate::events::EventsDescription; +use crate::hazards::Hazard; use crate::route::RouteConfigs; -/// Trait for device kind types. -/// -/// Firmware authors implement this on their own enum. -/// -/// # Example -/// -/// ```rust -/// use tosca::device::DeviceKindTrait; -/// -/// #[derive(Debug, Clone, Copy, PartialEq)] -/// enum MyDeviceKind { -/// Relay, -/// MotorController, -/// } -/// -/// impl DeviceKindTrait for MyDeviceKind { -/// fn name(&self) -> &'static str { -/// match self { -/// Self::Relay => "Relay", -/// Self::MotorController => "MotorController", -/// } -/// } -/// } -/// ``` -pub trait DeviceKindTrait { - /// Returns the display name of this device kind. - fn name(&self) -> &'static str; -} - /// A device kind. -#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub enum DeviceKind { - /// Unknown. - Unknown, /// Light. Light, -} - -impl DeviceKindTrait for DeviceKind { - fn name(&self) -> &'static str { - match self { - Self::Unknown => "Unknown", - Self::Light => "Light", - } - } + /// Custom. + Custom(Cow<'static, str>), } impl core::fmt::Display for DeviceKind { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", self.name()) + f.write_str(match self { + Self::Light => "Light", + Self::Custom(kind) => kind, + }) } } -/// A string-backed device kind identifier. +/// A device scheme. /// -/// Used in [`DeviceData`] for serialization over the wire. The controller -/// deserializes into this type, so it can handle any device kind including -/// ones it has never seen before. +/// A scheme consists of: /// -/// On the firmware side, this is constructed automatically from any type -/// that implements [`DeviceKind`] via the [`From`] impl. +/// - A device kind +/// - A list of all mandatory route names +/// - A list of hazards allowed for the device #[derive(Debug, Clone, PartialEq, Serialize)] -#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] -#[serde(transparent)] -pub struct DeviceKindId(alloc::borrow::Cow<'static, str>); +pub struct DeviceScheme { + pub(super) kind: DeviceKind, + pub(super) mandatory_routes: &'static [&'static str], + pub(super) allowed_hazards: &'static [Hazard], +} + +impl DeviceScheme { + /// Creates a base custom scheme with a device kind only. + #[must_use] + #[inline] + pub fn base_custom_scheme(custom_kind: &'static str) -> Self { + Self::new(DeviceKind::Custom(Cow::Borrowed(custom_kind)), &[], &[]) + } + + /// Creates a custom scheme with device kind and mandatory routes. + #[must_use] + #[inline] + pub fn custom_scheme_with_mandatory_routes( + custom_kind: &'static str, + mandatory_routes: &'static [&'static str], + ) -> Self { + Self::new( + DeviceKind::Custom(Cow::Borrowed(custom_kind)), + mandatory_routes, + &[], + ) + } -impl DeviceKindId { - /// Creates a new [`DeviceKindId`] from a static string. + /// Creates a custom scheme with device kind and allowed hazards. #[must_use] - pub const fn new(name: &'static str) -> Self { - Self(alloc::borrow::Cow::Borrowed(name)) + #[inline] + pub fn custom_scheme_with_allowed_hazards( + custom_kind: &'static str, + allowed_hazards: &'static [Hazard], + ) -> Self { + Self::new( + DeviceKind::Custom(Cow::Borrowed(custom_kind)), + &[], + allowed_hazards, + ) } - /// Returns the device kind name. + /// Creates a complete custom scheme. #[must_use] - pub fn name(&self) -> &str { - &self.0 + #[inline] + pub fn custom_scheme( + custom_kind: &'static str, + mandatory_routes: &'static [&'static str], + allowed_hazards: &'static [Hazard], + ) -> Self { + Self::new( + DeviceKind::Custom(Cow::Borrowed(custom_kind)), + mandatory_routes, + allowed_hazards, + ) } - /// Checks if this ID matches a known [`DeviceKind`] value. + /// Returns an immutable reference to [`DeviceKind`]. #[must_use] - pub fn matches(&self, kind: &K) -> bool { - self.0 == kind.name() + pub const fn kind(&self) -> &DeviceKind { + &self.kind + } + + /// Returns an immutable reference to mandatory routes. + #[must_use] + pub const fn mandatory_routes(&self) -> &'static [&'static str] { + self.mandatory_routes + } + + /// Returns an immutable reference to allowed hazards. + #[must_use] + pub const fn allowed_hazards(&self) -> &'static [Hazard] { + self.allowed_hazards + } + + const fn new( + kind: DeviceKind, + mandatory_routes: &'static [&'static str], + allowed_hazards: &'static [Hazard], + ) -> Self { + Self { + kind, + mandatory_routes, + allowed_hazards, + } } } -impl core::fmt::Display for DeviceKindId { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", self.0) +/// A series of device schemes. +pub mod schemes { + use crate::hazards::Hazard; + + use super::{DeviceKind, DeviceScheme}; + + /// A light scheme. + pub const LIGHT_SCHEME: DeviceScheme = DeviceScheme::new( + DeviceKind::Light, + &["/on", "/off"], + &[ + Hazard::FireHazard, + Hazard::ElectricEnergyConsumption, + Hazard::LogEnergyConsumption, + Hazard::LogUsageTime, + Hazard::PowerOutage, + Hazard::PowerSurge, + ], + ); +} + +/// An owned device scheme. +/// +/// A scheme consists of: +/// +/// - A device kind +/// - A list of all mandatory route names +/// - A list of hazards allowed for the device +#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)] +#[cfg(feature = "deserialize")] +pub struct DeviceSchemeOwned { + kind: DeviceKind, + mandatory_routes: alloc::vec::Vec, + allowed_hazards: alloc::vec::Vec, +} + +#[cfg(feature = "deserialize")] +impl From for DeviceSchemeOwned { + fn from(device_scheme: DeviceScheme) -> Self { + Self { + kind: device_scheme.kind, + mandatory_routes: device_scheme + .mandatory_routes + .iter() + .copied() + .map(alloc::string::String::from) + .collect(), + allowed_hazards: device_scheme.allowed_hazards.to_vec(), + } } } -impl From<&K> for DeviceKindId { - fn from(kind: &K) -> Self { - Self::new(kind.name()) +#[cfg(feature = "deserialize")] +impl DeviceSchemeOwned { + /// Returns an immutable reference to [`DeviceKind`]. + #[must_use] + pub const fn kind(&self) -> &DeviceKind { + &self.kind + } + + /// Returns an immutable reference to mandatory routes. + #[must_use] + #[inline] + pub fn mandatory_routes(&self) -> &[alloc::string::String] { + &self.mandatory_routes + } + + /// Returns an immutable reference to allowed hazards. + #[must_use] + #[inline] + pub fn allowed_hazards(&self) -> &[Hazard] { + &self.allowed_hazards } } @@ -119,7 +213,7 @@ pub enum DeviceEnvironment { } /// Device metrics. -#[derive(Debug, PartialEq, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct DeviceMetrics { /// Energy metrics. @@ -171,15 +265,19 @@ impl DeviceMetrics { } /// Device data. -#[derive(Debug, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct DeviceData { - /// Device kind. - pub kind: DeviceKindId, + /// Device scheme. + #[cfg(not(feature = "deserialize"))] + pub scheme: DeviceScheme, + /// Device scheme. + #[cfg(feature = "deserialize")] + pub scheme: DeviceSchemeOwned, /// Device environment. pub environment: DeviceEnvironment, /// Device description. - pub description: Option>, + pub description: Option>, /// Wi-Fi MAC address. #[serde(skip_serializing_if = "Option::is_none")] pub wifi_mac: Option<[u8; 6]>, @@ -190,9 +288,12 @@ pub struct DeviceData { impl DeviceData { #[inline] - fn new(kind: DeviceKindId) -> Self { + fn new(scheme: DeviceScheme) -> Self { Self { - kind, + #[cfg(not(feature = "deserialize"))] + scheme, + #[cfg(feature = "deserialize")] + scheme: DeviceSchemeOwned::from(scheme), environment: DeviceEnvironment::Embedded, description: None, wifi_mac: None, @@ -202,18 +303,16 @@ impl DeviceData { } /// Device description. -#[derive(Debug, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct DeviceDescription { /// Device data. #[serde(flatten)] pub data: DeviceData, /// Device main route. - pub main_route: alloc::borrow::Cow<'static, str>, + pub main_route: Cow<'static, str>, /// All device route configurations. pub route_configs: RouteConfigs, - /// Number of mandatory routes. - pub mandatory_routes: u8, /// Events description. #[serde(skip_serializing_if = "Option::is_none")] pub events_description: Option, @@ -224,16 +323,14 @@ impl DeviceDescription { #[inline] #[must_use] pub fn new( - kind: DeviceKindId, - main_route: impl Into>, + scheme: DeviceScheme, + main_route: &'static str, route_configs: RouteConfigs, - mandatory_routes: u8, ) -> Self { Self { - data: DeviceData::new(kind), - main_route: main_route.into(), + data: DeviceData::new(scheme), + main_route: Cow::Borrowed(main_route), route_configs, - mandatory_routes, events_description: None, } } @@ -262,11 +359,8 @@ impl DeviceDescription { /// Sets the device text description. #[inline] #[must_use] - pub fn text_description( - mut self, - description: impl Into>, - ) -> Self { - self.data.description = Some(description.into()); + pub fn text_description(mut self, description: &'static str) -> Self { + self.data.description = Some(Cow::Borrowed(description)); self } @@ -279,9 +373,108 @@ impl DeviceDescription { } } +#[cfg(test)] +#[cfg(not(feature = "deserialize"))] +mod tests { + use alloc::borrow::Cow; + + use crate::hazards::Hazard; + + use super::{DeviceKind, DeviceScheme, schemes::LIGHT_SCHEME}; + + #[test] + fn test_default_schemes() { + assert_eq!( + LIGHT_SCHEME, + DeviceScheme::new( + DeviceKind::Light, + &["/on", "/off"], + &[ + Hazard::FireHazard, + Hazard::ElectricEnergyConsumption, + Hazard::LogEnergyConsumption, + Hazard::LogUsageTime, + Hazard::PowerOutage, + Hazard::PowerSurge, + ] + ) + ); + } + + #[test] + fn test_base_custom_scheme() { + assert_eq!( + DeviceScheme::base_custom_scheme("Thermostat"), + DeviceScheme::new(DeviceKind::Custom(Cow::Borrowed("Thermostat")), &[], &[]) + ); + } + + #[test] + fn test_custom_scheme_with_mandatory_routes() { + assert_eq!( + DeviceScheme::custom_scheme_with_mandatory_routes("Thermostat", &["/on", "/off"]), + DeviceScheme::new( + DeviceKind::Custom(Cow::Borrowed("Thermostat")), + &["/on", "/off"], + &[] + ) + ); + } + + #[test] + fn test_custom_scheme_with_allowed_hazards() { + assert_eq!( + DeviceScheme::custom_scheme_with_allowed_hazards( + "Thermostat", + &[Hazard::ElectricEnergyConsumption] + ), + DeviceScheme::new( + DeviceKind::Custom(Cow::Borrowed("Thermostat")), + &[], + &[Hazard::ElectricEnergyConsumption] + ) + ); + } + + #[test] + fn test_custom_scheme() { + assert_eq!( + DeviceScheme::custom_scheme( + "Thermostat", + &["/on", "/off"], + &[Hazard::ElectricEnergyConsumption] + ), + DeviceScheme::new( + DeviceKind::Custom(Cow::Borrowed("Thermostat")), + &["/on", "/off"], + &[Hazard::ElectricEnergyConsumption] + ) + ); + } + + #[test] + fn test_getter_methods() { + assert_eq!(LIGHT_SCHEME.kind, DeviceKind::Light); + assert_eq!(LIGHT_SCHEME.mandatory_routes(), &["/on", "/off"]); + assert_eq!( + LIGHT_SCHEME.allowed_hazards(), + &[ + Hazard::FireHazard, + Hazard::ElectricEnergyConsumption, + Hazard::LogEnergyConsumption, + Hazard::LogUsageTime, + Hazard::PowerOutage, + Hazard::PowerSurge, + ] + ); + } +} + #[cfg(test)] #[cfg(feature = "deserialize")] mod tests { + use alloc::borrow::Cow; + use crate::route::{Route, RouteConfigs}; use crate::economy::{Cost, CostTimespan, Costs, Economy, Roi, Rois}; @@ -289,9 +482,12 @@ mod tests { CarbonFootprint, CarbonFootprints, Energy, EnergyClass, EnergyEfficiencies, EnergyEfficiency, WaterUseEfficiency, }; + use crate::{deserialize, serialize}; - use super::{DeviceDescription, DeviceEnvironment, DeviceKind, DeviceKindId, DeviceMetrics}; + use super::{ + DeviceDescription, DeviceEnvironment, DeviceKind, DeviceMetrics, schemes::LIGHT_SCHEME, + }; fn energy() -> Energy { let energy_efficiencies = @@ -326,7 +522,10 @@ mod tests { #[test] fn test_device_kind() { - for device_kind in &[DeviceKind::Unknown, DeviceKind::Light] { + for device_kind in &[ + DeviceKind::Light, + DeviceKind::Custom(Cow::Borrowed("Thermostat")), + ] { assert_eq!( deserialize::(serialize(device_kind)), *device_kind @@ -346,30 +545,25 @@ mod tests { #[test] fn test_device_metrics() { - let device_metrics = DeviceMetrics::with_energy(energy()).add_economy(economy()); + let energy_economy_metrics = DeviceMetrics::with_energy(energy()).add_economy(economy()); assert_eq!( - deserialize::(serialize(&device_metrics)), - device_metrics + deserialize::(serialize(&energy_economy_metrics)), + energy_economy_metrics ); - let device_metrics = DeviceMetrics::with_economy(economy()).add_energy(energy()); + let economy_energy_metrics = DeviceMetrics::with_economy(economy()).add_energy(energy()); assert_eq!( - deserialize::(serialize(&device_metrics)), - device_metrics + deserialize::(serialize(&economy_energy_metrics)), + economy_energy_metrics ); } #[test] fn test_device_description() { - let device_description = DeviceDescription::new( - DeviceKindId::from(&DeviceKind::Light), - "/light", - routes(), - 2, - ) - .text_description("A light device."); + let device_description = DeviceDescription::new(LIGHT_SCHEME, "/light", routes()) + .text_description("A light device."); assert_eq!( deserialize::(serialize(&device_description)), diff --git a/crates/tosca/src/economy.rs b/crates/tosca/src/economy.rs index 2e7c8e82..e6661f38 100644 --- a/crates/tosca/src/economy.rs +++ b/crates/tosca/src/economy.rs @@ -8,7 +8,7 @@ use crate::energy::EnergyClass; use crate::macros::set; /// Timespan selected to estimate the device costs. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub enum CostTimespan { /// Week. @@ -36,7 +36,7 @@ impl core::fmt::Display for CostTimespan { } /// Device cost. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct Cost { /// Amount of money in USD currency. @@ -84,7 +84,7 @@ set! { } /// Return on investments (ROI). -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct Roi { /// Number of years used to calculate the ROI. @@ -136,7 +136,7 @@ set! { } /// Economy data related to a device. -#[derive(Debug, PartialEq, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct Economy { /// Costs. diff --git a/crates/tosca/src/energy.rs b/crates/tosca/src/energy.rs index 6dafbb91..ba4a17d5 100644 --- a/crates/tosca/src/energy.rs +++ b/crates/tosca/src/energy.rs @@ -7,7 +7,7 @@ use serde::Serialize; use crate::macros::set; /// Energy efficiency class. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub enum EnergyClass { /// A+++ @@ -63,7 +63,7 @@ const fn decimal_percentage(percentage: i8) -> f64 { } /// Energy efficiency. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct EnergyEfficiency { /// Represents the energy efficiency of an [`EnergyClass`] as a percentage. @@ -124,7 +124,7 @@ set! { } /// Carbon footprint. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct CarbonFootprint { /// Represents the amount of greenhouse gases emitted into the atmosphere @@ -189,7 +189,7 @@ set! { /// /// Metrics taken from: /// -#[derive(Debug, PartialEq, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct WaterUseEfficiency { /// Gross Primary Productivity (GPP). @@ -265,7 +265,7 @@ impl WaterUseEfficiency { } /// Energy information of a device. -#[derive(Debug, PartialEq, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct Energy { /// Energy efficiencies. diff --git a/crates/tosca/src/events.rs b/crates/tosca/src/events.rs index 11ca713c..2146de0c 100644 --- a/crates/tosca/src/events.rs +++ b/crates/tosca/src/events.rs @@ -8,7 +8,7 @@ use core::time::Duration; use serde::Serialize; /// Event broker data. -#[derive(Debug, PartialEq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct BrokerData { /// Broker address. @@ -33,10 +33,10 @@ mod private { } } +/// An event of a specific type. #[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(not(feature = "deserialize"), derive(Copy))] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] -/// An event of a specific type. pub struct Event { /// Event name. #[cfg(not(feature = "deserialize"))] @@ -206,13 +206,13 @@ impl Event { } } -#[derive(Debug, Clone, PartialEq, Serialize)] -#[cfg_attr(not(feature = "deserialize"), derive(Copy))] -#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] /// A periodic [`Event`]. /// /// An event is considered periodic when it is triggered or checked at regular, /// fixed intervals of time. +#[derive(Debug, Clone, PartialEq, Serialize)] +#[cfg_attr(not(feature = "deserialize"), derive(Copy))] +#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct PeriodicEvent { /// The [`Event`]. pub event: Event, @@ -272,12 +272,12 @@ impl PeriodicEvent { } } -#[derive(Debug, Clone, PartialEq, Serialize)] -#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] /// The topic for event publication over the network. /// /// This topic uniquely identifies all events coming from a device, allowing /// controllers to retrieve all related event data using it as a reference. +#[derive(Debug, Clone, PartialEq, Serialize)] +#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct Topic(String); impl Topic { @@ -301,12 +301,12 @@ impl Topic { } } -#[derive(Debug, Clone, PartialEq, Serialize)] -#[allow(clippy::struct_field_names)] -#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] /// All events types that can be generated by a device. /// /// Events of the same type are stored and displayed sequentially. +#[derive(Debug, Clone, PartialEq, Serialize)] +#[allow(clippy::struct_field_names)] +#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct Events { #[serde(skip_serializing_if = "Vec::is_empty", default)] bool_events: Vec>, @@ -719,10 +719,10 @@ impl Events { } } -#[derive(Debug, PartialEq, Serialize)] -#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] /// All events to be published over the network, including their associated /// topic and broker data. +#[derive(Debug, Clone, PartialEq, Serialize)] +#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub struct EventsDescription { /// Broker data. pub broker_data: BrokerData, @@ -851,7 +851,7 @@ mod tests { fn test_events_description() { let broker_data = BrokerData::new(Ipv4Addr::LOCALHOST.into(), 80); assert_eq!( - deserialize::(serialize(&broker_data)), + deserialize::(serialize(broker_data)), broker_data ); diff --git a/crates/tosca/src/hazards.rs b/crates/tosca/src/hazards.rs index 6b933a00..9655d521 100644 --- a/crates/tosca/src/hazards.rs +++ b/crates/tosca/src/hazards.rs @@ -2,7 +2,7 @@ use hashbrown::DefaultHashBuilder; use indexmap::set::{IndexSet, IntoIter, Iter}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use crate::macros::set; @@ -363,7 +363,8 @@ pub struct HazardData { pub const ALL_CATEGORIES: &[Category] = &[Category::Safety, Category::Privacy, Category::Financial]; /// Hazard categories. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize)] +#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] pub enum Category { /// Category including all financial-related hazards. Financial, @@ -447,6 +448,8 @@ impl Category { #[cfg(test)] #[cfg(feature = "deserialize")] mod tests { + use serde_json::json; + use crate::{deserialize, serialize}; use super::{ALL_CATEGORIES, ALL_HAZARDS, Category, Hazard}; @@ -461,7 +464,7 @@ mod tests { assert_eq!(Hazard::from_id(hazard.id()), Some(*hazard)); assert_eq!( serialize(hazard.data()), - serde_json::json!({ + json!({ "id": hazard.id(), "name": hazard.name(), "description": hazard.description(), diff --git a/crates/tosca/src/lib.rs b/crates/tosca/src/lib.rs index e9df31cb..71495ceb 100644 --- a/crates/tosca/src/lib.rs +++ b/crates/tosca/src/lib.rs @@ -47,12 +47,14 @@ pub mod route; #[cfg(test)] #[cfg(feature = "deserialize")] +#[inline] pub(crate) fn serialize(value: T) -> serde_json::Value { serde_json::to_value(value).unwrap() } #[cfg(test)] #[cfg(feature = "deserialize")] +#[inline] pub(crate) fn deserialize(value: serde_json::Value) -> T { serde_json::from_value(value).unwrap() } diff --git a/crates/tosca/src/macros/mandatory_routes.rs b/crates/tosca/src/macros/mandatory_routes.rs deleted file mode 100644 index 86fa0458..00000000 --- a/crates/tosca/src/macros/mandatory_routes.rs +++ /dev/null @@ -1,213 +0,0 @@ -/// Defines a mandatory route type with a fixed path and allowed HTTP methods. -/// -/// Mandatory routes enforce that certain routes must be registered on a device -/// before it can be built. This provides compile-time safety for device -/// construction. -/// -/// # Example -/// -/// ```rust,ignore -/// use tosca::mandatory_route; -/// -/// mandatory_route!(MyRoute, "/my-path", methods: [put, get]); -/// ``` -#[macro_export] -macro_rules! mandatory_route { - ( - $name:ident, - $path:expr, - methods: [$($method:ident),* $(,)?] - ) => { - #[doc = concat!("A mandatory [`", stringify!($name), "`].")] - #[derive(Debug)] - pub struct $name { - route: $crate::route::Route, - } - - impl $name { - $( - $crate::mandatory_route!(@method_fn $method, $name, $path); - )* - - #[doc = "Sets the route description."] - #[must_use] - pub fn description(mut self, description: &'static str) -> Self { - self.route = self.route.description(description); - self - } - - #[doc = "Changes the route name."] - #[must_use] - pub fn change_name(mut self, name: &'static str) -> Self { - self.route = self.route.change_name(name); - self - } - - #[doc = concat!("Adds [`Hazards`] to a [`", stringify!($name), "`].")] - #[must_use] - #[inline] - pub fn with_hazards(mut self, hazards: $crate::hazards::Hazards) -> Self { - self.route = self.route.with_hazards(hazards); - self - } - - #[doc = concat!("Adds an [`Hazard`] to a [`", stringify!($name), "`].")] - #[must_use] - #[inline] - pub fn with_hazard(mut self, hazard: $crate::hazards::Hazard) -> Self { - self.route = self.route.with_hazard(hazard); - self - } - - #[doc = concat!("Adds an array of [`Hazard`]s to a [`", stringify!($name), "`].")] - #[must_use] - #[inline] - pub fn with_array_of_hazards(mut self, hazards: [$crate::hazards::Hazard; N]) -> Self { - self.route = self.route.with_array_of_hazards(hazards); - self - } - - #[doc = concat!("Adds [`Parameters`] to a [`", stringify!($name), "`].")] - #[must_use] - #[inline] - pub fn with_parameters(mut self, parameters: $crate::parameters::Parameters) -> Self { - self.route = self.route.with_parameters(parameters); - self - } - - #[doc = "Returns the route path."] - #[must_use] - pub fn route(&self ) -> &str { - self.route.route() - } - - #[doc = concat!("Returns the [`RestKind`].")] - #[must_use] - pub const fn kind(&self) -> $crate::route::RestKind { - self.route.kind() - } - - #[doc = concat!("Returns all route [`Hazards`].")] - #[must_use] - pub const fn hazards(&self) -> &$crate::hazards::Hazards { - self.route.hazards() - } - - #[doc = concat!("Returns all route [`Parameters`].")] - #[must_use] - pub const fn parameters(&self) -> &$crate::parameters::Parameters { - self.route.parameters() - } - - #[doc = "Returns the internal [`Route`]."] - #[must_use] - pub fn into_route(self) -> $crate::route::Route { - self.route - } - } - }; - - (@method_fn get, $name:ident, $path:expr) => { - #[doc = concat!("Creates a [`", stringify!($name), "`] through a `GET` API.")] - #[must_use] - #[inline] - pub fn get(name: &'static str) -> Self { - Self { - route: $crate::route::Route::get(name, $path), - } - } - }; - - (@method_fn put, $name:ident, $path:expr) => { - #[doc = concat!("Creates a [`", stringify!($name), "`] through a `PUT` API.")] - #[must_use] - #[inline] - pub fn put(name: &'static str) -> Self { - Self { - route: $crate::route::Route::put(name, $path), - } - } - }; - - (@method_fn post, $name:ident, $path:expr) => { - #[doc = concat!("Creates a [`", stringify!($name), "`] through a `POST` API.")] - #[must_use] - #[inline] - pub fn post(name: &'static str) -> Self { - Self { - route: $crate::route::Route::post(name, $path), - } - } - }; - - (@method_fn delete, $name:ident, $path:expr) => { - #[doc = concat!("Creates a [`", stringify!($name), "`] through a `DELETE` API.")] - #[must_use] - #[inline] - pub fn delete(name: &'static str) -> Self { - Self { - route: $crate::route::Route::delete(name, $path), - } - } - }; -} - -#[cfg(test)] -mod tests { - use crate::hazards::{Hazard, Hazards}; - use crate::parameters::Parameters; - use crate::route::RestKind; - - mandatory_route!(TestRoute, "/test", methods: [get, put, post, delete]); - - #[test] - fn test_mandatory_route_constructors() { - let get = TestRoute::get("Get"); - assert_eq!(get.route(), "/test"); - assert_eq!(get.kind(), RestKind::Get); - - let put = TestRoute::put("Put"); - assert_eq!(put.kind(), RestKind::Put); - - let post = TestRoute::post("Post"); - assert_eq!(post.kind(), RestKind::Post); - - let delete = TestRoute::delete("Delete"); - assert_eq!(delete.kind(), RestKind::Delete); - } - - #[test] - fn test_mandatory_route_builder_chain() { - let route = TestRoute::put("On") - .description("Turn on.") - .with_hazard(Hazard::FireHazard) - .with_parameters(Parameters::new().bool("enabled", false)); - - assert_eq!(route.route(), "/test"); - assert_eq!(route.kind(), RestKind::Put); - assert!(!route.hazards().is_empty()); - assert!(!route.parameters().is_empty()); - } - - #[test] - fn test_mandatory_route_into_route() { - let route = TestRoute::get("Toggle").description("Toggle.").into_route(); - - assert_eq!(route.route(), "/test"); - assert_eq!(route.kind(), RestKind::Get); - } - - // FIXME: This test should be deleted or improved. Only used to remove a - // warning for now. - #[test] - fn test_mandatory_route_unused_methods() { - let route = TestRoute::get("On") - .change_name("OnGet") - .with_hazards(Hazards::new().insert(Hazard::FireHazard)) - .with_array_of_hazards([Hazard::FireHazard; 1]); - - assert_eq!(route.route(), "/test"); - assert!(!route.hazards().is_empty()); - assert!(route.parameters().is_empty()); - } -} diff --git a/crates/tosca/src/macros/mod.rs b/crates/tosca/src/macros/mod.rs index f6d45249..3da5ad47 100644 --- a/crates/tosca/src/macros/mod.rs +++ b/crates/tosca/src/macros/mod.rs @@ -1,4 +1,3 @@ -mod mandatory_routes; mod map; mod set; diff --git a/crates/tosca/src/parameters.rs b/crates/tosca/src/parameters.rs index c42d20e2..66a6c7ae 100644 --- a/crates/tosca/src/parameters.rs +++ b/crates/tosca/src/parameters.rs @@ -2,7 +2,7 @@ #![allow(clippy::trivially_copy_pass_by_ref)] use alloc::borrow::Cow; -use alloc::string::String; +use alloc::string::{String, ToString}; use hashbrown::DefaultHashBuilder; @@ -116,11 +116,11 @@ pub enum ParameterKind { /// when the parameter is missing. default: u8, /// The minimum allowed [`u8`] value. - #[serde(skip_serializing_if = "is_u8_max")] + #[serde(skip_serializing_if = "is_u8_min")] #[serde(default)] min: u8, /// The maximum allowed [`u8`] value. - #[serde(skip_serializing_if = "is_u8_min")] + #[serde(skip_serializing_if = "is_u8_max")] #[serde(default = "u8_max")] max: u8, }, @@ -130,11 +130,11 @@ pub enum ParameterKind { /// when the parameter is missing. default: u16, /// The minimum allowed [`u16`] value. - #[serde(skip_serializing_if = "is_u16_max")] + #[serde(skip_serializing_if = "is_u16_min")] #[serde(default)] min: u16, /// The maximum allowed [`u16`] value. - #[serde(skip_serializing_if = "is_u16_min")] + #[serde(skip_serializing_if = "is_u16_max")] #[serde(default = "u16_max")] max: u16, }, @@ -144,11 +144,11 @@ pub enum ParameterKind { /// when the parameter is missing. default: u32, /// The minimum allowed [`u32`] value. - #[serde(skip_serializing_if = "is_u32_max")] + #[serde(skip_serializing_if = "is_u32_min")] #[serde(default)] min: u32, /// The maximum allowed [`u32`] value. - #[serde(skip_serializing_if = "is_u32_min")] + #[serde(skip_serializing_if = "is_u32_max")] #[serde(default = "u32_max")] max: u32, }, @@ -158,11 +158,11 @@ pub enum ParameterKind { /// when the parameter is missing. default: u64, /// The minimum allowed [`u64`] value. - #[serde(skip_serializing_if = "is_u64_max")] + #[serde(skip_serializing_if = "is_u64_min")] #[serde(default)] min: u64, /// The maximum allowed [`u64`] value. - #[serde(skip_serializing_if = "is_u64_min")] + #[serde(skip_serializing_if = "is_u64_max")] #[serde(default = "u64_max")] max: u64, }, @@ -172,11 +172,11 @@ pub enum ParameterKind { /// when the parameter is missing. default: f32, /// The minimum allowed [`f32`] value. - #[serde(skip_serializing_if = "is_f32_max")] + #[serde(skip_serializing_if = "is_f32_min")] #[serde(default = "f32_min")] min: f32, /// The maximum allowed [`f32`] value. - #[serde(skip_serializing_if = "is_f32_min")] + #[serde(skip_serializing_if = "is_f32_max")] #[serde(default = "f32_max")] max: f32, /// The decimal step for the [`f32`] value. @@ -190,11 +190,11 @@ pub enum ParameterKind { /// when the parameter is missing. default: f64, /// The minimum allowed [`f64`] value. - #[serde(skip_serializing_if = "is_f64_max")] + #[serde(skip_serializing_if = "is_f64_min")] #[serde(default = "f64_min")] min: f64, /// The maximum allowed [`f64`] value. - #[serde(skip_serializing_if = "is_f64_min")] + #[serde(skip_serializing_if = "is_f64_max")] #[serde(default = "f64_max")] max: f64, /// The decimal step for the [`f64`] value. @@ -317,7 +317,7 @@ map! { /// corresponding [`ParameterKind`]. #[derive(Debug, Clone, PartialEq, Serialize)] #[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] - pub struct ParametersData(IndexMap); + pub struct ParametersData(IndexMap, ParameterKind, DefaultHashBuilder>); } impl ParametersData { @@ -363,8 +363,8 @@ impl Parameters { name, ParameterKind::U8 { default, - min: u8::MAX, - max: u8::MIN, + min: u8::MIN, + max: u8::MAX, }, ) } @@ -384,8 +384,8 @@ impl Parameters { name, ParameterKind::U16 { default, - min: u16::MAX, - max: u16::MIN, + min: u16::MIN, + max: u16::MAX, }, ) } @@ -405,8 +405,8 @@ impl Parameters { name, ParameterKind::U32 { default, - min: u32::MAX, - max: u32::MIN, + min: u32::MIN, + max: u32::MAX, }, ) } @@ -426,8 +426,8 @@ impl Parameters { name, ParameterKind::U64 { default, - min: u64::MAX, - max: u64::MIN, + min: u64::MIN, + max: u64::MAX, }, ) } @@ -447,8 +447,8 @@ impl Parameters { name, ParameterKind::F32 { default, - min: f32::MAX, - max: f32::MIN, + min: f32::MIN, + max: f32::MAX, step: 0., }, ) @@ -597,15 +597,11 @@ impl Parameters { /// Adds a sequence of characters. #[must_use] #[inline] - pub fn characters_sequence( - self, - name: &'static str, - default: impl Into>, - ) -> Self { + pub fn characters_sequence(self, name: &'static str, default: &'static str) -> Self { self.create_parameter( name, ParameterKind::CharsSequence { - default: default.into(), + default: Cow::Borrowed(default), }, ) } @@ -663,7 +659,7 @@ pub enum ParameterValue { /// A [`f64`] value. F64(f64), /// A sequence of characters. - CharsSequence(Cow<'static, str>), + CharsSequence(String), } impl core::fmt::Display for ParameterValue { @@ -700,7 +696,9 @@ impl ParameterValue { ParameterKind::F64 { default, .. } | ParameterKind::RangeF64 { default, .. } => { Self::F64(*default) } - ParameterKind::CharsSequence { default, .. } => Self::CharsSequence(default.clone()), + ParameterKind::CharsSequence { default, .. } => { + Self::CharsSequence(default.to_string()) + } } } @@ -748,33 +746,33 @@ impl ParameterValue { /// A map associating each parameter name with its /// corresponding [`ParameterValue`]. #[derive(Debug, PartialEq, Deserialize)] -pub struct ParametersValues<'a>(IndexMap, ParameterValue, DefaultHashBuilder>); +pub struct ParametersValues(IndexMap); -impl Default for ParametersValues<'_> { +impl Default for ParametersValues { fn default() -> Self { Self::new() } } -impl<'a> IntoIterator for ParametersValues<'a> { - type Item = (Cow<'a, str>, ParameterValue); - type IntoIter = IntoIter, ParameterValue>; +impl IntoIterator for ParametersValues { + type Item = (String, ParameterValue); + type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } -impl<'a> IntoIterator for &'a ParametersValues<'a> { - type Item = (&'a Cow<'a, str>, &'a ParameterValue); - type IntoIter = Iter<'a, Cow<'a, str>, ParameterValue>; +impl<'a> IntoIterator for &'a ParametersValues { + type Item = (&'a String, &'a ParameterValue); + type IntoIter = Iter<'a, String, ParameterValue>; fn into_iter(self) -> Self::IntoIter { self.iter() } } -impl<'a> ParametersValues<'a> { +impl ParametersValues { /// Creates [`ParametersValues`]. #[must_use] #[inline] @@ -784,65 +782,57 @@ impl<'a> ParametersValues<'a> { /// Adds a [`ParameterValue`]. #[inline] - pub fn parameter_value( - &mut self, - name: impl Into>, - parameter_value: ParameterValue, - ) -> &mut Self { + pub fn parameter_value(&mut self, name: &str, parameter_value: ParameterValue) -> &mut Self { let _ = self.0.insert(name.into(), parameter_value); self } /// Adds a [`bool`] value. #[inline] - pub fn bool(&mut self, name: impl Into>, value: bool) -> &mut Self { + pub fn bool(&mut self, name: &str, value: bool) -> &mut Self { self.parameter_value(name, ParameterValue::Bool(value)) } /// Adds an [`u8`] parameter. #[inline] - pub fn u8(&mut self, name: impl Into>, value: u8) -> &mut Self { + pub fn u8(&mut self, name: &str, value: u8) -> &mut Self { self.parameter_value(name, ParameterValue::U8(value)) } /// Adds an [`u16`] parameter. #[inline] - pub fn u16(&mut self, name: impl Into>, value: u16) -> &mut Self { + pub fn u16(&mut self, name: &str, value: u16) -> &mut Self { self.parameter_value(name, ParameterValue::U16(value)) } /// Adds an [`u32`] parameter. #[inline] - pub fn u32(&mut self, name: impl Into>, value: u32) -> &mut Self { + pub fn u32(&mut self, name: &str, value: u32) -> &mut Self { self.parameter_value(name, ParameterValue::U32(value)) } /// Adds an [`u64`] parameter. #[inline] - pub fn u64(&mut self, name: impl Into>, value: u64) -> &mut Self { + pub fn u64(&mut self, name: &str, value: u64) -> &mut Self { self.parameter_value(name, ParameterValue::U64(value)) } /// Adds a [`f32`] parameter. #[inline] - pub fn f32(&mut self, name: impl Into>, value: f32) -> &mut Self { + pub fn f32(&mut self, name: &str, value: f32) -> &mut Self { self.parameter_value(name, ParameterValue::F32(value)) } /// Adds a [`f64`] parameter. #[inline] - pub fn f64(&mut self, name: impl Into>, value: f64) -> &mut Self { + pub fn f64(&mut self, name: &str, value: f64) -> &mut Self { self.parameter_value(name, ParameterValue::F64(value)) } /// Adds a sequence of characters. #[inline] - pub fn characters_sequence( - &mut self, - name: impl Into>, - value: String, - ) -> &mut Self { - self.parameter_value(name, ParameterValue::CharsSequence(value.into())) + pub fn characters_sequence(&mut self, name: &str, value: String) -> &mut Self { + self.parameter_value(name, ParameterValue::CharsSequence(value)) } /// Retrieves a [`ParameterValue`] by name. @@ -850,8 +840,8 @@ impl<'a> ParametersValues<'a> { /// Returns [`None`] if the parameter does not exist. #[must_use] #[inline] - pub fn get<'b>(&'b self, name: impl Into>) -> Option<&'b ParameterValue> { - self.0.get(&name.into()) + pub fn get<'b>(&'b self, name: &str) -> Option<&'b ParameterValue> { + self.0.get(name) } /// Returns an iterator over [`ParameterValue`]s. @@ -859,7 +849,7 @@ impl<'a> ParametersValues<'a> { /// **Iterates over the elements in the order they were inserted.** #[must_use] #[inline] - pub fn iter(&self) -> Iter<'_, Cow<'_, str>, ParameterValue> { + pub fn iter(&self) -> Iter<'_, String, ParameterValue> { self.0.iter() } } @@ -884,17 +874,17 @@ impl ParameterPayload { map! { /// A map associating each parameter name with its /// corresponding [`ParameterPayload`]. - pub struct ParametersPayloads<'a>(IndexMap, ParameterPayload, DefaultHashBuilder>); + pub struct ParametersPayloads(IndexMap); } -impl<'a> ParametersPayloads<'a> { +impl ParametersPayloads { /// Retrieves a [`ParameterPayload`] by name. /// /// Returns [`None`] if the parameter does not exist. #[must_use] #[inline] - pub fn get<'b>(&'b self, name: impl Into>) -> Option<&'b ParameterPayload> { - self.0.get(&name.into()) + pub fn get<'b>(&'b self, name: &str) -> Option<&'b ParameterPayload> { + self.0.get(name) } /// Extracts a [`ParameterPayload`] by name. @@ -904,16 +894,14 @@ impl<'a> ParametersPayloads<'a> { /// Returns [`None`] if the parameter does not exist. #[must_use] #[inline] - pub fn extract(&mut self, name: impl Into>) -> Option { - self.0.swap_remove(&name.into()) + pub fn extract(&mut self, name: &str) -> Option { + self.0.swap_remove(name) } } #[cfg(test)] #[cfg(feature = "deserialize")] mod tests { - use alloc::string::String; - use crate::{deserialize, serialize}; use super::{ParameterKind, Parameters, ParametersData, ParametersValues}; @@ -1026,7 +1014,7 @@ mod tests { .rangeu64_with_default("rangeu64", (0, 20, 1), 5) .rangef64_with_default("rangef64", (0., 20., 0.1), 5.) .characters_sequence("greeting", "hello") - .characters_sequence("greeting2", String::from("hello")) + .characters_sequence("greeting2", "hello") // Adds a duplicate to see whether that value is maintained or // removed. .u16("u16", 0); @@ -1050,6 +1038,6 @@ mod tests { "three": 3.0, }); - assert_eq!(deserialize::>(json_value), parameters); + assert_eq!(deserialize::(json_value), parameters); } } diff --git a/crates/tosca/src/route.rs b/crates/tosca/src/route.rs index e93c0c92..3c723d71 100644 --- a/crates/tosca/src/route.rs +++ b/crates/tosca/src/route.rs @@ -13,7 +13,6 @@ use crate::parameters::{Parameters, ParametersData}; use crate::response::ResponseKind; use crate::macros::set; -use crate::mandatory_route; /// The kind of `REST` request. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] @@ -70,8 +69,8 @@ impl PartialEq for RouteData { impl RouteData { fn new(route: Route) -> Self { Self { - name: route.name.into(), - path: route.path.into(), + name: Cow::Borrowed(route.name), + path: Cow::Borrowed(route.path), description: route.description.map(core::convert::Into::into), hazards: route.hazards, parameters: route.parameters.serialize_data(), @@ -327,8 +326,33 @@ set! { pub struct Routes(IndexSet); } -mandatory_route!(LightOnRoute, "/on", methods: [post, put]); -mandatory_route!(LightOffRoute, "/off", methods: [post, put]); +#[cfg(test)] +#[cfg(not(feature = "deserialize"))] +mod tests { + use crate::route::{Hazard, Hazards}; + + use super::Route; + + #[test] + fn test_allowed_hazards() { + const ALLOWED_HAZARDS: &[Hazard] = &[Hazard::FireHazard, Hazard::ElectricEnergyConsumption]; + + // Wrong AirPoisoning hazard. + let route = Route::get("Route", "/route") + .description("A GET route") + .with_hazards( + Hazards::new() + .insert(Hazard::FireHazard) + .insert(Hazard::AirPoisoning), + ); + + let expected_hazards = Hazards::init(Hazard::FireHazard); + assert_eq!( + route.remove_prohibited_hazards(ALLOWED_HAZARDS).hazards, + expected_hazards + ); + } +} #[cfg(test)] #[cfg(feature = "deserialize")] @@ -469,15 +493,25 @@ mod tests { RestKind::Get, Hazards::new(), "A GET route", - ParametersData::new().insert( - "rangeu64".into(), - ParameterKind::RangeU64 { - min: 0, - max: 20, - step: 1, - default: 5, - }, - ), + ParametersData::new() + .insert( + "rangeu64".into(), + ParameterKind::RangeU64 { + min: 0, + max: 20, + step: 1, + default: 5, + }, + ) + .insert( + "rangef64".into(), + ParameterKind::RangeF64 { + min: 0., + max: 20., + step: 0.1, + default: 0., + }, + ), ); assert_eq!( @@ -495,31 +529,3 @@ mod tests { ); } } - -#[cfg(test)] -#[cfg(not(feature = "deserialize"))] -mod tests { - use crate::route::{Hazard, Hazards}; - - use super::Route; - - #[test] - fn test_allowed_hazards() { - const ALLOWED_HAZARDS: &[Hazard] = &[Hazard::FireHazard, Hazard::ElectricEnergyConsumption]; - - // Wrong AirPoisoning hazard. - let route = Route::get("Route", "/route") - .description("A GET route") - .with_hazards( - Hazards::new() - .insert(Hazard::FireHazard) - .insert(Hazard::AirPoisoning), - ); - - let expected_hazards = Hazards::init(Hazard::FireHazard); - assert_eq!( - route.remove_prohibited_hazards(ALLOWED_HAZARDS).hazards, - expected_hazards - ); - } -} diff --git a/examples/controller-events-receiver/Cargo.toml b/examples/controller-events-receiver/Cargo.toml index a39b495b..61680da6 100644 --- a/examples/controller-events-receiver/Cargo.toml +++ b/examples/controller-events-receiver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "controller-events-receiver" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["Michele Valsesia "] description = """ diff --git a/examples/controller-events-sse-receiver/Cargo.toml b/examples/controller-events-sse-receiver/Cargo.toml index 359b1f27..594cfd21 100644 --- a/examples/controller-events-sse-receiver/Cargo.toml +++ b/examples/controller-events-sse-receiver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "controller-events-sse-receiver" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["Michele Valsesia "] description = """ diff --git a/examples/os-ip-camera/Cargo.toml b/examples/os-ip-camera/Cargo.toml index 08570d7e..8aaff07e 100644 --- a/examples/os-ip-camera/Cargo.toml +++ b/examples/os-ip-camera/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "os-ip-camera" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["Michele Valsesia "] description = "An ip-camera implemented with the tosca-os crate." diff --git a/examples/os-ip-camera/src/main.rs b/examples/os-ip-camera/src/main.rs index 2840f836..ebb84cc2 100644 --- a/examples/os-ip-camera/src/main.rs +++ b/examples/os-ip-camera/src/main.rs @@ -6,6 +6,7 @@ mod stream; use std::net::Ipv4Addr; use std::sync::Arc; +use tosca::device::DeviceScheme; use tosca::hazards::Hazard; use tosca::parameters::Parameters; use tosca::route::Route; @@ -380,14 +381,17 @@ async fn main() -> Result<(), Error> { Route::get("View info", "/view-info").description("View current camera data."); // A camera device which is going to be run on the server. - let device = Device::with_state(InternalState::new(camera)) - .main_route("/camera") - .route(stream_stateful(camera_stream_route, show_camera_stream)) - .route(serial_stateless(view_cameras_route, show_available_cameras)) - .route(serial_stateful(camera_info_route, show_camera_info)); + let device = Device::with_state( + DeviceScheme::base_custom_scheme("Camera"), + InternalState::new(camera), + ) + .main_route("/camera") + .route(stream_stateful(camera_stream_route, show_camera_stream)) + .route(serial_stateless(view_cameras_route, show_available_cameras)) + .route(serial_stateful(camera_info_route, show_camera_info)); let device = change_format(device); - let device = screenshot(device); + let device = screenshot(device).build().map_err(Error::Tosca)?; Server::new(device) .address(cli.address) diff --git a/examples/os-light/Cargo.toml b/examples/os-light/Cargo.toml index e33d6afd..12227fa0 100644 --- a/examples/os-light/Cargo.toml +++ b/examples/os-light/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "os-light" -version = "0.1.0" +version = "0.2.0" edition = "2024" authors = ["Michele Valsesia "] description = "A light implemented with the tosca-os crate." diff --git a/examples/os-light/src/main.rs b/examples/os-light/src/main.rs index 761b5148..ffc5283a 100644 --- a/examples/os-light/src/main.rs +++ b/examples/os-light/src/main.rs @@ -4,18 +4,19 @@ use std::net::Ipv4Addr; use std::sync::Arc; use tosca::device::DeviceMetrics; +use tosca::device::schemes::LIGHT_SCHEME; use tosca::energy::{Energy, EnergyClass, EnergyEfficiencies, EnergyEfficiency}; use tosca::hazards::Hazard; use tosca::parameters::Parameters; -use tosca::route::{LightOffRoute, LightOnRoute, Route}; +use tosca::route::Route; -use tosca_os::devices::light::Light; +use tosca_os::device::Device; use tosca_os::error::Error; use tosca_os::extract::{FromRef, Json, State}; use tosca_os::responses::error::ErrorResponse; use tosca_os::responses::info::{InfoResponse, info_stateful}; -use tosca_os::responses::ok::{OkResponse, mandatory_ok_stateful, ok_stateful}; -use tosca_os::responses::serial::{SerialResponse, mandatory_serial_stateful, serial_stateful}; +use tosca_os::responses::ok::{OkResponse, ok_stateful}; +use tosca_os::responses::serial::{SerialResponse, serial_stateful}; use tosca_os::server::Server; use tosca_os::service::{ServiceConfig, TransportProtocol}; @@ -225,7 +226,7 @@ async fn main() -> Result<(), Error> { ); // Turn light on `PUT` route. - let light_on_route = LightOnRoute::put("On") + let light_on_route = Route::put("On", "/on") .description("Turn light on.") .with_hazard(Hazard::ElectricEnergyConsumption) .with_parameters( @@ -245,7 +246,7 @@ async fn main() -> Result<(), Error> { ); // Turn light off `PUT` route. - let light_off_route = LightOffRoute::put("Off").description("Turn light off."); + let light_off_route = Route::put("Off", "/off").description("Turn light off."); // Toggle `PUT` route. let toggle_route = Route::put("Toggle", "/toggle") @@ -262,20 +263,18 @@ async fn main() -> Result<(), Error> { .description("Update energy efficiency.") .with_hazard(Hazard::LogEnergyConsumption); - // A light device which is going to be run on the server. - let device = Light::with_state(state) - // This method is mandatory, if not called, a compiler error is raised. - .turn_light_on(light_on_route, mandatory_serial_stateful(turn_light_on)) - // This method is mandatory, if not called, a compiler error is raised. - .turn_light_off(light_off_route, mandatory_ok_stateful(turn_light_off)) - .route(serial_stateful(light_on_post_route, turn_light_on))? - .route(ok_stateful(toggle_route, toggle))? + // A light device. + let device = Device::with_state(LIGHT_SCHEME, state) + .route(serial_stateful(light_on_route, turn_light_on)) + .route(ok_stateful(light_off_route, turn_light_off)) + .route(serial_stateful(light_on_post_route, turn_light_on)) + .route(ok_stateful(toggle_route, toggle)) .info_route(info_stateful(info_route, info)) .info_route(info_stateful( update_energy_efficiency_route, update_energy_efficiency, )) - .build(); + .build()?; // Run a discovery service and the device on the server. Server::new(device)