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