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 "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
208fn 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)]
305pub(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#[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 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 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 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 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 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 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 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 {}