Lines
8.86 %
Functions
6.06 %
Branches
100 %
use derive_more::From;
use finance::{
account::Account, commodity::Commodity, error::FinanceError, price::Price, split::Split,
tag::Tag, transaction::Transaction,
};
use num_rational::Rational64;
use serde::{Deserialize, Serialize};
use sqlx::{
types::Uuid,
types::chrono::{DateTime, Utc},
use std::{
collections::HashMap,
fmt::{self, Debug},
use thiserror::Error;
use crate::{config::ConfigError, error::ServerError};
pub mod account;
pub mod commodity;
pub mod config;
pub mod report;
pub mod split;
pub mod ssh_key;
pub mod transaction;
pub mod user;
#[derive(Debug, Clone)]
pub struct CommodityInfo {
pub commodity_id: Uuid,
pub symbol: String,
pub name: String,
}
pub struct PaginationInfo {
pub total_count: i64,
pub limit: i64,
pub offset: i64,
pub has_more: bool,
#[derive(Debug, From)]
pub enum FinanceEntity {
Commodity(Commodity),
Tag(Tag),
Split(Split),
Transaction(Transaction),
Price(Price),
Account(Account),
pub enum Argument {
String(String),
Rational(Rational64),
Uuid(Uuid),
Data(Vec<u8>),
FinanceEntity(FinanceEntity),
FinanceEntities(Vec<FinanceEntity>),
DateTime(DateTime<Utc>),
impl From<Argument> for String {
fn from(arg: Argument) -> Self {
match arg {
Argument::String(s) => s,
_ => panic!("Cannot convert {arg:?} to String"),
impl From<Argument> for Rational64 {
Argument::Rational(r) => r,
_ => panic!("Cannot convert {arg:?} to Rational64"),
impl From<Argument> for Vec<u8> {
Argument::Data(d) => d,
_ => panic!("Cannot convert {arg:?} to Vec<u8>"),
#[derive(Debug, Clone, Serialize)]
pub struct CommodityAmount {
pub commodity_symbol: String,
pub amount: Rational64,
pub struct ReportNode {
pub account_id: Uuid,
pub account_name: String,
pub account_path: String,
pub depth: usize,
pub account_type: Option<String>,
pub amounts: Vec<CommodityAmount>,
pub children: Vec<ReportNode>,
pub struct ReportMeta {
pub date_from: Option<DateTime<Utc>>,
pub date_to: Option<DateTime<Utc>>,
pub target_commodity_id: Option<Uuid>,
pub struct PeriodData {
pub label: Option<String>,
pub roots: Vec<ReportNode>,
pub struct ReportData {
pub meta: ReportMeta,
pub periods: Vec<PeriodData>,
/// One pane of an `ActivityReport`. The caller supplies a label, a filter
/// that selects which splits belong in the pane, and a display-time
/// sign-flip hint. The command returns raw accountant values; `flip_sign`
/// just travels with the data so UIs know how to render it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivityGroup {
pub label: String,
pub filter: ReportFilter,
#[serde(default)]
pub flip_sign: bool,
pub struct ActivityGroupResult {
pub struct ActivityPeriod {
pub groups: Vec<ActivityGroupResult>,
pub struct ActivityData {
pub periods: Vec<ActivityPeriod>,
/// Sentinel value used in `BreakdownRow::tag_value` for splits that have no
/// value for the pivot tag.
pub const UNCATEGORIZED_KEY: &str = "__uncategorized__";
pub struct BreakdownRow {
pub tag_value: String,
pub is_uncategorized: bool,
pub struct BreakdownPeriod {
pub rows: Vec<BreakdownRow>,
pub struct BreakdownData {
pub tag_name: String,
pub periods: Vec<BreakdownPeriod>,
#[serde(rename_all = "lowercase")]
pub enum FilterEntity {
Account,
Transaction,
Split,
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PeriodGrouping {
Month,
Quarter,
Year,
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum BreakdownSort {
#[default]
AmountDesc,
AmountAsc,
NameAsc,
NameDesc,
#[serde(tag = "op", content = "args", rename_all = "snake_case")]
pub enum ReportFilter {
AccountEq(Uuid),
AccountIn(Vec<Uuid>),
AccountSubtree(Uuid),
CounterpartyEq(Uuid),
CounterpartyIn(Vec<Uuid>),
CommodityEq(Uuid),
CommodityIn(Vec<Uuid>),
AmountGt(Rational64),
AmountLt(Rational64),
AmountEq(Rational64),
Tag {
entity: FilterEntity,
name: String,
value: String,
},
TagIn {
values: Vec<String>,
And(Vec<ReportFilter>),
Or(Vec<ReportFilter>),
Not(Box<ReportFilter>),
#[derive(Debug, Error)]
pub enum CmdError {
#[error("Wrong arguments: {0}")]
Args(String),
#[error("Config: {0}")]
Config(#[from] ConfigError),
#[error("Database: {0}")]
DB(#[from] sqlx::Error),
#[error("Server: {0}")]
Server(#[from] ServerError),
#[error("Finance: {0}")]
Finance(#[from] FinanceError),
#[error("Script execution failed: {0}")]
Script(String),
// Implementing CmdResult as an enum with String and Rational returning options
#[derive(Debug)]
pub enum CmdResult {
Bool(bool),
Lines(Vec<String>),
Entity(FinanceEntity),
Entities(Vec<FinanceEntity>),
TaggedEntities {
entities: Vec<(FinanceEntity, HashMap<String, FinanceEntity>)>,
pagination: Option<PaginationInfo>,
CommodityInfoList(Vec<CommodityInfo>),
MultiCurrencyBalance(Vec<(Commodity, Rational64)>),
Report(ReportData),
Breakdown(BreakdownData),
Activity(ActivityData),
SshKeys(Vec<ssh_key::SshKeyRecord>),
impl From<String> for CmdResult {
fn from(s: String) -> Self {
CmdResult::String(s)
pub struct LinesView<'view>(&'view CmdResult);
pub struct LinesViewMut<'view>(&'view mut CmdResult);
impl std::ops::Deref for LinesView<'_> {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
match self.0 {
CmdResult::Lines(lines) => lines,
_ => panic!("Attempted to use Lines view on non-Lines variant"),
impl std::ops::Deref for LinesViewMut<'_> {
&mut CmdResult::Lines(ref lines) => lines,
impl std::ops::DerefMut for LinesViewMut<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut CmdResult::Lines(ref mut lines) => lines,
impl CmdResult {
#[must_use]
pub fn as_lines(&self) -> LinesView<'_> {
LinesView(self)
pub fn as_lines_mut(&mut self) -> LinesViewMut<'_> {
LinesViewMut(self)
impl fmt::Display for CmdResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CmdResult::String(s) => write!(f, "{s}"),
CmdResult::Rational(r) => write!(f, "{r}"),
CmdResult::Data(d) => write!(f, "CmdResult<Data>: \"{}\" bytes", d.len()),
CmdResult::Lines(l) => {
// Find the maximum width for alignment
let max_width = l.iter().map(std::string::String::len).max().unwrap_or(0);
// Write header
writeln!(f, "CmdResult<Lines>: {} items", l.len())?;
// Write each item in a column format
for (i, item) in l.iter().enumerate() {
writeln!(f, "{:>4}. {:<width$}", i + 1, item, width = max_width)?;
Ok(())
CmdResult::Entity(e) => write!(f, "CmdResult<FinanceEntity>: \"{e:?}\""),
CmdResult::Entities(e) => write!(f, "CmdResult<FinanceEntities>: \"{}\"", e.len()),
CmdResult::TaggedEntities {
entities,
pagination,
} => match pagination {
Some(p) => write!(
f,
"CmdResult<TaggedEntities>: {} of {} (offset: {})",
entities.len(),
p.total_count,
p.offset
),
None => write!(f, "CmdResult<TaggedEntities>: \"{}\"", entities.len()),
CmdResult::CommodityInfoList(e) => {
write!(f, "CmdResult<CommodityInfoList>: \"{}\"", e.len())
CmdResult::MultiCurrencyBalance(e) => {
write!(f, "CmdResult<MultiCurrencyBalance>: \"{}\"", e.len())
CmdResult::Report(r) => {
write!(f, "CmdResult<Report>: {} periods", r.periods.len())
CmdResult::Breakdown(b) => {
write!(
"CmdResult<Breakdown>: tag={} periods={}",
b.tag_name,
b.periods.len()
)
CmdResult::Activity(a) => {
"CmdResult<Activity>: {} periods, {} groups",
a.periods.len(),
a.periods.first().map_or(0, |p| p.groups.len()),
CmdResult::Uuid(id) => write!(f, "{id}"),
CmdResult::Bool(b) => write!(f, "{b}"),
CmdResult::SshKeys(keys) => {
write!(f, "CmdResult<SshKeys>: {} keys", keys.len())