1
use derive_more::From;
2
use finance::{
3
    account::Account, commodity::Commodity, error::FinanceError, price::Price, split::Split,
4
    tag::Tag, transaction::Transaction,
5
};
6
use num_rational::Rational64;
7
use serde::Serialize;
8
use sqlx::{
9
    types::Uuid,
10
    types::chrono::{DateTime, Utc},
11
};
12
use std::{
13
    collections::HashMap,
14
    fmt::{self, Debug},
15
};
16
use thiserror::Error;
17

            
18
use crate::{config::ConfigError, error::ServerError};
19

            
20
pub mod account;
21
pub mod commodity;
22
pub mod config;
23
pub mod report;
24
pub mod split;
25
pub mod transaction;
26

            
27
#[derive(Debug, Clone)]
28
pub struct CommodityInfo {
29
    pub commodity_id: Uuid,
30
    pub symbol: String,
31
    pub name: String,
32
}
33

            
34
#[derive(Debug, Clone)]
35
pub struct PaginationInfo {
36
    pub total_count: i64,
37
    pub limit: i64,
38
    pub offset: i64,
39
    pub has_more: bool,
40
}
41

            
42
#[derive(Debug, From)]
43
pub enum FinanceEntity {
44
    Commodity(Commodity),
45
    Tag(Tag),
46
    Split(Split),
47
    Transaction(Transaction),
48
    Price(Price),
49
    Account(Account),
50
}
51

            
52
#[derive(Debug, From)]
53
pub enum Argument {
54
    String(String),
55
    Rational(Rational64),
56
    Uuid(Uuid),
57
    Data(Vec<u8>),
58
    FinanceEntity(FinanceEntity),
59
    FinanceEntities(Vec<FinanceEntity>),
60
    DateTime(DateTime<Utc>),
61
}
62

            
63
impl From<Argument> for String {
64
    fn from(arg: Argument) -> Self {
65
        match arg {
66
            Argument::String(s) => s,
67
            _ => panic!("Cannot convert {arg:?} to String"),
68
        }
69
    }
70
}
71

            
72
impl From<Argument> for Rational64 {
73
    fn from(arg: Argument) -> Self {
74
        match arg {
75
            Argument::Rational(r) => r,
76
            _ => panic!("Cannot convert {arg:?} to Rational64"),
77
        }
78
    }
79
}
80

            
81
impl From<Argument> for Vec<u8> {
82
    fn from(arg: Argument) -> Self {
83
        match arg {
84
            Argument::Data(d) => d,
85
            _ => panic!("Cannot convert {arg:?} to Vec<u8>"),
86
        }
87
    }
88
}
89

            
90
#[derive(Debug, Clone, Serialize)]
91
pub struct CommodityAmount {
92
    pub commodity_id: Uuid,
93
    pub commodity_symbol: String,
94
    pub amount: Rational64,
95
}
96

            
97
#[derive(Debug, Clone, Serialize)]
98
pub struct ReportNode {
99
    pub account_id: Uuid,
100
    pub account_name: String,
101
    pub account_path: String,
102
    pub depth: usize,
103
    pub amounts: Vec<CommodityAmount>,
104
    pub children: Vec<ReportNode>,
105
}
106

            
107
#[derive(Debug, Clone, Serialize)]
108
pub struct ReportMeta {
109
    pub date_from: Option<DateTime<Utc>>,
110
    pub date_to: Option<DateTime<Utc>>,
111
    pub target_commodity_id: Option<Uuid>,
112
}
113

            
114
#[derive(Debug, Clone, Serialize)]
115
pub struct PeriodData {
116
    pub label: Option<String>,
117
    pub roots: Vec<ReportNode>,
118
}
119

            
120
#[derive(Debug, Clone, Serialize)]
121
pub struct ReportData {
122
    pub meta: ReportMeta,
123
    pub periods: Vec<PeriodData>,
124
}
125

            
126
#[derive(Debug, Clone)]
127
pub enum FilterEntity {
128
    Account,
129
    Transaction,
130
    Split,
131
}
132

            
133
#[derive(Debug, Clone)]
134
pub enum ReportFilter {
135
    AccountEq(Uuid),
136
    AccountIn(Vec<Uuid>),
137
    AccountSubtree(Uuid),
138
    CounterpartyEq(Uuid),
139
    CounterpartyIn(Vec<Uuid>),
140
    CommodityEq(Uuid),
141
    CommodityIn(Vec<Uuid>),
142
    AmountGt(Rational64),
143
    AmountLt(Rational64),
144
    AmountEq(Rational64),
145
    Tag {
146
        entity: FilterEntity,
147
        name: String,
148
        value: String,
149
    },
150
    TagIn {
151
        entity: FilterEntity,
152
        name: String,
153
        values: Vec<String>,
154
    },
155
    And(Vec<ReportFilter>),
156
    Or(Vec<ReportFilter>),
157
    Not(Box<ReportFilter>),
158
}
159

            
160
#[derive(Debug, Error)]
161
pub enum CmdError {
162
    #[error("Wrong arguments: {0}")]
163
    Args(String),
164
    #[error("Config: {0}")]
165
    Config(#[from] ConfigError),
166
    #[error("Database: {0}")]
167
    DB(#[from] sqlx::Error),
168
    #[error("Server: {0}")]
169
    Server(#[from] ServerError),
170
    #[error("Finance: {0}")]
171
    Finance(#[from] FinanceError),
172
    #[error("Script execution failed: {0}")]
173
    Script(String),
174
}
175

            
176
// Implementing CmdResult as an enum with String and Rational returning options
177
#[derive(Debug)]
178
pub enum CmdResult {
179
    String(String),
180
    Rational(Rational64),
181
    Data(Vec<u8>),
182
    Lines(Vec<String>),
183
    Entity(FinanceEntity),
184
    Entities(Vec<FinanceEntity>),
185
    TaggedEntities {
186
        entities: Vec<(FinanceEntity, HashMap<String, FinanceEntity>)>,
187
        pagination: Option<PaginationInfo>,
188
    },
189
    CommodityInfoList(Vec<CommodityInfo>),
190
    MultiCurrencyBalance(Vec<(Commodity, Rational64)>),
191
    Report(ReportData),
192
}
193

            
194
impl From<String> for CmdResult {
195
52
    fn from(s: String) -> Self {
196
52
        CmdResult::String(s)
197
52
    }
198
}
199

            
200
pub struct LinesView<'view>(&'view CmdResult);
201
pub struct LinesViewMut<'view>(&'view mut CmdResult);
202

            
203
impl std::ops::Deref for LinesView<'_> {
204
    type Target = Vec<String>;
205

            
206
    fn deref(&self) -> &Self::Target {
207
        match self.0 {
208
            CmdResult::Lines(lines) => lines,
209
            _ => panic!("Attempted to use Lines view on non-Lines variant"),
210
        }
211
    }
212
}
213

            
214
impl std::ops::Deref for LinesViewMut<'_> {
215
    type Target = Vec<String>;
216

            
217
    fn deref(&self) -> &Self::Target {
218
        match self.0 {
219
            &mut CmdResult::Lines(ref lines) => lines,
220
            _ => panic!("Attempted to use Lines view on non-Lines variant"),
221
        }
222
    }
223
}
224

            
225
impl std::ops::DerefMut for LinesViewMut<'_> {
226
    fn deref_mut(&mut self) -> &mut Self::Target {
227
        match self.0 {
228
            &mut CmdResult::Lines(ref mut lines) => lines,
229
            _ => panic!("Attempted to use Lines view on non-Lines variant"),
230
        }
231
    }
