Skip to main content

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::{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            // 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    // a bit of a hack to be able to fail on unknown or bad option and still have the parser consume the input
164    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
217// case insenstive parsing
218fn 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)]
314/// ParserOptions are only those which affect the core parsing.
315pub(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/// All options read in from `option` pragmas, excluding those for internal processing only.
372#[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            // already assimilated into ParserOptions
427            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            // already assimilated into ParserOptions
483            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            // these values contain nothing
492            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    // equivalent to a HashSet where the key has a source
541    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    /// return the tolerance default for one particular currency
607    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    /// return the tolerance default fallback in case not defined for a particular currency
618    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    /// return the tolerance defaults for all currencies, with None as the 'any' currency
625    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
710// this is a bit of a grotty hack, sorry
711// but it's private to the crate, and we don't leak a default for Span
712impl 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 {}