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, thiserror::Error)]
15pub enum Error {
16    /// Builder has no source identifier (missing or empty).
17    #[error("configuration source is required")]
18    MissingSource,
19    /// Invalid configuration source string.
20    #[error(transparent)]
21    Parse(#[from] ParseError),
22}
23
24/// A pipeline stage whose errors a [`Source`] can choose to tolerate.
25///
26/// Declared in the source string via the reserved `on_error` option, e.g.
27/// `file(on_error=(load=skip,validate=skip)):/etc/app`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum Stage {
30    /// The load stage (fetching the raw bytes).
31    Load,
32    /// The parse stage (turning bytes into values).
33    Parse,
34    /// The validate stage (checking values against a schema).
35    Validate,
36}
37
38impl Stage {
39    /// The reserved-option key name for this stage (`load` / `parse` / `validate`).
40    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/// What to do when a stage fails for a given [`Source`].
56///
57/// The default for every stage is [`OnError::Fail`]; a source opts into tolerance per stage with
58/// `on_error=(<stage>=skip)`.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
60pub enum OnError {
61    /// Abort the pipeline with the error (default).
62    #[default]
63    Fail,
64    /// Skip this source's contribution (load/parse) or fall back to a default (validate).
65    Skip,
66}
67
68/// Kind of value stored in [`OptionValue`].
69#[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/// Dynamically typed loader option or nested option map.
93#[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/// Ordered map of loader options.
301#[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/// One configuration source declaration.
391///
392/// See the [crate-level documentation](crate) for the source string format, parsing rules, and
393/// examples.
394#[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    /// Build a bare source with just a name — no options, no resource — infallibly.
408    ///
409    /// Used for synthetic origins (e.g. `tanzim_value::Location`s that do not come from parsing a
410    /// real source string). Unlike [`Source::parse`] this performs no validation, so the name may
411    /// even be empty for a placeholder origin.
412    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    /// The error policy this source declares for `stage`, via its reserved `on_error` option.
422    ///
423    /// Defaults to [`OnError::Fail`] when the option is absent, malformed, or does not mention the
424    /// stage. `on_error=(load=skip,validate=skip)` yields [`OnError::Skip`] for those two stages.
425    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/// Builds a [`Source`].
516#[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}