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/"
+ }
}
}