1use crate::model::*;
2use std::io;
3
4#[non_exhaustive]
5pub struct SerializerSettings {
6 pub indent: String,
7 pub eol: String,
8
9 pub transaction_date_format: String,
10 pub commodity_date_format: String,
11
12 pub posting_comments_sameline: bool,
14}
15
16impl SerializerSettings {
17 pub fn with_indent(mut self, indent: &str) -> Self {
18 indent.clone_into(&mut self.indent);
19 self
20 }
21
22 pub fn with_eol(mut self, eol: &str) -> Self {
23 eol.clone_into(&mut self.eol);
24 self
25 }
26}
27
28impl Default for SerializerSettings {
29 fn default() -> Self {
30 Self {
31 indent: " ".to_owned(),
32 eol: "\n".to_owned(),
33 transaction_date_format: "%Y-%m-%d".to_owned(),
34 commodity_date_format: "%Y-%m-%d %H:%M:%S".to_owned(),
35 posting_comments_sameline: false,
36 }
37 }
38}
39
40pub trait Serializer {
41 fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
42 where
43 W: io::Write;
44
45 fn to_string_pretty(&self, settings: &SerializerSettings) -> String {
46 let mut res = Vec::new();
47 self.write(&mut res, settings).unwrap();
48 return std::str::from_utf8(&res).unwrap().to_owned();
49 }
50}
51
52impl Serializer for Ledger {
53 fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
54 where
55 W: io::Write,
56 {
57 for item in &self.items {
58 item.write(writer, settings)?;
59 }
60 Ok(())
61 }
62}
63
64impl Serializer for LedgerItem {
65 fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
66 where
67 W: io::Write,
68 {
69 match self {
70 LedgerItem::EmptyLine => write!(writer, "{}", settings.eol)?,
71 LedgerItem::LineComment(comment) => write!(writer, "; {}{}", comment, settings.eol)?,
72 LedgerItem::Transaction(transaction) => {
73 transaction.write(writer, settings)?;
74 write!(writer, "{}", settings.eol)?;
75 }
76 LedgerItem::CommodityPrice(commodity_price) => {
77 commodity_price.write(writer, settings)?;
78 write!(writer, "{}", settings.eol)?;
79 }
80 LedgerItem::Include(file) => write!(writer, "include {}{}", file, settings.eol)?,
81 }
82 Ok(())
83 }
84}
85
86impl Serializer for Transaction {
87 fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
88 where
89 W: io::Write,
90 {
91 write!(
92 writer,
93 "{}",
94 self.date.format(&settings.transaction_date_format)
95 )?;
96
97 if let Some(effective_date) = self.effective_date {
98 write!(
99 writer,
100 "={}",
101 effective_date.format(&settings.transaction_date_format)
102 )?;
103 }
104
105 if let Some(ref status) = self.status {
106 write!(writer, " ")?;
107 status.write(writer, settings)?;
108 }
109
110 if let Some(ref code) = self.code {
111 write!(writer, " ({})", code)?;
112 }
113
114 if let Some(ref description) = self.description {
116 if !description.is_empty() {
117 write!(writer, " {}", description)?;
118 }
119 }
120
121 if let Some(ref comment) = self.comment {
122 for comment in comment.split('\n') {
123 write!(writer, "{}{}; {}", settings.eol, settings.indent, comment)?;
124 }
125 }
126
127 for tag in &self.posting_metadata.tags {
128 write!(writer, "{}{}; {}", settings.eol, settings.indent, tag.name)?;
129 if let Some(ref value) = tag.value {
130 write!(writer, ": {}", value)?;
131 };
132 }
133
134 for posting in &self.postings {
135 write!(writer, "{}{}", settings.eol, settings.indent)?;
136 posting.write(writer, settings)?;
137 }
138
139 Ok(())
140 }
141}
142
143impl Serializer for TransactionStatus {
144 fn write<W>(&self, writer: &mut W, _settings: &SerializerSettings) -> Result<(), io::Error>
145 where
146 W: io::Write,
147 {
148 match self {
149 TransactionStatus::Pending => write!(writer, "!"),
150 TransactionStatus::Cleared => write!(writer, "*"),
151 }
152 }
153}
154
155impl Serializer for Posting {
156 fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
157 where
158 W: io::Write,
159 {
160 if let Some(ref status) = self.status {
161 status.write(writer, settings)?;
162 write!(writer, " ")?;
163 }
164
165 match self.reality {
166 Reality::Real => write!(writer, "{}", self.account)?,
167 Reality::BalancedVirtual => write!(writer, "[{}]", self.account)?,
168 Reality::UnbalancedVirtual => write!(writer, "({})", self.account)?,
169 }
170
171 if self.amount.is_some() || self.balance.is_some() {
172 write!(writer, "{}", settings.indent)?;
173 }
174
175 if let Some(ref amount) = self.amount {
176 amount.write(writer, settings)?;
177 }
178
179 if let Some(ref balance) = self.balance {
180 write!(writer, " = ")?;
181 balance.write(writer, settings)?;
182 }
183
184 for tag in &self.metadata.tags {
185 write!(writer, "{}; {}", settings.indent, tag.name)?;
186 if let Some(ref value) = tag.value {
187 write!(writer, ": {}", value)?;
188 };
189 }
190
191 if let Some(ref comment) = self.comment {
192 if !comment.contains('\n') && settings.posting_comments_sameline {
193 write!(writer, "{}; {}", settings.indent, comment)?;
194 } else {
195 for comment in comment.split('\n') {
196 write!(writer, "{}{}; {}", settings.eol, settings.indent, comment)?;
197 }
198 }
199 }
200
201 Ok(())
202 }
203}
204
205impl Serializer for PostingAmount {
206 fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
207 where
208 W: io::Write,
209 {
210 self.amount.write(writer, settings)?;
211
212 if let Some(ref lot_price) = self.lot_price {
213 match lot_price {
214 Price::Unit(amount) => {
215 write!(writer, " {{")?;
216 amount.write(writer, settings)?;
217 write!(writer, "}}")?;
218 }
219 Price::Total(amount) => {
220 write!(writer, " {{{{")?;
221 amount.write(writer, settings)?;
222 write!(writer, "}}}}")?;
223 }
224 }
225 }
226
227 if let Some(ref lot_price) = self.price {
228 match lot_price {
229 Price::Unit(amount) => {
230 write!(writer, " @ ")?;
231 amount.write(writer, settings)?;
232 }
233 Price::Total(amount) => {
234 write!(writer, " @@ ")?;
235 amount.write(writer, settings)?;
236 }
237 }
238 }
239
240 Ok(())
241 }
242}
243
244impl Serializer for Amount {
245 fn write<W>(&self, writer: &mut W, _settings: &SerializerSettings) -> Result<(), io::Error>
246 where
247 W: io::Write,
248 {
249 match self.commodity.position {
250 CommodityPosition::Left => write!(writer, "{}{}", self.commodity.name, self.quantity),
251 CommodityPosition::Right => write!(writer, "{} {}", self.quantity, self.commodity.name),
252 }
253 }
254}
255
256impl Serializer for Balance {
257 fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
258 where
259 W: io::Write,
260 {
261 match self {
262 Balance::Zero => write!(writer, "0"),
263 Balance::Amount(ref balance) => balance.write(writer, settings),
264 }
265 }
266}
267
268impl Serializer for CommodityPrice {
269 fn write<W>(&self, writer: &mut W, settings: &SerializerSettings) -> Result<(), io::Error>
270 where
271 W: io::Write,
272 {
273 write!(
274 writer,
275 "P {} {} ",
276 self.datetime.format(&settings.commodity_date_format),
277 self.commodity_name
278 )?;
279 self.amount.write(writer, settings)?;
280 Ok(())
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn serialize_transaction() {
290 let ledger = crate::parse(
291 r#"2018/10/01 (123) Payee 123
292 TEST:ABC 123 $1.20
293 TEST:DEF 123"#,
294 )
295 .expect("parsing test transaction");
296
297 let mut buf = Vec::new();
298 ledger
299 .write(&mut buf, &SerializerSettings::default())
300 .expect("serializing test transaction");
301
302 assert_eq!(
303 String::from_utf8(buf).unwrap(),
304 r#"2018-10-01 (123) Payee 123
305 TEST:ABC 123 $1.20
306 TEST:DEF 123
307"#
308 );
309 }
310
311 #[test]
312 fn serialize_with_custom_date_format() {
313 let ledger = crate::parse(
314 r#"2018-10-01 (123) Payee 123
315 TEST:ABC 123 $1.20
316 TEST:DEF 123"#,
317 )
318 .expect("parsing test transaction");
319
320 let mut buf = Vec::new();
321 ledger
322 .write(
323 &mut buf,
324 &SerializerSettings {
325 transaction_date_format: "%Y/%m/%d".to_owned(),
326 ..SerializerSettings::default()
327 },
328 )
329 .expect("serializing test transaction");
330
331 assert_eq!(
332 String::from_utf8(buf).unwrap(),
333 r#"2018/10/01 (123) Payee 123
334 TEST:ABC 123 $1.20
335 TEST:DEF 123
336"#
337 );
338 }
339
340 #[test]
341 fn serialize_tags() {
342 let ledger = crate::parse(
343 r#"2018-10-01 (123) Payee 123
344 ; Tag1: Foo bar
345 TEST:ABC 123 $1.20 ; Tag2: Fizz bazz
346 TEST:DEF 123"#,
347 )
348 .expect("parsing test transaction");
349
350 let mut buf = Vec::new();
351 ledger
352 .write(&mut buf, &SerializerSettings::default())
353 .expect("serializing test transaction");
354
355 assert_eq!(
356 String::from_utf8(buf).unwrap(),
357 r#"2018-10-01 (123) Payee 123
358 ; Tag1: Foo bar
359 TEST:ABC 123 $1.20 ; Tag2: Fizz bazz
360 TEST:DEF 123
361"#
362 );
363 }
364
365 #[test]
366 fn serialize_posting_comments_sameline() {
367 let ledger = crate::parse(
368 r#"2018-10-01 Payee 123
369 TEST:ABC 123 $1.20
370 ; This is a one-line comment
371 TEST:DEF 123
372 ; This is a two-
373 ; line comment"#,
374 )
375 .expect("parsing test transaction");
376
377 let mut buf = Vec::new();
378 ledger
379 .write(
380 &mut buf,
381 &SerializerSettings {
382 posting_comments_sameline: true,
383 ..SerializerSettings::default()
384 },
385 )
386 .expect("serializing test transaction");
387
388 assert_eq!(
389 String::from_utf8(buf).unwrap(),
390 r#"2018-10-01 Payee 123
391 TEST:ABC 123 $1.20 ; This is a one-line comment
392 TEST:DEF 123
393 ; This is a two-
394 ; line comment
395"#
396 );
397 }
398}