finproof 0.1.0

Deterministic financial programming language and verification engine.
#[derive(Debug, Clone, PartialEq)]
pub struct Program {
    pub declarations: Vec<Declaration>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Declaration {
    Currency(CurrencyDeclaration),
    Account(AccountDeclaration),
    Transaction(Transaction),
}

#[derive(Debug, Clone, PartialEq)]
pub struct CurrencyDeclaration {
    pub code: String,
}

#[derive(Debug, Clone, PartialEq)]
pub struct AccountDeclaration {
    pub name: String,
    pub account_type: AccountType,
    pub currency: String,
    pub initial_balance: crate::types::MoneyAmount,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AccountType {
    Asset,
    Liability,
    Equity,
    Revenue,
    Expense,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Transaction {
    pub name: String,
    pub statements: Vec<Statement>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Statement {
    Pay(Payment),
    Transfer(Transfer),
    Debit(Debit),
    Credit(Credit),
}

#[derive(Debug, Clone, PartialEq)]
pub struct Payment {
    pub amount: Expression,
    pub from: String,
    pub to: String,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Transfer {
    pub amount: Expression,
    pub from: String,
    pub to: String,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Debit {
    pub account: String,
    pub amount: Expression,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Credit {
    pub account: String,
    pub amount: Expression,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Expression {
    Money(MoneyLiteral),

    Binary {
        left: Box<Expression>,
        operator: BinaryOperator,
        right: Box<Expression>,
    },
}

#[derive(Debug, Clone, PartialEq)]
pub enum BinaryOperator {
    Add,
    Subtract,
}

#[derive(Debug, Clone, PartialEq)]
pub struct MoneyLiteral {
    pub amount: String,
    pub currency: String,
}

#[allow(dead_code)]
impl Program {
    pub fn canonical_representation(&self) -> String {
        self.declarations
            .iter()
            .map(Declaration::canonical_representation)
            .collect::<Vec<_>>()
            .join("\n")
    }
}

impl Declaration {
    fn canonical_representation(&self) -> String {
        match self {
            Self::Currency(currency) => {
                format!("Currency|code={}", currency.code)
            }

            Self::Account(account) => {
                let account_type = match account.account_type {
                    AccountType::Asset => "Asset",
                    AccountType::Liability => "Liability",
                    AccountType::Equity => "Equity",
                    AccountType::Revenue => "Revenue",
                    AccountType::Expense => "Expense",
                };

                format!(
                    "Account|name={}|account_type={}|currency={}|initial_balance_minor_units={}",
                    account.name,
                    account_type,
                    account.currency,
                    account.initial_balance.minor_units()
                )
            }

            Self::Transaction(transaction) => {
                let statements = transaction
                    .statements
                    .iter()
                    .map(Statement::canonical_representation)
                    .collect::<Vec<_>>()
                    .join(";");

                format!(
                    "Transaction|name={}|statements={}",
                    transaction.name, statements
                )
            }
        }
    }
}

impl Statement {
    fn canonical_representation(&self) -> String {
        match self {
            Self::Pay(payment) => format!(
                "Pay|from={}|to={}|amount={}",
                payment.from,
                payment.to,
                payment.amount.canonical_representation()
            ),

            Self::Transfer(transfer) => format!(
                "Transfer|from={}|to={}|amount={}",
                transfer.from,
                transfer.to,
                transfer.amount.canonical_representation()
            ),

            Self::Debit(debit) => format!(
                "Debit|account={}|amount={}",
                debit.account,
                debit.amount.canonical_representation()
            ),

            Self::Credit(credit) => format!(
                "Credit|account={}|amount={}",
                credit.account,
                credit.amount.canonical_representation()
            ),
        }
    }
}

impl Expression {
    fn canonical_representation(&self) -> String {
        match self {
            Self::Money(money) => {
                let amount = crate::types::MoneyAmount::from_decimal_str(&money.amount)
                    .expect("type checker guarantees valid monetary literals");

                format!(
                    "Money|amount_minor_units={}|currency={}",
                    amount.minor_units(),
                    money.currency
                )
            }

            Self::Binary {
                left,
                operator,
                right,
            } => {
                let operator = match operator {
                    BinaryOperator::Add => "Add",
                    BinaryOperator::Subtract => "Subtract",
                };

                format!(
                    "Binary|operator={}|left=[{}]|right=[{}]",
                    operator,
                    left.canonical_representation(),
                    right.canonical_representation()
                )
            }
        }
    }
}
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn creates_currency() {
        let currency = CurrencyDeclaration {
            code: "USD".to_string(),
        };

        assert_eq!(currency.code, "USD");
    }

    #[test]
    fn creates_asset_account() {
        let account = AccountDeclaration {
            name: "Cash".to_string(),
            account_type: AccountType::Asset,
            currency: "USD".to_string(),
            initial_balance: crate::types::MoneyAmount::from_minor_units(0),
        };

        assert_eq!(account.name, "Cash");
        assert_eq!(account.account_type, AccountType::Asset);
        assert_eq!(account.currency, "USD");
    }

    #[test]
    fn creates_liability_account() {
        let account = AccountDeclaration {
            name: "Loan".to_string(),
            account_type: AccountType::Liability,
            currency: "USD".to_string(),
            initial_balance: crate::types::MoneyAmount::from_minor_units(0),
        };

        assert_eq!(account.account_type, AccountType::Liability);
    }

    #[test]
    fn account_types_are_distinct() {
        assert_ne!(AccountType::Asset, AccountType::Liability);
        assert_ne!(AccountType::Revenue, AccountType::Expense);
    }

    #[test]
    fn creates_transfer() {
        let transfer = Transfer {
            amount: Expression::Money(MoneyLiteral {
                amount: "250".to_string(),
                currency: "USD".to_string(),
            }),
            from: "Cash".to_string(),
            to: "Savings".to_string(),
        };

        assert_eq!(transfer.from, "Cash");
        assert_eq!(transfer.to, "Savings");
    }

    #[test]
    fn statement_supports_transfer() {
        let statement = Statement::Transfer(Transfer {
            amount: Expression::Money(MoneyLiteral {
                amount: "250".to_string(),
                currency: "USD".to_string(),
            }),
            from: "Cash".to_string(),
            to: "Savings".to_string(),
        });

        assert!(matches!(statement, Statement::Transfer(_)));
    }
}