1#![doc = include_str!("../README.md")]
2
3mod impls;
4mod parse;
5
6pub use parse::{ParseError, parse};
7
8#[cfg(feature = "serde")]
9mod serde;
10
11use std::fmt::{Debug, Display, Formatter};
12
13#[derive(Debug, thiserror::Error)]
15pub enum Error {
16 #[error("configuration source is required")]
18 MissingSource,
19 #[error(transparent)]
21 Parse(#[from] ParseError),
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum Stage {
30 Load,
32 Parse,
34 Validate,
36}
37
38impl Stage {
39 pub fn as_str(self) -> &'static str {
41 match self {
42 Self::Load => "load",
43 Self::Parse => "parse",
44 Self::Validate => "validate",
45 }
46 }
47}
48
49impl Display for Stage {
50 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
51 f.write_str(self.as_str())
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
60pub enum OnError {
61 #[default]
63 Fail,
64 Skip,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum OptionValueType {
71 Bool,
72 Integer,
73 Float,
74 String,
75 Map,
76 List,
77}
78
79impl Display for OptionValueType {
80 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
81 f.write_str(match self {
82 Self::Bool => "boolean",
83 Self::Integer => "integer",
84 Self::Float => "float",
85 Self::String => "string",
86 Self::Map => "map",
87 Self::List => "list",
88 })
89 }
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub enum OptionValue {
95 Bool(bool),
96 Integer(i64),
97 Float(f64),
98 String(String),
99 List(Vec<OptionValue>),
100 Map(Options),
101}
102
103impl OptionValue {
104 pub fn new_map() -> Self {
105 Self::Map(Options::default())
106 }
107
108 pub fn new_list() -> Self {
109 Self::List(Vec::new())
110 }
111
112 pub fn new_string() -> Self {
113 Self::String(String::new())
114 }
115
116 pub fn is_bool(&self) -> bool {
117 matches!(self, Self::Bool(_))
118 }
119
120 pub fn as_bool(&self) -> Option<bool> {
121 match self {
122 Self::Bool(value) => Some(*value),
123 _ => None,
124 }
125 }
126
127 pub fn into_bool(self) -> Option<bool> {
128 match self {
129 Self::Bool(value) => Some(value),
130 _ => None,
131 }
132 }
133
134 pub fn bool_mut(&mut self) -> Option<&mut bool> {
135 match self {
136 Self::Bool(value) => Some(value),
137 _ => None,
138 }
139 }
140
141 pub fn is_integer(&self) -> bool {
142 matches!(self, Self::Integer(_))
143 }
144
145 pub fn as_integer(&self) -> Option<i64> {
146 match self {
147 Self::Integer(value) => Some(*value),
148 _ => None,
149 }
150 }
151
152 pub fn into_integer(self) -> Option<i64> {
153 match self {
154 Self::Integer(value) => Some(value),
155 _ => None,
156 }
157 }
158
159 pub fn integer_mut(&mut self) -> Option<&mut i64> {
160 match self {
161 Self::Integer(value) => Some(value),
162 _ => None,
163 }
164 }
165
166 pub fn is_float(&self) -> bool {
167 matches!(self, Self::Float(_))
168 }
169
170 pub fn as_float(&self) -> Option<f64> {
171 match self {
172 Self::Float(value) => Some(*value),
173 _ => None,
174 }
175 }
176
177 pub fn into_float(self) -> Option<f64> {
178 match self {
179 Self::Float(value) => Some(value),
180 _ => None,
181 }
182 }
183
184 pub fn float_mut(&mut self) -> Option<&mut f64> {
185 match self {
186 Self::Float(value) => Some(value),
187 _ => None,
188 }
189 }
190
191 pub fn is_string(&self) -> bool {
192 matches!(self, Self::String(_))
193 }
194
195 pub fn as_string(&self) -> Option<&String> {
196 match self {
197 Self::String(value) => Some(value),
198 _ => None,
199 }
200 }
201
202 pub fn into_string(self) -> Option<String> {
203 match self {
204 Self::String(value) => Some(value),
205 _ => None,
206 }
207 }
208
209 pub fn string_mut(&mut self) -> Option<&mut String> {
210 match self {
211 Self::String(value) => Some(value),
212 _ => None,
213 }
214 }
215
216 pub fn is_list(&self) -> bool {
217 matches!(self, Self::List(_))
218 }
219
220 pub fn as_list(&self) -> Option<&Vec<OptionValue>> {
221 match self {
222 Self::List(value) => Some(value),
223 _ => None,
224 }
225 }
226
227 pub fn into_list(self) -> Option<Vec<OptionValue>> {
228 match self {
229 Self::List(value) => Some(value),
230 _ => None,
231 }
232 }
233
234 pub fn list_mut(&mut self) -> Option<&mut Vec<OptionValue>> {
235 match self {
236 Self::List(value) => Some(value),
237 _ => None,
238 }
239 }
240
241 pub fn is_map(&self) -> bool {
242 matches!(self, Self::Map(_))
243 }
244
245 pub fn as_map(&self) -> Option<&Options> {
246 match self {
247 Self::Map(value) => Some(value),
248 _ => None,
249 }
250 }
251
252 pub fn into_map(self) -> Option<Options> {
253 match self {
254 Self::Map(value) => Some(value),
255 _ => None,
256 }
257 }
258
259 pub fn map_mut(&mut self) -> Option<&mut Options> {
260 match self {
261 Self::Map(value) => Some(value),
262 _ => None,
263 }
264 }
265
266 pub fn type_name(&self) -> OptionValueType {
267 match self {
268 Self::Bool(_) => OptionValueType::Bool,
269 Self::Integer(_) => OptionValueType::Integer,
270 Self::Float(_) => OptionValueType::Float,
271 Self::String(_) => OptionValueType::String,
272 Self::List(_) => OptionValueType::List,
273 Self::Map(_) => OptionValueType::Map,
274 }
275 }
276}
277
278impl Display for OptionValue {
279 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
280 match self {
281 Self::Bool(value) => write!(f, "{value}"),
282 Self::Integer(value) => write!(f, "{value}"),
283 Self::Float(value) => write!(f, "{value}"),
284 Self::String(value) => write!(f, "{value:?}"),
285 Self::List(value) => {
286 write!(f, "[")?;
287 for (index, inner_value) in value.iter().enumerate() {
288 if index > 0 {
289 write!(f, ", ")?;
290 }
291 write!(f, "{inner_value}")?;
292 }
293 write!(f, "]")
294 }
295 Self::Map(value) => write!(f, "{value}"),
296 }
297 }
298}
299
300#[derive(Debug, Clone, PartialEq, Default)]
302pub struct Options {
303 entries: Vec<(String, OptionValue)>,
304}
305
306impl Options {
307 pub fn new() -> Self {
308 Self::default()
309 }
310
311 pub fn len(&self) -> usize {
312 self.entries.len()
313 }
314
315 pub fn is_empty(&self) -> bool {
316 self.entries.is_empty()
317 }
318
319 pub fn contains_key(&self, key: &str) -> bool {
320 self.entries.iter().any(|(entry_key, _)| entry_key == key)
321 }
322
323 pub fn get(&self, key: &str) -> Option<&OptionValue> {
324 self.entries
325 .iter()
326 .rfind(|(entry_key, _)| entry_key == key)
327 .map(|(_, value)| value)
328 }
329
330 pub fn get_mut(&mut self, key: &str) -> Option<&mut OptionValue> {
331 let index = self
332 .entries
333 .iter()
334 .rposition(|(entry_key, _)| entry_key == key)?;
335 Some(&mut self.entries[index].1)
336 }
337
338 pub fn insert<K: Into<String>, V: Into<OptionValue>>(
339 &mut self,
340 key: K,
341 value: V,
342 ) -> Option<OptionValue> {
343 let key = key.into();
344 let value = value.into();
345 let old = self.remove(&key);
346 self.entries.push((key, value));
347 old
348 }
349
350 pub fn remove(&mut self, key: &str) -> Option<OptionValue> {
351 let index = self
352 .entries
353 .iter()
354 .rposition(|(entry_key, _)| entry_key == key)?;
355 Some(self.entries.remove(index).1)
356 }
357
358 pub fn iter(&self) -> impl Iterator<Item = (&str, &OptionValue)> {
359 self.entries
360 .iter()
361 .map(|(key, value)| (key.as_str(), value))
362 }
363
364 pub fn keys(&self) -> impl Iterator<Item = &str> {
365 self.entries.iter().map(|(key, _)| key.as_str())
366 }
367
368 pub fn values(&self) -> impl Iterator<Item = &OptionValue> {
369 self.entries.iter().map(|(_, value)| value)
370 }
371
372 pub(crate) fn entries(&self) -> &[(String, OptionValue)] {
373 &self.entries
374 }
375}
376
377impl Display for Options {
378 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
379 write!(f, "{{")?;
380 for (index, (key, value)) in self.entries.iter().enumerate() {
381 if index > 0 {
382 write!(f, ", ")?;
383 }
384 write!(f, "{key:?}: {value}")?;
385 }
386 write!(f, "}}")
387 }
388}
389
390#[derive(Debug, Clone, PartialEq)]
395pub struct Source {
396 pub(crate) source: String,
397 pub(crate) options: Options,
398 pub(crate) resource: String,
399 pub(crate) resource_colon: bool,
400}
401
402impl Source {
403 pub fn parse(input: &str) -> Result<Self, ParseError> {
404 parse::parse(input)
405 }
406
407 pub fn named(name: impl Into<String>) -> Self {
413 Self {
414 source: name.into(),
415 options: Options::default(),
416 resource: String::new(),
417 resource_colon: false,
418 }
419 }
420
421 pub fn on_error(&self, stage: Stage) -> OnError {
426 let Some(OptionValue::Map(map)) = self.options.get("on_error") else {
427 return OnError::Fail;
428 };
429 match map.get(stage.as_str()) {
430 Some(OptionValue::String(value)) if value.eq_ignore_ascii_case("skip") => OnError::Skip,
431 _ => OnError::Fail,
432 }
433 }
434
435 pub fn source(&self) -> &str {
436 self.source.as_str()
437 }
438
439 pub fn source_mut(&mut self) -> &mut String {
440 &mut self.source
441 }
442
443 pub fn set_source(&mut self, source: impl Into<String>) {
444 self.source = source.into();
445 }
446
447 pub fn with_source(mut self, source: impl Into<String>) -> Self {
448 self.source = source.into();
449 self
450 }
451
452 pub fn options(&self) -> &Options {
453 &self.options
454 }
455
456 pub fn options_mut(&mut self) -> &mut Options {
457 &mut self.options
458 }
459
460 pub fn set_options(&mut self, options: Options) {
461 self.options = options;
462 }
463
464 pub fn with_options(mut self, options: Options) -> Self {
465 self.options = options;
466 self
467 }
468
469 pub fn set_option<K: Into<String>, V: Into<OptionValue>>(&mut self, key: K, value: V) {
470 self.options.insert(key, value);
471 }
472
473 pub fn with_option<K: Into<String>, V: Into<OptionValue>>(mut self, key: K, value: V) -> Self {
474 self.options.insert(key, value);
475 self
476 }
477
478 pub fn resource(&self) -> &str {
479 self.resource.as_str()
480 }
481
482 pub fn resource_mut(&mut self) -> &mut String {
483 &mut self.resource
484 }
485
486 pub fn set_resource(&mut self, resource: impl Into<String>) {
487 self.resource = resource.into();
488 if !self.resource.is_empty() {
489 self.resource_colon = true;
490 }
491 }
492
493 pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
494 self.resource = resource.into();
495 if !self.resource.is_empty() {
496 self.resource_colon = true;
497 }
498 self
499 }
500
501 pub fn resource_colon(&self) -> bool {
502 self.resource_colon
503 }
504
505 pub fn set_resource_colon(&mut self, resource_colon: bool) {
506 self.resource_colon = resource_colon;
507 }
508
509 pub fn with_resource_colon(mut self, resource_colon: bool) -> Self {
510 self.resource_colon = resource_colon;
511 self
512 }
513}
514
515#[derive(Debug, Clone, PartialEq, Default)]
517pub struct SourceBuilder {
518 source: Option<String>,
519 options: Options,
520 resource: String,
521 resource_colon: bool,
522}
523
524impl SourceBuilder {
525 pub fn new() -> Self {
526 Self::default()
527 }
528
529 pub fn with_source(mut self, source: impl Into<String>) -> Self {
530 self.source = Some(source.into());
531 self
532 }
533
534 pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
535 self.resource = resource.into();
536 self
537 }
538
539 pub fn with_options(mut self, options: Options) -> Self {
540 self.options = options;
541 self
542 }
543
544 pub fn with_option<K: Into<String>, V: Into<OptionValue>>(mut self, key: K, value: V) -> Self {
545 self.options.insert(key, value);
546 self
547 }
548
549 pub fn with_resource_colon(mut self, resource_colon: bool) -> Self {
550 self.resource_colon = resource_colon;
551 self
552 }
553
554 pub fn build(self) -> Result<Source, Error> {
555 let source = self.source.ok_or(Error::MissingSource)?;
556 if source.is_empty() {
557 return Err(Error::MissingSource);
558 }
559 let resource_colon = self.resource_colon || !self.resource.is_empty();
560 Ok(Source {
561 source,
562 options: self.options,
563 resource: self.resource,
564 resource_colon,
565 })
566 }
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn builder_requires_source() {
575 let error = SourceBuilder::new().build().unwrap_err();
576 assert!(matches!(error, Error::MissingSource));
577
578 let error = SourceBuilder::new().with_source("").build().unwrap_err();
579 assert!(matches!(error, Error::MissingSource));
580 }
581
582 #[test]
583 fn builder_with_option_and_into_string() {
584 let source = SourceBuilder::new()
585 .with_source("env")
586 .with_resource("")
587 .with_option("prefix", "APP")
588 .with_option("timeout", 30_i64)
589 .with_option("retry", true)
590 .build()
591 .unwrap();
592
593 assert_eq!(source.source(), "env");
594 assert_eq!(source.resource(), "");
595 assert_eq!(
596 source.options().get("prefix"),
597 Some(&OptionValue::String("APP".into()))
598 );
599 assert_eq!(
600 source.options().get("timeout"),
601 Some(&OptionValue::Integer(30))
602 );
603 assert_eq!(
604 source.options().get("retry"),
605 Some(&OptionValue::Bool(true))
606 );
607 }
608
609 #[test]
610 fn options_last_key_wins() {
611 let mut options = Options::new();
612 options.insert("prefix", "OLD");
613 options.insert("prefix", "NEW");
614 assert_eq!(options.len(), 1);
615 assert_eq!(
616 options.get("prefix"),
617 Some(&OptionValue::String("NEW".into()))
618 );
619 }
620
621 #[test]
622 fn option_value_accessors_and_type_name() {
623 let value = OptionValue::from(vec!["a", "b"]);
624 assert!(value.is_list());
625 assert_eq!(value.type_name(), OptionValueType::List);
626 assert_eq!(value.as_list().unwrap().len(), 2);
627
628 let mut map = OptionValue::new_map();
629 map.map_mut()
630 .unwrap()
631 .insert("enabled", OptionValue::from(true));
632 assert_eq!(map.type_name(), OptionValueType::Map);
633 }
634
635 #[test]
636 fn config_source_setters() {
637 let mut source = SourceBuilder::new()
638 .with_source("file")
639 .with_resource("/etc/app")
640 .build()
641 .unwrap();
642
643 source.set_source("http");
644 source.set_resource("https://example.com/config.json");
645 source.set_option("timeout", 5_u32);
646
647 assert_eq!(source.source(), "http");
648 assert_eq!(source.resource(), "https://example.com/config.json");
649 assert_eq!(
650 source.options().get("timeout"),
651 Some(&OptionValue::Integer(5))
652 );
653 }
654
655 #[test]
656 fn builder_with_options() {
657 let mut options = Options::new();
658 options.insert("prefix", "APP_");
659 let source = SourceBuilder::new()
660 .with_source("env")
661 .with_options(options)
662 .build()
663 .unwrap();
664 assert_eq!(
665 source.options().get("prefix"),
666 Some(&OptionValue::String("APP_".into()))
667 );
668 }
669
670 #[test]
671 fn on_error_reads_reserved_option() {
672 let fail = Source::parse("file:/etc/app").unwrap();
673 assert_eq!(fail.on_error(Stage::Load), OnError::Fail);
674 assert_eq!(fail.on_error(Stage::Validate), OnError::Fail);
675
676 let source = Source::parse("file(on_error=(load=skip,validate=skip)):/etc/app").unwrap();
677 assert_eq!(source.on_error(Stage::Load), OnError::Skip);
678 assert_eq!(source.on_error(Stage::Parse), OnError::Fail);
679 assert_eq!(source.on_error(Stage::Validate), OnError::Skip);
680 }
681
682 #[test]
683 fn named_builds_bare_source() {
684 let source = Source::named("schema");
685 assert_eq!(source.source(), "schema");
686 assert_eq!(source.resource(), "");
687 assert!(source.options().is_empty());
688 assert_eq!(source.on_error(Stage::Validate), OnError::Fail);
689 }
690
691 #[test]
692 fn options_remove_and_option_value_mutators() {
693 let mut options = Options::new();
694 options.insert("keep", "yes");
695 options.insert("drop", "no");
696 options.remove("drop");
697 assert!(!options.contains_key("drop"));
698 assert!(options.contains_key("keep"));
699
700 let mut value = OptionValue::Integer(1);
701 assert_eq!(value.as_integer(), Some(1));
702 if let Some(number) = value.integer_mut() {
703 *number = 2;
704 }
705 assert_eq!(value.as_integer(), Some(2));
706 assert_eq!(value.into_integer(), Some(2));
707 }
708
709 #[test]
710 fn options_display_iter_and_mutators() {
711 let mut options = Options::new();
712 options.insert("a", 1_i64);
713 options.insert("b", "two");
714 assert_eq!(options.len(), 2);
715 assert!(!options.is_empty());
716
717 let keys: Vec<&str> = options.keys().collect();
718 assert_eq!(keys, vec!["a", "b"]);
719
720 let mut values = 0;
721 for (_, value) in options.iter() {
722 if value.is_integer() || value.is_string() {
723 values += 1;
724 }
725 }
726 assert_eq!(values, 2);
727
728 if let Some(value) = options.get_mut("a") {
729 *value = OptionValue::Integer(9);
730 }
731 assert_eq!(options.get("a"), Some(&OptionValue::Integer(9)));
732
733 let previous = options.insert("a", 3_i64);
734 assert_eq!(previous, Some(OptionValue::Integer(9)));
735
736 let display = options.to_string();
737 assert!(display.contains("\"a\""));
738 assert!(display.contains("two"));
739 }
740
741 #[test]
742 fn option_value_and_type_display() {
743 assert_eq!(OptionValueType::Map.to_string(), "map");
744 assert_eq!(OptionValueType::List.to_string(), "list");
745
746 let list = OptionValue::from(vec![1_i64, 2_i64]);
747 assert_eq!(list.to_string(), "[1, 2]");
748
749 let mut map = Options::new();
750 map.insert("enabled", true);
751 let map_value = OptionValue::Map(map);
752 assert!(map_value.to_string().contains("enabled"));
753 }
754
755 #[test]
756 fn source_display_and_builder_resource_colon() {
757 let source = SourceBuilder::new()
758 .with_source("env")
759 .with_resource_colon(true)
760 .build()
761 .unwrap();
762 assert!(source.resource_colon());
763 assert_eq!(source.to_string(), "env:");
764
765 let mut source = SourceBuilder::new()
766 .with_source("file")
767 .with_option("ignore", vec!["not-found"])
768 .with_resource("/tmp/x")
769 .build()
770 .unwrap();
771 source.set_resource_colon(false);
772 source.options_mut().insert("extra", "yes");
773 assert_eq!(source.source(), "file");
774 let text = source.to_string();
775 assert!(text.contains("/tmp/x"));
776 assert!(text.contains("extra=yes"));
777 }
778
779 #[test]
780 fn source_with_mutators_update_fields() {
781 let source = SourceBuilder::new()
782 .with_source("env")
783 .build()
784 .unwrap()
785 .with_source("file")
786 .with_resource("/etc/app")
787 .with_option("lowercase", false);
788 assert_eq!(source.source(), "file");
789 assert_eq!(source.resource(), "/etc/app");
790 assert_eq!(
791 source.options().get("lowercase"),
792 Some(&OptionValue::Bool(false))
793 );
794 }
795
796 #[test]
797 fn error_wraps_parse_failure() {
798 match SourceBuilder::try_from("env(prefix=)") {
799 Ok(_) => panic!("expected parse error"),
800 Err(error) => assert!(matches!(error, Error::Parse(ParseError::EmptyValue { .. }))),
801 }
802 }
803
804 #[test]
805 fn option_value_coercions_and_type_names() {
806 let float = OptionValue::from(1.5_f64);
807 assert!(float.is_float());
808 assert_eq!(float.type_name(), OptionValueType::Float);
809 assert_eq!(float.as_float(), Some(1.5));
810
811 let text = OptionValue::from("hello");
812 assert!(text.into_string().is_some());
813
814 let mut flag = OptionValue::Bool(false);
815 if let Some(value) = flag.bool_mut() {
816 *value = true;
817 }
818 assert_eq!(flag.as_bool(), Some(true));
819 }
820}