Skip to main content

tanzim_source/
lib.rs

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/// Error from building or parsing a [`Source`].
14#[derive(Debug)]
15pub enum Error {
16    /// Builder has no source identifier (missing or empty).
17    MissingSource,
18    /// Invalid configuration source string.
19    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            // Transparent: forward Display (and its alternate form) to the wrapped error.
27            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/// A pipeline stage whose errors a [`Source`] can choose to tolerate.
48///
49/// Declared in the source string via the reserved `on_error` option, e.g.
50/// `file(on_error=(load=skip,validate=skip)):/etc/app`.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum Stage {
53    /// The load stage (fetching the raw bytes).
54    Load,
55    /// The parse stage (turning bytes into values).
56    Parse,
57    /// The validate stage (checking values against a schema).
58    Validate,
59}
60
61impl Stage {
62    /// The reserved-option key name for this stage (`load` / `parse` / `validate`).
63    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/// What to do when a stage fails for a given [`Source`].
79///
80/// The default for every stage is [`OnError::Fail`]; a source opts into tolerance per stage with
81/// `on_error=(<stage>=skip)`.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
83pub enum OnError {
84    /// Abort the pipeline with the error (default).
85    #[default]
86    Fail,
87    /// Skip this source's contribution (load/parse) or fall back to a default (validate).
88    Skip,
89}
90
91/// Kind of value stored in [`OptionValue`].
92#[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/// Dynamically typed loader option or nested option map.
116#[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/// Ordered map of loader options.
324#[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/// One configuration source declaration.
414///
415/// See the [crate-level documentation](crate) for the source string format, parsing rules, and
416/// examples.
417#[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    /// Build a bare source with just a name — no options, no resource — infallibly.
431    ///
432    /// Used for synthetic origins (e.g. `tanzim_value::Location`s that do not come from parsing a
433    /// real source string). Unlike [`Source::parse`] this performs no validation, so the name may
434    /// even be empty for a placeholder origin.
435    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    /// The error policy this source declares for `stage`, via its reserved `on_error` option.
445    ///
446    /// Defaults to [`OnError::Fail`] when the option is absent, malformed, or does not mention the
447    /// stage. `on_error=(load=skip,validate=skip)` yields [`OnError::Skip`] for those two stages.
448    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/// Builds a [`Source`].
539#[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}