1mod from_wrapper;
4mod to_wrapper;
5
6use rustledger_core::{Directive, NaiveDate};
7
8use crate::types::{DirectiveData, DirectiveWrapper};
9
10use from_wrapper::{
12 data_to_balance, data_to_close, data_to_commodity, data_to_custom, data_to_document,
13 data_to_event, data_to_note, data_to_open, data_to_pad, data_to_price, data_to_query,
14 data_to_transaction,
15};
16use to_wrapper::{
17 balance_to_data, close_to_data, commodity_to_data, custom_to_data, document_to_data,
18 event_to_data, note_to_data, open_to_data, pad_to_data, price_to_data, query_to_data,
19 transaction_to_data,
20};
21
22#[derive(Debug, Clone, thiserror::Error)]
24pub enum ConversionError {
25 #[error("invalid date format: {0}")]
27 InvalidDate(String),
28 #[error("invalid number format: {0}")]
30 InvalidNumber(String),
31 #[error("invalid flag: {0}")]
33 InvalidFlag(String),
34 #[error("unknown directive type: {0}")]
36 UnknownDirective(String),
37}
38
39pub fn directive_to_wrapper(directive: &Directive) -> DirectiveWrapper {
44 match directive {
45 Directive::Transaction(txn) => DirectiveWrapper {
46 directive_type: "transaction".to_string(),
47 date: txn.date.to_string(),
48 filename: None,
49 lineno: None,
50 data: DirectiveData::Transaction(transaction_to_data(txn)),
51 },
52 Directive::Balance(bal) => DirectiveWrapper {
53 directive_type: "balance".to_string(),
54 date: bal.date.to_string(),
55 filename: None,
56 lineno: None,
57 data: DirectiveData::Balance(balance_to_data(bal)),
58 },
59 Directive::Open(open) => DirectiveWrapper {
60 directive_type: "open".to_string(),
61 date: open.date.to_string(),
62 filename: None,
63 lineno: None,
64 data: DirectiveData::Open(open_to_data(open)),
65 },
66 Directive::Close(close) => DirectiveWrapper {
67 directive_type: "close".to_string(),
68 date: close.date.to_string(),
69 filename: None,
70 lineno: None,
71 data: DirectiveData::Close(close_to_data(close)),
72 },
73 Directive::Commodity(comm) => DirectiveWrapper {
74 directive_type: "commodity".to_string(),
75 date: comm.date.to_string(),
76 filename: None,
77 lineno: None,
78 data: DirectiveData::Commodity(commodity_to_data(comm)),
79 },
80 Directive::Pad(pad) => DirectiveWrapper {
81 directive_type: "pad".to_string(),
82 date: pad.date.to_string(),
83 filename: None,
84 lineno: None,
85 data: DirectiveData::Pad(pad_to_data(pad)),
86 },
87 Directive::Event(event) => DirectiveWrapper {
88 directive_type: "event".to_string(),
89 date: event.date.to_string(),
90 filename: None,
91 lineno: None,
92 data: DirectiveData::Event(event_to_data(event)),
93 },
94 Directive::Note(note) => DirectiveWrapper {
95 directive_type: "note".to_string(),
96 date: note.date.to_string(),
97 filename: None,
98 lineno: None,
99 data: DirectiveData::Note(note_to_data(note)),
100 },
101 Directive::Document(doc) => DirectiveWrapper {
102 directive_type: "document".to_string(),
103 date: doc.date.to_string(),
104 filename: None,
105 lineno: None,
106 data: DirectiveData::Document(document_to_data(doc)),
107 },
108 Directive::Price(price) => DirectiveWrapper {
109 directive_type: "price".to_string(),
110 date: price.date.to_string(),
111 filename: None,
112 lineno: None,
113 data: DirectiveData::Price(price_to_data(price)),
114 },
115 Directive::Query(query) => DirectiveWrapper {
116 directive_type: "query".to_string(),
117 date: query.date.to_string(),
118 filename: None,
119 lineno: None,
120 data: DirectiveData::Query(query_to_data(query)),
121 },
122 Directive::Custom(custom) => DirectiveWrapper {
123 directive_type: "custom".to_string(),
124 date: custom.date.to_string(),
125 filename: None,
126 lineno: None,
127 data: DirectiveData::Custom(custom_to_data(custom)),
128 },
129 }
130}
131
132pub fn directives_to_wrappers(directives: &[Directive]) -> Vec<DirectiveWrapper> {
134 directives.iter().map(directive_to_wrapper).collect()
135}
136
137pub fn wrapper_to_directive(wrapper: &DirectiveWrapper) -> Result<Directive, ConversionError> {
139 let date = NaiveDate::parse_from_str(&wrapper.date, "%Y-%m-%d")
140 .map_err(|_| ConversionError::InvalidDate(wrapper.date.clone()))?;
141
142 match &wrapper.data {
143 DirectiveData::Transaction(data) => {
144 Ok(Directive::Transaction(data_to_transaction(data, date)?))
145 }
146 DirectiveData::Balance(data) => Ok(Directive::Balance(data_to_balance(data, date)?)),
147 DirectiveData::Open(data) => Ok(Directive::Open(data_to_open(data, date))),
148 DirectiveData::Close(data) => Ok(Directive::Close(data_to_close(data, date))),
149 DirectiveData::Commodity(data) => Ok(Directive::Commodity(data_to_commodity(data, date))),
150 DirectiveData::Pad(data) => Ok(Directive::Pad(data_to_pad(data, date))),
151 DirectiveData::Event(data) => Ok(Directive::Event(data_to_event(data, date))),
152 DirectiveData::Note(data) => Ok(Directive::Note(data_to_note(data, date))),
153 DirectiveData::Document(data) => Ok(Directive::Document(data_to_document(data, date))),
154 DirectiveData::Price(data) => Ok(Directive::Price(data_to_price(data, date)?)),
155 DirectiveData::Query(data) => Ok(Directive::Query(data_to_query(data, date))),
156 DirectiveData::Custom(data) => Ok(Directive::Custom(data_to_custom(data, date))),
157 }
158}
159
160pub fn wrappers_to_directives(
162 wrappers: &[DirectiveWrapper],
163) -> Result<Vec<Directive>, ConversionError> {
164 wrappers.iter().map(wrapper_to_directive).collect()
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use rustledger_core::{
171 Amount, Balance, Close, Commodity, Custom, Decimal, Document, Event, IncompleteAmount,
172 MetaValue, Metadata, Note, Open, Pad, Posting, Price, Query, Transaction,
173 };
174 use std::str::FromStr;
175
176 fn dec(s: &str) -> Decimal {
177 Decimal::from_str(s).unwrap()
178 }
179
180 #[test]
181 fn test_roundtrip_transaction() {
182 let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
183 let txn = Transaction {
184 date,
185 flag: '*',
186 payee: Some("Grocery Store".into()),
187 narration: "Weekly groceries".into(),
188 tags: vec!["food".into()],
189 links: vec!["grocery-2024".into()],
190 meta: Metadata::default(),
191 postings: vec![
192 Posting {
193 account: "Expenses:Food".into(),
194 units: Some(IncompleteAmount::Complete(Amount::new(dec("50.00"), "USD"))),
195 cost: None,
196 price: None,
197 flag: None,
198 meta: Metadata::default(),
199 },
200 Posting {
201 account: "Assets:Checking".into(),
202 units: None,
203 cost: None,
204 price: None,
205 flag: None,
206 meta: Metadata::default(),
207 },
208 ],
209 };
210
211 let directive = Directive::Transaction(txn);
212 let wrapper = directive_to_wrapper(&directive);
213 let roundtrip = wrapper_to_directive(&wrapper).unwrap();
214
215 if let (Directive::Transaction(orig), Directive::Transaction(rt)) = (&directive, &roundtrip)
216 {
217 assert_eq!(orig.date, rt.date);
218 assert_eq!(orig.flag, rt.flag);
219 assert_eq!(orig.payee, rt.payee);
220 assert_eq!(orig.narration, rt.narration);
221 assert_eq!(orig.tags, rt.tags);
222 assert_eq!(orig.links, rt.links);
223 assert_eq!(orig.postings.len(), rt.postings.len());
224 } else {
225 panic!("Expected Transaction directive");
226 }
227 }
228
229 #[test]
230 fn test_roundtrip_balance() {
231 let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
232 let balance = Balance {
233 date,
234 account: "Assets:Checking".into(),
235 amount: Amount::new(dec("1000.00"), "USD"),
236 tolerance: Some(dec("0.01")),
237 meta: Metadata::default(),
238 };
239
240 let directive = Directive::Balance(balance);
241 let wrapper = directive_to_wrapper(&directive);
242 let roundtrip = wrapper_to_directive(&wrapper).unwrap();
243
244 if let (Directive::Balance(orig), Directive::Balance(rt)) = (&directive, &roundtrip) {
245 assert_eq!(orig.date, rt.date);
246 assert_eq!(orig.account, rt.account);
247 assert_eq!(orig.amount, rt.amount);
248 assert_eq!(orig.tolerance, rt.tolerance);
249 } else {
250 panic!("Expected Balance directive");
251 }
252 }
253
254 #[test]
255 fn test_roundtrip_open() {
256 let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
257 let open = Open {
258 date,
259 account: "Assets:Checking".into(),
260 currencies: vec!["USD".into(), "EUR".into()],
261 booking: Some("FIFO".to_string()),
262 meta: Metadata::default(),
263 };
264
265 let directive = Directive::Open(open);
266 let wrapper = directive_to_wrapper(&directive);
267 let roundtrip = wrapper_to_directive(&wrapper).unwrap();
268
269 if let (Directive::Open(orig), Directive::Open(rt)) = (&directive, &roundtrip) {
270 assert_eq!(orig.date, rt.date);
271 assert_eq!(orig.account, rt.account);
272 assert_eq!(orig.currencies, rt.currencies);
273 assert_eq!(orig.booking, rt.booking);
274 } else {
275 panic!("Expected Open directive");
276 }
277 }
278
279 #[test]
280 fn test_roundtrip_price() {
281 let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
282 let price = Price {
283 date,
284 currency: "AAPL".into(),
285 amount: Amount::new(dec("185.50"), "USD"),
286 meta: Metadata::default(),
287 };
288
289 let directive = Directive::Price(price);
290 let wrapper = directive_to_wrapper(&directive);
291 let roundtrip = wrapper_to_directive(&wrapper).unwrap();
292
293 if let (Directive::Price(orig), Directive::Price(rt)) = (&directive, &roundtrip) {
294 assert_eq!(orig.date, rt.date);
295 assert_eq!(orig.currency, rt.currency);
296 assert_eq!(orig.amount, rt.amount);
297 } else {
298 panic!("Expected Price directive");
299 }
300 }
301
302 #[test]
303 fn test_roundtrip_all_directive_types() {
304 let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
305
306 let directives = vec![
307 Directive::Open(Open {
308 date,
309 account: "Assets:Test".into(),
310 currencies: vec![],
311 booking: None,
312 meta: Metadata::default(),
313 }),
314 Directive::Close(Close {
315 date,
316 account: "Assets:Test".into(),
317 meta: Metadata::default(),
318 }),
319 Directive::Commodity(Commodity {
320 date,
321 currency: "TEST".into(),
322 meta: Metadata::default(),
323 }),
324 Directive::Pad(Pad {
325 date,
326 account: "Assets:Checking".into(),
327 source_account: "Equity:Opening".into(),
328 meta: Metadata::default(),
329 }),
330 Directive::Event(Event {
331 date,
332 event_type: "location".to_string(),
333 value: "Home".to_string(),
334 meta: Metadata::default(),
335 }),
336 Directive::Note(Note {
337 date,
338 account: "Assets:Test".into(),
339 comment: "Test note".to_string(),
340 meta: Metadata::default(),
341 }),
342 Directive::Document(Document {
343 date,
344 account: "Assets:Test".into(),
345 path: "/path/to/doc.pdf".to_string(),
346 tags: vec![],
347 links: vec![],
348 meta: Metadata::default(),
349 }),
350 Directive::Query(Query {
351 date,
352 name: "test_query".to_string(),
353 query: "SELECT * FROM transactions".to_string(),
354 meta: Metadata::default(),
355 }),
356 Directive::Custom(Custom {
357 date,
358 custom_type: "budget".to_string(),
359 values: vec![MetaValue::String("monthly".to_string())],
360 meta: Metadata::default(),
361 }),
362 ];
363
364 let wrappers = directives_to_wrappers(&directives);
365 let roundtrip = wrappers_to_directives(&wrappers).unwrap();
366
367 assert_eq!(directives.len(), roundtrip.len());
368 }
369}