use std::fmt::{self, Debug}; use std::hash::{Hash, Hasher}; use serde::{Deserialize, Serialize}; use siphasher::sip::SipHasher13; use uuid::Uuid; use crate::api::Resource; #[derive(Copy, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] pub enum RType { /// Only used in [`ResourceLink`] references AuthV1, BehaviorInstance, BehaviorScript, Bridge, BridgeHome, Button, CameraMotion, Contact, Device, DevicePower, DeviceSoftwareUpdate, Entertainment, EntertainmentConfiguration, GeofenceClient, Geolocation, GroupedLight, GroupedLightLevel, GroupedMotion, Homekit, Light, LightLevel, Matter, MatterFabric, Motion, /// Only used in [`ResourceLink`] references PrivateGroup, /// Only used in [`ResourceLink`] references PublicImage, RelativeRotary, Room, Scene, ServiceGroup, SmartScene, #[serde(rename = "taurus_7455")] Taurus, Tamper, Temperature, ZgpConnectivity, ZigbeeConnectivity, ZigbeeDeviceDiscovery, Zone, } /// Manually implement Hash, so any future additions/reordering of [`RType`] /// does not affect output of [`RType::deterministic()`] impl Hash for RType { fn hash(&self, state: &mut H) { // these are all set in stone! // // never change any of these assignments. // // use a new unique number for future variants let index: u64 = match self { Self::AuthV1 => 0, Self::BehaviorInstance => 1, Self::BehaviorScript => 2, Self::Bridge => 3, Self::BridgeHome => 4, Self::Button => 5, Self::Device => 6, Self::DevicePower => 7, Self::DeviceSoftwareUpdate => 8, Self::Entertainment => 9, Self::EntertainmentConfiguration => 10, Self::GeofenceClient => 11, Self::Geolocation => 12, Self::GroupedLight => 13, Self::GroupedLightLevel => 14, Self::GroupedMotion => 15, Self::Homekit => 16, Self::Light => 17, Self::LightLevel => 18, Self::Matter => 19, Self::Motion => 20, Self::PrivateGroup => 21, Self::PublicImage => 22, Self::RelativeRotary => 23, Self::Room => 24, Self::Scene => 25, Self::SmartScene => 26, Self::Taurus => 27, Self::Temperature => 28, Self::ZigbeeConnectivity => 29, Self::ZigbeeDeviceDiscovery => 30, Self::Zone => 31, /* Added later, so not sorted alphabetically */ Self::CameraMotion => 32, Self::Contact => 33, Self::MatterFabric => 34, Self::ServiceGroup => 35, Self::Tamper => 36, Self::ZgpConnectivity => 37, }; index.hash(state); } } fn hash(t: &T) -> u64 { let mut s = SipHasher13::new(); t.hash(&mut s); s.finish() } impl RType { #[must_use] pub const fn link_to(self, rid: Uuid) -> ResourceLink { ResourceLink { rid, rtype: self } } #[must_use] pub fn deterministic(self, data: impl Hash) -> ResourceLink { /* hash resource type (i.e., self) */ let h1 = hash(&self); /* hash data */ let h2 = hash(&data); /* use resulting bytes for uuid seed */ let seed: &[u8] = &[h1.to_le_bytes(), h2.to_le_bytes()].concat(); let rid = Uuid::new_v5(&Uuid::NAMESPACE_OID, seed); self.link_to(rid) } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ResourceRecord { pub id: Uuid, #[serde(skip_serializing_if = "Option::is_none")] pub id_v1: Option, #[serde(flatten)] pub obj: Resource, } impl ResourceRecord { #[must_use] pub const fn new(id: Uuid, id_v1: Option, obj: Resource) -> Self { Self { id, id_v1, obj } } } #[derive(Copy, Hash, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct ResourceLink { pub rid: Uuid, pub rtype: RType, } impl ResourceLink { #[must_use] pub const fn new(rid: Uuid, rtype: RType) -> Self { Self { rid, rtype } } } impl Debug for ResourceLink { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // we need serde(rename_all = "snake_case") translation let rtype = serde_json::to_string(&self.rtype).unwrap(); let rid = self.rid; write!(f, "{}/{rid}", rtype.trim_matches('"')) } } #[cfg(test)] mod tests { use uuid::uuid; use crate::api::RType; #[test] fn rlink_hash_uses_input() { let a = RType::Room.deterministic("foo"); let b = RType::Room.deterministic("bar"); // these must be different - otherwise we forgot to use input assert_ne!(a, b); } #[test] fn rlink_hash_uses_rtype() { let a = RType::Room.deterministic("foo"); let b = RType::Scene.deterministic("foo"); // these must be different - otherwise we forgot to use type assert_ne!(a, b); } macro_rules! assert_hash { ($rtype:path, $uuid:expr) => { assert_eq!($rtype.deterministic("foo").rid, uuid!($uuid)); }; } #[test] fn rlink_hash_deterministic() { assert_hash!(RType::AuthV1, "9c9dc594-12c4-5db8-bc01-3bd26c09cf0f"); assert_hash!(RType::Device, "fa83ad4c-fbd8-519c-b543-d7aaf2041c75"); assert_hash!(RType::Light, "020d5289-53f8-5051-ac97-7ea60043223e"); assert_hash!(RType::Room, "03585677-7f50-5379-b7a6-8c4d70d63c67"); assert_hash!(RType::GroupedLight, "b2126c4a-16e3-59f4-b11f-4c674c9130f5"); assert_hash!(RType::Scene, "02808610-c1ec-5774-8eaf-453b83cf1981"); assert_hash!(RType::Zone, "1cc85d96-7bb6-5e75-938c-df4207136480"); } }