From ca8821b4060bdbde6d61901a9fdc9f47350057a6 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Wed, 12 Apr 2023 01:17:06 +0200 Subject: [PATCH] Rewrote impl_cast as a proc_macro to make it easier to work with --- Cargo.lock | 44 +++++---- Cargo.toml | 18 ++-- google-home/src/device.rs | 5 +- google-home/src/traits.rs | 12 ++- impl_cast/Cargo.toml | 10 ++- impl_cast/src/lib.rs | 182 +++++++++++++++++++++++++++++++------- src/devices.rs | 88 ++++++++---------- src/light_sensor.rs | 1 + src/mqtt.rs | 3 +- src/presence.rs | 1 + 10 files changed, 243 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31299c7..552b41c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,7 +31,7 @@ checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -42,7 +42,7 @@ checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -292,7 +292,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -479,7 +479,8 @@ dependencies = [ name = "impl_cast" version = "0.1.0" dependencies = [ - "paste", + "quote", + "syn 2.0.13", ] [[package]] @@ -659,7 +660,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -682,18 +683,18 @@ checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -916,7 +917,7 @@ checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -947,7 +948,7 @@ checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -1022,6 +1023,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "0.1.1" @@ -1045,7 +1057,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -1098,7 +1110,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -1203,7 +1215,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", ] [[package]] @@ -1342,7 +1354,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.105", "wasm-bindgen-shared", ] @@ -1376,7 +1388,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.105", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index f44cd79..18f46cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,18 @@ members = [ [dependencies] rumqttc = "0.18" -serde = { version ="1.0.149", features = ["derive"] } +serde = { version = "1.0.149", features = ["derive"] } serde_json = "1.0.89" -impl_cast = {path = "./impl_cast"} -google-home = {path = "./google-home"} +impl_cast = { path = "./impl_cast", features = ["debug"] } +google-home = { path = "./google-home" } paste = "1.0.10" tokio = { version = "1", features = ["rt-multi-thread"] } toml = "0.5.10" dotenvy = "0.15.0" -reqwest = { version = "0.11.13", features = ["json", "rustls-tls"], default-features = false } # Use rustls, since the other packages also use rustls +reqwest = { version = "0.11.13", features = [ + "json", + "rustls-tls", +], default-features = false } # Use rustls, since the other packages also use rustls axum = "0.6.1" serde_repr = "0.1.10" tracing = "0.1.37" @@ -30,10 +33,13 @@ regex = "1.7.0" async-trait = "0.1.61" async-recursion = "1.0.0" futures = "0.3.25" -eui48 = { version = "1.1.0", default-features = false, features = ["disp_hexstring", "serde"] } +eui48 = { version = "1.1.0", default-features = false, features = [ + "disp_hexstring", + "serde", +] } thiserror = "1.0.38" anyhow = "1.0.68" wakey = "0.3.0" [profile.release] -lto=true +lto = true diff --git a/google-home/src/device.rs b/google-home/src/device.rs index 00dc87f..2d919d3 100644 --- a/google-home/src/device.rs +++ b/google-home/src/device.rs @@ -4,11 +4,12 @@ use crate::{ errors::{DeviceError, ErrorCode}, request::execute::CommandType, response, - traits::{As, OnOff, Scene, Trait}, + traits::{OnOff, Scene, Trait}, types::Type, }; -pub trait GoogleHomeDevice: As + As + 'static { +#[impl_cast::device(As: OnOff + Scene)] +pub trait GoogleHomeDevice: Sync + Send + 'static { fn get_device_type(&self) -> Type; fn get_device_name(&self) -> Name; fn get_id(&self) -> &str; diff --git a/google-home/src/traits.rs b/google-home/src/traits.rs index 2597d98..2a0f12f 100644 --- a/google-home/src/traits.rs +++ b/google-home/src/traits.rs @@ -1,6 +1,6 @@ use serde::Serialize; -use crate::{device::GoogleHomeDevice, errors::ErrorCode}; +use crate::errors::ErrorCode; #[derive(Debug, Serialize)] pub enum Trait { @@ -10,11 +10,8 @@ pub enum Trait { Scene, } -impl_cast::impl_setup!(); -impl_cast::impl_cast!(GoogleHomeDevice, OnOff); -impl_cast::impl_cast!(GoogleHomeDevice, Scene); - -pub trait OnOff: std::fmt::Debug + Sync + Send + 'static { +#[impl_cast::device_trait] +pub trait OnOff { fn is_command_only(&self) -> Option { None } @@ -28,7 +25,8 @@ pub trait OnOff: std::fmt::Debug + Sync + Send + 'static { fn set_on(&mut self, on: bool) -> Result<(), ErrorCode>; } -pub trait Scene: std::fmt::Debug + Sync + Send + 'static { +#[impl_cast::device_trait] +pub trait Scene { fn is_scene_reversible(&self) -> Option { None } diff --git a/impl_cast/Cargo.toml b/impl_cast/Cargo.toml index bce1db2..4fb7d65 100644 --- a/impl_cast/Cargo.toml +++ b/impl_cast/Cargo.toml @@ -3,7 +3,13 @@ name = "impl_cast" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +proc-macro = true [dependencies] -paste = "1.0.10" +syn = { version = "2.0", features = ["extra-traits", "full"] } +quote = "1.0" + +[features] +debug = [ +] # If enabled it will add std::fmt::Debug as a trait bound to device_traits diff --git a/impl_cast/src/lib.rs b/impl_cast/src/lib.rs index d6adba2..38e75ad 100644 --- a/impl_cast/src/lib.rs +++ b/impl_cast/src/lib.rs @@ -1,47 +1,161 @@ -pub extern crate paste; +use proc_macro::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{parse::Parse, parse_macro_input, Ident, ItemTrait, Path, Token, TypeParamBound}; -#[macro_export] -macro_rules! impl_setup { - () => { - pub trait As { +struct Attr { + name: Ident, + traits: Vec, +} + +impl Parse for Attr { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut traits = Vec::new(); + + let name = input.parse::()?; + input.parse::()?; + + loop { + let ty = input.parse()?; + traits.push(ty); + + if input.is_empty() { + break; + } + + input.parse::()?; + } + + Ok(Attr { name, traits }) + } +} + +/// This macro enables optional trait bounds on a trait with an appropriate cast trait to convert +/// to the optional traits +/// # Example +/// +/// ``` +/// #![feature(specialization)] +/// +/// // Create some traits +/// #[impl_cast::device_trait] +/// trait OnOff {} +/// #[impl_cast::device_trait] +/// trait Brightness {} +/// +/// // Create the main device trait +/// #[impl_cast::device(As: OnOff + Brightness)] +/// trait Device {} +/// +/// // Create an implementation +/// struct ExampleDevice {} +/// impl Device for ExampleDevice {} +/// impl OnOff for ExampleDevice {} +/// +/// // Creates a boxed instance of the example device +/// let example_device: Box = Box::new(ExampleDevice {}); +/// +/// // Cast to the OnOff trait, which is implemented +/// let as_on_off = As::::cast(example_device.as_ref()); +/// assert!(as_on_off.is_some()); +/// +/// // Cast to the Brightness trait, which is not implemented +/// let as_on_off = As::::cast(example_device.as_ref()); +/// assert!(as_on_off.is_none()); +/// +/// // Finally we are going to consume the example device into an instance of the OnOff trait +/// let consumed = As::::consume(example_device); +/// assert!(consumed.is_some()) +/// ``` +#[proc_macro_attribute] +pub fn device(attr: TokenStream, item: TokenStream) -> TokenStream { + let Attr { name, traits } = parse_macro_input!(attr); + let mut interface: ItemTrait = parse_macro_input!(item); + + let prefix = quote! { + pub trait #name { fn consume(self: Box) -> Option>; fn cast(&self) -> Option<&T>; fn cast_mut(&mut self) -> Option<&mut T>; } }; -} -#[macro_export] -macro_rules! impl_cast { - ($base:ident, $trait:ident) => { - $crate::paste::paste! { - impl As for T { - fn consume(self: Box) -> Option> { - Some(self) - } + traits.iter().for_each(|device_trait| { + interface.supertraits.push(TypeParamBound::Verbatim(quote! { + #name + })); + }); - fn cast(&self) -> Option<&dyn $trait> { - Some(self) - } + let interface_ident = format_ident!("{}", interface.ident); + let impls = traits + .iter() + .map(|device_trait| { + quote! { + impl #name for T + where + T: #interface_ident + #device_trait + 'static, + { + fn consume(self: Box) -> Option> { + Some(self) + } - fn cast_mut(&mut self) -> Option<&mut dyn $trait> { - Some(self) - } + fn cast(&self) -> Option<&(dyn #device_trait + 'static)> { + Some(self) + } + + fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> { + Some(self) + } + } + + impl #name for T + where + T: #interface_ident + ?Sized, + { + default fn consume(self: Box) -> Option> { + None + } + + default fn cast(&self) -> Option<&(dyn #device_trait + 'static)> { + None + } + + default fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> { + None + } + } } - - impl As for T { - default fn consume(self: Box) -> Option> { - None - } - - default fn cast(&self) -> Option<&dyn $trait> { - None - } - - default fn cast_mut(&mut self) -> Option<&mut dyn $trait> { - None - } + }) + .fold(quote! {}, |acc, x| { + quote! { + // Not sure if this is the right way to do this + #acc + #x } - } + }); + + let tokens = quote! { + #interface + #prefix + #impls }; + + tokens.into() +} + +// TODO: Not sure if this makes sense to have? +/// This macro ensures that the device traits have the correct trait bounds +#[proc_macro_attribute] +pub fn device_trait(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut interface: ItemTrait = parse_macro_input!(item); + + interface.supertraits.push(TypeParamBound::Verbatim(quote! { + ::core::marker::Sync + ::core::marker::Send + 'static + })); + + #[cfg(feature = "debug")] + interface.supertraits.push(TypeParamBound::Verbatim(quote! { + ::std::fmt::Debug + })); + + interface.into_token_stream().into() } diff --git a/src/devices.rs b/src/devices.rs index c9494ec..1c12121 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -25,48 +25,18 @@ use crate::{ presence::{self, OnPresence}, }; -impl_cast::impl_setup!(); -impl_cast::impl_cast!(Device, OnMqtt); -impl_cast::impl_cast!(Device, OnPresence); -impl_cast::impl_cast!(Device, OnDarkness); -impl_cast::impl_cast!(Device, GoogleHomeDevice); -impl_cast::impl_cast!(Device, OnOff); - -pub trait Device: - As - + As - + As - + As - + As - + std::fmt::Debug - + Sync - + Send - + 'static -{ +#[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + GoogleHomeDevice + OnOff)] +pub trait Device: std::fmt::Debug + Sync + Send { fn get_id(&self) -> &str; } // TODO: Add an inner type that we can wrap with Arc> to make this type a little bit nicer // to work with +#[derive(Debug)] struct Devices { devices: HashMap>, } -macro_rules! get_cast { - ($trait:ident) => { - paste::paste! { - pub fn [< as_ $trait:snake s >](&mut self) -> HashMap<&str, &mut dyn $trait> { - self.devices - .iter_mut() - .filter_map(|(id, device)| { - As::::cast_mut(device.as_mut()) - .map(|listener| (id.as_str(), listener)) - }).collect() - } - } - }; -} - #[derive(Debug)] pub enum Command { Fullfillment { @@ -165,7 +135,7 @@ impl Devices { tx, } => { let result = - google_home.handle_request(payload, &mut self.as_google_home_devices()); + google_home.handle_request(payload, &mut self.get::()); tx.send(result).ok(); } Command::AddDevice { device, tx } => { @@ -181,41 +151,53 @@ impl Devices { self.devices.insert(device.get_id().to_owned(), device); } - get_cast!(OnMqtt); - get_cast!(OnPresence); - get_cast!(OnDarkness); - get_cast!(GoogleHomeDevice); + fn get(&mut self) -> HashMap<&str, &mut T> + where + T: ?Sized + 'static, + (dyn Device): As, + { + self.devices + .iter_mut() + .filter_map(|(id, device)| As::::cast_mut(device.as_mut()).map(|t| (id.as_str(), t))) + .collect() + } } #[async_trait] impl OnMqtt for Devices { async fn on_mqtt(&mut self, message: &rumqttc::Publish) { - self.as_on_mqtts().iter_mut().for_each(|(id, listener)| { - let _span = span!(Level::TRACE, "on_mqtt").entered(); - trace!(id, "Handling"); - listener.on_mqtt(message).block_on(); - }) + self.get::() + .iter_mut() + .for_each(|(id, listener)| { + let _span = span!(Level::TRACE, "on_mqtt").entered(); + trace!(id, "Handling"); + listener.on_mqtt(message).block_on(); + }) } } #[async_trait] impl OnPresence for Devices { async fn on_presence(&mut self, presence: bool) { - self.as_on_presences().iter_mut().for_each(|(id, device)| { - let _span = span!(Level::TRACE, "on_presence").entered(); - trace!(id, "Handling"); - device.on_presence(presence).block_on(); - }) + self.get::() + .iter_mut() + .for_each(|(id, device)| { + let _span = span!(Level::TRACE, "on_presence").entered(); + trace!(id, "Handling"); + device.on_presence(presence).block_on(); + }) } } #[async_trait] impl OnDarkness for Devices { async fn on_darkness(&mut self, dark: bool) { - self.as_on_darknesss().iter_mut().for_each(|(id, device)| { - let _span = span!(Level::TRACE, "on_darkness").entered(); - trace!(id, "Handling"); - device.on_darkness(dark).block_on(); - }) + self.get::() + .iter_mut() + .for_each(|(id, device)| { + let _span = span!(Level::TRACE, "on_darkness").entered(); + trace!(id, "Handling"); + device.on_darkness(dark).block_on(); + }) } } diff --git a/src/light_sensor.rs b/src/light_sensor.rs index 3eb52f5..8f3896c 100644 --- a/src/light_sensor.rs +++ b/src/light_sensor.rs @@ -17,6 +17,7 @@ pub trait OnDarkness: Sync + Send + 'static { pub type Receiver = watch::Receiver; type Sender = watch::Sender; +#[derive(Debug)] struct LightSensor { mqtt: MqttDeviceConfig, min: isize, diff --git a/src/mqtt.rs b/src/mqtt.rs index cd0134b..bd3ff5a 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -10,7 +10,8 @@ use rumqttc::{Event, EventLoop, Incoming, Publish}; use tokio::sync::broadcast; #[async_trait] -pub trait OnMqtt: Sync + Send + 'static { +#[impl_cast::device_trait] +pub trait OnMqtt { async fn on_mqtt(&mut self, message: &Publish); } diff --git a/src/presence.rs b/src/presence.rs index f8b6cfa..3fd20ae 100644 --- a/src/presence.rs +++ b/src/presence.rs @@ -19,6 +19,7 @@ pub trait OnPresence: Sync + Send + 'static { pub type Receiver = watch::Receiver; type Sender = watch::Sender; +#[derive(Debug)] struct Presence { devices: HashMap, mqtt: MqttDeviceConfig,