7 releases (stable)
Uses new Rust 2024
| new 2.3.0 | May 11, 2026 |
|---|---|
| 2.2.0 | May 9, 2026 |
| 1.0.0 | May 6, 2026 |
| 0.2.0 | Apr 27, 2026 |
| 0.1.0 | Apr 23, 2026 |
#36 in Finance
Used in 2 crates
1MB
16K
SLoC
doppio
A typed compiler pipeline for Ledger plain-text accounting -- built to be embedded.
This is the library crate. For the dop command-line interface,
install doppio-cli instead;
it depends on this crate and ships the binary.
What it does
doppio parses .ledger (and .hledger, .journal) source files
through four sequential stages:
source text -> parse -> resolution -> elaboration -> .dop
It enforces double-entry balance and balance-assertion semantics at
elaboration time, then exposes the result as a strongly-typed
elaboration::Journal (a prost-generated Protocol Buffers message)
ready for queries, reporting, or serialisation to the .dop binary
format.
Use cases
- Bank import pipelines. Build
resolution::Transactionvalues programmatically from imported records (Mercury, Plaid, SimpleFIN, etc.) and run them througheval_transactionto validate balance before writing back to source. - Reporting tools. Compile a journal once with
compile, then serialise to a.dopfile withwrite_dopand load it in milliseconds withread_dopfor repeated queries. - Cross-language consumers. The
.dopbinary format is defined byproto/doppio.protoin the source repo. Any language with a Protocol Buffers binding can read.dopfiles directly without re-parsing the source.
Quick example
use doppio::resolution::{Context, Transaction, Posting};
use chrono::NaiveDate;
use rust_decimal::Decimal;
let txn = Transaction::new(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(), "Groceries")
.with_posting(Posting::new("Expenses:Food").with_amount((Decimal::from(50u32), "$")))
.with_posting(Posting::new("Assets:Checking"));
// Validate and elaborate -- the null posting on Assets:Checking is inferred as -$50.
let journal_txn = doppio::eval_transaction(txn, &Context::default())?;
Public API surface
The intended construction layer is resolution::Transaction /
resolution::Posting (builder API). The intended read layer is
elaboration::* (the prost-generated proto types) plus their
inherent ergonomic methods (Decimal::to_decimal(),
Amount::iter(), Posting::amount_in(commodity), etc.).
The Frontend trait pluralises the parser dispatch -- LedgerFrontend
and HledgerFrontend are shipped; new formats (e.g. Beancount) can
implement the trait without modifying the library.
Using doppio in tests
When writing tests that consume elaboration::Journal directly, use the
doppio::testing module to build fixtures without wrestling with proto3 quirks
(Option<Amount>, epoch-days dates, state: i32 enum casts). Enable it via
the testing feature in your dev-dependencies:
[dev-dependencies]
doppio = { version = "...", features = ["testing"] }
Then construct journals with the fluent builder:
use doppio::testing::{journal, txn, posting};
use chrono::NaiveDate;
let j = journal()
.with_txn(txn(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(), "Groceries")
.with_posting(posting("Expenses:Food").with_amount(50, "$"))
.with_posting(posting("Assets:Checking").with_amount(-50, "$")))
.build();
assert_eq!(j.transactions.len(), 1);
assert_eq!(j.transactions[0].postings.len(), 2);
The testing feature is intentionally excluded from default and production
builds so it does not bloat binary consumers.
Companion crates
doppio-cli-- thedopcommand-line interface (compile, balance, register, print, stats, accounts, commodities, with--exchange,--cleared,--depth,--format text|json|csv, etc.).doppio-categorize-- counter-account suggestions for new transactions, given an existing journal as the training corpus.
Documentation
- Repository -- README, CLI reference, supported feature matrix.
docs/-- proto evolution policy, exchange-rate semantics, supported features.proto/doppio.proto-- the canonical wire format with multilingual reassembly recipes.
License
ISC. See LICENSE.
Dependencies
~6–13MB
~165K SLoC