beancount_parser_lima/
options.rs

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