Rewrote impl_cast as a proc_macro to make it easier to work with

This commit is contained in:
Dreaded_X 2023-04-12 01:17:06 +02:00
parent b54c9512b9
commit ca8821b406
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
10 changed files with 243 additions and 121 deletions

44
Cargo.lock generated
View File

@ -31,7 +31,7 @@ checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -42,7 +42,7 @@ checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -292,7 +292,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -479,7 +479,8 @@ dependencies = [
name = "impl_cast" name = "impl_cast"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"paste", "quote",
"syn 2.0.13",
] ]
[[package]] [[package]]
@ -659,7 +660,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -682,18 +683,18 @@ checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.47" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.21" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -916,7 +917,7 @@ checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -947,7 +948,7 @@ checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -1022,6 +1023,17 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "sync_wrapper" name = "sync_wrapper"
version = "0.1.1" version = "0.1.1"
@ -1045,7 +1057,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -1098,7 +1110,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -1203,7 +1215,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
] ]
[[package]] [[package]]
@ -1342,7 +1354,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -1376,7 +1388,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.105",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View File

@ -11,15 +11,18 @@ members = [
[dependencies] [dependencies]
rumqttc = "0.18" rumqttc = "0.18"
serde = { version ="1.0.149", features = ["derive"] } serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89" serde_json = "1.0.89"
impl_cast = {path = "./impl_cast"} impl_cast = { path = "./impl_cast", features = ["debug"] }
google-home = {path = "./google-home"} google-home = { path = "./google-home" }
paste = "1.0.10" paste = "1.0.10"
tokio = { version = "1", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }
toml = "0.5.10" toml = "0.5.10"
dotenvy = "0.15.0" 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" axum = "0.6.1"
serde_repr = "0.1.10" serde_repr = "0.1.10"
tracing = "0.1.37" tracing = "0.1.37"
@ -30,10 +33,13 @@ regex = "1.7.0"
async-trait = "0.1.61" async-trait = "0.1.61"
async-recursion = "1.0.0" async-recursion = "1.0.0"
futures = "0.3.25" 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" thiserror = "1.0.38"
anyhow = "1.0.68" anyhow = "1.0.68"
wakey = "0.3.0" wakey = "0.3.0"
[profile.release] [profile.release]
lto=true lto = true

View File

@ -4,11 +4,12 @@ use crate::{
errors::{DeviceError, ErrorCode}, errors::{DeviceError, ErrorCode},
request::execute::CommandType, request::execute::CommandType,
response, response,
traits::{As, OnOff, Scene, Trait}, traits::{OnOff, Scene, Trait},
types::Type, types::Type,
}; };
pub trait GoogleHomeDevice: As<dyn OnOff> + As<dyn Scene> + 'static { #[impl_cast::device(As: OnOff + Scene)]
pub trait GoogleHomeDevice: Sync + Send + 'static {
fn get_device_type(&self) -> Type; fn get_device_type(&self) -> Type;
fn get_device_name(&self) -> Name; fn get_device_name(&self) -> Name;
fn get_id(&self) -> &str; fn get_id(&self) -> &str;

View File

