1use rust_decimal::Decimal;
4use std::collections::{HashMap, HashSet};
5use std::str::FromStr;
6
7const KNOWN_OPTIONS: &[&str] = &[
9 "title",
10 "filename",
11 "operating_currency",
12 "name_assets",
13 "name_liabilities",
14 "name_equity",
15 "name_income",
16 "name_expenses",
17 "account_rounding",
18 "account_previous_balances",
19 "account_previous_earnings",
20 "account_previous_conversions",
21 "account_current_earnings",
22 "account_current_conversions",
23 "account_unrealized_gains",
24 "conversion_currency",
25 "inferred_tolerance_default",
26 "inferred_tolerance_multiplier",
27 "infer_tolerance_from_cost",
28 "use_legacy_fixed_tolerances",
29 "experiment_explicit_tolerances",
30 "booking_method",
31 "render_commas",
32 "display_precision",
33 "allow_pipe_separator",
34 "long_string_maxlines",
35 "documents",
36 "insert_pythonpath",
37 "plugin_processing_mode",
38 "plugin", "tolerance_multiplier", ];
41
42const REPEATABLE_OPTIONS: &[&str] = &[
44 "operating_currency",
45 "insert_pythonpath",
46 "documents",
47 "inferred_tolerance_default",
48 "display_precision",
49];
50
51const READONLY_OPTIONS: &[&str] = &["filename"];
53
54#[derive(Debug, Clone)]
56pub struct OptionWarning {
57 pub code: &'static str,
59 pub message: String,
61 pub option: String,
63 pub value: String,
65}
66
67#[derive(Debug, Clone)]
71pub struct Options {
72 pub title: Option<String>,
74
75 pub filename: Option<String>,
77
78 pub operating_currency: Vec<String>,
80
81 pub name_assets: String,
83
84 pub name_liabilities: String,
86
87 pub name_equity: String,
89
90 pub name_income: String,
92
93 pub name_expenses: String,
95
96 pub account_rounding: Option<String>,
98
99 pub account_previous_balances: String,
101
102 pub account_previous_earnings: String,
104
105 pub account_previous_conversions: String,
107
108 pub account_current_earnings: String,
110
111 pub account_current_conversions: Option<String>,
113
114 pub account_unrealized_gains: Option<String>,
116
117 pub conversion_currency: Option<String>,
119
120 pub inferred_tolerance_default: HashMap<String, Decimal>,
122
123 pub inferred_tolerance_multiplier: Decimal,
125
126 pub infer_tolerance_from_cost: bool,
128
129 pub use_legacy_fixed_tolerances: bool,
131
132 pub experiment_explicit_tolerances: bool,
134
135 pub booking_method: String,
137
138 pub render_commas: bool,
140
141 pub display_precision: HashMap<String, u32>,
144
145 pub allow_pipe_separator: bool,
147
148 pub long_string_maxlines: u32,
150
151 pub documents: Vec<String>,
153
154 pub plugin_processing_mode: String,
156
157 pub custom: HashMap<String, String>,
159
160 #[doc(hidden)]
162 pub set_options: HashSet<String>,
163
164 pub warnings: Vec<OptionWarning>,
166}
167
168impl Default for Options {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174impl Options {
175 #[must_use]
177 pub fn new() -> Self {
178 Self {
179 title: None,
180 filename: None,
181 operating_currency: Vec::new(),
182 name_assets: "Assets".to_string(),
183 name_liabilities: "Liabilities".to_string(),
184 name_equity: "Equity".to_string(),
185 name_income: "Income".to_string(),
186 name_expenses: "Expenses".to_string(),
187 account_rounding: None,
188 account_previous_balances: "Equity:Opening-Balances".to_string(),
189 account_previous_earnings: "Equity:Earnings:Previous".to_string(),
190 account_previous_conversions: "Equity:Conversions:Previous".to_string(),
191 account_current_earnings: "Equity:Earnings:Current".to_string(),
192 account_current_conversions: None,
193 account_unrealized_gains: None,
194 conversion_currency: None,
195 inferred_tolerance_default: HashMap::new(),
196 inferred_tolerance_multiplier: Decimal::new(5, 1), infer_tolerance_from_cost: false,
198 use_legacy_fixed_tolerances: false,
199 experiment_explicit_tolerances: false,
200 booking_method: "STRICT".to_string(),
201 render_commas: false, display_precision: HashMap::new(),
203 allow_pipe_separator: false,
204 long_string_maxlines: 64,
205 documents: Vec::new(),
206 plugin_processing_mode: "default".to_string(),
207 custom: HashMap::new(),
208 set_options: HashSet::new(),
209 warnings: Vec::new(),
210 }
211 }
212
213 pub fn set(&mut self, key: &str, value: &str) {
217 let is_known = KNOWN_OPTIONS.contains(&key);
219 if !is_known {
220 self.warnings.push(OptionWarning {
221 code: "E7001",
222 message: format!("Unknown option \"{key}\""),
223 option: key.to_string(),
224 value: value.to_string(),
225 });
226 }
227
228 if READONLY_OPTIONS.contains(&key) {
230 self.warnings.push(OptionWarning {
231 code: "E7005",
232 message: format!("Option '{key}' may not be set"),
233 option: key.to_string(),
234 value: value.to_string(),
235 });
236 return; }
238
239 let is_repeatable = REPEATABLE_OPTIONS.contains(&key);
241 if is_known && !is_repeatable && self.set_options.contains(key) {
242 self.warnings.push(OptionWarning {
243 code: "E7003",
244 message: format!("Option \"{key}\" can only be specified once"),
245 option: key.to_string(),
246 value: value.to_string(),
247 });
248 }
249
250 self.set_options.insert(key.to_string());
252
253 match key {
255 "title" => self.title = Some(value.to_string()),
256 "operating_currency" => self.operating_currency.push(value.to_string()),
257 "name_assets" => self.name_assets = value.to_string(),
258 "name_liabilities" => self.name_liabilities = value.to_string(),
259 "name_equity" => self.name_equity = value.to_string(),
260 "name_income" => self.name_income = value.to_string(),
261 "name_expenses" => self.name_expenses = value.to_string(),
262 "account_rounding" => {
263 if !Self::is_valid_account(value) {
264 self.warnings.push(OptionWarning {
265 code: "E7002",
266 message: format!("Invalid leaf account name: '{value}'"),
267 option: key.to_string(),
268 value: value.to_string(),
269 });
270 }
271 self.account_rounding = Some(value.to_string());
272 }
273 "account_current_conversions" => {
274 if !Self::is_valid_account(value) {
275 self.warnings.push(OptionWarning {
276 code: "E7002",
277 message: format!("Invalid leaf account name: '{value}'"),
278 option: key.to_string(),
279 value: value.to_string(),
280 });
281 }
282 self.account_current_conversions = Some(value.to_string());
283 }
284 "account_unrealized_gains" => {
285 if !Self::is_valid_account(value) {
286 self.warnings.push(OptionWarning {
287 code: "E7002",
288 message: format!("Invalid leaf account name: '{value}'"),
289 option: key.to_string(),
290 value: value.to_string(),
291 });
292 }
293 self.account_unrealized_gains = Some(value.to_string());
294 }
295 "inferred_tolerance_multiplier" => {
296 self.warnings.push(OptionWarning {
298 code: "E7004",
299 message: "Renamed to 'tolerance_multiplier'.".to_string(),
300 option: key.to_string(),
301 value: value.to_string(),
302 });
303 if let Ok(d) = Decimal::from_str(value) {
304 self.inferred_tolerance_multiplier = d;
305 } else {
306 self.warnings.push(OptionWarning {
308 code: "E7002",
309 message: format!(
310 "Invalid value \"{value}\" for option \"{key}\": expected decimal number"
311 ),
312 option: key.to_string(),
313 value: value.to_string(),
314 });
315 }
316 }
317 "tolerance_multiplier" => {
318 if let Ok(d) = Decimal::from_str(value) {
319 self.inferred_tolerance_multiplier = d;
320 } else {
321 self.warnings.push(OptionWarning {
322 code: "E7002",
323 message: format!(
324 "Invalid value \"{value}\" for option \"{key}\": expected decimal number"
325 ),
326 option: key.to_string(),
327 value: value.to_string(),
328 });
329 }
330 }
331 "infer_tolerance_from_cost" => {
332 if !value.eq_ignore_ascii_case("true") && !value.eq_ignore_ascii_case("false") {
333 self.warnings.push(OptionWarning {
334 code: "E7002",
335 message: format!(
336 "Invalid value \"{value}\" for option \"{key}\": expected TRUE or FALSE"
337 ),
338 option: key.to_string(),
339 value: value.to_string(),
340 });
341 }
342 self.infer_tolerance_from_cost = value.eq_ignore_ascii_case("true");
343 }
344 "booking_method" => {
345 let valid_methods = [
346 "STRICT",
347 "STRICT_WITH_SIZE",
348 "FIFO",
349 "LIFO",
350 "HIFO",
351 "AVERAGE",
352 "NONE",
353 ];
354 if !valid_methods.contains(&value.to_uppercase().as_str()) {
355 self.warnings.push(OptionWarning {
356 code: "E7002",
357 message: format!(
358 "Invalid value \"{}\" for option \"{}\": expected one of {}",
359 value,
360 key,
361 valid_methods.join(", ")
362 ),
363 option: key.to_string(),
364 value: value.to_string(),
365 });
366 }
367 self.booking_method = value.to_string();
368 }
369 "render_commas" => {
370 let is_true = value.eq_ignore_ascii_case("true") || value == "1";
372 let is_false = value.eq_ignore_ascii_case("false") || value == "0";
373 if !is_true && !is_false {
374 self.warnings.push(OptionWarning {
375 code: "E7002",
376 message: format!(
377 "Invalid value \"{value}\" for option \"{key}\": expected TRUE or FALSE"
378 ),
379 option: key.to_string(),
380 value: value.to_string(),
381 });
382 }
383 self.render_commas = is_true;
384 }
385 "display_precision" => {
386 if let Some((curr, example)) = value.split_once(':') {
390 if let Ok(d) = Decimal::from_str(example) {
391 let precision = d.scale();
393 self.display_precision.insert(curr.to_string(), precision);
394 } else {
395 self.warnings.push(OptionWarning {
396 code: "E7002",
397 message: format!(
398 "Invalid precision value \"{example}\" in option \"{key}\""
399 ),
400 option: key.to_string(),
401 value: value.to_string(),
402 });
403 }
404 } else {
405 self.warnings.push(OptionWarning {
406 code: "E7002",
407 message: format!(
408 "Invalid format for option \"{key}\": expected CURRENCY:EXAMPLE (e.g., CHF:0.01)"
409 ),
410 option: key.to_string(),
411 value: value.to_string(),
412 });
413 }
414 }
415 "filename" => self.filename = Some(value.to_string()),
416 "account_previous_balances" => {
417 if !Self::is_valid_account(value) {
418 self.warnings.push(OptionWarning {
419 code: "E7002",
420 message: format!("Invalid leaf account name: '{value}'"),
421 option: key.to_string(),
422 value: value.to_string(),
423 });
424 }
425 self.account_previous_balances = value.to_string();
426 }
427 "account_previous_earnings" => {
428 if !Self::is_valid_account(value) {
429 self.warnings.push(OptionWarning {
430 code: "E7002",
431 message: format!("Invalid leaf account name: '{value}'"),
432 option: key.to_string(),
433 value: value.to_string(),
434 });
435 }
436 self.account_previous_earnings = value.to_string();
437 }
438 "account_previous_conversions" => {
439 if !Self::is_valid_account(value) {
440 self.warnings.push(OptionWarning {
441 code: "E7002",
442 message: format!("Invalid leaf account name: '{value}'"),
443 option: key.to_string(),
444 value: value.to_string(),
445 });
446 }
447 self.account_previous_conversions = value.to_string();
448 }
449 "account_current_earnings" => {
450 if !Self::is_valid_account(value) {
451 self.warnings.push(OptionWarning {
452 code: "E7002",
453 message: format!("Invalid leaf account name: '{value}'"),
454 option: key.to_string(),
455 value: value.to_string(),
456 });
457 }
458 self.account_current_earnings = value.to_string();
459 }
460 "conversion_currency" => self.conversion_currency = Some(value.to_string()),
461 "inferred_tolerance_default" => {
462 if let Some((curr, tol)) = value.split_once(':') {
464 if let Ok(d) = Decimal::from_str(tol) {
465 self.inferred_tolerance_default.insert(curr.to_string(), d);
466 } else {
467 self.warnings.push(OptionWarning {
468 code: "E7002",
469 message: format!(
470 "Invalid tolerance value \"{tol}\" in option \"{key}\""
471 ),
472 option: key.to_string(),
473 value: value.to_string(),
474 });
475 }
476 } else {
477 self.warnings.push(OptionWarning {
478 code: "E7002",
479 message: format!(
480 "Invalid format for option \"{key}\": expected CURRENCY:TOLERANCE"
481 ),
482 option: key.to_string(),
483 value: value.to_string(),
484 });
485 }
486 }
487 "use_legacy_fixed_tolerances" => {
488 self.use_legacy_fixed_tolerances = value.eq_ignore_ascii_case("true");
489 }
490 "experiment_explicit_tolerances" => {
491 self.experiment_explicit_tolerances = value.eq_ignore_ascii_case("true");
492 }
493 "allow_pipe_separator" => {
494 self.warnings.push(OptionWarning {
496 code: "E7004",
497 message: "Option 'allow_pipe_separator' is deprecated".to_string(),
498 option: key.to_string(),
499 value: value.to_string(),
500 });
501 self.allow_pipe_separator = value.eq_ignore_ascii_case("true");
502 }
503 "long_string_maxlines" => {
504 if let Ok(n) = value.parse::<u32>() {
505 self.long_string_maxlines = n;
506 } else {
507 self.warnings.push(OptionWarning {
508 code: "E7002",
509 message: format!(
510 "Invalid value \"{value}\" for option \"{key}\": expected integer"
511 ),
512 option: key.to_string(),
513 value: value.to_string(),
514 });
515 }
516 }
517 "documents" => {
518 if !std::path::Path::new(value).exists() {
520 self.warnings.push(OptionWarning {
521 code: "E7006",
522 message: format!("Document root '{value}' does not exist"),
523 option: key.to_string(),
524 value: value.to_string(),
525 });
526 }
527 self.documents.push(value.to_string());
528 }
529 "plugin_processing_mode" => {
530 if value != "default" && value != "raw" {
532 self.warnings.push(OptionWarning {
533 code: "E7002",
534 message: format!("Invalid value '{value}'"),
535 option: key.to_string(),
536 value: value.to_string(),
537 });
538 }
539 self.plugin_processing_mode = value.to_string();
540 }
541 "plugin" => {
542 self.warnings.push(OptionWarning {
544 code: "E7004",
545 message: "Option 'plugin' is deprecated; use the 'plugin' directive instead"
546 .to_string(),
547 option: key.to_string(),
548 value: value.to_string(),
549 });
550 }
551 _ => {
552 self.custom.insert(key.to_string(), value.to_string());
554 }
555 }
556 }
557
558 #[must_use]
560 pub fn get(&self, key: &str) -> Option<&str> {
561 self.custom.get(key).map(String::as_str)
562 }
563
564 #[must_use]
566 pub fn account_types(&self) -> [&str; 5] {
567 [
568 &self.name_assets,
569 &self.name_liabilities,
570 &self.name_equity,
571 &self.name_income,
572 &self.name_expenses,
573 ]
574 }
575
576 fn is_valid_account(value: &str) -> bool {
581 if !value.contains(':') {
583 return false;
584 }
585
586 for part in value.split(':') {
588 if let Some(first) = part.chars().next() {
590 if !first.is_ascii_uppercase() {
591 return false;
592 }
593 } else {
594 return false;
596 }
597 }
598
599 true
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 #[test]
608 fn test_default_options() {
609 let opts = Options::new();
610 assert_eq!(opts.name_assets, "Assets");
611 assert_eq!(opts.booking_method, "STRICT");
612 assert!(!opts.infer_tolerance_from_cost);
613 }
614
615 #[test]
616 fn test_set_options() {
617 let mut opts = Options::new();
618 opts.set("title", "My Ledger");
619 opts.set("operating_currency", "USD");
620 opts.set("operating_currency", "EUR");
621 opts.set("booking_method", "FIFO");
622
623 assert_eq!(opts.title, Some("My Ledger".to_string()));
624 assert_eq!(opts.operating_currency, vec!["USD", "EUR"]);
625 assert_eq!(opts.booking_method, "FIFO");
626 }
627
628 #[test]
629 fn test_custom_options() {
630 let mut opts = Options::new();
631 opts.set("my_custom_option", "my_value");
632
633 assert_eq!(opts.get("my_custom_option"), Some("my_value"));
634 assert_eq!(opts.get("nonexistent"), None);
635 }
636
637 #[test]
638 fn test_unknown_option_warning() {
639 let mut opts = Options::new();
640 opts.set("unknown_option", "value");
641
642 assert_eq!(opts.warnings.len(), 1);
643 assert_eq!(opts.warnings[0].code, "E7001");
644 assert!(opts.warnings[0].message.contains("Unknown option"));
645 }
646
647 #[test]
648 fn test_duplicate_option_warning() {
649 let mut opts = Options::new();
650 opts.set("title", "First Title");
651 opts.set("title", "Second Title");
652
653 assert_eq!(opts.warnings.len(), 1);
654 assert_eq!(opts.warnings[0].code, "E7003");
655 assert!(opts.warnings[0].message.contains("only be specified once"));
656 }
657
658 #[test]
659 fn test_repeatable_option_no_warning() {
660 let mut opts = Options::new();
661 opts.set("operating_currency", "USD");
662 opts.set("operating_currency", "EUR");
663
664 assert!(
666 opts.warnings.is_empty(),
667 "Should not warn for repeatable options: {:?}",
668 opts.warnings
669 );
670 assert_eq!(opts.operating_currency, vec!["USD", "EUR"]);
671 }
672
673 #[test]
674 fn test_invalid_tolerance_value() {
675 let mut opts = Options::new();
676 opts.set("inferred_tolerance_multiplier", "not_a_number");
677
678 assert_eq!(opts.warnings.len(), 2);
680 assert_eq!(opts.warnings[0].code, "E7004");
681 assert!(opts.warnings[0].message.contains("Renamed"));
682 assert_eq!(opts.warnings[1].code, "E7002");
683 assert!(opts.warnings[1].message.contains("expected decimal"));
684 }
685
686 #[test]
687 fn test_tolerance_multiplier_new_name() {
688 let mut opts = Options::new();
689 opts.set("tolerance_multiplier", "1.5");
690
691 assert!(opts.warnings.is_empty());
692 assert_eq!(opts.inferred_tolerance_multiplier, Decimal::new(15, 1));
693 }
694
695 #[test]
696 fn test_inferred_tolerance_multiplier_deprecated() {
697 let mut opts = Options::new();
698 opts.set("inferred_tolerance_multiplier", "1.01");
699
700 assert_eq!(opts.warnings.len(), 1);
701 assert_eq!(opts.warnings[0].code, "E7004");
702 assert!(
703 opts.warnings[0]
704 .message
705 .contains("Renamed to 'tolerance_multiplier'")
706 );
707 assert_eq!(
708 opts.inferred_tolerance_multiplier,
709 Decimal::from_str("1.01").unwrap()
710 );
711 }
712
713 #[test]
714 fn test_invalid_boolean_value() {
715 let mut opts = Options::new();
716 opts.set("infer_tolerance_from_cost", "maybe");
717
718 assert_eq!(opts.warnings.len(), 1);
719 assert_eq!(opts.warnings[0].code, "E7002");
720 assert!(opts.warnings[0].message.contains("TRUE or FALSE"));
721 }
722
723 #[test]
724 fn test_invalid_booking_method() {
725 let mut opts = Options::new();
726 opts.set("booking_method", "RANDOM");
727
728 assert_eq!(opts.warnings.len(), 1);
729 assert_eq!(opts.warnings[0].code, "E7002");
730 assert!(opts.warnings[0].message.contains("STRICT"));
731 }
732
733 #[test]
734 fn test_valid_booking_methods() {
735 for method in &["STRICT", "FIFO", "LIFO", "AVERAGE", "NONE"] {
736 let mut opts = Options::new();
737 opts.set("booking_method", method);
738 assert!(
739 opts.warnings.is_empty(),
740 "Should accept {method} as valid booking method"
741 );
742 }
743 }
744
745 #[test]
746 fn test_readonly_option_warning() {
747 let mut opts = Options::new();
748 opts.set("filename", "/some/path.beancount");
749
750 assert_eq!(opts.warnings.len(), 1);
751 assert_eq!(opts.warnings[0].code, "E7005");
752 assert!(opts.warnings[0].message.contains("may not be set"));
753 }
754
755 #[test]
756 fn test_invalid_account_name_validation() {
757 let mut opts = Options::new();
759 opts.set("account_rounding", "invalid");
760
761 assert_eq!(opts.warnings.len(), 1);
762 assert_eq!(opts.warnings[0].code, "E7002");
763 assert!(opts.warnings[0].message.contains("Invalid leaf account"));
764 }
765
766 #[test]
767 fn test_valid_account_name() {
768 let mut opts = Options::new();
769 opts.set("account_rounding", "Equity:Rounding");
770
771 assert!(
772 opts.warnings.is_empty(),
773 "Valid account name should not produce warnings: {:?}",
774 opts.warnings
775 );
776 assert_eq!(opts.account_rounding, Some("Equity:Rounding".to_string()));
777 }
778
779 #[test]
780 fn test_render_commas_with_numeric_values() {
781 let mut opts = Options::new();
782 opts.set("render_commas", "1");
783 assert!(opts.render_commas);
784 assert!(opts.warnings.is_empty());
785
786 let mut opts2 = Options::new();
787 opts2.set("render_commas", "0");
788 assert!(!opts2.render_commas);
789 assert!(opts2.warnings.is_empty());
790 }
791
792 #[test]
793 fn test_plugin_processing_mode_validation() {
794 let mut opts = Options::new();
796 opts.set("plugin_processing_mode", "default");
797 assert!(opts.warnings.is_empty());
798 assert_eq!(opts.plugin_processing_mode, "default");
799
800 let mut opts2 = Options::new();
801 opts2.set("plugin_processing_mode", "raw");
802 assert!(opts2.warnings.is_empty());
803 assert_eq!(opts2.plugin_processing_mode, "raw");
804
805 let mut opts3 = Options::new();
807 opts3.set("plugin_processing_mode", "invalid");
808 assert_eq!(opts3.warnings.len(), 1);
809 assert_eq!(opts3.warnings[0].code, "E7002");
810 }
811
812 #[test]
813 fn test_deprecated_plugin_option() {
814 let mut opts = Options::new();
815 opts.set("plugin", "some.plugin");
816
817 assert_eq!(opts.warnings.len(), 1);
818 assert_eq!(opts.warnings[0].code, "E7004");
819 assert!(opts.warnings[0].message.contains("deprecated"));
820 }
821
822 #[test]
823 fn test_deprecated_allow_pipe_separator() {
824 let mut opts = Options::new();
825 opts.set("allow_pipe_separator", "true");
826
827 assert_eq!(opts.warnings.len(), 1);
828 assert_eq!(opts.warnings[0].code, "E7004");
829 assert!(opts.warnings[0].message.contains("deprecated"));
830 }
831
832 #[test]
833 fn test_is_valid_account() {
834 assert!(Options::is_valid_account("Assets:Bank"));
836 assert!(Options::is_valid_account("Equity:Rounding:Precision"));
837
838 assert!(!Options::is_valid_account("invalid")); assert!(!Options::is_valid_account("assets:bank")); assert!(!Options::is_valid_account("Assets:")); assert!(!Options::is_valid_account(":Bank")); }
844
845 #[test]
846 fn test_account_validation_options() {
847 let account_options = [
849 "account_rounding",
850 "account_current_conversions",
851 "account_unrealized_gains",
852 "account_previous_balances",
853 "account_previous_earnings",
854 "account_previous_conversions",
855 "account_current_earnings",
856 ];
857
858 for opt in account_options {
859 let mut opts = Options::new();
860 opts.set(opt, "lowercase:invalid");
861
862 assert!(
863 !opts.warnings.is_empty(),
864 "Option '{opt}' should warn on invalid account name"
865 );
866 assert_eq!(opts.warnings[0].code, "E7002");
867 }
868 }
869
870 #[test]
871 fn test_inferred_tolerance_default() {
872 let mut opts = Options::new();
873 opts.set("inferred_tolerance_default", "USD:0.005");
874
875 assert!(opts.warnings.is_empty());
876 assert_eq!(
877 opts.inferred_tolerance_default.get("USD"),
878 Some(&rust_decimal_macros::dec!(0.005))
879 );
880
881 let mut opts2 = Options::new();
883 opts2.set("inferred_tolerance_default", "*:0.01");
884 assert!(opts2.warnings.is_empty());
885 assert_eq!(
886 opts2.inferred_tolerance_default.get("*"),
887 Some(&rust_decimal_macros::dec!(0.01))
888 );
889
890 let mut opts3 = Options::new();
892 opts3.set("inferred_tolerance_default", "INVALID");
893 assert_eq!(opts3.warnings.len(), 1);
894 assert_eq!(opts3.warnings[0].code, "E7002");
895 }
896}