use core::fmt;
use time::Date;
use super::entry::{EntryBody, byte_enum};
use super::{
AccountHandle, AccountPath, Amount, Balance, CaptureTime, Currency, Decode, DecodeError,
Document, Encode, EntryId, HoldAction, IdempotencyKey, Label, Layer, LedgerId, LedgerPolicy,
MAX_LEAF_BYTES, MemoCommitment, Rate, Reader, Side, Sink, ValueError, Writer, entry::Entry,
};
use crate::log::{Hash, leaf_hash};
use crate::model::Dimensions;
pub const FORMAT: &str = "doubleentry/1";
pub const BALANCE_LEAF_KIND: u8 = 0x81;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AccountKind {
Asset,
Liability,
Equity,
Income,
Expense,
}
impl fmt::Display for AccountKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
super::encoding::pad(
f,
match self {
Self::Asset => "asset",
Self::Liability => "liability",
Self::Equity => "equity",
Self::Income => "income",
Self::Expense => "expense",
},
)
}
}
impl AccountKind {
#[must_use]
pub const fn normal_side(self) -> Side {
match self {
Self::Asset | Self::Expense => Side::Debit,
Self::Liability | Self::Equity | Self::Income => Side::Credit,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum BalanceLimit {
Unlimited,
NoCreditBalance,
NoDebitBalance,
}
byte_enum!(AccountKind, "account kind" { Asset = 0, Liability = 1, Equity = 2, Income = 3, Expense = 4 });
byte_enum!(BalanceLimit, "balance limit" { Unlimited = 0, NoCreditBalance = 1, NoDebitBalance = 2 });
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum RecordKind {
Genesis = 1,
Account = 2,
Currency = 3,
Policy = 4,
Period = 5,
Entry = 6,
Clearing = 7,
Seal = 8,
}
impl RecordKind {
#[must_use]
pub const fn from_u8(byte: u8) -> Option<Self> {
Some(match byte {
1 => Self::Genesis,
2 => Self::Account,
3 => Self::Currency,
4 => Self::Policy,
5 => Self::Period,
6 => Self::Entry,
7 => Self::Clearing,
8 => Self::Seal,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Genesis {
pub ledger: LedgerId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountEvent {
pub action: AccountAction,
pub effective: Date,
pub actor: Label,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccountAction {
Register(AccountRegistration),
Change {
handle: AccountHandle,
change: AccountChange,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountRegistration {
pub handle: AccountHandle,
pub path: AccountPath,
pub kind: AccountKind,
pub class: Option<Label>,
pub open_from: Date,
pub open_to: Option<Date>,
pub limit: BalanceLimit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccountChange {
OpenTo(Option<Date>),
Limit(BalanceLimit),
Class(Option<Label>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CurrencyDeclaration {
pub code: Currency,
pub exponent: u8,
pub effective: Date,
pub actor: Label,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyEvent {
pub policy: LedgerPolicy,
pub effective: Date,
pub actor: Label,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeriodDefinition {
id: Label,
start: Date,
end: Date,
}
impl PeriodDefinition {
pub fn new(id: Label, start: Date, end: Date) -> Result<Self, ValueError> {
if start > end {
return Err(ValueError::OutOfRange);
}
Ok(Self { id, start, end })
}
#[must_use]
pub fn id(&self) -> &Label {
&self.id
}
#[must_use]
pub const fn start(&self) -> Date {
self.start
}
#[must_use]
pub const fn end(&self) -> Date {
self.end
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PostingRecord<const P: u8 = 2> {
pub handle: AccountHandle,
pub side: Side,
pub amount: Amount<P>,
pub currency: Currency,
pub layer: Layer,
pub dimensions: Dimensions,
}
impl<const P: u8> Encode for PostingRecord<P> {
fn encode<S: Sink>(&self, w: &mut Writer<S>) {
w.value(&self.handle);
w.value(&self.side);
w.value(&self.amount);
w.value(&self.currency);
w.value(&self.layer);
w.value(&self.dimensions);
}
}
impl<const P: u8> Decode for PostingRecord<P> {
fn decode(r: &mut Reader<'_>, _field: &'static str) -> Result<Self, DecodeError> {
Ok(Self {
handle: r.value("posting handle")?,
side: r.value("side")?,
amount: r.value("posting amount")?,
currency: r.value("posting currency")?,
layer: r.value("layer")?,
dimensions: r.value("dimensions")?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntryParts<const P: u8 = 2> {
pub key: IdempotencyKey,
pub booking_date: Date,
pub value_date: Date,
pub captured_at: Option<CaptureTime>,
pub kind: Option<Label>,
pub memo: Option<MemoCommitment>,
pub documents: Vec<Document>,
pub postings: Vec<PostingRecord<P>>,
pub reverses: Option<EntryId>,
pub hold: Option<HoldAction<P>>,
pub rates: Vec<Rate>,
}
impl<const P: u8> EntryParts<P> {
fn body(&self) -> EntryBody<'_, P, &[PostingRecord<P>], MemoCommitment> {
EntryBody {
key: self.key.as_str(),
booking_date: self.booking_date,
value_date: self.value_date,
captured_at: self.captured_at.as_ref(),
kind: self.kind.as_ref(),
memo: self.memo.as_ref(),
documents: &self.documents,
postings: &self.postings,
reverses: self.reverses.as_ref(),
hold: self.hold.as_ref(),
rates: &self.rates,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntryRecord<const P: u8 = 2>(EntryParts<P>);
fn at_most(found: usize, max: usize) -> Result<(), ValueError> {
if found > max {
Err(ValueError::TooLong { max, found })
} else {
Ok(())
}
}
fn strictly_ascending<K: Ord>(keys: impl IntoIterator<Item = K>) -> Result<(), ValueError> {
let mut previous: Option<K> = None;
for key in keys {
if let Some(previous) = &previous {
match previous.cmp(&key) {
core::cmp::Ordering::Less => {}
core::cmp::Ordering::Equal => return Err(ValueError::Duplicate),
core::cmp::Ordering::Greater => return Err(ValueError::NotSorted),
}
}
previous = Some(key);
}
Ok(())
}
fn fits_leaf(body: &impl Encode) -> Result<(), ValueError> {
let found = body.encoded_len().saturating_add(1);
if found > MAX_LEAF_BYTES {
Err(ValueError::TooLarge {
max: MAX_LEAF_BYTES,
found,
})
} else {
Ok(())
}
}
impl<const P: u8> EntryRecord<P> {
pub fn new(parts: EntryParts<P>) -> Result<Self, ValueError> {
at_most(parts.documents.len(), Entry::<P>::MAX_DOCUMENTS)?;
at_most(parts.postings.len(), Entry::<P>::MAX_POSTINGS)?;
at_most(parts.rates.len(), Entry::<P>::MAX_RATES)?;
strictly_ascending(parts.rates.iter().map(|rate| (rate.base(), rate.quote())))?;
fits_leaf(&parts.body())?;
Ok(Self(parts))
}
#[must_use]
pub fn parts(&self) -> &EntryParts<P> {
&self.0
}
#[must_use]
pub fn into_parts(self) -> EntryParts<P> {
self.0
}
#[must_use]
pub fn id(&self, ledger: &LedgerId) -> EntryId {
EntryId::derive(ledger, &self.0.key)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PostingRef {
pub entry: EntryId,
pub index: u16,
}
impl PostingRef {
#[must_use]
pub const fn at(entry: EntryId, index: u16) -> Self {
Self { entry, index }
}
}
impl Encode for PostingRef {
fn encode<S: Sink>(&self, w: &mut Writer<S>) {
w.value(&self.entry);
w.u16(self.index);
}
}
impl Decode for PostingRef {
fn decode(r: &mut Reader<'_>, field: &'static str) -> Result<Self, DecodeError> {
Ok(Self::at(r.value(field)?, r.u16()?))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClearingEvent<const P: u8 = 2> {
pub action: ClearingAction<P>,
pub date: Date,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ClearingAction<const P: u8 = 2> {
Apply {
key: IdempotencyKey,
debit: PostingRef,
credit: PostingRef,
amount: Amount<P>,
},
Reset {
key: IdempotencyKey,
clearing: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TreeHeadEntry<const P: u8 = 2> {
pub currency: Currency,
pub layer: Layer,
pub root: Hash,
pub totals: Balance<P>,
}
impl<const P: u8> Encode for TreeHeadEntry<P> {
fn encode<S: Sink>(&self, w: &mut Writer<S>) {
w.value(&self.currency);
w.value(&self.layer);
w.value(&self.root);
w.value(&self.totals);
}
}
impl<const P: u8> Decode for TreeHeadEntry<P> {
fn decode(r: &mut Reader<'_>, _field: &'static str) -> Result<Self, DecodeError> {
Ok(Self {
currency: r.value("tree currency")?,
layer: r.value("tree layer")?,
root: r.value("tree root")?,
totals: r.value("tree totals")?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SealParts<const P: u8 = 2> {
pub period: Label,
pub start: Date,
pub end: Date,
pub through: u64,
pub accounts: u32,
pub trees: Vec<TreeHeadEntry<P>>,
}
impl<const P: u8> Encode for SealParts<P> {
fn encode<S: Sink>(&self, w: &mut Writer<S>) {
w.value(&self.period);
w.date(self.start);
w.date(self.end);
w.u64(self.through);
w.u32(self.accounts);
w.seq(&self.trees);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Seal<const P: u8 = 2>(SealParts<P>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum SealOrderError {
#[error("the seal does not start the day after the earlier seal ends")]
NotContiguous,
#[error("the seal covers no record beyond the earlier seal")]
ThroughNotIncreasing,
#[error("fewer accounts than at the earlier seal")]
AccountsDecreased,
#[error("the {currency} {layer:?} tree of the earlier seal is missing")]
TreeMissing {
currency: Currency,
layer: Layer,
},
#[error("a {currency} {layer:?} total is smaller than at the earlier seal")]
TotalDecreased {
currency: Currency,
layer: Layer,
},
}
impl<const P: u8> fmt::Display for Seal<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parts = &self.0;
let trees = match parts.trees.len() {
1 => String::from("1 balance tree"),
count => format!("{count} balance trees"),
};
let text = format!(
"seal of {} ({} to {}) through position {}: {} accounts, {trees}",
parts.period, parts.start, parts.end, parts.through, parts.accounts
);
super::encoding::pad(f, &text)
}
}
impl<const P: u8> Seal<P> {
pub fn new(parts: SealParts<P>) -> Result<Self, ValueError> {
if parts.start > parts.end {
return Err(ValueError::OutOfRange);
}
strictly_ascending(parts.trees.iter().map(|tree| (tree.currency, tree.layer)))?;
fits_leaf(&parts)?;
Ok(Self(parts))
}
#[must_use]
pub fn parts(&self) -> &SealParts<P> {
&self.0
}
#[must_use]
pub fn tree(&self, currency: Currency, layer: Layer) -> Option<&TreeHeadEntry<P>> {
self.0
.trees
.binary_search_by_key(&(currency, layer), |tree| (tree.currency, tree.layer))
.ok()
.and_then(|index| self.0.trees.get(index))
}
pub fn check_follows(&self, earlier: &Self) -> Result<(), SealOrderError> {
let (this, earlier) = (&self.0, &earlier.0);
if earlier.end.next_day() != Some(this.start) {
return Err(SealOrderError::NotContiguous);
}
if this.through <= earlier.through {
return Err(SealOrderError::ThroughNotIncreasing);
}
if this.accounts < earlier.accounts {
return Err(SealOrderError::AccountsDecreased);
}
for before in &earlier.trees {
let (currency, layer) = (before.currency, before.layer);
let after = self
.tree(currency, layer)
.ok_or(SealOrderError::TreeMissing { currency, layer })?;
let (a, b) = (&after.totals, &before.totals);
if a.debit() < b.debit() || a.credit() < b.credit() || a.postings() < b.postings() {
return Err(SealOrderError::TotalDecreased { currency, layer });
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Record<const P: u8 = 2> {
Genesis(Genesis),
Account(AccountEvent),
Currency(CurrencyDeclaration),
Policy(PolicyEvent),
Period(PeriodDefinition),
Entry(EntryRecord<P>),
Clearing(ClearingEvent<P>),
Seal(Seal<P>),
}
impl<const P: u8> Record<P> {
#[must_use]
pub const fn kind(&self) -> RecordKind {
match self {
Self::Genesis(_) => RecordKind::Genesis,
Self::Account(_) => RecordKind::Account,
Self::Currency(_) => RecordKind::Currency,
Self::Policy(_) => RecordKind::Policy,
Self::Period(_) => RecordKind::Period,
Self::Entry(_) => RecordKind::Entry,
Self::Clearing(_) => RecordKind::Clearing,
Self::Seal(_) => RecordKind::Seal,
}
}
#[must_use]
pub fn to_leaf_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
#[must_use]
pub fn leaf_hash(&self) -> Hash {
leaf_hash(&self.to_leaf_bytes())
}
pub fn decode(leaf_bytes: &[u8]) -> Result<Self, DecodeError> {
if leaf_bytes.len() > MAX_LEAF_BYTES {
return Err(DecodeError::TooLarge {
max: MAX_LEAF_BYTES,
found: leaf_bytes.len(),
});
}
let mut r = Reader::new(leaf_bytes);
let kind = r.u8()?;
let record = match RecordKind::from_u8(kind).ok_or(DecodeError::UnknownKind(kind))? {
RecordKind::Genesis => Self::Genesis(decode_genesis::<P>(&mut r)?),
RecordKind::Account => Self::Account(decode_account(&mut r)?),
RecordKind::Currency => Self::Currency(decode_currency::<P>(&mut r)?),
RecordKind::Policy => Self::Policy(PolicyEvent {
policy: r.value("policy")?,
effective: r.date()?,
actor: r.value("actor")?,
}),
RecordKind::Period => {
let id = r.value("period id")?;
let (start, end) = (r.date()?, r.date()?);
Self::Period(PeriodDefinition::new(id, start, end).map_err(|error| {
DecodeError::Value {
field: "period",
error,
}
})?)
}
RecordKind::Entry => Self::Entry(decode_entry(&mut r)?),
RecordKind::Clearing => Self::Clearing(decode_clearing(&mut r)?),
RecordKind::Seal => Self::Seal(decode_seal(&mut r)?),
};
r.finish()?;
Ok(record)
}
}
impl<const P: u8> Encode for Record<P> {
fn encode<S: Sink>(&self, w: &mut Writer<S>) {
w.u8(self.kind() as u8);
match self {
Self::Genesis(genesis) => {
w.value(&genesis.ledger);
w.u8(P);
w.str(FORMAT);
}
Self::Account(event) => {
match &event.action {
AccountAction::Register(account) => {
w.u8(0);
w.value(&account.handle);
w.value(&account.path);
w.value(&account.kind);
w.opt(account.class.as_ref());
w.date(account.open_from);
w.opt(account.open_to.as_ref());
w.value(&account.limit);
}
AccountAction::Change { handle, change } => {
w.u8(1);
w.value(handle);
match change {
AccountChange::OpenTo(open_to) => {
w.u8(0);
w.opt(open_to.as_ref());
}
AccountChange::Limit(limit) => {
w.u8(1);
w.value(limit);
}
AccountChange::Class(class) => {
w.u8(2);
w.opt(class.as_ref());
}
}
}
}
w.date(event.effective);
w.value(&event.actor);
}
Self::Currency(declaration) => {
w.value(&declaration.code);
w.u8(declaration.exponent);
w.date(declaration.effective);
w.value(&declaration.actor);
}
Self::Policy(event) => {
w.value(&event.policy);
w.date(event.effective);
w.value(&event.actor);
}
Self::Period(period) => {
w.value(&period.id);
w.date(period.start);
w.date(period.end);
}
Self::Entry(entry) => w.value(&entry.0.body()),
Self::Clearing(clearing) => {
match &clearing.action {
ClearingAction::Apply {
key,
debit,
credit,
amount,
} => {
w.u8(0);
w.value(key);
w.value(debit);
w.value(credit);
w.value(amount);
}
ClearingAction::Reset { key, clearing } => {
w.u8(1);
w.value(key);
w.u64(*clearing);
}
}
w.date(clearing.date);
}
Self::Seal(seal) => w.value(&seal.0),
}
}
}
fn decode_genesis<const P: u8>(r: &mut Reader<'_>) -> Result<Genesis, DecodeError> {
let ledger = r.value("ledger")?;
let precision = r.u8()?;
if precision != P {
return Err(DecodeError::PrecisionMismatch {
expected: P,
found: precision,
});
}
if r.str("format")? != FORMAT {
return Err(DecodeError::UnknownFormat);
}
Ok(Genesis { ledger })
}
fn decode_account(r: &mut Reader<'_>) -> Result<AccountEvent, DecodeError> {
let action = match r.u8()? {
0 => AccountAction::Register(AccountRegistration {
handle: r.value("handle")?,
path: r.value("path")?,
kind: r.value("account kind")?,
class: r.opt("class", |r| r.value("class"))?,
open_from: r.date()?,
open_to: r.opt("open_to", Reader::date)?,
limit: r.value("balance limit")?,
}),
1 => {
let handle = r.value("handle")?;
let change = match r.u8()? {
0 => AccountChange::OpenTo(r.opt("open_to", Reader::date)?),
1 => AccountChange::Limit(r.value("balance limit")?),
2 => AccountChange::Class(r.opt("class", |r| r.value("class"))?),
tag => {
return Err(DecodeError::InvalidTag {
field: "account field",
tag,
});
}
};
AccountAction::Change { handle, change }
}
tag => {
return Err(DecodeError::InvalidTag {
field: "account action",
tag,
});
}
};
Ok(AccountEvent {
action,
effective: r.date()?,
actor: r.value("actor")?,
})
}
fn decode_currency<const P: u8>(r: &mut Reader<'_>) -> Result<CurrencyDeclaration, DecodeError> {
let code = r.value("currency")?;
let exponent = r.u8()?;
if exponent > P {
return Err(DecodeError::Value {
field: "exponent",
error: ValueError::ExponentAboveScale { exponent, scale: P },
});
}
Ok(CurrencyDeclaration {
code,
exponent,
effective: r.date()?,
actor: r.value("actor")?,
})
}
fn decode_seq<T>(
r: &mut Reader<'_>,
field: &'static str,
max: usize,
mut read: impl FnMut(&mut Reader<'_>) -> Result<T, DecodeError>,
) -> Result<Vec<T>, DecodeError> {
let count = r.count(field, max)?;
let mut items = Vec::new();
for _ in 0..count {
items.push(read(r)?);
}
Ok(items)
}
fn canonical(field: &'static str) -> impl Fn(ValueError) -> DecodeError {
move |error| match error {
ValueError::NotSorted | ValueError::Duplicate => DecodeError::NonCanonical { field },
error => DecodeError::Value { field, error },
}
}
fn decode_entry<const P: u8>(r: &mut Reader<'_>) -> Result<EntryRecord<P>, DecodeError> {
let parts = EntryParts {
key: r.value("key")?,
booking_date: r.date()?,
value_date: r.date()?,
captured_at: r.opt("captured_at", |r| r.value("captured_at"))?,
kind: r.opt("kind", |r| r.value("kind"))?,
memo: r.opt("memo", |r| r.value("memo"))?,
documents: decode_seq(r, "documents", Entry::<P>::MAX_DOCUMENTS, |r| {
r.value("document")
})?,
postings: decode_seq(r, "postings", Entry::<P>::MAX_POSTINGS, |r| {
r.value("posting")
})?,
reverses: r.opt("reverses", |r| r.value("reverses"))?,
hold: r.opt("hold", |r| r.value("hold"))?,
rates: decode_seq(r, "rates", Entry::<P>::MAX_RATES, |r| r.value("rate"))?,
};
EntryRecord::new(parts).map_err(canonical("rates"))
}
fn decode_clearing<const P: u8>(r: &mut Reader<'_>) -> Result<ClearingEvent<P>, DecodeError> {
let action = match r.u8()? {
0 => ClearingAction::Apply {
key: r.value("key")?,
debit: r.value("debit")?,
credit: r.value("credit")?,
amount: r.value("amount")?,
},
1 => ClearingAction::Reset {
key: r.value("key")?,
clearing: r.u64()?,
},
tag => {
return Err(DecodeError::InvalidTag {
field: "clearing action",
tag,
});
}
};
Ok(ClearingEvent {
action,
date: r.date()?,
})
}
fn decode_seal<const P: u8>(r: &mut Reader<'_>) -> Result<Seal<P>, DecodeError> {
let period = r.value("period id")?;
let start = r.date()?;
let end = r.date()?;
let through = r.u64()?;
let accounts = r.u32()?;
let max_trees = r.remaining().div_euclid(82);
let trees = decode_seq(r, "trees", max_trees, |r| r.value("tree"))?;
Seal::new(SealParts {
period,
start,
end,
through,
accounts,
trees,
})
.map_err(canonical("trees"))
}