Skip to main content

tanzim_source/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4mod impls;
5mod parse;
6
7pub use parse::{ParseError, parse};
8
9#[cfg(feature = "serde")]
10mod serde;
11
12use std::fmt::{Debug, Display, Formatter};
13
14/// Error from building or parsing a [`Source`].
15#[derive(Debug)]
16pub enum Error {
17    /// Builder has no source identifier (missing or empty).
18    MissingSource,
19    /// Invalid configuration source string.
20    Parse(ParseError),
21}
22
23impl Display for Error {
24    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::MissingSource => write!(f, "configuration source is required"),
27            // Transparent: forward Display (and its alternate form) to the wrapped error.
28            Self::Parse(error) => Display::fmt(error, f),
29        }
30    }
31}
32
33impl std::error::Error for Error {
34    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
35        match self {
36            Self::Parse(error) => Some(error),
37            Self::MissingSource => None,
38        }
39    }
40}
41
42impl From<ParseError> for Error {
43    fn from(error: ParseError) -> Self {
44        Self::Parse(error)
45    }
46}
47
48/// A pipeline stage whose errors a [`Source`] can choose to tolerate.
49///
50/// Declared in the source string via the reserved `on_error` option, e.g.
51/// `file(on_error=(load=skip,validate=skip)):/etc/app`.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum Stage {
54    /// The load stage (fetching the raw bytes).
55    Load,
56    /// The parse stage (turning bytes into values).
57    Parse,
58    /// The validate stage (checking values against a schema).
59    Validate,
60}
61
62impl Stage {
63    /// The reserved-option key name for this stage (`load` / `parse` / `validate`).
64    pub fn as_str(self) -> &'static str {
65        match self {
66            Self::Load => "load",
67            Self::Parse => "parse",
68            Self::Validate => "validate",
69        }
70    }
71}
72
73impl Display for Stage {
74    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
75        f.write_str(self.as_str())
76    }
77}
78
79/// What to do when a stage fails for a given [`Source`].
80///
81/// The default for every stage is [`OnError::Fail`]; a source opts into tolerance per stage with
82/// `on_error=(<stage>=skip)`.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
84pub enum OnError {
85    /// Abort the pipeline with the error (default).
86    #[default]
87    Fail,
88    /// Skip this source's contribution (load/parse) or fall back to a default (validate).
89    Skip,
90}
91
92/// Kind of value stored in [`OptionValue`].
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum OptionValueType {
95    /// A boolean value.
96    Bool,
97    /// A 64-bit signed integer.
98    Integer,
99    /// A 64-bit floating-point number.
100    Float,
101    /// A string value.
102    String,
103    /// A nested option map.
104    Map,
105    /// A list of values.
106    List,
107}
108
109impl Display for OptionValueType {
110    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
111        f.write_str(match self {
112            Self::Bool => "boolean",
113            Self::Integer => "integer",
114            Self::Float => "float",
115            Self::String => "string",
116            Self::Map => "map",
117            Self::List => "list",
118        })
119    }
120}
121
122/// Dynamically typed loader option or nested option map.
123///
124/// # Examples
125///
126/// ```rust
127/// use tanzim_source::{OptionValue, OptionValueType};
128///
129/// let value = OptionValue::Integer(3);
130/// assert_eq!(value.type_name(), OptionValueType::Integer);
131/// assert_eq!(value.as_integer(), Some(3));
132/// assert_eq!(value.as_bool(), None);
133/// ```
134#[derive(Debug, Clone, PartialEq)]
135pub enum OptionValue {
136    /// A boolean value.
137    Bool(bool),
138    /// A 64-bit signed integer.
139    Integer(i64),
140    /// A 64-bit floating-point number.
141    Float(f64),
142    /// A string value.
143    String(String),
144    /// A list of values.
145    List(Vec<OptionValue>),
146    /// A nested option map.
147    Map(Options),
148}
149
150impl OptionValue {
151    /// Build an empty [`OptionValue::Map`].
152    pub fn new_map() -> Self {
153        Self::Map(Options::default())
154    }
155
156    /// Build an empty [`OptionValue::List`].
157    pub fn new_list() -> Self {
158        Self::List(Vec::new())
159    }
160
161    /// Build an empty [`OptionValue::String`].
162    pub fn new_string() -> Self {
163        Self::String(String::new())
164    }
165
166    /// `true` if this is a [`OptionValue::Bool`].
167    pub fn is_bool(&self) -> bool {
168        matches!(self, Self::Bool(_))
169    }
170
171    /// The boolean value, or `None` if this is not a [`OptionValue::Bool`].
172    pub fn as_bool(&self) -> Option<bool> {
173        match self {
174            Self::Bool(value) => Some(*value),
175            _ => None,
176        }
177    }
178
179    /// Convert into the boolean value, or `None` if this is not a [`OptionValue::Bool`].
180    pub fn into_bool(self) -> Option<bool> {
181        match self {
182            Self::Bool(value) => Some(value),
183            _ => None,
184        }
185    }
186
187    /// A mutable reference to the boolean value, or `None` if this is not a [`OptionValue::Bool`].
188    pub fn bool_mut(&mut self) -> Option<&mut bool> {
189        match self {
190            Self::Bool(value) => Some(value),
191            _ => None,
192        }
193    }
194
195    /// `true` if this is a [`OptionValue::Integer`].
196    pub fn is_integer(&self) -> bool {
197        matches!(self, Self::Integer(_))
198    }
199
200    /// The integer value, or `None` if this is not a [`OptionValue::Integer`].
201    pub fn as_integer(&self) -> Option<i64> {
202        match self {
203            Self::Integer(value) => Some(*value),
204            _ => None,
205        }
206    }
207
208    /// Convert into the integer value, or `None` if this is not a [`OptionValue::Integer`].
209    pub fn into_integer(self) -> Option<i64> {
210        match self {
211            Self::Integer(value) => Some(value),
212            _ => None,
213        }
214    }
215
216    /// A mutable reference to the integer value, or `None` if this is not a [`OptionValue::Integer`].
217    pub fn integer_mut(&mut self) -> Option<&mut i64> {
218        match self {
219            Self::Integer(value) => Some(value),
220            _ => None,
221        }
222    }
223
224    /// `true` if this is a [`OptionValue::Float`].
225    pub fn is_float(&self) -> bool {
226        matches!(self, Self::Float(_))
227    }
228
229    /// The float value, or `None` if this is not a [`OptionValue::Float`].
230    pub fn as_float(&self) -> Option<f64> {
231        match self {
232            Self::Float(value) => Some(*value),
233            _ => None,
234        }
235    }
236
237    /// Convert into the float value, or `None` if this is not a [`OptionValue::Float`].
238    pub fn into_float(self) -> Option<f64> {
239        match self {
240            Self::Float(value) => Some(value),
241            _ => None,
242        }
243    }
244
245    /// A mutable reference to the float value, or `None` if this is not a [`OptionValue::Float`].
246    pub fn float_mut(&mut self) -> Option<&mut f64> {
247        match self {
248            Self::Float(value) => Some(value),
249            _ => None,
250        }
251    }
252
253    /// `true` if this is a [`OptionValue::String`].
254    pub fn is_string(&self) -> bool {
255        matches!(self, Self::String(_))
256    }
257
258    /// The string value, or `None` if this is not a [`OptionValue::String`].
259    pub fn as_string(&self) -> Option<&String> {
260        match self {
261            Self::String(value) => Some(value),
262            _ => None,
263        }
264    }
265
266    /// Convert into the string value, or `None` if this is not a [`OptionValue::String`].
267    pub fn into_string(self) -> Option<String> {
268        match self {
269            Self::String(value) => Some(value),
270            _ => None,
271        }
272    }
273
274    /// A mutable reference to the string value, or `None` if this is not a [`OptionValue::String`].
275    pub fn string_mut(&mut self) -> Option<&mut String> {
276        match self {
277            Self::String(value) => Some(value),
278            _ => None,
279        }
280    }
281
282    /// `true` if this is a [`OptionValue::List`].
283    pub fn is_list(&self) -> bool {
284        matches!(self, Self::List(_))
285    }
286
287    /// The list value, or `None` if this is not a [`OptionValue::List`].
288    pub fn as_list(&self) -> Option<&Vec<OptionValue>> {
289        match self {
290            Self::List(value) => Some(value),
291            _ => None,
292        }
293    }
294
295    /// Convert into the list value, or `None` if this is not a [`OptionValue::List`].
296    pub fn into_list(self) -> Option<Vec<OptionValue>> {
297        match self {
298            Self::List(value) => Some(value),
299            _ => None,
300        }
301    }
302
303    /// A mutable reference to the list value, or `None` if this is not a [`OptionValue::List`].
304    pub fn list_mut(&mut self) -> Option<&mut Vec<OptionValue>> {
305        match self {
306            Self::List(value) => Some(value),
307            _ => None,
308        }
309    }
310
311    /// `true` if this is a [`OptionValue::Map`].
312    pub fn is_map(&self) -> bool {
313        matches!(self, Self::Map(_))
314    }
315
316    /// The map value, or `None` if this is not a [`OptionValue::Map`].
317    pub fn as_map(&self) -> Option<&Options> {
318        match self {
319            Self::Map(value) => Some(value),
320            _ => None,
321        }
322    }
323
324    /// Convert into the map value, or `None` if this is not a [`OptionValue::Map`].
325    pub fn into_map(self) -> Option<Options> {
326        match self {
327            Self::Map(value) => Some(value),
328            _ => None,
329        }
330    }
331
332    /// A mutable reference to the map value, or `None` if this is not a [`OptionValue::Map`].
333    pub fn map_mut(&mut self) -> Option<&mut Options> {
334        match self {
335            Self::Map(value) => Some(value),
336            _ => None,
337        }
338    }
339
340    /// The [`OptionValueType`] kind of this value.
341    pub fn type_name(&self) -> OptionValueType {
342        match self {
343            Self::Bool(_) => OptionValueType::Bool,
344            Self::Integer(_) => OptionValueType::Integer,
345            Self::Float(_) => OptionValueType::Float,
346            Self::String(_) => OptionValueType::String,
347            Self::List(_) => OptionValueType::List,
348            Self::Map(_) => OptionValueType::Map,
349        }
350    }
351}
352
353impl Display for OptionValue {
354    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
355        match self {
356            Self::Bool(value) => write!(f, "{value}"),
357            Self::Integer(value) => write!(f, "{value}"),
358            Self::Float(value) => write!(f, "{value}"),
359            Self::String(value) => write!(f, "{value:?}"),
360            Self::List(value) => {
361                write!(f, "[")?;
362                for (index, inner_value) in value.iter().enumerate() {
363                    if index > 0 {
364                        write!(f, ", ")?;
365                    }
366                    write!(f, "{inner_value}")?;
367                }
368                write!(f, "]")
369            }
370            Self::Map(value) => write!(f, "{value}"),
371        }
372    }
373}
374
375/// Ordered map of loader options.
376#[derive(Debug, Clone, PartialEq, Default)]
377pub struct Options {
378    entries: Vec<(String, OptionValue)>,
379}
380
381impl Options {
382    /// Build an empty [`Options`] map.
383    pub fn new() -> Self {
384        Self::default()
385    }
386
387    /// Number of entries.
388    pub fn len(&self) -> usize {
389        self.entries.len()
390    }
391
392    /// `true` if there are no entries.
393    pub fn is_empty(&self) -> bool {
394        self.entries.is_empty()
395    }
396
397    /// `true` if `key` is present.
398    pub fn contains_key(&self, key: &str) -> bool {
399        self.entries.iter().any(|(entry_key, _)| entry_key == key)
400    }
401
402    /// The value for `key`, or `None` if absent.
403    pub fn get(&self, key: &str) -> Option<&OptionValue> {
404        self.entries
405            .iter()
406            .rfind(|(entry_key, _)| entry_key == key)
407            .map(|(_, value)| value)
408    }
409
410    /// A mutable reference to the value for `key`, or `None` if absent.
411    pub fn get_mut(&mut self, key: &str) -> Option<&mut OptionValue> {
412        let index = self
413            .entries
414            .iter()
415            .rposition(|(entry_key, _)| entry_key == key)?;
416        Some(&mut self.entries[index].1)
417    }
418
419    /// Insert `key` = `value`, returning the previous value if `key` was already present.
420    ///
421    /// A duplicate key replaces the earlier entry, matching the "last wins" rule for parsed
422    /// source strings.
423    pub fn insert<K: Into<String>, V: Into<OptionValue>>(
424        &mut self,
425        key: K,
426        value: V,
427    ) -> Option<OptionValue> {
428        let key = key.into();
429        let value = value.into();
430        let old = self.remove(&key);
431        self.entries.push((key, value));
432        old
433    }
434
435    /// Remove and return the value for `key`, or `None` if absent.
436    pub fn remove(&mut self, key: &str) -> Option<OptionValue> {
437        let index = self
438            .entries
439            .iter()
440            .rposition(|(entry_key, _)| entry_key == key)?;
441        Some(self.entries.remove(index).1)
442    }
443
444    /// Iterate over `(key, value)` pairs in insertion order.
445    pub fn iter(&self) -> impl Iterator<Item = (&str, &OptionValue)> {
446        self.entries
447            .iter()
448            .map(|(key, value)| (key.as_str(), value))
449    }
450
451    /// Iterate over keys in insertion order.
452    pub fn keys(&self) -> impl Iterator<Item = &str> {
453        self.entries.iter().map(|(key, _)| key.as_str())
454    }
455
456    /// Iterate over values in insertion order.
457    pub fn values(&self) -> impl Iterator<Item = &OptionValue> {
458        self.entries.iter().map(|(_, value)| value)
459    }
460
461    pub(crate) fn entries(&self) -> &[(String, OptionValue)] {
462        &self.entries
463    }
464}
465
466impl Display for Options {
467    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
468        write!(f, "{{")?;
469        for (index, (key, value)) in self.entries.iter().enumerate() {
470            if index > 0 {
471                write!(f, ", ")?;
472            }
473            write!(f, "{key:?}: {value}")?;
474        }
475        write!(f, "}}")
476    }
477}
478
479/// One configuration source declaration.
480///
481/// A [`Source`] holds a loader name, its options, and an optional resource address, as parsed
482/// from the `SOURCE[(OPTIONS)][:RESOURCE]` string format. See the
483/// [crate-level documentation](crate) for the full format, parsing rules, and examples.
484///
485/// # Examples
486///
487/// ```rust
488/// use tanzim_source::Source;
489///
490/// let source = Source::parse("env(prefix=APP_)")?;
491/// assert_eq!(source.source(), "env");
492/// assert_eq!(
493///     source.options().get("prefix").unwrap().as_string().unwrap(),
494///     "APP_"
495/// );
496/// assert_eq!(source.resource(), "");
497/// assert_eq!(source.to_string(), "env(prefix=APP_)");
498///
499/// let file = Source::parse("file:app.toml")?;
500/// assert_eq!(file.source(), "file");
501/// assert_eq!(file.resource(), "app.toml");
502/// # Ok::<(), tanzim_source::ParseError>(())
503/// ```
504#[derive(Debug, Clone, PartialEq)]
505pub struct Source {
506    pub(crate) source: String,
507    pub(crate) options: Options,
508    pub(crate) resource: String,
509    pub(crate) resource_colon: bool,
510}
511
512impl Source {
513    /// Parse a `SOURCE[(OPTIONS)][:RESOURCE]` string into a [`Source`].
514    ///
515    /// Equivalent to the free function [`parse`]. See the [crate-level documentation](crate) for
516    /// the format and rules.
517    pub fn parse(input: &str) -> Result<Self, ParseError> {
518        parse::parse(input)
519    }
520
521    /// Build a bare source with just a name — no options, no resource — infallibly.
522    ///
523    /// Used for synthetic origins (e.g. `tanzim_value::Location`s that do not come from parsing a
524    /// real source string). Unlike [`Source::parse`] this performs no validation, so the name may
525    /// even be empty for a placeholder origin.
526    pub fn named(name: impl Into<String>) -> Self {
527        Self {
528            source: name.into(),
529            options: Options::default(),
530            resource: String::new(),
531            resource_colon: false,
532        }
533    }
534
535    /// The error policy this source declares for `stage`, via its reserved `on_error` option.
536    ///
537    /// Defaults to [`OnError::Fail`] when the option is absent, malformed, or does not mention the
538    /// stage. `on_error=(load=skip,validate=skip)` yields [`OnError::Skip`] for those two stages.
539    pub fn on_error(&self, stage: Stage) -> OnError {
540        let Some(OptionValue::Map(map)) = self.options.get("on_error") else {
541            return OnError::Fail;
542        };
543        match map.get(stage.as_str()) {
544            Some(OptionValue::String(value)) if value.eq_ignore_ascii_case("skip") => OnError::Skip,
545            _ => OnError::Fail,
546        }
547    }
548
549    /// The loader name (e.g. `env`, `file`, `http`).
550    pub fn source(&self) -> &str {
551        self.source.as_str()
552    }
553
554    /// A mutable reference to the loader name.
555    pub fn source_mut(&mut self) -> &mut String {
556        &mut self.source
557    }
558
559    /// Set the loader name.
560    pub fn set_source(&mut self, source: impl Into<String>) {
561        self.source = source.into();
562    }
563
564    /// Set the loader name, builder-style.
565    pub fn with_source(mut self, source: impl Into<String>) -> Self {
566        self.source = source.into();
567        self
568    }
569
570    /// The loader options.
571    pub fn options(&self) -> &Options {
572        &self.options
573    }
574
575    /// A mutable reference to the loader options.
576    pub fn options_mut(&mut self) -> &mut Options {
577        &mut self.options
578    }
579
580    /// Replace the loader options.
581    pub fn set_options(&mut self, options: Options) {
582        self.options = options;
583    }
584
585    /// Replace the loader options, builder-style.
586    pub fn with_options(mut self, options: Options) -> Self {
587        self.options = options;
588        self
589    }
590
591    /// Set a single loader option.
592    pub fn set_option<K: Into<String>, V: Into<OptionValue>>(&mut self, key: K, value: V) {
593        self.options.insert(key, value);
594    }
595
596    /// Set a single loader option, builder-style.
597    pub fn with_option<K: Into<String>, V: Into<OptionValue>>(mut self, key: K, value: V) -> Self {
598        self.options.insert(key, value);
599        self
600    }
601
602    /// The resource address (path, URL, …), or empty if none was given.
603    pub fn resource(&self) -> &str {
604        self.resource.as_str()
605    }
606
607    /// A mutable reference to the resource address.
608    pub fn resource_mut(&mut self) -> &mut String {
609        &mut self.resource
610    }
611
612    /// Set the resource address; a non-empty value also sets [`Source::resource_colon`].
613    pub fn set_resource(&mut self, resource: impl Into<String>) {
614        self.resource = resource.into();
615        if !self.resource.is_empty() {
616            self.resource_colon = true;
617        }
618    }
619
620    /// Set the resource address, builder-style; a non-empty value also sets
621    /// [`Source::resource_colon`].
622    pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
623        self.resource = resource.into();
624        if !self.resource.is_empty() {
625            self.resource_colon = true;
626        }
627        self
628    }
629
630    /// `true` if the source string had a `:` separator before the resource, even when the
631    /// resource itself is empty (e.g. `file:`).
632    pub fn resource_colon(&self) -> bool {
633        self.resource_colon
634    }
635
636    /// Set whether the `:` separator is present before the resource.
637    pub fn set_resource_colon(&mut self, resource_colon: bool) {
638        self.resource_colon = resource_colon;
639    }
640
641    /// Set whether the `:` separator is present before the resource, builder-style.
642    pub fn with_resource_colon(mut self, resource_colon: bool) -> Self {
643        self.resource_colon = resource_colon;
644        self
645    }
646}
647
648/// Builds a [`Source`].
649///
650/// # Examples
651///
652/// ```rust
653/// use tanzim_source::SourceBuilder;
654///
655/// let source = SourceBuilder::new()
656///     .with_source("env")
657///     .with_option("prefix", "APP_")
658///     .build()?;
659///
660/// assert_eq!(source.to_string(), "env(prefix=APP_)");
661/// # Ok::<(), tanzim_source::Error>(())
662/// ```
663#[derive(Debug, Clone, PartialEq, Default)]
664pub struct SourceBuilder {
665    source: Option<String>,
666    options: Options,
667    resource: String,
668    resource_colon: bool,
669}
670
671impl SourceBuilder {
672    /// Build an empty [`SourceBuilder`].
673    pub fn new() -> Self {
674        Self::default()
675    }
676
677    /// Set the loader name.
678    pub fn with_source(mut self, source: impl Into<String>) -> Self {
679        self.source = Some(source.into());
680        self
681    }
682
683    /// Set the resource address.
684    pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
685        self.resource = resource.into();
686        self
687    }
688
689    /// Replace the loader options.
690    pub fn with_options(mut self, options: Options) -> Self {
691        self.options = options;
692        self
693    }
694
695    /// Set a single loader option.
696    pub fn with_option<K: Into<String>, V: Into<OptionValue>>(mut self, key: K, value: V) -> Self {
697        self.options.insert(key, value);
698        self
699    }
700
701    /// Set whether the `:` separator is present before the resource.
702    pub fn with_resource_colon(mut self, resource_colon: bool) -> Self {
703        self.resource_colon = resource_colon;
704        self
705    }
706
707    /// Build the [`Source`], failing if no (non-empty) source name was set.
708    pub fn build(self) -> Result<Source, Error> {
709        let source = self.source.ok_or(Error::MissingSource)?;
710        if source.is_empty() {
711            return Err(Error::MissingSource);
712        }
713        let resource_colon = self.resource_colon || !self.resource.is_empty();
714        Ok(Source {
715            source,
716            options: self.options,
717            resource: self.resource,
718            resource_colon,
719        })
720    }
721}