Plugins and user-supplied code
Plugins in limabean are Clojure transducers, which may run early on raw directives before the booking algorithm, or later on fully booked directives.
In addition, arbitrary user-provided code may be loaded into the REPL (below).
With the newly added support for raw plugins, the previous internal plugins have been removed.
Plugins are referenced in the Beanfile by their namespace, e.g.
plugin "limabean.contrib.plugins.examples.magic-money"
A single argument may be supplied, which is a single Clojure value in a Beancount string, i.e. with escaped quotes, e.g.
plugin "limabean.contrib.plugins.examples.magic-money" "{:units 1000.00M :cur \"USD\" :acc \"Equity:Rich-American-Uncle\"}"
In fact, any Clojure value may be supplied, not necessarily a map. But it must match what the particular plugin is expecting.
Note that EDN does not support the # dispatch macro, so in particular regular expressions in plugin config in Beancount files must be written as tagged literals, e.g. #regex "i-am-a-regex".
The Clojure namespace must define one or both of the functions raw-xf and booked-xf, each of which is a function returning a Clojure transducer on raw or booked directives respectively.
Running plugins
Plugins are run automatically when loading a beanfile. Any errors resolving a particular plugin inhibit any further processing of the beanfile.
The original directives loaded from the file are available in the REPL as (:raw-directives *beans*), with the post-plugin raw directives available as (:raw-xf-directives *beans*), booked directives as (:booked-directives *beans*), and post-plugin booked directives as (:booked-xf-directives *beans*).
Any errors in actually running any plugin cause the loader to abort with whatever partial state was reached, for further investigation by the user using the REPL.
Core plugins
The core plugins previously available with OG Beancount are intended to be bundled with limabean. Currently this list comprises only auto-accounts. Adding to these is a work-in-progress. These plugins live in the beancount.plugins namespace.
Configuration required to resolve other plugins
Plugins are Clojure code, so they must be on the Java class-path in order to be resolvable at runtime. The limabean launcher supports running the clojure command line with the -Sdeps option to pass the required dependencies.
In order to run anything beyond the bundled plugins, the Clojure package containing the desired plugin should be passed in the environment variable LIMABEAN_CLJ_DEPS, as e.g. io.github.tesujimath/limabean-contrib {:mvn/version "0.1.0"}. This environment variable comprises a space-separated list of package name, co-ordinate pairs, that is without the {:deps {...} } wrapper.
See the Clojure deps reference for what is possible, which includes local directories and git repos.
The following examples make use of limabean-contrib as a source for plugins, but you are free to create your own. But do please consider contributing your plugins to limabean-contrib.
To use a specified version from Clojars: io.github.tesujimath/limabean-contrib {:mvn/version "0.1.0"}
To use a library directly from GitHub: io.github.tesujimath/limabean-contrib {:git/sha "bc55aa4105ca1b050fffe12301e1829c908a4689"} - in this case the GitHub organization and repo are inferred from the library name, unless overridden using :git/url.
To use a library from a local path: io.github.tesujimath/limabean-contrib {:local/root "/path/to/limabean-contrib"}
As always, run with limabean -v to see what is going on with the Clojure invocation.
Note: it is not possible to load additional plugins when running in the standalone mode, which uses java rather than clojure. This is essentially a constraint imposed by the Clojure tools.
Writing plugins
Plugin namespaces
A limabean plugin namespace is simply a Clojure namespace. Please avoid defining your own plugins in the limabean namespace, although limabean.contrib.plugins is a good choice if you want to contribute your plugin there (please do!). Otherwise, use your own domain.
The intention is that limabean.contrib.plugins is a place for development and refinement of plugins, which upon gaining stability may be promoted into the limabean.plugins itself.
Legacy plugins appear with their original names, e.g. beancount.plugins.auto-accounts. Because Clojure prefers hyphen to underscore in namespace names, any plugin name from a Beancount file containing underscores gets changed to hyphens before resolving as a Clojure namespace. Therefore, such plugins may continue to be referenced by their original names from Beancount files.
Errors
Any errors detected by plugins may be reported using the limabean.plugin/dct-error! macro, as shown for example in the limabean.test.plugins.fail plugin, which annotates the directive emitted with the error indication.
Errors reported to the user do not include a full stack trace, but this may be found in *exception*.
Testing
Plugins are tested using the limabean-test library, which compares actual output with pre-generated golden test output.
Each test comprises a Beancount file with a sibling golden output directory, e.g. test-my-plugin.beancount and test-my-plugin.golden. The golden directory contains either or both of raw-xf-directives.edn and directives.edn, the former being the raw plugin output prior to booking.
These files may be generated by clojure -X:gen-golden, which rewrites all existing golden output files. So the process is to first create whichever output files are required (with any content), before running clojure -X:gen-golden.
In addition to each EDN file created, a corresponding .fyi.beancount is written, which contains the human-readable equivalent. This file is ignored during testing; it simply serves as documentation of plugin behaviour.
(The inventory, journal, and rollup files which may also appear in the golden directory are more of an application test than a plugin test.)
Examples
Set narration
The test plugin set-narration is the simplest possible plugin example, which overrides the narration field of each transaction according to its configuration, as in this example beancount file.
kiri> limabean --beanfile ./test-cases/set-narration-plugin-with-config.beancount
[Rebel readline] Type :repl/help for online help info
[limabean] 4 directives loaded from ./examples/beancount/set-narration-plugin.beancount
[limabean] 4 directives resulting from running plugins
user=> (show (journal))
2023-05-29 Expenses:Groceries New World Plugins rule ok! 10.00 NZD 10.00 NZD
2023-05-29 Assets:Bank:Current New World Plugins rule ok! -10.00 NZD
2023-05-30 Expenses:Groceries Countdown Plugins rule ok! 17.50 NZD 17.50 NZD
2023-05-30 Assets:Bank:Current Countdown Plugins rule ok! -17.50 NZD
:ok
Auto accounts
The original Beancount plugin auto_accounts has been implemented as a raw plugin.
Magic Money
The magic-money example is a more sophisticated plugin which inserts additional directives, namely a transaction after every open directive to add some money to the account, from a specified equity account. It works as a stateful transducer.
kiri> limabean --beanfile ./examples/beancount/magic-money-plugin.beancount
[Rebel readline] Type :repl/help for online help info
[limabean] 4 directives loaded from ./examples/beancount/magic-money-plugin.beancount
[limabean] 7 directives resulting from running plugins
user=> (show (journal))
2016-03-01 Equity:Rich-American-Uncle -1000.00 USD -1000.00 USD
2016-03-01 Assets:Bank:Current magical benefactor 1000.00 USD
2016-03-01 Equity:Rich-American-Uncle -1000.00 USD -1000.00 USD
2016-03-01 Expenses:Groceries magical benefactor 1000.00 USD
2023-05-29 Expenses:Groceries New World 10.00 NZD 10.00 NZD
2023-05-29 Assets:Bank:Current New World -10.00 NZD
2023-05-30 Expenses:Groceries Countdown 17.50 NZD 17.50 NZD
2023-05-30 Assets:Bank:Current Countdown -17.50 NZD
:ok
Running plugins manually
The resolved plugins are readily available in (:plugins *beans*), so may be applied manually.
user=> (:plugins *beans*)
[{:name "limabean.contrib.plugins.examples.set-narration",
:config "{:narration \"Plugins rule ok!\"}",
:booked-xf #object[limabean.contrib.plugins.examples.set_narration$booked_xf$fn__16968 0x1ecc1a99
"limabean.contrib.plugins.examples.set_narration$booked_xf$fn__16968@1ecc1a99"]}]
user=> (def set-narration-xf (get-in (:plugins *beans*) [0 :raw-xf]))
user=> (into [] set-narration-xf (:raw-directives *beans*))
[2016-03-01 open Assets:Bank:Current
2016-03-01 open Expenses:Groceries
2023-05-29 * "New World" "Plugins rule ok!"
Expenses:Groceries 10.00 NZD
Assets:Bank:Current
2023-05-30 * "Countdown" "Plugins rule ok!"
Expenses:Groceries 17.50 NZD
Assets:Bank:Current
]
With the newly added output formatting for directives, it is necessary to use pprint to see the underlying Clojure data structures.
user=> (pprint (into [] set-narration-xf (:raw-directives *beans*)))
[{:span [0 82 119],
:date #time/date "2016-03-01",
:dct :open,
:acc "Assets:Bank:Current"}
{:span [0 119 155],
:date #time/date "2016-03-01",
:dct :open,
:acc "Expenses:Groceries"}
{:span [0 155 238],
:date #time/date "2023-05-29",
:dct :txn,
:flag "*",
:payee "New World",
:postings
[{:span [0 185 214],
:acc "Expenses:Groceries",
:units 10.00M,
:cur "NZD"}
{:span [0 217 236], :acc "Assets:Bank:Current"}],
:narration "Plugins rule ok!"}
{:span [0 238 320],
:date #time/date "2023-05-30",
:dct :txn,
:flag "*",
:payee "Countdown",
:postings
[{:span [0 268 297],
:acc "Expenses:Groceries",
:units 17.50M,
:cur "NZD"}
{:span [0 300 319], :acc "Assets:Bank:Current"}],
:narration "Plugins rule ok!"}]
User-provided code
The user may provide their own Clojure code. The environment variable LIMABEAN_USER_CLJ is a colon-separated list of Clojure source files, which are loaded in order, and made available in the REPL. This facility is not suitable for plugins, because the functions are loaded too late. But it is a useful place for defining custom filters.
For a very simple example, see the user-supplied fy function for a customized financial year filter.