hc_homie5_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Fields, ItemEnum, ItemStruct, Path};
4
5#[proc_macro_attribute]
6pub fn homie_device(_attr: TokenStream, item: TokenStream) -> TokenStream {
7    // Parse the input as a struct
8    let mut input = parse_macro_input!(item as ItemStruct);
9
10    // Ensure the struct has named fields
11    let fields = match &mut input.fields {
12        Fields::Named(fields_named) => &mut fields_named.named,
13        _ => {
14            return syn::Error::new_spanned(
15                input,
16                "implement_homie_device only supports structs with named fields",
17            )
18            .to_compile_error()
19            .into();
20        }
21    };
22
23    // Add the required fields for HomieDevice
24    fields.push(syn::parse_quote! { device_ref: DeviceRef });
25    fields.push(syn::parse_quote! { status: HomieDeviceStatus });
26    fields.push(syn::parse_quote! { device_desc: HomieDeviceDescription });
27    fields.push(syn::parse_quote! { homie_proto: Homie5DeviceProtocol });
28    fields.push(syn::parse_quote! { homie_client: HomieMQTTClient });
29
30    // Extract the struct name
31    let struct_name = &input.ident;
32
33    // Generate the necessary use statements
34    let use_statements = quote! {
35        use hc_homie5::{HomieDeviceCore, HomieMQTTClient};
36        use homie5::{
37            device_description::HomieDeviceDescription, Homie5DeviceProtocol, HomieDeviceStatus,
38            HomieDomain, HomieID, DeviceRef,
39        };
40    };
41
42    // Generate the default implementation for the HomieDevice trait
43    let trait_impl = quote! {
44        impl HomieDeviceCore for #struct_name {
45
46            fn homie_domain(&self) -> &HomieDomain {
47                self.device_ref.homie_domain()
48            }
49
50            fn homie_id(&self) -> &HomieID {
51                self.device_ref.device_id()
52            }
53
54            fn device_ref(&self) -> &DeviceRef {
55                &self.device_ref
56            }
57
58            fn description(&self) -> &HomieDeviceDescription {
59                &self.device_desc
60            }
61
62            fn client(&self) -> &HomieMQTTClient {
63                &self.homie_client
64            }
65
66            fn homie_proto(&self) -> &Homie5DeviceProtocol {
67                &self.homie_proto
68            }
69
70            fn state(&self) -> HomieDeviceStatus {
71                self.status
72            }
73
74            fn set_state(&mut self, state: HomieDeviceStatus) {
75                self.status = state;
76            }
77
78        }
79    };
80
81    // Combine the modified struct and the trait implementation
82    let expanded = quote! {
83        #use_statements
84
85        #input
86
87        #trait_impl
88    };
89
90    expanded.into()
91}
92
93/// Usage:
94///
95/// #[homie_device_enum(crate::error::AppError)]
96/// pub enum Devices {
97///     Generic(GenericDevice),
98///     Group(GroupDevice),
99/// }
100///
101/// Requirements:
102/// - Each variant is a tuple variant with exactly one field: Variant(InnerType)
103/// - Each inner type implements `hc_homie5::HomieDevice`
104/// - All inner types use the same error type `ErrorType`
105#[proc_macro_attribute]
106pub fn homie_device_enum(attr: TokenStream, item: TokenStream) -> TokenStream {
107    // Attribute is just a type path: #[homie_device_enum(ErrorType)]
108    // or #[homie_device_enum(crate::error::AppError)]
109    let error_ty: Path = parse_macro_input!(attr as Path);
110
111    // The enum we’re attached to
112    let input = parse_macro_input!(item as ItemEnum);
113    let enum_name = &input.ident;
114    let variants = &input.variants;
115
116    // Enforce tuple variants with exactly one field
117    for v in variants {
118        match &v.fields {
119            Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {}
120            _ => {
121                return syn::Error::new_spanned(
122                    v,
123                    "homie_device_enum only supports tuple variants with a single field, e.g. Variant(InnerType)",
124                )
125                .to_compile_error()
126                .into();
127            }
128        }
129    }
130
131    // Generate match arms for HomieDeviceCore
132    let homie_domain_arms = variants.iter().map(|v| {
133        let vident = &v.ident;
134        quote! { #enum_name::#vident(inner) => inner.homie_domain(), }
135    });
136
137    let homie_id_arms = variants.iter().map(|v| {
138        let vident = &v.ident;
139        quote! { #enum_name::#vident(inner) => inner.homie_id(), }
140    });
141
142    let device_ref_arms = variants.iter().map(|v| {
143        let vident = &v.ident;
144        quote! { #enum_name::#vident(inner) => inner.device_ref(), }
145    });
146
147    let description_arms = variants.iter().map(|v| {
148        let vident = &v.ident;
149        quote! { #enum_name::#vident(inner) => inner.description(), }
150    });
151
152    let client_arms = variants.iter().map(|v| {
153        let vident = &v.ident;
154        quote! { #enum_name::#vident(inner) => inner.client(), }
155    });
156
157    let proto_arms = variants.iter().map(|v| {
158        let vident = &v.ident;
159        quote! { #enum_name::#vident(inner) => inner.homie_proto(), }
160    });
161
162    let state_arms = variants.iter().map(|v| {
163        let vident = &v.ident;
164        quote! { #enum_name::#vident(inner) => inner.state(), }
165    });
166
167    let set_state_arms = variants.iter().map(|v| {
168        let vident = &v.ident;
169        quote! { #enum_name::#vident(inner) => inner.set_state(state), }
170    });
171
172    // Generate match arms for HomieDevice methods we need to delegate
173    let publish_prop_values_arms = variants.iter().map(|v| {
174        let vident = &v.ident;
175        quote! { #enum_name::#vident(inner) => inner.publish_property_values().await, }
176    });
177
178    let handle_set_command_arms = variants.iter().map(|v| {
179        let vident = &v.ident;
180        quote! {
181            #enum_name::#vident(inner) => inner.handle_set_command(property, set_value).await,
182        }
183    });
184
185    let publish_meta_arms = variants.iter().map(|v| {
186        let vident = &v.ident;
187        quote! { #enum_name::#vident(inner) => inner.publish_meta().await, }
188    });
189
190    // Final expanded code:
191    //  - original enum
192    //  - impl HomieDeviceCore for Enum
193    //  - impl HomieDevice for Enum
194    let expanded = quote! {
195        #input
196
197        impl hc_homie5::HomieDeviceCore for #enum_name {
198            fn homie_domain(&self) -> &homie5::HomieDomain {
199                match self {
200                    #(#homie_domain_arms)*
201                }
202            }
203
204            fn homie_id(&self) -> &homie5::HomieID {
205                match self {
206                    #(#homie_id_arms)*
207                }
208            }
209
210            fn device_ref(&self) -> &homie5::DeviceRef {
211                match self {
212                    #(#device_ref_arms)*
213                }
214            }
215
216            fn description(&self) -> &homie5::device_description::HomieDeviceDescription {
217                match self {
218                    #(#description_arms)*
219                }
220            }
221
222            fn client(&self) -> &hc_homie5::HomieMQTTClient {
223                match self {
224                    #(#client_arms)*
225                }
226            }
227
228            fn homie_proto(&self) -> &homie5::Homie5DeviceProtocol {
229                match self {
230                    #(#proto_arms)*
231                }
232            }
233
234            fn state(&self) -> homie5::HomieDeviceStatus {
235                match self {
236                    #(#state_arms)*
237                }
238            }
239
240            fn set_state(&mut self, state: homie5::HomieDeviceStatus) {
241                match self {
242                    #(#set_state_arms)*
243                }
244            }
245        }
246
247        impl hc_homie5::HomieDevice for #enum_name {
248            type ResultError = #error_ty;
249
250            fn publish_property_values(
251                &mut self,
252            ) -> impl std::future::Future<Output = Result<(), Self::ResultError>> + Send {
253                async move {
254                    match self {
255                        #(#publish_prop_values_arms)*
256                    }
257                }
258            }
259
260            fn handle_set_command(
261                &mut self,
262                property: &homie5::PropertyRef,
263                set_value: &str,
264            ) -> impl std::future::Future<Output = Result<(), Self::ResultError>> + Send {
265                async move {
266                    match self {
267                        #(#handle_set_command_arms)*
268                    }
269                }
270            }
271
272            fn publish_meta(
273                &mut self,
274            ) -> impl std::future::Future<Output = Result<(), Self::ResultError>> + Send {
275                async move {
276                    match self {
277                        #(#publish_meta_arms)*
278                    }
279                }
280            }
281        }
282    };
283
284    TokenStream::from(expanded)
285}