@ -1,6 +1,6 @@
use serde::Serialize; use serde::Serialize;
use crate::{device::GoogleHomeDevice, errors::ErrorCode}; use crate::errors::ErrorCode;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub enum Trait { pub enum Trait {
@ -10,11 +10,8 @@ pub enum Trait {
Scene, Scene,
} }
impl_cast::impl_setup!(); #[impl_cast::device_trait]
impl_cast::impl_cast!(GoogleHomeDevice, OnOff); pub trait OnOff {
impl_cast::impl_cast!(GoogleHomeDevice, Scene);
pub trait OnOff: std::fmt::Debug + Sync + Send + 'static {
fn is_command_only(&self) -> Option<bool> { fn is_command_only(&self) -> Option<bool> {
None None
} }
@ -28,7 +25,8 @@ pub trait OnOff: std::fmt::Debug + Sync + Send + 'static {
fn set_on(&mut self, on: bool) -> Result<(), ErrorCode>; 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<bool> { fn is_scene_reversible(&self) -> Option<bool> {
None None
} }

View File

@ -3,7 +3,13 @@ name = "impl_cast"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib]
proc-macro = true
[dependencies] [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

View File

@ -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] struct Attr {
macro_rules! impl_setup { name: Ident,
() => { traits: Vec<Path>,
pub trait As<T: ?Sized> { }
impl Parse for Attr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut traits = Vec::new();
let name = input.parse::<Ident>()?;
input.parse::<Token![:]>()?;
loop {
let ty = input.parse()?;
traits.push(ty);
if input.is_empty() {
break;
}
input.parse::<Token![+]>()?;
}
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<dyn Device> = Box::new(ExampleDevice {});
///
/// // Cast to the OnOff trait, which is implemented
/// let as_on_off = As::<dyn OnOff>::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::<dyn Brightness>::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::<dyn OnOff>::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<T: ?Sized + 'static> {
fn consume(self: Box<Self>) -> Option<Box<T>>; fn consume(self: Box<Self>) -> Option<Box<T>>;
fn cast(&self) -> Option<&T>; fn cast(&self) -> Option<&T>;
fn cast_mut(&mut self) -> Option<&mut T>; fn cast_mut(&mut self) -> Option<&mut T>;
} }
}; };
}
#[macro_export] traits.iter().for_each(|device_trait| {
macro_rules! impl_cast { interface.supertraits.push(TypeParamBound::Verbatim(quote! {
($base:ident, $trait:ident) => { #name<dyn #device_trait>
$crate::paste::paste! { }));
impl<T: $base + $trait> As<dyn $trait> for T { });
fn consume(self: Box<Self>) -> Option<Box<dyn $trait>> {
Some(self)
}
fn cast(&self) -> Option<&dyn $trait> { let interface_ident = format_ident!("{}", interface.ident);
Some(self) let impls = traits
} .iter()
.map(|device_trait| {
quote! {
impl<T> #name<dyn #device_trait> for T
where
T: #interface_ident + #device_trait + 'static,
{
fn consume(self: Box<Self>) -> Option<Box<dyn #device_trait>> {
Some(self)
}
fn cast_mut(&mut self) -> Option<&mut dyn $trait> { fn cast(&self) -> Option<&(dyn #device_trait + 'static)> {
Some(self) Some(self)
} }
fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> {
Some(self)
}
}
impl<T> #name<dyn #device_trait> for T
where
T: #interface_ident + ?Sized,
{
default fn consume(self: Box<Self>) -> Option<Box<dyn #device_trait>> {
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<T: $base> As<dyn $trait> for T { .fold(quote! {}, |acc, x| {
default fn consume(self: Box<Self>) -> Option<Box<dyn $trait>> { quote! {
None // Not sure if this is the right way to do this
} #acc
#x
default fn cast(&self) -> Option<&dyn $trait> {
None
}
default fn cast_mut(&mut self) -> Option<&mut dyn $trait> {
None
}
} }
} });
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()
} }

View File

@ -25,48 +25,18 @@ use crate::{
presence::{self, OnPresence}, presence::{self, OnPresence},
}; };
impl_cast::impl_setup!(); #[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + GoogleHomeDevice + OnOff)]
impl_cast::impl_cast!(Device, OnMqtt); pub trait Device: std::fmt::Debug + Sync + Send {
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<dyn GoogleHomeDevice>
+ As<dyn OnMqtt>
+ As<dyn OnPresence>
+ As<dyn OnDarkness>
+ As<dyn OnOff>
+ std::fmt::Debug
+ Sync
+ Send
+ 'static
{
fn get_id(&self) -> &str; fn get_id(&self) -> &str;
} }
// TODO: Add an inner type that we can wrap with Arc<RwLock<>> to make this type a little bit nicer // TODO: Add an inner type that we can wrap with Arc<RwLock<>> to make this type a little bit nicer
// to work with // to work with
#[derive(Debug)]
struct Devices { struct Devices {
devices: HashMap<String, Box<dyn Device>>, devices: HashMap<String, Box<dyn Device>>,
} }
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::<dyn $trait>::cast_mut(device.as_mut())
.map(|listener| (id.as_str(), listener))
}).collect()
}
}
};
}
#[derive(Debug)] #[derive(Debug)]
pub enum Command { pub enum Command {
Fullfillment { Fullfillment {
@ -165,7 +135,7 @@ impl Devices {
tx, tx,
} => { } => {
let result = let result =
google_home.handle_request(payload, &mut self.as_google_home_devices()); google_home.handle_request(payload, &mut self.get::<dyn GoogleHomeDevice>());
tx.send(result).ok(); tx.send(result).ok();
} }
Command::AddDevice { device, tx } => { Command::AddDevice { device, tx } => {
@ -181,41 +151,53 @@ impl Devices {
self.devices.insert(device.get_id().to_owned(), device); self.devices.insert(device.get_id().to_owned(), device);
} }
get_cast!(OnMqtt); fn get<T>(&mut self) -> HashMap<&str, &mut T>
get_cast!(OnPresence); where
get_cast!(OnDarkness); T: ?Sized + 'static,
get_cast!(GoogleHomeDevice); (dyn Device): As<T>,
{
self.devices
.iter_mut()
.filter_map(|(id, device)| As::<T>::cast_mut(device.as_mut()).map(|t| (id.as_str(), t)))
.collect()
}
} }
#[async_trait] #[async_trait]
impl OnMqtt for Devices { impl OnMqtt for Devices {
async fn on_mqtt(&mut self, message: &rumqttc::Publish) { async fn on_mqtt(&mut self, message: &rumqttc::Publish) {
self.as_on_mqtts().iter_mut().for_each(|(id, listener)| { self.get::<dyn OnMqtt>()
let _span = span!(Level::TRACE, "on_mqtt").entered(); .iter_mut()
trace!(id, "Handling"); .for_each(|(id, listener)| {
listener.on_mqtt(message).block_on(); let _span = span!(Level::TRACE, "on_mqtt").entered();
}) trace!(id, "Handling");
listener.on_mqtt(message).block_on();
})
} }
} }
#[async_trait] #[async_trait]
impl OnPresence for Devices { impl OnPresence for Devices {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
self.as_on_presences().iter_mut().for_each(|(id, device)| { self.get::<dyn OnPresence>()
let _span = span!(Level::TRACE, "on_presence").entered(); .iter_mut()
trace!(id, "Handling"); .for_each(|(id, device)| {
device.on_presence(presence).block_on(); let _span = span!(Level::TRACE, "on_presence").entered();
}) trace!(id, "Handling");
device.on_presence(presence).block_on();
})
} }
} }
#[async_trait] #[async_trait]
impl OnDarkness for Devices { impl OnDarkness for Devices {
async fn on_darkness(&mut self, dark: bool) { async fn on_darkness(&mut self, dark: bool) {
self.as_on_darknesss().iter_mut().for_each(|(id, device)| { self.get::<dyn OnDarkness>()
let _span = span!(Level::TRACE, "on_darkness").entered(); .iter_mut()
trace!(id, "Handling"); .for_each(|(id, device)| {
device.on_darkness(dark).block_on(); let _span = span!(Level::TRACE, "on_darkness").entered();
}) trace!(id, "Handling");
device.on_darkness(dark).block_on();
})
} }
} }

View File

@ -17,6 +17,7 @@ pub trait OnDarkness: Sync + Send + 'static {
pub type Receiver = watch::Receiver<bool>; pub type Receiver = watch::Receiver<bool>;
type Sender = watch::Sender<bool>; type Sender = watch::Sender<bool>;
#[derive(Debug)]
struct LightSensor { struct LightSensor {
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
min: isize, min: isize,

View File

@ -10,7 +10,8 @@ use rumqttc::{Event, EventLoop, Incoming, Publish};
use tokio::sync::broadcast; use tokio::sync::broadcast;
#[async_trait] #[async_trait]
pub trait OnMqtt: Sync + Send + 'static { #[impl_cast::device_trait]
pub trait OnMqtt {
async fn on_mqtt(&mut self, message: &Publish); async fn on_mqtt(&mut self, message: &Publish);
} }

View File

@ -19,6 +19,7 @@ pub trait OnPresence: Sync + Send + 'static {
pub type Receiver = watch::Receiver<bool>; pub type Receiver = watch::Receiver<bool>;
type Sender = watch::Sender<bool>; type Sender = watch::Sender<bool>;
#[derive(Debug)]
struct Presence { struct Presence {
devices: HashMap<String, bool>, devices: HashMap<String, bool>,
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,