beancount_parser_lima/
options.rs

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            // any of case-insensitive true, false, 0, 1
136            "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
207// case insenstive parsing
208fn 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)]
304/// ParserOptions are only those which affect the core parsing.
305pub(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/// All options read in from `option` pragmas, excluding those for internal processing only.
366#[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            // already assimilated into ParserOptions
425            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            // already assimilated into ParserOptions
479            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            // this value contains nothing
488            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    // equivalent to a HashSet where the key has a source
553    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    /// return the tolerance default for one particular currency
615    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    /// return the tolerance default fallback in case not defined for a particular currency
626    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    /// return the tolerance defaults for all currencies, with None as the 'any' currency
633    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 {}