From 54d26502d7a4d1009da38682ac39d5a631ca128b Mon Sep 17 00:00:00 2001 From: "t.galimkhanov" Date: Fri, 10 Apr 2026 10:19:02 +0500 Subject: [PATCH] improve: std - homework 2 --- .vscode/settings.json | 7 ++ src/devices/device.rs | 19 +++++ src/error.rs | 26 ++++++ src/home/room.rs | 163 ++++++++++++----------------------- src/home/smart_home.rs | 187 +++++++++++++++++++---------------------- src/lib.rs | 175 ++++++++++++++++++++++++++------------ src/main.rs | 184 ++++++++++++++++++++-------------------- src/report.rs | 12 +++ tests/integration.rs | 92 +++++++++++--------- 9 files changed, 470 insertions(+), 395 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/error.rs create mode 100644 src/report.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bf83d6c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "workbench.editorAssociations": { + "*.parquet": "default", + "*.csv": "duckdb.dataFileViewer", + "*.tsv": "duckdb.dataFileViewer" + } +} \ No newline at end of file diff --git a/src/devices/device.rs b/src/devices/device.rs index 479a706..f84f55e 100644 --- a/src/devices/device.rs +++ b/src/devices/device.rs @@ -1,5 +1,6 @@ use super::socket::Socket; use super::thermometer::Thermometer; +use crate::report::Report; /// Trait for all smart home devices pub trait DeviceInfo { @@ -66,3 +67,21 @@ impl DeviceInfo for Device { } } } + +impl From for Device { + fn from(value: Thermometer) -> Self { + Device::Thermometer(value) + } +} + +impl From for Device { + fn from(value: Socket) -> Self { + Device::Socket(value) + } +} + +impl Report for Device { + fn report(&self) -> String { + format!("{}\n", DeviceInfo::state(self)) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..3025724 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,26 @@ +use std::error::Error; +use std::fmt; + +/// Error when resolving a room or device in [`crate::SmartHome`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SmartHomeError { + /// No room with the given key. + RoomNotFound { room: String }, + /// Room exists, but no device with the given key. + DeviceNotFound { room: String, device: String }, +} + +impl fmt::Display for SmartHomeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SmartHomeError::RoomNotFound { room } => { + write!(f, "room not found: {room}") + } + SmartHomeError::DeviceNotFound { room, device } => { + write!(f, "device not found: {device} (in room {room})") + } + } + } +} + +impl Error for SmartHomeError {} diff --git a/src/home/room.rs b/src/home/room.rs index 9771c87..5a17ad0 100644 --- a/src/home/room.rs +++ b/src/home/room.rs @@ -1,145 +1,86 @@ +use std::collections::HashMap; + use crate::devices::Device; +use crate::report::Report; -/// Room containing an array of smart devices +/// Room containing smart devices keyed by string identifiers. #[derive(Debug, Clone, PartialEq)] pub struct Room { name: String, - devices: Vec, + devices: HashMap, } impl Room { - /// Room constructor accepting an array of devices - /// - /// # Arguments - /// - /// * `name` - Room name - /// * `devices` - Array of devices in the room + /// Creates a room with the given display name and device map. /// /// # Examples /// /// ``` /// use smart_home::{Room, Device, Thermometer, Temperature}; + /// use std::collections::HashMap; /// - /// let devices = vec![ + /// let mut map = HashMap::new(); + /// map.insert( + /// "t1".to_string(), /// Device::Thermometer(Thermometer::new("Thermometer".to_string(), Temperature::celsius(22.0))), - /// ]; - /// let room = Room::new("Living Room".to_string(), devices); + /// ); + /// let room = Room::new("Living Room".to_string(), map); /// assert_eq!(room.name(), "Living Room"); /// ``` - pub fn new(name: String, devices: Vec) -> Self { + pub fn new(name: String, devices: HashMap) -> Self { Self { name, devices } } - /// Get a reference to the device at the specified index - /// - /// # Arguments - /// - /// * `index` - Device index - /// - /// # Panics - /// - /// Panics if the index is out of bounds - /// - /// # Examples - /// - /// ``` - /// use smart_home::{Room, Device, Socket, Power, DeviceInfo}; - /// - /// let devices = vec![ - /// Device::Socket(Socket::new("TV".to_string(), true, Power::new(120.0).unwrap())), - /// ]; - /// let room = Room::new("Living Room".to_string(), devices); - /// let device = room.device(0); - /// assert_eq!(device.name(), "TV"); - /// ``` - pub fn device(&self, index: usize) -> &Device { - self.devices.get(index).expect("Device index out of bounds") + /// Reference to a device by key, if present. + pub fn device(&self, key: &str) -> Option<&Device> { + self.devices.get(key) } - /// Get a mutable reference to the device at the specified index - /// - /// # Arguments - /// - /// * `index` - Device index - /// - /// # Panics - /// - /// Panics if the index is out of bounds - /// - /// # Examples - /// - /// ``` - /// use smart_home::{Room, Device, Socket, Power}; - /// - /// let devices = vec![ - /// Device::Socket(Socket::new("Lamp".to_string(), false, Power::new(60.0).unwrap())), - /// ]; - /// let mut room = Room::new("Bedroom".to_string(), devices); - /// let device = room.device_mut(0); - /// if let Device::Socket(socket) = device { - /// socket.turn_on(); - /// } - /// ``` - pub fn device_mut(&mut self, index: usize) -> &mut Device { - self.devices - .get_mut(index) - .expect("Device index out of bounds") + /// Mutable reference to a device by key, if present. + pub fn device_mut(&mut self, key: &str) -> Option<&mut Device> { + self.devices.get_mut(key) } - /// Print a report of all devices in the room to stdout - /// - /// # Examples - /// - /// ``` - /// use smart_home::{Room, Device, Thermometer, Temperature}; - /// - /// let devices = vec![ - /// Device::Thermometer(Thermometer::new("Sensor".to_string(), Temperature::celsius(20.0))), - /// ]; - /// let room = Room::new("Kitchen".to_string(), devices); - /// room.print_report(); - /// ``` - pub fn print_report(&self) { - println!(" Room '{}':", self.name); - if self.devices.is_empty() { - println!(" (no devices)"); - } else { - for device in &self.devices { - print!(" "); - device.print_state(); - } - } + /// Inserts or replaces a device under `key`. Returns the previous device, if any. + pub fn insert_device(&mut self, key: String, device: Device) -> Option { + self.devices.insert(key, device) } - /// Returns the room name - /// - /// # Examples - /// - /// ``` - /// use smart_home::Room; - /// - /// let room = Room::new("Kitchen".to_string(), vec![]); - /// assert_eq!(room.name(), "Kitchen"); - /// ``` + /// Removes a device by key. Returns it if it existed. + pub fn remove_device(&mut self, key: &str) -> Option { + self.devices.remove(key) + } + + /// Room display name. pub fn name(&self) -> &str { &self.name } - /// Returns the number of devices in the room - /// - /// # Examples - /// - /// ``` - /// use smart_home::{Room, Device, Socket, Power}; - /// - /// let devices = vec![ - /// Device::Socket(Socket::new("S1".to_string(), true, Power::new(100.0).unwrap())), - /// Device::Socket(Socket::new("S2".to_string(), false, Power::new(50.0).unwrap())), - /// ]; - /// let room = Room::new("Kitchen".to_string(), devices); - /// assert_eq!(room.device_count(), 2); - /// ``` + /// Number of devices in the room. pub fn device_count(&self) -> usize { self.devices.len() } + + /// Iterator over `(key, device)` pairs (arbitrary order). + pub fn devices(&self) -> impl Iterator { + self.devices.iter() + } +} + +impl Report for Room { + fn report(&self) -> String { + let mut lines = vec![format!(" Room '{}':\n", self.name)]; + if self.devices.is_empty() { + lines.push(" (no devices)\n".to_string()); + } else { + let mut keys: Vec<&String> = self.devices.keys().collect(); + keys.sort(); + for k in keys { + if let Some(d) = self.devices.get(k.as_str()) { + lines.push(format!(" {}", d.report())); + } + } + } + lines.concat() + } } diff --git a/src/home/smart_home.rs b/src/home/smart_home.rs index b4c97d5..6aaeb56 100644 --- a/src/home/smart_home.rs +++ b/src/home/smart_home.rs @@ -1,134 +1,119 @@ +use std::collections::HashMap; + +use crate::devices::Device; +use crate::error::SmartHomeError; +use crate::report::Report; + use super::room::Room; -/// Smart home containing an array of rooms +/// Smart home: rooms stored in a map keyed by string identifiers. #[derive(Debug, Clone, PartialEq)] pub struct SmartHome { name: String, - rooms: Vec, + rooms: HashMap, } impl SmartHome { - /// Smart home constructor accepting an array of rooms - /// - /// # Arguments - /// - /// * `name` - Home name - /// * `rooms` - Array of rooms + /// Creates a home with the given name and room map. /// /// # Examples /// /// ``` /// use smart_home::{SmartHome, Room}; + /// use std::collections::HashMap; /// - /// let home = SmartHome::new("My Home".to_string(), vec![]); + /// let home = SmartHome::new("My Home".to_string(), HashMap::new()); /// assert_eq!(home.name(), "My Home"); /// ``` - pub fn new(name: String, rooms: Vec) -> Self { + pub fn new(name: String, rooms: HashMap) -> Self { Self { name, rooms } } - /// Get a reference to the room at the specified index - /// - /// # Arguments - /// - /// * `index` - Room index - /// - /// # Panics - /// - /// Panics if the index is out of bounds - /// - /// # Examples - /// - /// ``` - /// use smart_home::{SmartHome, Room}; - /// - /// let rooms = vec![Room::new("Living Room".to_string(), vec![])]; - /// let home = SmartHome::new("Home".to_string(), rooms); - /// let room = home.room(0); - /// assert_eq!(room.name(), "Living Room"); - /// ``` - pub fn room(&self, index: usize) -> &Room { - self.rooms.get(index).expect("Room index out of bounds") + /// Room by key, if present. + pub fn room(&self, key: &str) -> Option<&Room> { + self.rooms.get(key) } - /// Get a mutable reference to the room at the specified index - /// - /// # Arguments - /// - /// * `index` - Room index - /// - /// # Panics - /// - /// Panics if the index is out of bounds - /// - /// # Examples - /// - /// ``` - /// use smart_home::{SmartHome, Room, Device, Socket, Power}; - /// - /// let devices = vec![ - /// Device::Socket(Socket::new("TV".to_string(), true, Power::new(120.0).unwrap())), - /// ]; - /// let rooms = vec![Room::new("Living Room".to_string(), devices)]; - /// let mut home = SmartHome::new("Home".to_string(), rooms); - /// let room = home.room_mut(0); - /// assert_eq!(room.name(), "Living Room"); - /// ``` - pub fn room_mut(&mut self, index: usize) -> &mut Room { - self.rooms.get_mut(index).expect("Room index out of bounds") + /// Mutable room by key, if present. + pub fn room_mut(&mut self, key: &str) -> Option<&mut Room> { + self.rooms.get_mut(key) } - /// Print a report of all rooms to stdout - /// - /// # Examples - /// - /// ``` - /// use smart_home::{SmartHome, Room}; - /// - /// let home = SmartHome::new("My Home".to_string(), vec![]); - /// home.print_report(); - /// ``` - pub fn print_report(&self) { - println!("Smart home '{}':", self.name); - if self.rooms.is_empty() { - println!(" (no rooms)"); - } else { - for room in &self.rooms { - room.print_report(); - } - } - println!(); + /// Inserts or replaces a room under `key`. Returns the previous room, if any. + pub fn insert_room(&mut self, key: String, room: Room) -> Option { + self.rooms.insert(key, room) + } + + /// Removes a room by key. Returns it if it existed. + pub fn remove_room(&mut self, key: &str) -> Option { + self.rooms.remove(key) + } + + /// Resolves a device by room key and device key. + /// + /// Returns [`SmartHomeError`] indicating whether the room or the device was missing. + pub fn device(&self, room_name: &str, device_name: &str) -> Result<&Device, SmartHomeError> { + let room = self + .rooms + .get(room_name) + .ok_or_else(|| SmartHomeError::RoomNotFound { + room: room_name.to_string(), + })?; + room.device(device_name) + .ok_or_else(|| SmartHomeError::DeviceNotFound { + room: room_name.to_string(), + device: device_name.to_string(), + }) + } + + /// Same as [`SmartHome::device`] but mutable. + pub fn device_mut( + &mut self, + room_name: &str, + device_name: &str, + ) -> Result<&mut Device, SmartHomeError> { + let room = self + .rooms + .get_mut(room_name) + .ok_or_else(|| SmartHomeError::RoomNotFound { + room: room_name.to_string(), + })?; + room.device_mut(device_name) + .ok_or_else(|| SmartHomeError::DeviceNotFound { + room: room_name.to_string(), + device: device_name.to_string(), + }) } - /// Returns the home name - /// - /// # Examples - /// - /// ``` - /// use smart_home::SmartHome; - /// - /// let home = SmartHome::new("My Home".to_string(), vec![]); - /// assert_eq!(home.name(), "My Home"); - /// ``` pub fn name(&self) -> &str { &self.name } - /// Returns the number of rooms in the home - /// - /// # Examples - /// - /// ``` - /// use smart_home::{SmartHome, Room}; - /// - /// let rooms = vec![ - /// Room::new("Kitchen".to_string(), vec![]), - /// Room::new("Living Room".to_string(), vec![]), - /// ]; - /// let home = SmartHome::new("Home".to_string(), rooms); - /// assert_eq!(home.room_count(), 2); - /// ``` pub fn room_count(&self) -> usize { self.rooms.len() } + + /// Iterator over `(key, room)` pairs (arbitrary order). + pub fn rooms(&self) -> impl Iterator { + self.rooms.iter() + } +} + +impl Report for SmartHome { + fn report(&self) -> String { + let mut lines = vec![format!("Smart home '{}':\n", self.name)]; + if self.rooms.is_empty() { + lines.push(" (no rooms)\n".to_string()); + } else { + let mut keys: Vec<&String> = self.rooms.keys().collect(); + keys.sort(); + for k in keys { + if let Some(room) = self.rooms.get(k.as_str()) { + lines.push(room.report()); + } + } + } + lines.push('\n'.to_string()); + lines.concat() + } } diff --git a/src/lib.rs b/src/lib.rs index 4557a21..97a45e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,36 +5,64 @@ //! - [`devices`] — [`Thermometer`], [`Socket`], [`Device`], [`DeviceInfo`] //! - [`home`] — [`Room`], [`SmartHome`] //! - [`types`] — [`Power`], [`Temperature`] +//! - [`report`] — [`Report`] +//! - [`error`] — [`SmartHomeError`] //! -//! Types are re-exported at the crate root, so you can write -//! `use smart_home::{SmartHome, Room, Device, ...}`. +//! Types are re-exported at the crate root. //! //! # Example //! //! ``` //! use smart_home::{ -//! Device, Room, SmartHome, Socket, Thermometer, Temperature, Power, +//! room, Device, Room, SmartHome, Socket, Thermometer, Temperature, Power, //! }; +//! use std::collections::HashMap; //! -//! let devices = vec![ -//! Device::Thermometer(Thermometer::new("t".to_string(), Temperature::celsius(20.0))), -//! Device::Socket(Socket::new("s".to_string(), false, Power::new(10.0).unwrap())), -//! ]; -//! let room = Room::new("Kitchen".to_string(), devices); -//! let _home = SmartHome::new("Home".to_string(), vec![room]); +//! let r = room!( +//! "Kitchen", +//! "t" => Thermometer::new("Kitchen sensor".to_string(), Temperature::celsius(20.0)), +//! "s" => Socket::new("Kettle".to_string(), false, Power::new(10.0).unwrap()), +//! ); +//! let mut rooms = HashMap::new(); +//! rooms.insert("Kitchen".to_string(), r); +//! let _home = SmartHome::new("Home".to_string(), rooms); //! ``` pub mod devices; +pub mod error; pub mod home; +pub mod report; pub mod types; -// Re-exports for convenience pub use devices::{Device, DeviceInfo, Socket, Thermometer}; +pub use error::SmartHomeError; pub use home::{Room, SmartHome}; +pub use report::Report; pub use types::{Power, Temperature}; +/// Prints a report for any type implementing [`Report`]. +pub fn print_report_value(item: &T) { + item.print_report(); +} + +/// Builds a [`Room`] from a name and `key => socket_or_thermometer` pairs. +/// +/// Uses `=>` so device expressions may contain commas (e.g. `Thermometer::new(..., ...)`). +#[macro_export] +macro_rules! room { + ($name:expr $(, $key:expr => $dev:expr)* $(,)?) => {{ + let mut map = ::std::collections::HashMap::new(); + $( + map.insert($key.to_string(), $crate::Device::from($dev)); + )* + $crate::Room::new($name.to_string(), map) + }}; +} + #[cfg(test)] mod tests { + use std::collections::HashMap; + use super::*; #[test] @@ -69,29 +97,21 @@ mod tests { #[test] fn test_room_get_device() { - let devices = vec![ - Device::Thermometer(Thermometer::new( - "T1".to_string(), - Temperature::celsius(20.0), - )), - Device::Socket(Socket::new( - "S1".to_string(), - true, - Power::new(50.0).unwrap(), - )), - ]; - let room = Room::new("Kitchen".to_string(), devices); + let room = room!( + "Kitchen", + "t1" => Thermometer::new("T1".to_string(), Temperature::celsius(20.0)), + "s1" => Socket::new("S1".to_string(), true, Power::new(50.0).unwrap()), + ); assert_eq!(room.name(), "Kitchen"); - // Check device retrieval by index - let device0 = room.device(0); + let device0 = room.device("t1").expect("t1"); match device0 { Device::Thermometer(t) => assert_eq!(t.name(), "T1"), _ => panic!("Expected thermometer"), } - let device1 = room.device(1); + let device1 = room.device("s1").expect("s1"); match device1 { Device::Socket(s) => assert_eq!(s.name(), "S1"), _ => panic!("Expected socket"), @@ -100,15 +120,12 @@ mod tests { #[test] fn test_room_get_device_mut() { - let devices = vec![Device::Socket(Socket::new( - "S1".to_string(), - true, - Power::new(50.0).unwrap(), - ))]; - let mut room = Room::new("Bedroom".to_string(), devices); + let mut room = room!( + "Bedroom", + "s1" => Socket::new("S1".to_string(), true, Power::new(50.0).unwrap()), + ); - // Modify device through mutable reference - let device = room.device_mut(0); + let device = room.device_mut("s1").expect("s1"); match device { Device::Socket(s) => { s.turn_off(); @@ -116,8 +133,7 @@ mod tests { _ => panic!("Expected socket"), } - // Verify changes persisted - let device = room.device(0); + let device = room.device("s1").expect("s1"); match device { Device::Socket(s) => assert!(!s.is_on()), _ => panic!("Expected socket"), @@ -125,44 +141,78 @@ mod tests { } #[test] - #[should_panic(expected = "Device index out of bounds")] - fn test_room_get_device_out_of_bounds() { - let room = Room::new("Room".to_string(), vec![]); - room.device(0); + fn test_room_get_device_missing() { + let room = Room::new("Room".to_string(), HashMap::new()); + assert!(room.device("x").is_none()); } #[test] fn test_smart_home_get_room() { - let rooms = vec![ - Room::new("Living Room".to_string(), vec![]), - Room::new("Kitchen".to_string(), vec![]), - ]; + let mut rooms = HashMap::new(); + rooms.insert( + "Living Room".to_string(), + Room::new("Living Room".to_string(), HashMap::new()), + ); + rooms.insert( + "Kitchen".to_string(), + Room::new("Kitchen".to_string(), HashMap::new()), + ); let home = SmartHome::new("My Home".to_string(), rooms); assert_eq!(home.name(), "My Home"); - assert_eq!(home.room(0).name(), "Living Room"); - assert_eq!(home.room(1).name(), "Kitchen"); + assert_eq!(home.room("Living Room").expect("lr").name(), "Living Room"); + assert_eq!(home.room("Kitchen").expect("k").name(), "Kitchen"); } #[test] fn test_smart_home_get_room_mut() { - let rooms = vec![Room::new("Living Room".to_string(), vec![])]; + let mut rooms = HashMap::new(); + let devices = HashMap::new(); + rooms.insert( + "Living Room".to_string(), + Room::new("Living Room".to_string(), devices), + ); let mut home = SmartHome::new("My Home".to_string(), rooms); - let room = home.room_mut(0); + let room = home.room_mut("Living Room").expect("lr"); assert_eq!(room.name(), "Living Room"); } #[test] - #[should_panic(expected = "Room index out of bounds")] - fn test_smart_home_get_room_out_of_bounds() { - let home = SmartHome::new("Home".to_string(), vec![]); - home.room(0); + fn test_smart_home_get_room_missing() { + let home = SmartHome::new("Home".to_string(), HashMap::new()); + assert!(home.room("nope").is_none()); + } + + #[test] + fn test_device_resolve() { + let mut rooms = HashMap::new(); + rooms.insert( + "Kitchen".to_string(), + room!( + "Kitchen", + "kettle" => Socket::new("K".to_string(), true, Power::new(1.0).unwrap()), + ), + ); + let home = SmartHome::new("H".to_string(), rooms); + assert!(home.device("Kitchen", "kettle").is_ok()); + assert_eq!( + home.device("Bad", "kettle").unwrap_err(), + SmartHomeError::RoomNotFound { + room: "Bad".to_string() + } + ); + assert_eq!( + home.device("Kitchen", "missing").unwrap_err(), + SmartHomeError::DeviceNotFound { + room: "Kitchen".to_string(), + device: "missing".to_string() + } + ); } #[test] fn test_device_print_state() { - // Test doesn't verify output, but ensures the method runs without panic let thermometer = Device::Thermometer(Thermometer::new( "T1".to_string(), Temperature::celsius(22.5), @@ -207,4 +257,25 @@ mod tests { let temp_f = Temperature::fahrenheit(32.0); assert!((temp_f.as_celsius() - 0.0).abs() < 0.01); } + + #[test] + fn test_from_socket_thermometer_to_device() { + let t: Device = Thermometer::new("x".to_string(), Temperature::celsius(1.0)).into(); + assert!(matches!(t, Device::Thermometer(_))); + let s: Device = Socket::new("y".to_string(), false, Power::new(1.0).unwrap()).into(); + assert!(matches!(s, Device::Socket(_))); + } + + #[test] + fn test_report_trait() { + let room = room!( + "R", + "a" => Thermometer::new("T".to_string(), Temperature::celsius(1.0)), + ); + assert!(room.report().contains("Room 'R'")); + let mut rooms = HashMap::new(); + rooms.insert("R".to_string(), room); + let home = SmartHome::new("H".to_string(), rooms); + assert!(home.report().contains("Smart home 'H'")); + } } diff --git a/src/main.rs b/src/main.rs index 16ff522..aa0fd4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,117 +1,117 @@ -use smart_home::{Device, DeviceInfo, Power, Room, SmartHome, Socket, Temperature, Thermometer}; +use std::collections::HashMap; + +use smart_home::{ + Device, DeviceInfo, Power, Room, SmartHome, SmartHomeError, Socket, Temperature, Thermometer, + print_report_value, room, +}; fn main() { println!("=== Smart Home Library Demo ===\n"); - // Create kitchen devices - let kitchen_devices = vec![ - Device::Thermometer(Thermometer::new( - "Kitchen thermometer".to_string(), - Temperature::celsius(22.5), - )), - Device::Socket(Socket::new( - "Kettle".to_string(), - true, - Power::new(1500.0).unwrap(), - )), - Device::Socket(Socket::new( - "Fridge".to_string(), - true, - Power::new(200.0).unwrap(), - )), - ]; - - // Create living room devices - let living_room_devices = vec![ - Device::Thermometer(Thermometer::new( - "Living room thermometer".to_string(), - Temperature::celsius(24.0), - )), - Device::Socket(Socket::new( - "TV".to_string(), - true, - Power::new(120.0).unwrap(), - )), - Device::Socket(Socket::new( - "Floor lamp".to_string(), - false, - Power::new(60.0).unwrap(), - )), - ]; - - // Create bedroom devices - let bedroom_devices = vec![ - Device::Thermometer(Thermometer::new( - "Bedroom thermometer".to_string(), - Temperature::celsius(21.0), - )), - Device::Socket(Socket::new( - "Humidifier".to_string(), - true, - Power::new(30.0).unwrap(), - )), - ]; - - // Create rooms - let rooms = vec![ - Room::new("Kitchen".to_string(), kitchen_devices), - Room::new("Living Room".to_string(), living_room_devices), - Room::new("Bedroom".to_string(), bedroom_devices), - ]; + let kitchen = room!( + "Kitchen", + "kitchen_thermometer" => + Thermometer::new("Kitchen thermometer".to_string(), Temperature::celsius(22.5)), + "kettle" => Socket::new("Kettle".to_string(), true, Power::new(1500.0).unwrap()), + "fridge" => Socket::new("Fridge".to_string(), true, Power::new(200.0).unwrap()), + ); + + let living_room = room!( + "Living Room", + "living_thermometer" => + Thermometer::new("Living room thermometer".to_string(), Temperature::celsius(24.0)), + "tv" => Socket::new("TV".to_string(), true, Power::new(120.0).unwrap()), + "floor_lamp" => + Socket::new("Floor lamp".to_string(), false, Power::new(60.0).unwrap()), + ); + + let bedroom = room!( + "Bedroom", + "bedroom_thermometer" => + Thermometer::new("Bedroom thermometer".to_string(), Temperature::celsius(21.0)), + "humidifier" => Socket::new("Humidifier".to_string(), true, Power::new(30.0).unwrap()), + ); + + let mut rooms = HashMap::new(); + rooms.insert("Kitchen".to_string(), kitchen); + rooms.insert("Living Room".to_string(), living_room); + rooms.insert("Bedroom".to_string(), bedroom); - // Create smart home let mut home = SmartHome::new("My Smart Home".to_string(), rooms); - // Print initial report println!("Initial home state:"); - home.print_report(); + print_report_value(&home); + + println!("Dynamic rooms: add 'Office', then remove 'Bedroom'\n"); + let office = room!( + "Office", + "office_thermometer" => + Thermometer::new("Office thermometer".to_string(), Temperature::celsius(23.0)), + ); + home.insert_room("Office".to_string(), office); + let removed_bedroom = home.remove_room("Bedroom"); + println!( + "Removed bedroom (had {} devices): {:?}\n", + removed_bedroom + .as_ref() + .map(Room::device_count) + .unwrap_or(0), + removed_bedroom.as_ref().map(Room::name) + ); + + println!("Dynamic devices: add socket to Kitchen, remove 'kettle' from Kitchen\n"); + if let Some(kitchen) = home.room_mut("Kitchen") { + kitchen.insert_device( + "desk_lamp".to_string(), + Socket::new("Desk lamp".to_string(), true, Power::new(40.0).unwrap()).into(), + ); + let kettle = kitchen.remove_device("kettle"); + println!("Removed kettle: {:?}\n", kettle.as_ref().map(Device::name)); + } - // Turn off TV in living room (room index 1, device index 1) - println!("Turning off TV in living room...\n"); - let living_room = home.room_mut(1); - if let Device::Socket(socket) = living_room.device_mut(1) { + println!("Turn off TV, turn on floor lamp (via keys):\n"); + if let Ok(Device::Socket(socket)) = home.device_mut("Living Room", "tv") { socket.turn_off(); } - - // Turn on floor lamp in living room (device index 2) - println!("Turning on floor lamp in living room...\n"); - let living_room = home.room_mut(1); - if let Device::Socket(socket) = living_room.device_mut(2) { + if let Ok(Device::Socket(socket)) = home.device_mut("Living Room", "floor_lamp") { socket.turn_on(); } - // Turn off kettle in kitchen (room index 0, device index 1) - println!("Turning off kettle in kitchen...\n"); - let kitchen = home.room_mut(0); - if let Device::Socket(socket) = kitchen.device_mut(1) { - socket.turn_off(); - } - - // Print updated report println!("Updated home state:"); - home.print_report(); - - // Demonstrate getting info about specific devices - println!("=== Additional Info ===\n"); + print_report_value(&home); - // Get kitchen temperature - let kitchen = home.room(0); - if let Device::Thermometer(thermometer) = kitchen.device(0) { - println!( - "Kitchen temperature: {:.1}°C", - thermometer.temperature().as_celsius() - ); + println!("=== Reports via print_report_value (home / room / device) ===\n"); + print_report_value(&home); + if let Some(lr) = home.room("Living Room") { + print_report_value(lr); + } + match home.device("Kitchen", "fridge") { + Ok(dev) => print_report_value(dev), + Err(e) => println!("{e}"), } - // Get fridge power consumption - let kitchen = home.room(0); - if let Device::Socket(socket) = kitchen.device(2) { - println!("Fridge consumption: {:.1} W", socket.power().watts()); + println!("=== SmartHome::device error handling ===\n"); + for (room_key, dev_key) in [ + ("Unknown", "x"), + ("Kitchen", "no_such_device"), + ("Kitchen", "fridge"), + ] { + match home.device(room_key, dev_key) { + Ok(d) => println!("OK {room_key}/{dev_key}: {}", d.state()), + Err(e) => println!("Err {room_key}/{dev_key}: {e} ({e:?})"), + } } + println!("\n=== SmartHomeError as std::error::Error ===\n"); + let err: SmartHomeError = SmartHomeError::DeviceNotFound { + room: "Kitchen".to_string(), + device: "ghost".to_string(), + }; + println!("display: {err}"); + println!("source: {:?}", std::error::Error::source(&err)); + println!("\n=== DeviceInfo trait demo ===\n"); - // Demonstrate DeviceInfo trait usage let devices: Vec> = vec![ Box::new(Thermometer::new( "Sensor 1".to_string(), diff --git a/src/report.rs b/src/report.rs new file mode 100644 index 0000000..b94d8f8 --- /dev/null +++ b/src/report.rs @@ -0,0 +1,12 @@ +//! [`Report`] — textual report for devices, rooms, and the whole home. + +/// Types that can produce a human-readable report string. +pub trait Report { + /// Full report text (typically multiple lines). + fn report(&self) -> String; + + /// Prints the report to stdout. + fn print_report(&self) { + print!("{}", self.report()); + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 6fe0c9a..b61b51a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,47 +1,43 @@ //! Integration tests: use only the public `smart_home` API (as a downstream crate would). -use smart_home::{Device, DeviceInfo, Power, Room, SmartHome, Socket, Temperature, Thermometer}; +use std::collections::HashMap; + +use smart_home::{ + Device, DeviceInfo, Power, Report, Room, SmartHome, Socket, Temperature, Thermometer, room, +}; #[test] fn full_home_workflow() { - let kitchen_devices = vec![ - Device::Thermometer(Thermometer::new( - "Kitchen".to_string(), - Temperature::celsius(22.5), - )), - Device::Socket(Socket::new( - "Kettle".to_string(), - true, - Power::new(1500.0).unwrap(), - )), - ]; - - let living_room_devices = vec![Device::Socket(Socket::new( - "TV".to_string(), - true, - Power::new(120.0).unwrap(), - ))]; - - let rooms = vec![ - Room::new("Kitchen".to_string(), kitchen_devices), - Room::new("Living Room".to_string(), living_room_devices), - ]; + let kitchen = room!( + "Kitchen", + "kitchen_sensor" => Thermometer::new("Kitchen".to_string(), Temperature::celsius(22.5)), + "kettle" => Socket::new("Kettle".to_string(), true, Power::new(1500.0).unwrap()), + ); + + let living_room = room!( + "Living Room", + "tv" => Socket::new("TV".to_string(), true, Power::new(120.0).unwrap()), + ); + + let mut rooms = HashMap::new(); + rooms.insert("Kitchen".to_string(), kitchen); + rooms.insert("Living Room".to_string(), living_room); let mut home = SmartHome::new("Test Home".to_string(), rooms); assert_eq!(home.room_count(), 2); - assert_eq!(home.room(0).device_count(), 2); - assert_eq!(home.room(1).device_count(), 1); + assert_eq!(home.room("Kitchen").unwrap().device_count(), 2); + assert_eq!(home.room("Living Room").unwrap().device_count(), 1); - let kitchen = home.room_mut(0); - if let Device::Socket(socket) = kitchen.device_mut(1) { + if let Ok(Device::Socket(socket)) = home.device_mut("Kitchen", "kettle") { socket.turn_off(); } - let kitchen = home.room(0); - if let Device::Socket(socket) = kitchen.device(1) { + if let Ok(Device::Socket(socket)) = home.device("Kitchen", "kettle") { assert!(!socket.is_on()); assert_eq!(socket.power().watts(), 0.0); + } else { + panic!("expected kettle"); } } @@ -60,28 +56,46 @@ fn clone_and_equality() { #[test] fn room_with_multiple_devices() { - let devices: Vec = (0..10) - .map(|i| { + let mut map = HashMap::new(); + for i in 0..10 { + map.insert( + format!("s{i}"), Device::Socket(Socket::new( - format!("Socket {}", i), + format!("Socket {i}"), true, Power::new(100.0 + i as f32).unwrap(), - )) - }) - .collect(); + )), + ); + } - let room = Room::new("Large Room".to_string(), devices); + let room = Room::new("Large Room".to_string(), map); assert_eq!(room.device_count(), 10); - let last_device = room.device(9); + let last_device = room.device("s9").expect("s9"); assert_eq!(last_device.name(), "Socket 9"); } #[test] fn empty_home_and_rooms() { - let home = SmartHome::new("Empty Home".to_string(), vec![]); + let home = SmartHome::new("Empty Home".to_string(), HashMap::new()); assert_eq!(home.room_count(), 0); - let room = Room::new("Empty Room".to_string(), vec![]); + let room = Room::new("Empty Room".to_string(), HashMap::new()); assert_eq!(room.device_count(), 0); } + +#[test] +fn report_contains_expected_sections() { + let r = room!( + "Kitchen", + "t" => Thermometer::new("T".to_string(), Temperature::celsius(20.0)), + ); + let s = r.report(); + assert!(s.contains("Kitchen")); + assert!(s.contains("20.0")); + + let mut rooms = HashMap::new(); + rooms.insert("Kitchen".to_string(), r); + let home = SmartHome::new("H".to_string(), rooms); + assert!(home.report().contains("Smart home 'H'")); +}