From e333881844dd75ab391d29b72796bf802b38be5a Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Wed, 22 Oct 2025 03:03:38 +0200 Subject: [PATCH] feat(config)!: Improve config module resolution The new system is slightly less flexible, but the code and lua definitions is now a lot simpler and easier to understand. In fact the old lua definition was not actually correct. It is likely that existing configs require not/minimal tweaks to work again. --- config/rooms.lua | 2 +- definitions/config.lua | 10 +- src/bin/automation.rs | 7 +- src/bin/generate_definitions.rs | 4 +- src/config.rs | 189 ++++++++++++++++++++++---------- 5 files changed, 145 insertions(+), 67 deletions(-) diff --git a/config/rooms.lua b/config/rooms.lua index 729be12..98fe996 100644 --- a/config/rooms.lua +++ b/config/rooms.lua @@ -1,4 +1,4 @@ ---- @type SetupTable +--- @type Module return { require("config.rooms.bathroom"), require("config.rooms.bedroom"), diff --git a/definitions/config.lua b/definitions/config.lua index db2d55d..32ed6aa 100644 --- a/definitions/config.lua +++ b/definitions/config.lua @@ -9,14 +9,16 @@ local FulfillmentConfig ---@class Config ---@field fulfillment FulfillmentConfig ----@field modules (Modules)? +---@field modules (Module)[] ---@field mqtt MqttConfig ---@field schedule (table)? local Config ----@alias SetupFunction fun(mqtt_client: AsyncClient): SetupTable? ----@alias SetupTable (DeviceInterface | { setup: SetupFunction? } | SetupTable)[] ----@alias Modules SetupFunction | SetupTable +---@class Module +---@field setup (fun(mqtt_client: AsyncClient): Module | DeviceInterface[] | nil)? +---@field devices (DeviceInterface)[]? +---@field [number] (Module)[]? +local Module ---@class MqttConfig ---@field host string diff --git a/src/bin/automation.rs b/src/bin/automation.rs index 4120120..8b759c3 100644 --- a/src/bin/automation.rs +++ b/src/bin/automation.rs @@ -140,10 +140,9 @@ async fn app() -> anyhow::Result<()> { let mqtt_client = mqtt::start(config.mqtt, &device_manager.event_channel()); - if let Some(modules) = config.modules { - for device in modules.setup(&lua, &mqtt_client).await? { - device_manager.add(device).await; - } + let resolved = config.modules.resolve(&lua, &mqtt_client).await?; + for device in resolved.devices { + device_manager.add(device).await; } start_scheduler(config.schedule).await?; diff --git a/src/bin/generate_definitions.rs b/src/bin/generate_definitions.rs index a2348f9..fbff60a 100644 --- a/src/bin/generate_definitions.rs +++ b/src/bin/generate_definitions.rs @@ -1,7 +1,7 @@ use std::fs::{self, File}; use std::io::Write; -use automation::config::{Config, FulfillmentConfig, Modules}; +use automation::config::{Config, FulfillmentConfig, Module as ConfigModule}; use automation_lib::Module; use automation_lib::mqtt::{MqttConfig, WrappedAsyncClient}; use lua_typed::Typed; @@ -35,7 +35,7 @@ fn config_definitions() -> String { output += "\n"; output += &Config::generate_full().expect("Config should have a definition"); output += "\n"; - output += &Modules::generate_full().expect("Setups should have a definition"); + output += &ConfigModule::generate_full().expect("Module should have a definition"); output += "\n"; output += &MqttConfig::generate_full().expect("MqttConfig should have a definition"); output += "\n"; diff --git a/src/config.rs b/src/config.rs index 0c537f8..1a3a945 100644 --- a/src/config.rs +++ b/src/config.rs @@ -34,85 +34,162 @@ pub struct FulfillmentConfig { pub port: u16, } +#[derive(Debug)] +pub struct SetupFunction(mlua::Function); + +impl Typed for SetupFunction { + fn type_name() -> String { + format!( + "fun(mqtt_client: {}): {} | DeviceInterface[] | nil", + WrappedAsyncClient::type_name(), + Module::type_name() + ) + } +} + +impl FromLua for SetupFunction { + fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result { + Ok(Self(FromLua::from_lua(value, lua)?)) + } +} + #[derive(Debug, Default)] -pub struct Modules(mlua::Value); +pub struct Module { + pub setup: Option, + pub devices: Vec>, + pub modules: Vec, +} -impl Modules { - pub async fn setup( - self, - lua: &mlua::Lua, - client: &WrappedAsyncClient, - ) -> mlua::Result>> { - let mut devices = Vec::new(); - let initial_table = match self.0 { - mlua::Value::Table(table) => table, - mlua::Value::Function(f) => f.call_async(client.clone()).await?, - _ => Err(mlua::Error::runtime(format!( - "Expected table or function, instead found: {}", - self.0.type_name() - )))?, - }; +// TODO: Add option to typed to rename field +impl Typed for Module { + fn type_name() -> String { + "Module".into() + } - let mut queue: VecDeque = [initial_table].into(); - loop { - let Some(table) = queue.pop_front() else { - break; - }; + fn generate_header() -> Option { + Some(format!("---@class {}\n", Self::type_name())) + } - for pair in table.pairs() { - let (name, value): (String, _) = pair?; + fn generate_members() -> Option { + Some(format!( + r#"---@field setup {} +---@field devices {}? +---@field [number] {}? +"#, + Option::::type_name(), + Vec::>::type_name(), + Vec::::type_name(), + )) + } - match value { - mlua::Value::Table(table) => queue.push_back(table), - mlua::Value::UserData(_) - if let Ok(device) = Box::from_lua(value.clone(), lua) => - { - devices.push(device); - } - mlua::Value::Function(f) if name == "setup" => { - let value: mlua::Value = f.call_async(client.clone()).await?; - if let Some(table) = value.as_table() { - queue.push_back(table.clone()); - } - } - _ => {} - } - } + fn generate_footer() -> Option { + let type_name = ::type_name(); + Some(format!("local {type_name}\n")) + } +} + +impl FromLua for Module { + fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result { + // When calling require it might return a result from the searcher indicating how the + // module was found, we want to ignore these entries. + // TODO: Find a better solution for this + if value.is_string() { + return Ok(Default::default()); } - Ok(devices) + let mlua::Value::Table(table) = value else { + return Err(mlua::Error::runtime(format!( + "Expected module table, instead found: {}", + value.type_name() + ))); + }; + + let setup = table.get("setup")?; + + let devices = table.get("devices").unwrap_or_else(|_| Vec::new()); + let mut modules = Vec::new(); + + for module in table.sequence_values::() { + modules.push(module?); + } + + Ok(Module { + setup, + devices, + modules, + }) + } +} + +#[derive(Debug, Default)] +pub struct Modules(Vec); + +impl Typed for Modules { + fn type_name() -> String { + Vec::::type_name() } } impl FromLua for Modules { - fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result { - Ok(Modules(value)) + fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result { + Ok(Self(FromLua::from_lua(value, lua)?)) } } -impl Typed for Modules { - fn type_name() -> String { - "Modules".into() - } +impl Modules { + pub async fn resolve( + self, + lua: &mlua::Lua, + client: &WrappedAsyncClient, + ) -> mlua::Result { + let mut modules: VecDeque<_> = self.0.into(); - fn generate_header() -> Option { - let type_name = Self::type_name(); - let client_type = WrappedAsyncClient::type_name(); + let mut devices = Vec::new(); - Some(format!( - r#"---@alias SetupFunction fun(mqtt_client: {client_type}): SetupTable? ----@alias SetupTable (DeviceInterface | {{ setup: SetupFunction? }} | SetupTable)[] ----@alias {type_name} SetupFunction | SetupTable -"#, - )) + loop { + let Some(module) = modules.pop_front() else { + break; + }; + + modules.extend(module.modules); + + if let Some(setup) = module.setup { + let result: mlua::Value = setup.0.call_async(client.clone()).await?; + + if result.is_nil() { + // We ignore nil results + } else if let Ok(d) = as FromLua>::from_lua(result.clone(), lua) + && !d.is_empty() + { + // This is a shortcut for the common pattern of setup functions that only + // return devices + devices.extend(d); + } else if let Ok(module) = FromLua::from_lua(result.clone(), lua) { + modules.push_back(module); + } else { + return Err(mlua::Error::runtime( + "Setup function returned data in an unexpected format", + )); + } + } + + devices.extend(module.devices); + } + + Ok(Resolved { devices }) } } +#[derive(Debug, Default)] +pub struct Resolved { + pub devices: Vec>, +} + #[derive(Debug, LuaDeviceConfig, Typed)] pub struct Config { pub fulfillment: FulfillmentConfig, #[device_config(from_lua, default)] - pub modules: Option, + pub modules: Modules, #[device_config(from_lua)] pub mqtt: MqttConfig, #[device_config(from_lua, default)]