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 = [
"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",
]

View File

@ -13,13 +13,16 @@ members = [
rumqttc = "0.18"
serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89"
impl_cast = {path = "./impl_cast"}
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,7 +33,10 @@ 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"

View File

@ -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<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_name(&self) -> Name;
fn get_id(&self) -> &str;

View File

@ -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<bool> {
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<bool> {
None
}

View File

@ -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

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]
macro_rules! impl_setup {
() => {
pub trait As<T: ?Sized> {
struct Attr {
name: Ident,
traits: Vec<Path>,
}
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 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<T: $base + $trait> As<dyn $trait> for T {
fn consume(self: Box<Self>) -> Option<Box<dyn $trait>> {
traits.iter().for_each(|device_trait| {
interface.supertraits.push(TypeParamBound::Verbatim(quote! {
#name<dyn #device_trait>
}));
});
let interface_ident = format_ident!("{}", interface.ident);
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(&self) -> Option<&dyn $trait> {
fn cast(&self) -> Option<&(dyn #device_trait + 'static)> {
Some(self)
}
fn cast_mut(&mut self) -> Option<&mut dyn $trait> {
fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> {
Some(self)
}
}
impl<T: $base> As<dyn $trait> for T {
default fn consume(self: Box<Self>) -> Option<Box<dyn $trait>> {
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 $trait> {
default fn cast(&self) -> Option<&(dyn #device_trait + 'static)> {
None
}
default fn cast_mut(&mut self) -> Option<&mut dyn $trait> {
default fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> {
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()
}

View File

@ -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<dyn GoogleHomeDevice>
+ As<dyn OnMqtt>
+ As<dyn OnPresence>
+ As<dyn OnDarkness>
+ As<dyn OnOff>
+ 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<RwLock<>> to make this type a little bit nicer
// to work with
#[derive(Debug)]
struct Devices {
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)]
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::<dyn GoogleHomeDevice>());
tx.send(result).ok();
}
Command::AddDevice { device, tx } => {
@ -181,16 +151,24 @@ 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<T>(&mut self) -> HashMap<&str, &mut T>
where
T: ?Sized + 'static,
(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]
impl OnMqtt for Devices {
async fn on_mqtt(&mut self, message: &rumqttc::Publish) {
self.as_on_mqtts().iter_mut().for_each(|(id, listener)| {
self.get::<dyn OnMqtt>()
.iter_mut()
.for_each(|(id, listener)| {
let _span = span!(Level::TRACE, "on_mqtt").entered();
trace!(id, "Handling");
listener.on_mqtt(message).block_on();
@ -201,7 +179,9 @@ impl OnMqtt for Devices {
#[async_trait]
impl OnPresence for Devices {
async fn on_presence(&mut self, presence: bool) {
self.as_on_presences().iter_mut().for_each(|(id, device)| {
self.get::<dyn OnPresence>()
.iter_mut()
.for_each(|(id, device)| {
let _span = span!(Level::TRACE, "on_presence").entered();
trace!(id, "Handling");
device.on_presence(presence).block_on();
@ -212,7 +192,9 @@ impl OnPresence for Devices {
#[async_trait]
impl OnDarkness for Devices {
async fn on_darkness(&mut self, dark: bool) {
self.as_on_darknesss().iter_mut().for_each(|(id, device)| {
self.get::<dyn OnDarkness>()
.iter_mut()
.for_each(|(id, device)| {
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>;
type Sender = watch::Sender<bool>;
#[derive(Debug)]
struct LightSensor {
mqtt: MqttDeviceConfig,
min: isize,

View File

@ -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);
}

View File

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