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.

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 will cause that plugin to be disabled (with an error message). See *plugins* to see what has been applied and what has not.

The original directives loaded from the file are available in the REPL as *booked-directives*, with the post-plugin ones available as *directives*. See the set-narration example below for how to use the original *booked-directives* instead of the post-plugin ones.

Any errors in actually running any plugin cause the whole pipeline to be discarded with an error message, in which case *directives* will be the same as *booked-directived*.

Configuration required to resolve 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.

The Clojure package containing the desired namespace 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 plugins when running in the standalone mode, which uses java rather than clojure.

Writing plugins

The plugin development framework is a work-in-progress. In particular, error handling and diagnostics are not yet addressed.

However, both raw and booked plugins are supported, as per the examples below.

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.

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 ../examples/beancount/set-narration-plugin.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

user=> (binding [*directives* *booked-directives*] (show (journal)))
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

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 the *plugins* map, so may be applied manually.

user=> *plugins*
[{: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* [0 :booked-xf]))

user=> (into [] set-narration-xf *booked-directives*)
[{:date #object[java.time.LocalDate 0x3922c5bc "2016-03-01"], :dct :open, :acc "Assets:Bank:Current"}
 {:date #object[java.time.LocalDate 0x63190b1 "2016-03-01"], :dct :open, :acc "Expenses:Groceries"}
 {:date #object[java.time.LocalDate 0x4325de9e "2023-05-29"], :dct :txn, :flag "*", :payee "New World",
  :postings [{:acc "Expenses:Groceries", :units 10.00M, :cur "NZD"}
             {:acc "Assets:Bank:Current", :units -10.00M, :cur "NZD"}], :narration "Plugins rule ok!"}
 {:date #object[java.time.LocalDate 0x1c0b38af "2023-05-30"], :dct :txn, :flag "*", :payee "Countdown",
  :postings [{:acc "Expenses:Groceries", :units 17.50M, :cur "NZD"}
             {:acc "Assets:Bank:Current", :units -17.50M, :cur "NZD"}], :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.