1use super::format::{format, plain};
2use super::types::*;
3use rust_decimal::Decimal;
4use rust_decimal_macros::dec;
5use std::path::{Path, PathBuf};
6use std::{
7 collections::{hash_map, HashMap},
8 fmt::{self, Display, Formatter},
9 hash::Hash,
10};
11use strum::IntoEnumIterator;
12
13#[derive(PartialEq, Eq, Clone, Debug)]
14pub(crate) struct BeancountOption<'a> {
15 source: Source,
16 variant: BeancountOptionVariant<'a>,
17}
18
19#[derive(PartialEq, Eq, Clone, Debug)]
20pub(crate) enum BeancountOptionVariant<'a> {
21 Title(&'a str),
22 AccountTypeName(AccountType, AccountTypeName<'a>),
23 AccountPreviousBalances(Subaccount<'a>),
24 AccountPreviousEarnings(Subaccount<'a>),
25 AccountPreviousConversions(Subaccount<'a>),
26 AccountCurrentEarnings(Subaccount<'a>),
27 AccountCurrentConversions(Subaccount<'a>),
28 AccountUnrealizedGains(Subaccount<'a>),
29 AccountRounding(Subaccount<'a>),
30 ConversionCurrency(Currency<'a>),
31 InferredToleranceDefault(CurrencyOrAny<'a>, Decimal),
32 InferredToleranceMultiplier(Decimal),
33 InferToleranceFromCost(bool),
34 Documents(PathBuf),
35 OperatingCurrency(Currency<'a>),
36 RenderCommas(bool),
37 LongStringMaxlines(usize),
38 BookingMethod(Booking),
39 PluginProcessingMode(PluginProcessingMode),
40 Assimilated,
41}
42
43#[derive(PartialEq, Eq, Hash, Clone, Debug)]
44pub(crate) enum CurrencyOrAny<'a> {
45 Currency(Currency<'a>),
46 Any,
47}
48
49impl<'a> TryFrom<&'a str> for CurrencyOrAny<'a> {
50 type Error = CurrencyError;
51
52 fn try_from(s: &'a str) -> Result<Self, Self::Error> {
53 if s == "*" {
54 Ok(CurrencyOrAny::Any)
55 } else {
56 Currency::try_from(s).map(CurrencyOrAny::Currency)
57 }
58 }
59}
60
61impl<'a> BeancountOption<'a> {
62 pub(crate) fn parse(
63 name: Spanned<&'a str>,
64 value: Spanned<&'a str>,
65 source_path: Option<&Path>,
66 ) -> Result<BeancountOption<'a>, BeancountOptionError> {
67 use BeancountOptionError::*;
68 use BeancountOptionVariant::*;
69
70 match name.item {
71 "title" => Ok(Title(value.item)),
72
73 "name_assets" => {
74 parse_account_type_name(value.item).map(|n| AccountTypeName(AccountType::Assets, n))
75 }
76
77 "name_liabilities" => parse_account_type_name(value.item)
78 .map(|n| AccountTypeName(AccountType::Liabilities, n)),
79
80 "name_equity" => {
81 parse_account_type_name(value.item).map(|n| AccountTypeName(AccountType::Equity, n))
82 }
83
84 "name_income" => {
85 parse_account_type_name(value.item).map(|n| AccountTypeName(AccountType::Income, n))
86 }
87
88 "name_expenses" => parse_account_type_name(value.item)
89 .map(|n| AccountTypeName(AccountType::Expenses, n)),
90
91 "account_previous_balances" => {
92 parse_subaccount(value.item).map(AccountPreviousBalances)
93 }
94
95 "account_previous_earnings" => {
96 parse_subaccount(value.item).map(AccountPreviousEarnings)
97 }
98
99 "account_previous_conversions" => {
100 parse_subaccount(value.item).map(AccountPreviousConversions)
101 }
102
103 "account_current_earnings" => parse_subaccount(value.item).map(AccountCurrentEarnings),
104
105 "account_current_conversions" => {
106 parse_subaccount(value.item).map(AccountCurrentConversions)
107 }
108
109 "account_unrealized_gains" => parse_subaccount(value.item).map(AccountUnrealizedGains),
110
111 "account_rounding" => parse_subaccount(value.item).map(AccountRounding),
112
113 "conversion_currency" => parse_currency(value.item).map(ConversionCurrency),
114
115 "inferred_tolerance_default" => {
116 parse_inferred_tolerance_default(value.item).map(|(currency_or_any, tolerance)| {
117 InferredToleranceDefault(currency_or_any, tolerance)
118 })
119 }
120
121 "inferred_tolerance_multiplier" => Decimal::try_from(value.item)
122 .map(InferredToleranceMultiplier)
123 .map_err(|e| BadValueErrorKind::Decimal(e).wrap()),
124
125 "infer_tolerance_from_cost" => parse_bool(value.item).map(InferToleranceFromCost),
126
127 "documents" => Ok(Documents(
128 source_path
129 .and_then(|path| path.parent())
130 .map_or(PathBuf::from(value.item), |parent| parent.join(value.item)),
131 )),
132
133 "operating_currency" => parse_currency(value.item).map(OperatingCurrency),
134
135 "render_commas" => parse_bool(value.item)
137 .or(parse_zero_or_one_as_bool(value.item))
138 .map(RenderCommas),
139
140 "long_string_maxlines" => value
141 .item
142 .parse::<usize>()
143 .map(LongStringMaxlines)
144 .map_err(|e| BadValueErrorKind::ParseIntError(e).wrap()),
145
146 "booking_method" => parse_booking(value.item).map(BookingMethod),
147
148 "plugin_processing_mode" => {
149 parse_plugin_processing_mode(value.item).map(PluginProcessingMode)
150 }
151
152 _ => Err(UnknownOption),
153 }
154 .map(|variant| BeancountOption {
155 source: Source {
156 name: name.span,
157 value: value.span,
158 },
159 variant,
160 })
161 }
162}
163
164fn parse_subaccount(colon_separated: &str) -> Result<Subaccount, BeancountOptionError> {
165 Subaccount::try_from(colon_separated).map_err(|e| BadValueErrorKind::AccountName(e).wrap())
166}
167
168fn parse_account_type_name(value: &str) -> Result<AccountTypeName, BeancountOptionError> {
169 AccountTypeName::try_from(value).map_err(|e| BadValueErrorKind::AccountTypeName(e).wrap())
170}
171
172fn parse_currency(value: &str) -> Result<Currency, BeancountOptionError> {
173 Currency::try_from(value).map_err(|e| BadValueErrorKind::Currency(e).wrap())
174}
175
176fn parse_inferred_tolerance_default(
177 value: &str,
178) -> Result<(CurrencyOrAny, Decimal), BeancountOptionError> {
179 use BadValueErrorKind as Bad;
180
181 let mut fields = value.split(':');
182 let currency_or_any = CurrencyOrAny::try_from(fields.by_ref().next().unwrap())
183 .map_err(|e| Bad::Currency(e).wrap())?;
184 let tolerance = fields
185 .by_ref()
186 .next()
187 .ok_or(Bad::MissingColon)
188 .and_then(|s| Decimal::try_from(s).map_err(Bad::Decimal))
189 .map_err(Bad::wrap)?;
190
191 if fields.next().is_none() {
192 Ok((currency_or_any, tolerance))
193 } else {
194 Err(Bad::TooManyColons.wrap())
195 }
196}
197
198fn parse_booking(value: &str) -> Result<Booking, BeancountOptionError> {
199 Booking::try_from(value).map_err(|e| BadValueErrorKind::Booking(e).wrap())
200}
201
202fn parse_plugin_processing_mode(value: &str) -> Result<PluginProcessingMode, BeancountOptionError> {
203 PluginProcessingMode::try_from(value)
204 .map_err(|e| BadValueErrorKind::PluginProcessingMode(e).wrap())
205}
206
207fn parse_bool(value: &str) -> Result<bool, BeancountOptionError> {
209 if value.eq_ignore_ascii_case("true") {
210 Ok(true)
211 } else if value.eq_ignore_ascii_case("false") {
212 Ok(false)
213 } else {
214 Err(BadValueErrorKind::Bool.wrap())
215 }
216}
217
218fn parse_zero_or_one_as_bool(value: &str) -> Result<bool, BeancountOptionError> {
219 if value == "1" {
220 Ok(true)
221 } else if value == "0" {
222 Ok(false)
223 } else {
224 Err(BadValueErrorKind::Bool.wrap())
225 }
226}
227
228#[derive(Debug)]
229pub(crate) enum BeancountOptionError {
230 UnknownOption,
231 BadValue(BadValueError),
232}
233
234impl Display for BeancountOptionError {
235 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
236 use BeancountOptionError::*;
237
238 match &self {
239 UnknownOption => f.write_str("unknown option"),
240 BadValue(BadValueError(e)) => write!(f, "{}", e),
241 }
242 }
243}
244
245impl std::error::Error for BeancountOptionError {}
246
247#[derive(Debug)]
248pub(crate) struct BadValueError(BadValueErrorKind);
249
250#[derive(Debug)]
251enum BadValueErrorKind {
252 AccountTypeName(AccountTypeNameError),
253 AccountTypeNames(AccountTypeNamesError),
254 AccountName(AccountNameError),
255 Currency(CurrencyError),
256 Booking(strum::ParseError),
257 PluginProcessingMode(strum::ParseError),
258 Decimal(rust_decimal::Error),
259 Bool,
260 MissingColon,
261 TooManyColons,
262 ParseIntError(std::num::ParseIntError),
263}
264
265impl Display for BadValueErrorKind {
266 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
267 use BadValueErrorKind::*;
268
269 match &self {
270 AccountTypeName(e) => write!(f, "{}", e),
271 AccountTypeNames(e) => write!(f, "{}", e),
272 AccountName(e) => write!(f, "{}", e),
273 Currency(e) => write!(f, "{}", e),
274 Booking(_e) => format(
275 f,
276 crate::types::Booking::iter(),
277 plain,
278 ", ",
279 Some("Expected one of "),
280 ),
281 PluginProcessingMode(_e) => format(
282 f,
283 self::PluginProcessingMode::iter(),
284 plain,
285 ", ",
286 Some("Expected one of "),
287 ),
288 Decimal(e) => write!(f, "{}", e),
289 Bool => f.write_str("must be true or false or case-insensitive equivalent"),
290 MissingColon => f.write_str("missing colon"),
291 TooManyColons => f.write_str("too many colons"),
292 ParseIntError(e) => write!(f, "{}", e),
293 }
294 }
295}
296
297impl BadValueErrorKind {
298 fn wrap(self) -> BeancountOptionError {
299 BeancountOptionError::BadValue(BadValueError(self))
300 }
301}
302
303#[derive(Debug)]
304pub(crate) struct ParserOptions<'a> {
306 pub(crate) account_type_names: AccountTypeNames<'a>,
307 pub(crate) long_string_maxlines: OptionallySourced<usize>,
308}
309
310impl Default for ParserOptions<'_> {
311 fn default() -> Self {
312 ParserOptions {
313 account_type_names: AccountTypeNames::default(),
314 long_string_maxlines: unsourced(64),
315 }
316 }
317}
318
319impl<'a> ParserOptions<'a> {
320 pub(crate) fn assimilate(
321 &mut self,
322 opt: BeancountOption<'a>,
323 ) -> Result<BeancountOption<'a>, ParserOptionsError> {
324 use BeancountOptionVariant::*;
325
326 let BeancountOption { source, variant } = opt;
327
328 match variant {
329 AccountTypeName(account_type, account_type_name) => self
330 .account_type_names
331 .update(account_type, account_type_name)
332 .map_err(ParserOptionsError)
333 .map(|_| Assimilated),
334
335 LongStringMaxlines(n) => {
336 self.long_string_maxlines = optionally_sourced(n, source);
337 Ok(Assimilated)
338 }
339
340 _ => Ok(variant),
341 }
342 .map(|variant| BeancountOption { source, variant })
343 }
344
345 pub(crate) fn account_type_name(&self, account_type: AccountType) -> &AccountTypeName {
346 &self.account_type_names.name_by_type[account_type as usize]
347 }
348
349 pub(crate) fn long_string_maxlines(&self) -> usize {
350 self.long_string_maxlines.item
351 }
352}
353
354#[derive(PartialEq, Eq, Debug)]
355pub(crate) struct ParserOptionsError(AccountTypeNamesError);
356
357impl Display for ParserOptionsError {
358 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
359 write!(f, "{}", self.0)
360 }
361}
362
363impl std::error::Error for ParserOptionsError {}
364
365#[derive(Debug)]
367pub struct Options<'a> {
368 title: OptionallySourced<&'a str>,
369 account_previous_balances: OptionallySourced<Subaccount<'a>>,
370 account_previous_earnings: OptionallySourced<Subaccount<'a>>,
371 account_previous_conversions: OptionallySourced<Subaccount<'a>>,
372 account_current_earnings: OptionallySourced<Subaccount<'a>>,
373 account_current_conversions: OptionallySourced<Subaccount<'a>>,
374 account_unrealized_gains: OptionallySourced<Subaccount<'a>>,
375 account_rounding: Option<Sourced<Subaccount<'a>>>,
376 conversion_currency: OptionallySourced<Currency<'a>>,
377 inferred_tolerance_default: HashMap<CurrencyOrAny<'a>, (Decimal, Source)>,
378 inferred_tolerance_multiplier: OptionallySourced<Decimal>,
379 infer_tolerance_from_cost: OptionallySourced<bool>,
380 documents: HashMap<PathBuf, Source>,
381 operating_currency: HashMap<Currency<'a>, Source>,
382 render_commas: OptionallySourced<bool>,
383 booking_method: OptionallySourced<Booking>,
384 plugin_processing_mode: OptionallySourced<PluginProcessingMode>,
385 parser_options: ParserOptions<'a>,
386}
387
388impl<'a> Options<'a> {
389 pub(crate) fn new(parser_options: ParserOptions<'a>) -> Self {
390 Options {
391 title: unsourced("Beancount"),
392 account_previous_balances: unsourced(parse_subaccount("Opening-Balances").unwrap()),
393 account_previous_earnings: unsourced(parse_subaccount("Earnings:Previous").unwrap()),
394 account_previous_conversions: unsourced(
395 parse_subaccount("Conversions:Previous").unwrap(),
396 ),
397 account_current_earnings: unsourced(parse_subaccount("Earnings:Current").unwrap()),
398 account_current_conversions: unsourced(
399 parse_subaccount("Conversions:Current").unwrap(),
400 ),
401 account_unrealized_gains: unsourced(parse_subaccount("Earnings:Unrealized").unwrap()),
402 account_rounding: None,
403 conversion_currency: unsourced(Currency::try_from("NOTHING").unwrap()),
404 inferred_tolerance_default: HashMap::new(),
405 inferred_tolerance_multiplier: unsourced(dec!(0.5)),
406 infer_tolerance_from_cost: unsourced(false),
407 documents: HashMap::new(),
408 operating_currency: HashMap::new(),
409 render_commas: unsourced(false),
410 booking_method: unsourced(Booking::Strict),
411 plugin_processing_mode: unsourced(PluginProcessingMode::Default),
412 parser_options,
413 }
414 }
415
416 pub(crate) fn assimilate(&mut self, opt: BeancountOption<'a>) -> Result<(), Error> {
417 use BeancountOptionVariant::*;
418 use OptionError::*;
419
420 let BeancountOption { source, variant } = opt;
421 match variant {
422 Title(value) => Self::update(&mut self.title, value, source),
423
424 AccountTypeName(_, _) => Ok(()),
426
427 AccountPreviousBalances(value) => {
428 Self::update(&mut self.account_previous_balances, value, source)
429 }
430 AccountPreviousEarnings(value) => {
431 Self::update(&mut self.account_previous_earnings, value, source)
432 }
433 AccountPreviousConversions(value) => {
434 Self::update(&mut self.account_previous_conversions, value, source)
435 }
436
437 AccountCurrentEarnings(value) => {
438 Self::update(&mut self.account_current_earnings, value, source)
439 }
440
441 AccountCurrentConversions(value) => {
442 Self::update(&mut self.account_current_conversions, value, source)
443 }
444
445 AccountUnrealizedGains(value) => {
446 Self::update(&mut self.account_unrealized_gains, value, source)
447 }
448
449 AccountRounding(value) => {
450 Self::update_optional(&mut self.account_rounding, value, source)
451 }
452
453 ConversionCurrency(value) => Self::update(&mut self.conversion_currency, value, source),
454
455 InferredToleranceDefault(currency_or_any, tolerance) => Self::update_hashmap(
456 &mut self.inferred_tolerance_default,
457 currency_or_any,
458 tolerance,
459 source,
460 ),
461
462 InferredToleranceMultiplier(value) => {
463 Self::update(&mut self.inferred_tolerance_multiplier, value, source)
464 }
465
466 InferToleranceFromCost(value) => {
467 Self::update(&mut self.infer_tolerance_from_cost, value, source)
468 }
469
470 Documents(path) => Self::update_unit_hashmap(&mut self.documents, path, source),
471
472 OperatingCurrency(value) => {
473 Self::update_unit_hashmap(&mut self.operating_currency, value, source)
474 }
475
476 RenderCommas(value) => Self::update(&mut self.render_commas, value, source),
477
478 LongStringMaxlines(_) => Ok(()),
480
481 BookingMethod(value) => Self::update(&mut self.booking_method, value, source),
482
483 PluginProcessingMode(value) => {
484 Self::update(&mut self.plugin_processing_mode, value, source)
485 }
486
487 Assimilated => Ok(()),
489 }
490 .map_err(|ref e| match e {
491 DuplicateOption(span) => Error::new("invalid option", "duplicate", source.name)
492 .related_to_named_span("option", *span),
493 DuplicateValue(span) => Error::new("invalid option", "duplicate value", source.value)
494 .related_to_named_span("option value", *span),
495 })
496 }
497
498 fn update<T>(
499 field: &mut OptionallySourced<T>,
500 value: T,
501 source: Source,
502 ) -> Result<(), OptionError> {
503 use OptionError::*;
504
505 match &field.source {
506 None => {
507 *field = optionally_sourced(value, source);
508 Ok(())
509 }
510 Some(source) => Err(DuplicateOption(source.name)),
511 }
512 }
513
514 fn update_optional<T>(
515 field: &mut Option<Sourced<T>>,
516 value: T,
517 source: Source,
518 ) -> Result<(), OptionError> {
519 use OptionError::*;
520
521 match field {
522 None => {
523 *field = Some(sourced(value, source));
524 Ok(())
525 }
526 Some(Sourced { source, .. }) => Err(DuplicateOption(source.name)),
527 }
528 }
529
530 fn update_hashmap<K, V>(
531 field: &mut HashMap<K, (V, Source)>,
532 key: K,
533 value: V,
534 source: Source,
535 ) -> Result<(), OptionError>
536 where
537 K: Eq + Hash,
538 {
539 use hash_map::Entry::*;
540 use OptionError::*;
541
542 match field.entry(key) {
543 Vacant(entry) => {
544 entry.insert((value, source));
545
546 Ok(())
547 }
548 Occupied(entry) => Err(DuplicateValue(entry.get().1.value)),
549 }
550 }
551
552 fn update_unit_hashmap<K>(
554 field: &mut HashMap<K, Source>,
555 key: K,
556 source: Source,
557 ) -> Result<(), OptionError>
558 where
559 K: Eq + Hash,
560 {
561 use hash_map::Entry::*;
562 use OptionError::*;
563
564 match field.entry(key) {
565 Vacant(entry) => {
566 entry.insert(source);
567
568 Ok(())
569 }
570 Occupied(entry) => Err(DuplicateValue(entry.get().value)),
571 }
572 }
573
574 pub fn account_type_name(&self, account_type: AccountType) -> &AccountTypeName {
575 self.parser_options.account_type_name(account_type)
576 }
577
578 pub fn title(&self) -> &str {
579 self.title.item
580 }
581
582 pub fn account_previous_balances(&self) -> &Subaccount {
583 &self.account_previous_balances.item
584 }
585
586 pub fn account_previous_earnings(&self) -> &Subaccount {
587 &self.account_previous_earnings.item
588 }
589
590 pub fn account_previous_conversions(&self) -> &Subaccount {
591 &self.account_previous_conversions.item
592 }
593
594 pub fn account_current_earnings(&self) -> &Subaccount {
595 &self.account_current_earnings.item
596 }
597
598 pub fn account_current_conversions(&self) -> &Subaccount {
599 &self.account_current_conversions.item
600 }
601
602 pub fn account_unrealized_gains(&self) -> &Subaccount {
603 &self.account_unrealized_gains.item
604 }
605
606 pub fn account_rounding(&self) -> Option<&Subaccount> {
607 self.account_rounding.as_ref().map(|x| &x.item)
608 }
609
610 pub fn conversion_currency(&self) -> &Currency {
611 &self.conversion_currency.item
612 }
613
614 pub fn inferred_tolerance_default(&self, currency: &Currency) -> Option<Decimal> {
616 self.inferred_tolerance_default
617 .get(&CurrencyOrAny::Currency(*currency))
618 .map(|d| d.0)
619 .or(self
620 .inferred_tolerance_default
621 .get(&CurrencyOrAny::Any)
622 .map(|d| d.0))
623 }
624
625 pub fn inferred_tolerance_default_fallback(&self) -> Option<Decimal> {
627 self.inferred_tolerance_default
628 .get(&CurrencyOrAny::Any)
629 .map(|d| d.0)
630 }
631
632 pub fn inferred_tolerance_defaults(&self) -> impl Iterator<Item = (Option<Currency>, Decimal)> {
634 self.inferred_tolerance_default
635 .iter()
636 .map(|(c, d)| match c {
637 CurrencyOrAny::Currency(c) => (Some(*c), d.0),
638 CurrencyOrAny::Any => (None, d.0),
639 })
640 }
641
642 pub fn inferred_tolerance_multiplier(&self) -> Decimal {
643 self.inferred_tolerance_multiplier.item
644 }
645
646 pub fn infer_tolerance_from_cost(&self) -> bool {
647 self.infer_tolerance_from_cost.item
648 }
649
650 pub fn documents(&self) -> impl Iterator<Item = &PathBuf> {
651 self.documents.iter().map(|document| document.0)
652 }
653
654 pub fn operating_currency(&self) -> impl Iterator<Item = &Currency> {
655 self.operating_currency.iter().map(|document| document.0)
656 }
657
658 pub fn render_commas(&self) -> bool {
659 self.render_commas.item
660 }
661
662 pub fn booking_method(&self) -> Booking {
663 self.booking_method.item
664 }
665
666 pub fn plugin_processing_mode(&self) -> PluginProcessingMode {
667 self.plugin_processing_mode.item
668 }
669
670 pub fn long_string_maxlines(&self) -> usize {
671 self.parser_options.long_string_maxlines.item
672 }
673}
674
675#[derive(Debug)]
676struct Sourced<T> {
677 item: T,
678 source: Source,
679}
680
681impl<T> PartialEq for Sourced<T>
682where
683 T: PartialEq,
684{
685 fn eq(&self, other: &Self) -> bool {
686 self.item.eq(&other.item)
687 }
688}
689
690impl<T> Eq for Sourced<T> where T: Eq {}
691
692fn sourced<T>(item: T, source: Source) -> Sourced<T> {
693 Sourced { item, source }
694}
695
696#[derive(Debug)]
697pub(crate) struct OptionallySourced<T> {
698 pub(crate) item: T,
699 pub(crate) source: Option<Source>,
700}
701
702fn unsourced<T>(item: T) -> OptionallySourced<T> {
703 OptionallySourced { item, source: None }
704}
705
706fn optionally_sourced<T>(item: T, source: Source) -> OptionallySourced<T> {
707 OptionallySourced {
708 item,
709 source: Some(source),
710 }
711}
712
713#[derive(PartialEq, Eq, Clone, Copy, Debug)]
714pub(crate) struct Source {
715 pub(crate) name: Span,
716 pub(crate) value: Span,
717}
718
719#[derive(PartialEq, Eq, Debug)]
720pub(crate) enum OptionError {
721 DuplicateOption(Span),
722 DuplicateValue(Span),
723}
724
725impl OptionError {
726 pub(crate) fn span(&self) -> Span {
727 use OptionError::*;
728
729 match self {
730 DuplicateOption(span) => *span,
731 DuplicateValue(span) => *span,
732 }
733 }
734}
735
736impl Display for OptionError {
737 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
738 use OptionError::*;
739
740 match &self {
741 DuplicateOption(_) => f.write_str("duplicate option"),
742 DuplicateValue(_) => f.write_str("duplicate value"),
743 }
744 }
745}
746
747impl std::error::Error for OptionError {}