From 1d3aa269b7f8c4204caa08681560aefbd53e1a4e Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 1 Jun 2025 02:24:13 +1000 Subject: [PATCH] Stub implementation of Lua plugins --- libdrcr/Cargo.lock | 102 ++++++++ libdrcr/Cargo.toml | 1 + libdrcr/plugins/austax/austax.luau | 86 +++++++ libdrcr/plugins/libdrcr.luau | 141 +++++++++++ libdrcr/src/lib.rs | 3 +- libdrcr/src/main.rs | 22 +- libdrcr/src/plugin.rs | 389 +++++++++++++++++++++++++++++ libdrcr/src/reporting/types.rs | 32 ++- libdrcr/src/serde.rs | 47 +++- src-tauri/Cargo.lock | 110 ++++++-- src-tauri/src/libdrcr_austax.rs | 8 +- src-tauri/src/libdrcr_bridge.rs | 49 +++- src-tauri/tauri.conf.json | 5 +- 13 files changed, 949 insertions(+), 46 deletions(-) create mode 100644 libdrcr/plugins/austax/austax.luau create mode 100644 libdrcr/plugins/libdrcr.luau create mode 100644 libdrcr/src/plugin.rs diff --git a/libdrcr/Cargo.lock b/libdrcr/Cargo.lock index 0e7cf86..6f26379 100644 --- a/libdrcr/Cargo.lock +++ b/libdrcr/Cargo.lock @@ -109,6 +109,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -293,6 +303,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -680,12 +700,23 @@ dependencies = [ "downcast-rs", "dyn-clone", "indexmap", + "mlua", "serde", "serde_json", "sqlx", "tokio", ] +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.15" @@ -725,6 +756,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "luau0-src" +version = "0.12.3+luau663" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ae337c644bbf86a8d8e9ce3ee023311833d41741baf5e51acc31b37843aba1" +dependencies = [ + "cc", +] + [[package]] name = "md-5" version = "0.10.6" @@ -761,6 +801,37 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mlua" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0" +dependencies = [ + "bstr", + "either", + "erased-serde", + "libloading", + "mlua-sys", + "num-traits", + "parking_lot", + "rustc-hash", + "rustversion", + "serde", + "serde-value", +] + +[[package]] +name = "mlua-sys" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93" +dependencies = [ + "cc", + "cfg-if", + "luau0-src", + "pkg-config", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -823,6 +894,15 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "parking" version = "2.2.1" @@ -1007,6 +1087,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustversion" version = "1.0.20" @@ -1034,6 +1120,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -1513,6 +1609,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" diff --git a/libdrcr/Cargo.toml b/libdrcr/Cargo.toml index 1203fe4..1f32f3d 100644 --- a/libdrcr/Cargo.toml +++ b/libdrcr/Cargo.toml @@ -9,6 +9,7 @@ chrono = "0.4.41" downcast-rs = "2.0.1" dyn-clone = "1.0.19" indexmap = "2.9.0" +mlua = { version = "0.10", features = ["luau", "serialize"] } serde = "1.0.219" serde_json = "1.0.140" sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] } diff --git a/libdrcr/plugins/austax/austax.luau b/libdrcr/plugins/austax/austax.luau new file mode 100644 index 0000000..29aaf7b --- /dev/null +++ b/libdrcr/plugins/austax/austax.luau @@ -0,0 +1,86 @@ +--!strict +-- DrCr: Web-based double-entry bookkeeping framework +-- Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +local libdrcr = require('../libdrcr') + +function requires(args, context) + return { + { + name = 'CombineOrdinaryTransactions', + kind = 'BalancesBetween', + args = { DateStartDateEndArgs = { date_start = context.sofy_date, date_end = context.eofy_date } }, + } + } +end + +function after_init_graph(args, steps, add_dependency, context) + for i = 1, #steps do + local other = steps[i] + if other.name == 'AllTransactionsExceptEarningsToEquity' then + -- AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax + -- TODO: Only in applicable years + + local other_args: libdrcr.ReportingStepArgs + if other.product_kinds[1] == 'Transactions' then + other_args = 'VoidArgs' + else + other_args = other.args + end + + add_dependency(other, { + name = 'CalculateIncomeTax', + kind = other.product_kinds[1], + args = other_args, + }) + end + end +end + +function execute(args, context, get_product) + print('Stub: CombineOrdinaryTransactions.execute') + + local product = get_product({ + name = 'CombineOrdinaryTransactions', + kind = 'BalancesBetween', + args = { DateStartDateEndArgs = { date_start = context.sofy_date, date_end = context.eofy_date } } + }) + + print(libdrcr.repr(product)) + + return { + [{ name = 'CalculateIncomeTax', kind = 'Transactions', args = 'VoidArgs' }] = { + Transactions = { + transactions = {} + } + } + } +end + +local plugin: libdrcr.Plugin = { + name = 'austax', + reporting_steps = { + { + name = 'CalculateIncomeTax', + product_kinds = {'DynamicReport', 'Transactions'}, + requires = requires, + after_init_graph = after_init_graph, + execute = execute, + } + }, +} + +return plugin diff --git a/libdrcr/plugins/libdrcr.luau b/libdrcr/plugins/libdrcr.luau new file mode 100644 index 0000000..7287c71 --- /dev/null +++ b/libdrcr/plugins/libdrcr.luau @@ -0,0 +1,141 @@ +--!strict +-- DrCr: Web-based double-entry bookkeeping framework +-- Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +------------------------ +-- Plugin specific types + +-- Represents a libdrcr plugin specification and implementation +export type Plugin = { + name: string, + reporting_steps: {ReportingStep}, +} + +-- Specifies a ReportingStep provided by the plugin +export type ReportingStep = { + name: string, + product_kinds: {ReportingProductKind}, + + requires: ( + ReportingStepArgs, + ReportingContext + ) -> {ReportingProductId}, + + after_init_graph: ( + ReportingStepArgs, + {ReportingStepId}, -- steps + (ReportingStepId, ReportingProductId) -> (), -- add_dependency + ReportingContext + ) -> (), + + execute: ( + ReportingStepArgs, + ReportingContext, + (ReportingProductId) -> ReportingProduct -- get_product + ) -> {[ReportingProductId]: ReportingProduct}, +} + +------------------------- +-- libdrcr internal types + +export type ReportingContext = { + sofy_date: string, + eofy_date: string, + reporting_commodity: string, +} + +-- Accounting types + +export type Transaction = { + id: number | nil, + dt: string, + description: string, + postings: {Posting}, +} + +export type Posting = { + id: number | nil, + transaction_id: number | nil, + description: number | nil, + account: string, + quantity: number, + commodity: string, + quantity_ascost: number | nil, +} + +-- Reporting products + +export type ReportingProduct = { + BalancesAt: BalancesAt?, + BalancesBetween: BalancesBetween?, + DynamicReport: DynamicReport?, + Transactions: Transactions?, +} + +export type BalancesAt = any +export type BalancesBetween = any +export type DynamicReport = any +export type Transactions = { transactions: {Transaction} } + +export type ReportingProductId = { + name: string, + kind: ReportingProductKind, + args: ReportingStepArgs, +} + +export type ReportingProductKind = 'BalancesAt' | 'BalancesBetween' | 'DynamicReport' | 'Transactions' + +-- Reporting steps + +export type ReportingStepId = { + name: string, + product_kinds: {ReportingProductKind}, + args: ReportingStepArgs, +} + +-- Reporting step args + +export type ReportingStepArgs = 'VoidArgs' | { DateArgs: DateArgs } | { DateStartDateEndArgs: DateStartDateEndArgs } | { MultipleDateArgs: MultipleDateArgs } | { MultipleDateStartDateEndArgs: MultipleDateStartDateEndArgs } + +export type DateArgs = { date: string } +export type DateStartDateEndArgs = { date_start: string, date_end: string } +export type MultipleDateArgs = { dates: {DateArgs} } +export type MultipleDateStartDateEndArgs = { dates: {DateStartDateEndArgs} } + +----------------- +-- Module exports + +local libdrcr = {} + +function libdrcr.repr(value: any): string + local result = '' + if type(value) == 'table' then + result = result .. '{' + for k, v in pairs(value) do + result = result .. k .. ' = ' .. libdrcr.repr(v) .. ', ' + end + result = result .. '}' + elseif type(value) == 'string' then + result = result .. "'" .. value .. "'" + elseif type(value) == 'number' then + result = result .. value + else + result = result .. '??' + end + return result +end + +return libdrcr diff --git a/libdrcr/src/lib.rs b/libdrcr/src/lib.rs index 2cd4fc2..8a198e8 100644 --- a/libdrcr/src/lib.rs +++ b/libdrcr/src/lib.rs @@ -1,7 +1,8 @@ pub mod account_config; -pub mod austax; +//pub mod austax; pub mod db; pub mod model; +pub mod plugin; pub mod reporting; pub mod serde; pub mod util; diff --git a/libdrcr/src/main.rs b/libdrcr/src/main.rs index 87a6651..cbec874 100644 --- a/libdrcr/src/main.rs +++ b/libdrcr/src/main.rs @@ -38,12 +38,14 @@ async fn main() { // Initialise ReportingContext let mut context = ReportingContext::new( db_connection, + "plugins".to_string(), + vec!["austax.austax".to_string()], NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), "$".to_string(), ); + libdrcr::plugin::register_lookup_fns(&mut context); libdrcr::reporting::steps::register_lookup_fns(&mut context); libdrcr::reporting::builders::register_dynamic_builders(&mut context); - libdrcr::austax::register_lookup_fns(&mut context); let context = Arc::new(context); @@ -109,16 +111,16 @@ async fn main() { .await .unwrap(); - let result = products - .get_or_err(&ReportingProductId { - name: "CalculateIncomeTax".to_string(), - kind: ReportingProductKind::DynamicReport, - args: ReportingStepArgs::VoidArgs, - }) - .unwrap(); + // let result = products + // .get_or_err(&ReportingProductId { + // name: "CalculateIncomeTax".to_string(), + // kind: ReportingProductKind::DynamicReport, + // args: ReportingStepArgs::VoidArgs, + // }) + // .unwrap(); - println!("Tax summary:"); - println!("{:?}", result); + // println!("Tax summary:"); + // println!("{:?}", result); let result = products .get_or_err(&ReportingProductId { diff --git a/libdrcr/src/plugin.rs b/libdrcr/src/plugin.rs new file mode 100644 index 0000000..00b0ad4 --- /dev/null +++ b/libdrcr/src/plugin.rs @@ -0,0 +1,389 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use std::fmt::Display; + +use async_trait::async_trait; +use chrono::NaiveDate; +use mlua::{FromLua, Function, Lua, LuaSerdeExt, Table, Value}; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use crate::reporting::calculator::ReportingGraphDependencies; +use crate::reporting::dynamic_report::DynamicReport; +use crate::reporting::executor::ReportingExecutionError; +use crate::reporting::types::{ + BalancesAt, BalancesBetween, ReportingContext, ReportingProduct, ReportingProductId, + ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, + Transactions, +}; +use crate::util::sofy_from_eofy; + +fn load_plugin(plugin_dir: &str, plugin_name: &str) -> (Lua, Plugin) { + let lua = Lua::new(); + + // Init Lua environment + let package = lua.globals().get::("package").unwrap(); + package + .set("path", format!("{}/?.luau", plugin_dir)) + .unwrap(); + + // Require and call the plugin + let require = lua.load("require").eval::().unwrap(); + let plugin = require.call::(plugin_name).expect("Lua error"); + + (lua, plugin) +} + +/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module +pub fn register_lookup_fns(context: &mut ReportingContext) { + for plugin_path in context.plugin_names.clone().iter() { + let (_, plugin) = load_plugin(&context.plugin_dir, plugin_path); + + for reporting_step in plugin.reporting_steps.iter() { + context.register_lookup_fn( + reporting_step.spec.name.clone(), + reporting_step.spec.product_kinds.clone(), + PluginReportingStep::takes_args, + PluginReportingStep::from_args, + ); + } + + context + .plugin_specs + .insert(plugin_path.clone(), plugin.into()); + } +} + +/// Represents a libdrcr plugin specification and implementation +#[derive(Debug)] +pub struct Plugin { + name: String, + reporting_steps: Vec, +} + +impl FromLua for Plugin { + fn from_lua(value: Value, _lua: &Lua) -> mlua::Result { + let value = value.as_table().unwrap(); + Ok(Self { + name: value.get("name")?, + reporting_steps: value.get("reporting_steps")?, + }) + } +} + +/// Represents a libdrcr plugin specification +#[derive(Debug, Deserialize, Serialize)] +pub struct PluginSpec { + name: String, + reporting_steps: Vec, +} + +impl From for PluginSpec { + fn from(value: Plugin) -> Self { + Self { + name: value.name, + reporting_steps: value.reporting_steps.into_iter().map(|s| s.spec).collect(), + } + } +} + +/// [ReportingStep] provided by the plugin specification and implementation +#[derive(Debug)] +pub struct LuaReportingStep { + spec: ReportingStepSpec, + requires: Function, + after_init_graph: Function, + execute: Function, +} + +impl FromLua for LuaReportingStep { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { + let value = value.as_table().unwrap(); + Ok(Self { + spec: ReportingStepSpec { + name: value.get("name")?, + product_kinds: lua.from_value(value.get("product_kinds")?)?, + }, + requires: value.get("requires")?, + after_init_graph: value.get("after_init_graph")?, + execute: value.get("execute")?, + }) + } +} + +/// [ReportingStep] provided by the plugin specification +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ReportingStepSpec { + name: String, + product_kinds: Vec, +} + +/// Represents a [ReportingProduct] which can be represented in Lua +#[derive(Deserialize, Serialize)] +enum LuaReportingProduct { + BalancesAt(BalancesAt), + BalancesBetween(BalancesBetween), + Transactions(Transactions), + DynamicReport(DynamicReport), +} + +impl Into for Box { + fn into(self) -> LuaReportingProduct { + if self.is::() { + LuaReportingProduct::BalancesAt(*self.downcast().unwrap()) + } else if self.is::() { + LuaReportingProduct::BalancesBetween(*self.downcast().unwrap()) + } else if self.is::() { + LuaReportingProduct::Transactions(*self.downcast().unwrap()) + } else if self.is::() { + LuaReportingProduct::DynamicReport(*self.downcast().unwrap()) + } else { + panic!("Attempt to convert unknown ReportingProduct type into LuaReportingProduct") + } + } +} + +impl Into> for LuaReportingProduct { + fn into(self) -> Box { + match self { + LuaReportingProduct::BalancesAt(product) => Box::new(product), + LuaReportingProduct::BalancesBetween(product) => Box::new(product), + LuaReportingProduct::Transactions(product) => Box::new(product), + LuaReportingProduct::DynamicReport(product) => Box::new(product), + } + } +} + +/// Represents subset of [ReportingContext] which is passed to Lua\ +#[derive(Deserialize, Serialize)] +struct LuaReportingContext { + #[serde(with = "crate::serde::naivedate_to_js")] + pub sofy_date: NaiveDate, + #[serde(with = "crate::serde::naivedate_to_js")] + pub eofy_date: NaiveDate, + pub reporting_commodity: String, +} + +impl LuaReportingContext { + fn from(context: &ReportingContext) -> Self { + Self { + sofy_date: sofy_from_eofy(context.eofy_date), + eofy_date: context.eofy_date, + reporting_commodity: context.reporting_commodity.clone(), + } + } +} + +/// Generic reporting step which is implemented by a plugin +#[derive(Debug)] +pub struct PluginReportingStep { + pub plugin_path: String, + pub spec: ReportingStepSpec, + pub args: ReportingStepArgs, // Currently only VoidArgs is supported +} + +impl PluginReportingStep { + fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool { + *args == ReportingStepArgs::VoidArgs + } + + fn from_args( + name: &str, + args: ReportingStepArgs, + context: &ReportingContext, + ) -> Box { + // Look up plugin + for (plugin_path, plugin_spec) in context.plugin_specs.iter() { + if let Some(reporting_step_spec) = + plugin_spec.reporting_steps.iter().find(|s| s.name == name) + { + return Box::new(Self { + plugin_path: plugin_path.to_string(), + spec: reporting_step_spec.clone(), + args, + }); + } + } + + panic!("No plugin provides step {}", name); + } +} + +impl Display for PluginReportingStep { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{PluginReportingStep}}", self.id())) + } +} + +#[async_trait] +impl ReportingStep for PluginReportingStep { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.spec.name.clone(), + product_kinds: self.spec.product_kinds.clone(), + args: self.args.clone(), + } + } + + fn requires(&self, context: &ReportingContext) -> Vec { + // Call to plugin + let (lua, plugin) = load_plugin(&context.plugin_dir, &self.plugin_path); + let plugin_step = plugin + .reporting_steps + .iter() + .find(|s| s.spec == self.spec) + .unwrap(); + + let result_table = plugin_step + .requires + .call::
(( + lua.to_value(&self.args).unwrap(), + lua.to_value(&LuaReportingContext::from(context)).unwrap(), + )) + .expect("Lua error"); + + // Convert result to Rust + let result = result_table + .sequence_values() + .map(|s| s.expect("Lua error")) + .map(|v| lua.from_value(v).expect("Deserialise error")) + .collect::>(); + + result + } + + fn after_init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + context: &ReportingContext, + ) { + // Load plugin + let (lua, plugin) = load_plugin(&context.plugin_dir, &self.plugin_path); + let plugin_step = plugin + .reporting_steps + .iter() + .find(|s| s.spec == self.spec) + .unwrap(); + + // Create a new scope since `add_dependency` depends on `dependencies` + lua.scope(|scope| { + // Init Lua environment + let add_dependency = scope.create_function_mut(|_, (step, product)| { + let step_id = lua.from_value::(step)?; + let product_id = lua.from_value::(product)?; + dependencies.add_dependency(step_id, product_id); + Ok(()) + })?; + + // Call to plugin + plugin_step.after_init_graph.call::(( + lua.to_value(&self.args).unwrap(), + lua.to_value(&steps.iter().map(|s| s.id()).collect::>()) + .unwrap(), + add_dependency, + lua.to_value(&LuaReportingContext::from(context)).unwrap(), + ))?; + + Ok(()) + }) + .expect("Lua error"); + } + + async fn execute( + &self, + context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &RwLock, + ) -> Result { + let products = products.read().await; + + // Load plugin + let (lua, plugin) = load_plugin(&context.plugin_dir, &self.plugin_path); + let plugin_step = plugin + .reporting_steps + .iter() + .find(|s| s.spec == self.spec) + .unwrap(); + + // Create a new scope since `get_product` depends on `products` + let result_table = lua + .scope(|scope| { + // Init Lua environment + let get_product = scope.create_function(|_, product| { + let product_id = lua.from_value::(product)?; + let product = products.get_or_err(&product_id).unwrap(); + let product_enum: LuaReportingProduct = product.clone().into(); + Ok(lua.to_value(&product_enum)) + })?; + + // Call to plugin + let result_table = plugin_step.execute.call::
(( + lua.to_value(&self.args).unwrap(), + lua.to_value(&LuaReportingContext::from(context)).unwrap(), + get_product, + ))?; + + Ok(result_table) + }) + .expect("Lua error"); + + // Convert to Rust + let mut products = ReportingProducts::new(); + for pair in result_table.pairs::() { + let pair = pair.expect("Lua error"); + let product_id = lua + .from_value::(pair.0) + .expect("Deserialise error"); + let product = lua + .from_value::(pair.1) + .expect("Deserialise error"); + + products.insert(product_id, product.into()); + } + + Ok(products) + } +} + +/// Format the [Table] as a string +fn _dbg_table(table: &Table) -> String { + format!( + "{{{}}}", + table + .pairs::() + .map(|p| p.expect("Lua error")) + .map(|(k, v)| format!( + "{}: {}", + if k.is_table() { + _dbg_table(k.as_table().unwrap()) + } else { + format!("{:?}", k) + }, + if v.is_table() { + _dbg_table(v.as_table().unwrap()) + } else { + format!("{:?}", v) + } + )) + .collect::>() + .join(", ") + ) +} diff --git a/libdrcr/src/reporting/types.rs b/libdrcr/src/reporting/types.rs index 961f9fc..bde0144 100644 --- a/libdrcr/src/reporting/types.rs +++ b/libdrcr/src/reporting/types.rs @@ -30,6 +30,7 @@ use tokio::sync::RwLock; use crate::db::DbConnection; use crate::model::transaction::TransactionWithPostings; +use crate::plugin::PluginSpec; use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; @@ -42,6 +43,8 @@ use super::executor::ReportingExecutionError; pub struct ReportingContext { // Configuration pub db_connection: DbConnection, + pub plugin_dir: String, + pub plugin_names: Vec, pub eofy_date: NaiveDate, pub reporting_commodity: String, @@ -51,21 +54,27 @@ pub struct ReportingContext { (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), >, pub(crate) step_dynamic_builders: Vec, + pub(crate) plugin_specs: HashMap, } impl ReportingContext { /// Initialise a new [ReportingContext] pub fn new( db_connection: DbConnection, + plugin_dir: String, + plugin_names: Vec, eofy_date: NaiveDate, reporting_commodity: String, ) -> Self { Self { db_connection, + plugin_dir, + plugin_names, eofy_date, reporting_commodity, step_lookup_fn: HashMap::new(), step_dynamic_builders: Vec::new(), + plugin_specs: HashMap::new(), } } @@ -139,7 +148,7 @@ pub struct ReportingStepDynamicBuilder { // REPORTING PRODUCTS /// Identifies a [ReportingProduct] -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ReportingProductId { pub name: String, pub kind: ReportingProductKind, @@ -155,7 +164,7 @@ impl Display for ReportingProductId { /// Identifies a type of [Box]ed [ReportingProduct] /// /// See [Box::downcast]. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum ReportingProductKind { /// The [Box]ed [ReportingProduct] is a [Transactions] Transactions, @@ -184,7 +193,7 @@ pub struct Transactions { impl ReportingProduct for Transactions {} /// Records cumulative account balances at a particular point in time -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct BalancesAt { pub balances: HashMap, } @@ -192,7 +201,7 @@ pub struct BalancesAt { impl ReportingProduct for BalancesAt {} /// Records the total value of transactions in each account between two points in time -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct BalancesBetween { pub balances: HashMap, } @@ -274,7 +283,7 @@ impl Display for ReportingProducts { // REPORTING STEPS /// Identifies a [ReportingStep] -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ReportingStepId { pub name: String, pub product_kinds: Vec, @@ -345,7 +354,7 @@ downcast_rs::impl_downcast!(ReportingStep); // REPORTING STEP ARGUMENTS /// Represents arguments to a [ReportingStep] -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum ReportingStepArgs { // This is an enum not a trait, to simply conversion to and from Lua /// [ReportingStepArgs] implementation which takes no arguments @@ -378,8 +387,9 @@ impl Display for ReportingStepArgs { } } -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct DateArgs { + #[serde(with = "crate::serde::naivedate_to_js")] pub date: NaiveDate, } @@ -399,9 +409,11 @@ impl Into for ReportingStepArgs { } } -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct DateStartDateEndArgs { + #[serde(with = "crate::serde::naivedate_to_js")] pub date_start: NaiveDate, + #[serde(with = "crate::serde::naivedate_to_js")] pub date_end: NaiveDate, } @@ -421,7 +433,7 @@ impl Into for ReportingStepArgs { } } -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct MultipleDateArgs { pub dates: Vec, } @@ -449,7 +461,7 @@ impl Into for ReportingStepArgs { } } -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct MultipleDateStartDateEndArgs { pub dates: Vec, } diff --git a/libdrcr/src/serde.rs b/libdrcr/src/serde.rs index bbeaafb..99ef85c 100644 --- a/libdrcr/src/serde.rs +++ b/libdrcr/src/serde.rs @@ -16,6 +16,51 @@ along with this program. If not, see . */ +/// Serialises [chrono::NaiveDate] in database format +/// +/// Use as `#[serde(with = "crate::serde::naivedate_to_js")]`, etc. +pub mod naivedate_to_js { + use std::fmt; + + use chrono::NaiveDate; + use serde::{ + de::{self, Unexpected, Visitor}, + Deserializer, Serializer, + }; + + pub(crate) fn serialize( + dt: &NaiveDate, + serializer: S, + ) -> Result { + serializer.serialize_str(&dt.format("%Y-%m-%d").to_string()) + } + + struct DateVisitor; + impl<'de> Visitor<'de> for DateVisitor { + type Value = NaiveDate; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a date string") + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + match NaiveDate::parse_from_str(s, "%Y-%m-%d") { + Ok(dt) => Ok(dt), + Err(_) => Err(de::Error::invalid_value(Unexpected::Str(s), &self)), + } + } + } + + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + deserializer.deserialize_str(DateVisitor) + } +} + /// Serialises [chrono::NaiveDateTime] in database format /// /// Use as `#[serde(with = "crate::serde::naivedatetime_to_js")]`, etc. @@ -40,7 +85,7 @@ pub mod naivedatetime_to_js { type Value = NaiveDateTime; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a date string") + write!(formatter, "a datetime string") } fn visit_str(self, s: &str) -> Result diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index baa75e3..a524e4e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -370,6 +370,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -878,7 +888,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading", + "libloading 0.8.8", ] [[package]] @@ -977,18 +987,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" -[[package]] -name = "dyn-eq" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388" - -[[package]] -name = "dyn-hash" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" - [[package]] name = "either" version = "1.13.0" @@ -2165,7 +2163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -2183,9 +2181,8 @@ dependencies = [ "chrono", "downcast-rs 2.0.1", "dyn-clone", - "dyn-eq", - "dyn-hash", "indexmap 2.9.0", + "mlua", "serde", "serde_json", "sqlx", @@ -2202,6 +2199,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.11" @@ -2257,6 +2264,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "luau0-src" +version = "0.12.3+luau663" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ae337c644bbf86a8d8e9ce3ee023311833d41741baf5e51acc31b37843aba1" +dependencies = [ + "cc", +] + [[package]] name = "mac" version = "0.1.1" @@ -2351,6 +2367,37 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mlua" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0" +dependencies = [ + "bstr", + "either", + "erased-serde", + "libloading 0.8.8", + "mlua-sys", + "num-traits", + "parking_lot", + "rustc-hash", + "rustversion", + "serde", + "serde-value", +] + +[[package]] +name = "mlua-sys" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93" +dependencies = [ + "cc", + "cfg-if", + "luau0-src", + "pkg-config", +] + [[package]] name = "muda" version = "0.15.3" @@ -2769,6 +2816,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3424,6 +3480,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3446,6 +3508,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.18" @@ -3549,6 +3617,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" diff --git a/src-tauri/src/libdrcr_austax.rs b/src-tauri/src/libdrcr_austax.rs index dd2e764..50fb0f5 100644 --- a/src-tauri/src/libdrcr_austax.rs +++ b/src-tauri/src/libdrcr_austax.rs @@ -18,15 +18,19 @@ use libdrcr::reporting::dynamic_report::DynamicReport; use libdrcr::reporting::types::{ReportingProductId, ReportingProductKind, ReportingStepArgs}; -use tauri::State; +use tauri::{AppHandle, State}; use tokio::sync::Mutex; use crate::libdrcr_bridge::get_report; use crate::AppState; #[tauri::command] -pub(crate) async fn get_tax_summary(state: State<'_, Mutex>) -> Result { +pub(crate) async fn get_tax_summary( + app: AppHandle, + state: State<'_, Mutex>, +) -> Result { Ok(get_report( + app, state, &ReportingProductId { name: "CalculateIncomeTax".to_string(), diff --git a/src-tauri/src/libdrcr_bridge.rs b/src-tauri/src/libdrcr_bridge.rs index a990cb0..0165748 100644 --- a/src-tauri/src/libdrcr_bridge.rs +++ b/src-tauri/src/libdrcr_bridge.rs @@ -30,18 +30,25 @@ use libdrcr::reporting::types::{ ReportingStepArgs, Transactions, }; use serde::{Deserialize, Serialize}; -use tauri::State; +use tauri::path::BaseDirectory; +use tauri::{AppHandle, Manager, State}; use tokio::sync::Mutex; use crate::AppState; fn prepare_reporting_context(context: &mut ReportingContext) { - libdrcr::austax::register_lookup_fns(context); libdrcr::reporting::steps::register_lookup_fns(context); libdrcr::reporting::builders::register_dynamic_builders(context); + libdrcr::plugin::register_lookup_fns(context); +} + +fn get_plugins() -> Vec { + // FIXME: Dynamically get this + vec!["austax.austax".to_string()] } pub(crate) async fn get_report( + app: AppHandle, state: State<'_, Mutex>, target: &ReportingProductId, ) -> Box { @@ -54,7 +61,18 @@ pub(crate) async fn get_report( // Initialise ReportingContext let eofy_date = db_connection.metadata().eofy_date; - let mut context = ReportingContext::new(db_connection, eofy_date, "$".to_string()); + let mut context = ReportingContext::new( + db_connection, + app.path() + .resolve("plugins", BaseDirectory::Resource) + .unwrap() + .to_str() + .unwrap() + .to_string(), + get_plugins(), + eofy_date, + "$".to_string(), + ); prepare_reporting_context(&mut context); // Get dynamic report @@ -75,9 +93,11 @@ pub(crate) async fn get_report( #[tauri::command] pub(crate) async fn get_all_transactions_except_earnings_to_equity( + app: AppHandle, state: State<'_, Mutex>, ) -> Result { let transactions = get_report( + app, state, &ReportingProductId { name: "AllTransactionsExceptEarningsToEquity".to_string(), @@ -97,10 +117,12 @@ pub(crate) async fn get_all_transactions_except_earnings_to_equity( #[tauri::command] pub(crate) async fn get_all_transactions_except_earnings_to_equity_for_account( + app: AppHandle, state: State<'_, Mutex>, account: String, ) -> Result { let transactions = get_report( + app, state, &ReportingProductId { name: "AllTransactionsExceptEarningsToEquity".to_string(), @@ -126,6 +148,7 @@ pub(crate) async fn get_all_transactions_except_earnings_to_equity_for_account( #[tauri::command] pub(crate) async fn get_balance_sheet( + app: AppHandle, state: State<'_, Mutex>, dates: Vec, ) -> Result { @@ -137,6 +160,7 @@ pub(crate) async fn get_balance_sheet( } Ok(get_report( + app, state, &ReportingProductId { name: "BalanceSheet".to_string(), @@ -154,6 +178,7 @@ pub(crate) async fn get_balance_sheet( #[tauri::command] pub(crate) async fn get_income_statement( + app: AppHandle, state: State<'_, Mutex>, dates: Vec<(String, String)>, ) -> Result { @@ -166,6 +191,7 @@ pub(crate) async fn get_income_statement( } Ok(get_report( + app, state, &ReportingProductId { name: "IncomeStatement".to_string(), @@ -183,12 +209,14 @@ pub(crate) async fn get_income_statement( #[tauri::command] pub(crate) async fn get_trial_balance( + app: AppHandle, state: State<'_, Mutex>, date: String, ) -> Result { let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").expect("Invalid date"); Ok(get_report( + app, state, &ReportingProductId { name: "TrialBalance".to_string(), @@ -211,6 +239,7 @@ struct ValidatedBalanceAssertion { #[tauri::command] pub(crate) async fn get_validated_balance_assertions( + app: AppHandle, state: State<'_, Mutex>, ) -> Result { let state = state.lock().await; @@ -233,8 +262,18 @@ pub(crate) async fn get_validated_balance_assertions( // Initialise ReportingContext let eofy_date = db_connection.metadata().eofy_date; - let mut context = - ReportingContext::new(db_connection, get_plugins(), eofy_date, "$".to_string()); + let mut context = ReportingContext::new( + db_connection, + app.path() + .resolve("plugins", BaseDirectory::Resource) + .unwrap() + .to_str() + .unwrap() + .to_string(), + get_plugins(), + eofy_date, + "$".to_string(), + ); prepare_reporting_context(&mut context); // Get report targets diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e824a48..591d634 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -26,6 +26,9 @@ "targets": "all", "icon": [ "icons/icon.png" - ] + ], + "resources": { + "../libdrcr/plugins/": "plugins/" + } } }