Improved the internals of the LuaDeviceConfig macro and improve the
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 7m3s
Build and deploy automation_rs / Build Docker image (push) Successful in 39s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped

usability of the macro
This commit is contained in:
Dreaded_X 2024-04-26 04:53:45 +02:00
parent 396ac4a9f1
commit 31169d32eb
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
16 changed files with 226 additions and 274 deletions

1
Cargo.lock generated
View File

@ -110,6 +110,7 @@ dependencies = [
name = "automation_macro"
version = "0.1.0"
dependencies = [
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.60",

View File

@ -7,6 +7,7 @@ edition = "2021"
proc-macro = true
[dependencies]
itertools = "0.12.1"
proc-macro2 = "1.0.81"
quote = "1.0.36"
syn = { version = "2.0.60", features = ["extra-traits"] }
syn = { version = "2.0.60", features = ["extra-traits", "full"] }

View File

@ -1,7 +1,11 @@
use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::quote;
use quote::{quote, quote_spanned};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, DeriveInput, Token};
use syn::spanned::Spanned;
use syn::token::Paren;
use syn::{parenthesized, parse_macro_input, DeriveInput, Expr, LitStr, Result, Token};
#[proc_macro_derive(LuaDevice, attributes(config))]
pub fn lua_device_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
@ -53,129 +57,110 @@ fn impl_lua_device_macro(ast: &syn::DeriveInput) -> TokenStream {
gen
}
#[derive(Debug)]
enum Arg {
Flatten,
UserData,
Rename(String),
With(TokenStream),
Default(Option<syn::Ident>),
}
impl syn::parse::Parse for Arg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let arg = match input.parse::<syn::Ident>()?.to_string().as_str() {
"flatten" => Arg::Flatten,
"user_data" => Arg::UserData,
"rename" => {
input.parse::<Token![=]>()?;
let lit = input.parse::<syn::Lit>()?;
if let syn::Lit::Str(lit_str) = lit {
Arg::Rename(lit_str.value())
} else {
panic!("Expected literal string");
}
}
"with" => {
input.parse::<Token![=]>()?;
let lit = input.parse::<syn::Lit>()?;
if let syn::Lit::Str(lit_str) = lit {
let token_stream: TokenStream = lit_str.parse()?;
Arg::With(token_stream)
} else {
panic!("Expected literal string");
}
}
"default" => {
if input.parse::<Token![=]>().is_ok() {
let func = input.parse::<syn::Ident>()?;
Arg::Default(Some(func))
} else {
Arg::Default(None)
}
}
name => todo!("Handle unknown arg: {name}"),
};
Ok(arg)
}
mod kw {
syn::custom_keyword!(device_config);
syn::custom_keyword!(flatten);
syn::custom_keyword!(from_lua);
syn::custom_keyword!(rename);
syn::custom_keyword!(with);
syn::custom_keyword!(from);
syn::custom_keyword!(default);
}
#[derive(Debug)]
struct ArgsParser {
args: Punctuated<Arg, Token![,]>,
enum Argument {
Flatten {
_keyword: kw::flatten,
},
FromLua {
_keyword: kw::from_lua,
},
Rename {
_keyword: kw::rename,
_paren: Paren,
ident: LitStr,
},
With {
_keyword: kw::with,
_paren: Paren,
// TODO: Ideally we capture this better
expr: Expr,
},
From {
_keyword: kw::from,
_paren: Paren,
ty: syn::Type,
},
Default {
_keyword: kw::default,
},
DefaultExpr {
_keyword: kw::default,
_paren: Paren,
expr: Expr,
},
}
impl syn::parse::Parse for ArgsParser {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args = input.parse_terminated(Arg::parse, Token![,])?;
Ok(Self { args })
impl Parse for Argument {
fn parse(input: ParseStream) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(kw::flatten) {
Ok(Self::Flatten {
_keyword: input.parse()?,
})
} else if lookahead.peek(kw::from_lua) {
Ok(Self::FromLua {
_keyword: input.parse()?,
})
} else if lookahead.peek(kw::rename) {
let content;
Ok(Self::Rename {
_keyword: input.parse()?,
_paren: parenthesized!(content in input),
ident: content.parse()?,
})
} else if lookahead.peek(kw::with) {
let content;
Ok(Self::With {
_keyword: input.parse()?,
_paren: parenthesized!(content in input),
expr: content.parse()?,
})
} else if lookahead.peek(kw::from) {
let content;
Ok(Self::From {
_keyword: input.parse()?,
_paren: parenthesized!(content in input),
ty: content.parse()?,
})
} else if lookahead.peek(kw::default) {
let keyword = input.parse()?;
if input.peek(Paren) {
let content;
Ok(Self::DefaultExpr {
_keyword: keyword,
_paren: parenthesized!(content in input),
expr: content.parse()?,
})
} else {
Ok(Self::Default { _keyword: keyword })
}
} else {
Err(lookahead.error())
}
}
}
#[derive(Debug)]
struct Args {
flatten: bool,
user_data: bool,
rename: Option<String>,
with: Option<TokenStream>,
default: Option<Option<syn::Ident>>,
args: Punctuated<Argument, Token![,]>,
}
impl Args {
fn new(args: Vec<Arg>) -> Self {
let mut result = Args {
flatten: false,
user_data: false,
rename: None,
with: None,
default: None,
};
for arg in args {
match arg {
Arg::Flatten => {
if result.flatten {
panic!("Option 'flatten' is already set")
}
result.flatten = true
}
Arg::UserData => {
if result.flatten {
panic!("Option 'user_data' is already set")
}
result.user_data = true
}
Arg::Rename(name) => {
if result.rename.is_some() {
panic!("Option 'rename' is already set")
}
result.rename = Some(name)
}
Arg::With(ty) => {
if result.with.is_some() {
panic!("Option 'with' is already set")
}
result.with = Some(ty)
}
Arg::Default(func) => {
if result.default.is_some() {
panic!("Option 'default' is already set")
}
result.default = Some(func)
}
}
}
if result.flatten && result.user_data {
panic!("The options 'flatten' and 'user_data' conflict with each other")
}
if result.flatten && result.default.is_some() {
panic!("The options 'flatten' and 'default' conflict with each other")
}
result
impl Parse for Args {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self {
args: input.parse_terminated(Argument::parse, Token![,])?,
})
}
}
@ -186,12 +171,8 @@ pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::T
impl_lua_device_config_macro(&ast).into()
}
// struct Args
fn impl_lua_device_config_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
// TODO: Handle errors properly
// This includes making sure one, and only one config is specified
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
..
@ -199,72 +180,91 @@ fn impl_lua_device_config_macro(ast: &syn::DeriveInput) -> TokenStream {
{
named
} else {
unimplemented!("Macro can only handle named structs");
return quote_spanned! {ast.span() => compile_error!("This macro only works on named structs")};
};
let fields: Vec<_> = fields
let field_names: Vec<_> = fields
.iter()
.map(|field| {
let field_name = field.ident.clone().unwrap();
let args: Vec<_> = field
.attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("device_config") {
let args: ArgsParser = attr.parse_args().unwrap();
Some(args.args)
} else {
None
}
})
.flatten()
.collect();
.map(|field| field.ident.clone().unwrap())
.collect();
let args = Args::new(args);
let fields: Vec<_> = fields
.iter()
.map(|field| {
let field_name = field.ident.clone().unwrap();
let (args, errors): (Vec<_>, Vec<_>) = field
.attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("device_config") {
Some(attr.parse_args::<Args>().map(|args| args.args))
} else {
None
}
})
.partition_result();
let table_name = if let Some(name) = args.rename {
name
} else {
field_name.to_string()
};
let errors: Vec<_> = errors
.iter()
.map(|error| error.to_compile_error())
.collect();
// TODO: Improve how optional fields are detected
let optional = if let syn::Type::Path(path) = field.ty.clone() {
path.path.segments.first().unwrap().ident == "Option"
} else {
false
if !errors.is_empty() {
return quote! { #(#errors)* };
}
let args: Vec<_> = args.into_iter().flatten().collect();
let table_name = match args
.iter()
.filter_map(|arg| match arg {
Argument::Rename { ident, .. } => Some(ident.value()),
_ => None,
})
.collect::<Vec<_>>()
.as_slice()
{
[] => field_name.to_string(),
[rename] => rename.to_owned(),
_ => return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'rename'")},
};
let default = if optional {
quote! { None }
} else if let Some(func) = args.default {
if func.is_some() {
quote! { #func() }
} else {
quote! { Default::default() }
}
} else {
let missing = format!("Missing field '{table_name}'");
quote! { panic!(#missing) }
};
// TODO: Detect Option<_> properly and use Default::default() as fallback automatically
let missing = format!("Missing field '{table_name}'");
let default = match args
.iter()
.filter_map(|arg| match arg {
Argument::Default { .. } => Some(quote! { Default::default() }),
Argument::DefaultExpr { expr, .. } => Some(quote! { (#expr) }),
_ => None,
})
.collect::<Vec<_>>()
.as_slice()
{
[] => quote! {panic!(#missing)},
[default] => default.to_owned(),
_ => return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'default'")},
};
let value = if args.flatten {
// println!("ValueFlatten: {}", field_name);
quote! {
mlua::LuaSerdeExt::from_value_with(lua, value.clone(), mlua::DeserializeOptions::new().deny_unsupported_types(false))?
}
} else if args.user_data {
// println!("UserData: {}", field_name);
quote! {
if table.contains_key(#table_name)? {
table.get(#table_name)?
} else {
#default
}
}
} else {
// println!("Value: {}", field_name);
quote! {
let value = match args
.iter()
.filter_map(|arg| match arg {
Argument::Flatten { .. } => Some(quote! {
mlua::LuaSerdeExt::from_value_with(lua, value.clone(), mlua::DeserializeOptions::new().deny_unsupported_types(false))?
}),
Argument::FromLua { .. } => Some(quote! {
if table.contains_key(#table_name)? {
table.get(#table_name)?
} else {
#default
}
}),
_ => None,
})
.collect::<Vec<_>>()
.as_slice() {
[] => quote! {
{
let #field_name: mlua::Value = table.get(#table_name)?;
if !#field_name.is_nil() {
@ -273,34 +273,40 @@ fn impl_lua_device_config_macro(ast: &syn::DeriveInput) -> TokenStream {
#default
}
}
}
},
[value] => value.to_owned(),
_ => return quote_spanned! {field.span() => compile_error!("Only one of either 'flatten' or 'from_lua' is allowed")},
};
let value = if let Some(temp_type) = args.with {
if optional {
quote! {
let value = match args
.iter()
.filter_map(|arg| match arg {
Argument::From { ty, .. } => Some(quote! {
{
let temp: #temp_type = #value;
temp.map(|v| v.into())
}
}
} else {
quote! {
{
let temp: #temp_type = #value;
let temp: #ty = #value;
temp.into()
}
}
}
} else {
value
}),
Argument::With { expr, .. } => Some(quote! {
{
let temp = #value;
(#expr)(temp)
}
}),
_ => None,
})
.collect::<Vec<_>>()
.as_slice() {
[] => value,
[value] => value.to_owned(),
_ => return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'as'")},
};
quote! {
#field_name: #value
}
})
.collect();
quote! { #value }
})
.zip(field_names)
.map(|(value, name)| quote! { #name: #value })
.collect();
let gen = quote! {
impl<'lua> mlua::FromLua<'lua> for #name {
@ -312,7 +318,7 @@ fn impl_lua_device_config_macro(ast: &syn::DeriveInput) -> TokenStream {
Ok(#name {
#(#fields,)*
})
})
}
}

View File

@ -22,7 +22,7 @@ pub struct AirFilterConfig {
info: InfoConfig,
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
#[device_config(user_data)]
#[device_config(from_lua)]
client: WrappedAsyncClient,
}

View File

@ -15,9 +15,9 @@ use crate::messages::{RemoteAction, RemoteMessage};
pub struct AudioSetupConfig {
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
#[device_config(user_data)]
#[device_config(from_lua)]
mixer: WrappedDevice,
#[device_config(user_data)]
#[device_config(from_lua)]
speakers: WrappedDevice,
}

View File

@ -13,7 +13,6 @@ use crate::device_manager::{DeviceConfig, WrappedDevice};
use crate::devices::{As, DEFAULT_PRESENCE};
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::helper::DurationSeconds;
use crate::messages::{ContactMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
@ -23,7 +22,7 @@ use crate::traits::Timeout;
pub struct PresenceDeviceConfig {
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(with = "DurationSeconds")]
#[device_config(with(Duration::from_secs))]
pub timeout: Duration,
}
@ -44,9 +43,9 @@ impl From<TriggerDevicesHelper> for Vec<(WrappedDevice, bool)> {
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct TriggerConfig {
#[device_config(user_data, with = "TriggerDevicesHelper")]
#[device_config(from_lua, from(TriggerDevicesHelper))]
devices: Vec<(WrappedDevice, bool)>,
#[device_config(with = "Option<DurationSeconds>")]
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
pub timeout: Option<Duration>,
}
@ -54,11 +53,11 @@ pub struct TriggerConfig {
pub struct ContactSensorConfig {
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
#[device_config(user_data)]
#[device_config(from_lua)]
presence: Option<PresenceDeviceConfig>,
#[device_config(user_data)]
#[device_config(from_lua)]
trigger: Option<TriggerConfig>,
#[device_config(user_data)]
#[device_config(from_lua)]
client: WrappedAsyncClient,
}

View File

@ -14,7 +14,7 @@ use crate::mqtt::WrappedAsyncClient;
pub struct DebugBridgeConfig {
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(user_data)]
#[device_config(from_lua)]
client: WrappedAsyncClient,
}

View File

@ -9,7 +9,6 @@ use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence};
use crate::helper::Ipv4SocketAddr;
#[derive(Debug)]
pub enum Flag {
@ -25,7 +24,7 @@ pub struct FlagIDs {
#[derive(Debug, LuaDeviceConfig, Clone)]
pub struct HueBridgeConfig {
#[device_config(rename = "ip", with = "Ipv4SocketAddr<80>")]
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
pub addr: SocketAddr,
pub login: String,
pub flags: FlagIDs,

View File

@ -14,13 +14,12 @@ use crate::config::MqttDeviceConfig;
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::event::OnMqtt;
use crate::helper::Ipv4SocketAddr;
use crate::messages::{RemoteAction, RemoteMessage};
use crate::traits::Timeout;
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct HueGroupConfig {
#[device_config(rename = "ip", with = "Ipv4SocketAddr<80>")]
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
pub addr: SocketAddr,
pub login: String,
pub group_id: isize,

View File

@ -17,7 +17,6 @@ use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::helper::DurationSeconds;
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
@ -36,21 +35,17 @@ pub struct IkeaOutletConfig {
info: InfoConfig,
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
#[device_config(default = default_outlet_type)]
#[device_config(default(OutletType::Outlet))]
outlet_type: OutletType,
#[device_config(with = "Option<DurationSeconds>")]
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
timeout: Option<Duration>,
#[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>,
#[device_config(user_data)]
#[device_config(from_lua)]
client: WrappedAsyncClient,
}
fn default_outlet_type() -> OutletType {
OutletType::Outlet
}
#[async_trait]
impl DeviceConfig for IkeaOutletConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {

View File

@ -15,11 +15,10 @@ use tracing::trace;
use super::Device;
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::helper::Ipv4SocketAddr;
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct KasaOutletConfig {
#[device_config(rename = "ip", with = "Ipv4SocketAddr<9999>")]
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))]
addr: SocketAddr,
}

View File

@ -7,8 +7,7 @@ use crate::config::MqttDeviceConfig;
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{self, Event, OnMqtt};
use crate::helper::TxHelper;
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::BrightnessMessage;
#[derive(Debug, Clone, LuaDeviceConfig)]
@ -17,7 +16,7 @@ pub struct LightSensorConfig {
pub mqtt: MqttDeviceConfig,
pub min: isize,
pub max: isize,
#[device_config(rename = "event_channel", user_data, with = "TxHelper")]
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
}

View File

@ -24,14 +24,10 @@ pub struct WakeOnLANConfig {
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
mac_address: MacAddress,
#[device_config(default = default_broadcast_ip)]
#[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
broadcast_ip: Ipv4Addr,
}
fn default_broadcast_ip() -> Ipv4Addr {
Ipv4Addr::new(255, 255, 255, 255)
}
#[async_trait]
impl DeviceConfig for WakeOnLANConfig {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {

View File

@ -8,8 +8,7 @@ use super::{Device, Notification};
use crate::config::MqttDeviceConfig;
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::event::{self, Event, OnMqtt};
use crate::helper::TxHelper;
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::PowerMessage;
#[derive(Debug, Clone, LuaDeviceConfig)]
@ -18,7 +17,7 @@ pub struct WasherConfig {
mqtt: MqttDeviceConfig,
// Power in Watt
threshold: f32,
#[device_config(rename = "event_channel", user_data, with = "TxHelper")]
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
}

View File

@ -1,40 +0,0 @@
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use mlua::FromLua;
use serde::Deserialize;
use crate::event::{self, EventChannel};
#[derive(Debug, Deserialize)]
pub struct DurationSeconds(u64);
impl From<DurationSeconds> for Duration {
fn from(value: DurationSeconds) -> Self {
Self::from_secs(value.0)
}
}
#[derive(Debug, Deserialize)]
pub struct Ipv4SocketAddr<const PORT: u16>(Ipv4Addr);
impl<const PORT: u16> From<Ipv4SocketAddr<PORT>> for SocketAddr {
fn from(ip: Ipv4SocketAddr<PORT>) -> Self {
Self::from((ip.0, PORT))
}
}
#[derive(Debug, Clone)]
pub struct TxHelper(EventChannel);
impl<'lua> FromLua<'lua> for TxHelper {
fn from_lua(value: mlua::Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> {
Ok(TxHelper(mlua::FromLua::from_lua(value, lua)?))
}
}
impl From<TxHelper> for event::Sender {
fn from(value: TxHelper) -> Self {
value.0.get_tx()
}
}

View File

@ -7,7 +7,6 @@ pub mod device_manager;
pub mod devices;
pub mod error;
pub mod event;
pub mod helper;
pub mod messages;
pub mod mqtt;
pub mod schedule;