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