232
}
233

            
234
impl CmdResult {
235
    #[must_use]
236
    pub fn as_lines(&self) -> LinesView<'_> {
237
        LinesView(self)
238
    }
239

            
240
    pub fn as_lines_mut(&mut self) -> LinesViewMut<'_> {
241
        LinesViewMut(self)
242
    }
243
}
244

            
245
impl fmt::Display for CmdResult {
246
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247
1
        match self {
248
1
            CmdResult::String(s) => write!(f, "{s}"),
249
            CmdResult::Rational(r) => write!(f, "{r}"),
250
            CmdResult::Data(d) => write!(f, "CmdResult<Data>: \"{}\" bytes", d.len()),
251
            CmdResult::Lines(l) => {
252
                // Find the maximum width for alignment
253
                let max_width = l.iter().map(std::string::String::len).max().unwrap_or(0);
254

            
255
                // Write header
256
                writeln!(f, "CmdResult<Lines>: {} items", l.len())?;
257

            
258
                // Write each item in a column format
259
                for (i, item) in l.iter().enumerate() {
260
                    writeln!(f, "{:>4}. {:<width$}", i + 1, item, width = max_width)?;
261
                }
262
                Ok(())
263
            }
264
            CmdResult::Entity(e) => write!(f, "CmdResult<FinanceEntity>: \"{e:?}\""),
265
            CmdResult::Entities(e) => write!(f, "CmdResult<FinanceEntities>: \"{}\"", e.len()),
266
            CmdResult::TaggedEntities {
267
                entities,
268
                pagination,
269
            } => match pagination {
270
                Some(p) => write!(
271
                    f,
272
                    "CmdResult<TaggedEntities>: {} of {} (offset: {})",
273
                    entities.len(),
274
                    p.total_count,
275
                    p.offset
276
                ),
277
                None => write!(f, "CmdResult<TaggedEntities>: \"{}\"", entities.len()),
278
            },
279
            CmdResult::CommodityInfoList(e) => {
280
                write!(f, "CmdResult<CommodityInfoList>: \"{}\"", e.len())
281
            }
282
            CmdResult::MultiCurrencyBalance(e) => {
283
                write!(f, "CmdResult<MultiCurrencyBalance>: \"{}\"", e.len())
284
            }
285
            CmdResult::Report(r) => {
286
                write!(f, "CmdResult<Report>: {} periods", r.periods.len())
287
            }
288
        }
289
1
    }
290
}