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::{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            // 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 {
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
220// case insenstive parsing
221fn 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)]
317/// ParserOptions are only those which affect the core parsing.
318pub(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/// All options read in from `option` pragmas, excluding those for internal processing only.
375#[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            // already assimilated into ParserOptions
430            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            // already assimilated into ParserOptions
486            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            // these values contain nothing
495            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    // equivalent to a HashSet where the key has a source
544    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    /// return the tolerance default for one particular currency
610    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    /// return the tolerance default fallback in case not defined for a particular currency
621    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    /// return the tolerance defaults for all currencies, with None as the 'any' currency
628    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 {}