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