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