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