sqlx-ledger 0.2.1

An embeddable double sided accounting ledger built on PG/SQLx
Documentation
use crate::error::*;
use rusty_money::{crypto, iso};

use cel_interpreter::{CelResult, CelValue};
use serde::{Deserialize, Serialize};

crate::entity_id! { AccountId }
crate::entity_id! { JournalId }
crate::entity_id! { TransactionId }
crate::entity_id! { EntryId }
crate::entity_id! { TxTemplateId }
crate::entity_id! { CorrelationId }

#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)]
#[sqlx(type_name = "Layer", rename_all = "snake_case")]
pub enum Layer {
    Settled,
    Pending,
    Encumbered,
}

impl<'a> TryFrom<CelResult<'a>> for Layer {
    type Error = SqlxLedgerError;

    fn try_from(CelResult { val, .. }: CelResult) -> Result<Self, Self::Error> {
        match val {
            CelValue::String(v) if v.as_ref() == "SETTLED" => Ok(Layer::Settled),
            CelValue::String(v) if v.as_ref() == "PENDING" => Ok(Layer::Pending),
            CelValue::String(v) if v.as_ref() == "ENCUMBERED" => Ok(Layer::Encumbered),
            v => Err(SqlxLedgerError::UnknownLayer(format!("{v:?}"))),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)]
#[sqlx(type_name = "DebitOrCredit", rename_all = "snake_case")]
pub enum DebitOrCredit {
    Debit,
    Credit,
}

impl<'a> TryFrom<CelResult<'a>> for DebitOrCredit {
    type Error = SqlxLedgerError;

    fn try_from(CelResult { val, .. }: CelResult) -> Result<Self, Self::Error> {
        match val {
            CelValue::String(v) if v.as_ref() == "DEBIT" => Ok(DebitOrCredit::Debit),
            CelValue::String(v) if v.as_ref() == "CREDIT" => Ok(DebitOrCredit::Credit),
            v => Err(SqlxLedgerError::UnknownDebitOrCredit(format!("{v:?}"))),
        }
    }
}

impl Default for DebitOrCredit {
    fn default() -> Self {
        Self::Credit
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)]
#[sqlx(type_name = "Status", rename_all = "snake_case")]
pub enum Status {
    Active,
}

impl Default for Status {
    fn default() -> Self {
        Self::Active
    }
}

#[derive(Debug, Clone, Copy, Eq, Serialize, Deserialize)]
#[serde(try_from = "String")]
#[serde(into = "&str")]
pub enum Currency {
    Iso(&'static iso::Currency),
    Crypto(&'static crypto::Currency),
}

impl Currency {
    pub fn code(&self) -> &'static str {
        match self {
            Currency::Iso(c) => c.iso_alpha_code,
            Currency::Crypto(c) => c.code,
        }
    }
}

impl std::fmt::Display for Currency {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.code())
    }
}

impl std::hash::Hash for Currency {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.code().hash(state);
    }
}

impl PartialEq for Currency {
    fn eq(&self, other: &Self) -> bool {
        self.code() == other.code()
    }
}

impl std::str::FromStr for Currency {
    type Err = SqlxLedgerError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match iso::find(s) {
            Some(c) => Ok(Currency::Iso(c)),
            _ => match crypto::find(s) {
                Some(c) => Ok(Currency::Crypto(c)),
                _ => Err(SqlxLedgerError::UnknownCurrency(s.to_string())),
            },
        }
    }
}

impl TryFrom<String> for Currency {
    type Error = SqlxLedgerError;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        s.parse()
    }
}

impl From<Currency> for &'static str {
    fn from(c: Currency) -> Self {
        c.code()
    }
}

impl<'a> TryFrom<CelResult<'a>> for Currency {
    type Error = SqlxLedgerError;

    fn try_from(CelResult { val, .. }: CelResult) -> Result<Self, Self::Error> {
        match val {
            CelValue::String(v) => v.as_ref().parse(),
            v => Err(SqlxLedgerError::UnknownCurrency(format!("{v:?}"))),
        }
    }
}

impl<'r, DB: sqlx::Database> sqlx::Decode<'r, DB> for Currency
where
    &'r str: sqlx::Decode<'r, DB>,
{
    fn decode(
        value: <DB as sqlx::database::HasValueRef<'r>>::ValueRef,
    ) -> Result<Currency, Box<dyn std::error::Error + 'static + Send + Sync>> {
        let value = <&str as sqlx::Decode<DB>>::decode(value)?;

        Ok(value.parse().map_err(Box::new)?)
    }
}