confpiler/
config.rs

1use config::{Config, ConfigError, File, Value, ValueKind};
2use std::collections::{HashMap, HashSet};
3use std::fmt;
4
5use crate::error::{ConfpilerError, Result};
6
7/// A representation of a flattened, compiled configuration.
8///
9/// When constructed via the builder, this produces a set of key/value pairs
10/// where the keys are produced from flattening the nested structure of a config
11/// file and converting the values into their string representations.
12///
13/// See the crate examples for more detailed usage.
14///
15/// # Examples
16/// Given
17/// ```text
18/// ## default.yaml
19/// foo:
20///     bar: 10
21///     baz: false
22/// hoof: doof
23///
24/// ## production.yaml
25/// foo:
26///     baz: true
27/// ```
28///
29/// then running the following
30/// ```no_run
31/// use confpiler::FlatConfig;
32/// # use confpiler::error::ConfpilerError;
33/// # fn main() -> Result<(), ConfpilerError> {
34/// let (conf, warnings) = FlatConfig::builder()
35///     .add_config("foo/default")
36///     .add_config("foo/production")
37///     .build()?;
38///
39/// // or equivalently
40/// let mut builder = FlatConfig::builder();
41/// builder.add_config("foo/default");
42/// builder.add_config("foo/production");
43/// let (conf, warnings) = builder.build()?;
44/// # Ok(())
45/// # }
46/// ```
47///
48/// produces a mapping like
49/// ```text
50/// "FOO__BAR": "10"
51/// "FOO__BAZ": "true"
52/// "HOOF": "doof"
53/// ```
54#[derive(Debug, Clone, Default, Eq, PartialEq)]
55pub struct FlatConfig {
56    origin: String,
57
58    items: HashMap<String, String>,
59}
60
61impl FlatConfig {
62    /// Get a [FlatConfigBuilder] instance.
63    ///
64    /// # Examples
65    /// ```
66    /// use confpiler::{FlatConfig, FlatConfigBuilder};
67    /// let builder = FlatConfig::builder();
68    ///
69    /// assert_eq!(builder, FlatConfigBuilder::default());
70    /// ```
71    pub fn builder() -> FlatConfigBuilder {
72        FlatConfigBuilder::default()
73    }
74
75    /// Convenience method for getting reference to the internal key/value map.
76    pub fn items(&self) -> &HashMap<String, String> {
77        &self.items
78    }
79
80    /// Merge another [FlatConfig] into `self`.
81    ///
82    /// See [MergeWarning] for the kinds of warnings returned by this function
83    /// and when/why they are generated.
84    ///
85    /// # Examples
86    /// ```
87    /// use confpiler::FlatConfig;
88    ///
89    /// // we're using default here for the example, making it pointless, since
90    /// // they're both empty, but this is just for illustration
91    /// let mut a = FlatConfig::default();
92    /// let b = FlatConfig::default();
93    ///
94    /// let warnings = a.merge(&b);
95    /// ```
96    pub fn merge(&mut self, other: &Self) -> Vec<MergeWarning> {
97        let mut warnings = Vec::new();
98
99        for (k, v) in other.items.iter() {
100            self.items
101                .entry(k.to_string())
102                .and_modify(|e| {
103                    if e == v {
104                        warnings.push(MergeWarning::RedundantValue {
105                            overrider: other.origin.clone(),
106                            key: k.to_string(),
107                            value: e.clone(),
108                        });
109                    } else {
110                        *e = v.to_string();
111                    }
112                })
113                .or_insert_with(|| v.to_string());
114        }
115
116        warnings
117    }
118}
119
120/// This is the builder for [FlatConfig].
121///
122/// An instance of this will normally be obtained by invoking [FlatConfig::builder]
123///
124/// # Examples
125/// ```
126/// // This example is included for reference, but prefer using
127/// // FlatConfig::builder() to get a builder instance.
128/// use confpiler::FlatConfigBuilder;
129///
130/// let mut builder = FlatConfigBuilder::default();
131/// builder.add_config("foo/default");
132/// builder.add_config("foo/production");
133/// builder.with_separator("__"); // this is the default
134/// builder.with_array_separator(","); // this is the default
135///
136/// // let's not actually do this in the docs
137/// // let (conf, warnings) = builder.build()?;
138/// ```
139#[derive(Debug, Clone, Eq, PartialEq)]
140pub struct FlatConfigBuilder {
141    prefix: Option<String>,
142    configs: Vec<String>,
143    separator: String,
144    array_separator: String,
145}
146
147impl FlatConfigBuilder {
148    pub const DEFAULT_SEPARATOR: &'static str = "__";
149    pub const DEFAULT_ARRAY_SEPARATOR: &'static str = ",";
150
151    /// Adds the given config path to the list of configs.
152    ///
153    ///
154    /// * Ordering is important here, as values in the last added config will
155    /// overwrite those in the previously added configs.
156    /// * Actual loading of the specified config files does not happen until
157    /// [build()](FlatConfigBuilder::build) is invoked.
158    /// * The supported config names are the same as supported by the `config-rs`
159    /// * Specifying the same config twice will result in an error when
160    /// [build()](FlatConfigBuilder::build) is invoked.
161    /// crate.
162    ///
163    /// # Examples
164    /// ```
165    /// use confpiler::FlatConfig;
166    /// let mut builder = FlatConfig::builder();
167    /// builder.add_config("foo/default");
168    /// ```
169    pub fn add_config(&mut self, config: &str) -> &mut Self {
170        self.configs.push(config.to_string());
171        self
172    }
173
174    /// Specifies the separator to use when flattening nested structures.
175    ///
176    /// The default separator is `__`, and is used to join the keys of a
177    /// nested structure into a single, top-level key.
178    ///
179    /// # Examples
180    /// ```
181    /// use confpiler::FlatConfig;
182    /// let mut builder = FlatConfig::builder();
183    /// builder.with_separator("__"); // this is the default
184    /// ```
185    pub fn with_separator(&mut self, separator: &str) -> &mut Self {
186        self.separator = separator.to_string();
187        self
188    }
189
190    /// Specifies the separator to use when joining arrays
191    ///
192    /// This default array separator is `,`, and is used to join the values of
193    /// an array into a single [String]. As a reminder, this crate only supports
194    /// "simple" arrays that do not contain additional nested structures.
195    ///
196    /// # Examples
197    /// ```
198    /// use confpiler::FlatConfig;
199    /// let mut builder = FlatConfig::builder();
200    /// builder.with_array_separator(","); // this is the default
201    /// ```
202    pub fn with_array_separator(&mut self, separator: &str) -> &mut Self {
203        self.array_separator = separator.to_string();
204        self
205    }
206
207    /// Specifies a prefix to be prepended to all generated keys.
208    ///
209    /// This prefix will **always** be converted to ascii uppercase and will be
210    /// be separated from the rest of the generated key by the separator used
211    /// by the builder.
212    ///
213    /// # Examples
214    /// ```
215    /// use confpiler::FlatConfig;
216    /// let mut builder = FlatConfig::builder();
217    /// builder.with_prefix("foo"); // this is the default
218    /// ```
219    pub fn with_prefix(&mut self, prefix: &str) -> &mut Self {
220        self.prefix = Some(prefix.to_ascii_uppercase());
221        self
222    }
223
224    /// Attempt to produce a [FlatConfig] without consuming the builder.
225    ///
226    /// This results in an error in the following scenarios:
227    /// * No configs were specified.
228    /// * Flattening any given config results in a duplicate key within the same
229    /// file (`foo:` and `Foo:` in the same file, `foo_bar:` and `foo: bar:` in
230    /// the same file, etc.).
231    /// * A config contains an array that itself contains some nested structure.
232    /// * A config is invalid or not found as far as `config-rs` can determine.
233    ///
234    /// # Examples
235    /// ```
236    /// use confpiler::FlatConfig;
237    /// ```
238    pub fn build(&self) -> Result<(FlatConfig, Vec<MergeWarning>)> {
239        if self.configs.is_empty() {
240            return Err(ConfpilerError::NoConfigSpecified);
241        }
242
243        let mut seen_configs: HashSet<&str> = HashSet::new();
244
245        // the origin for the overall config will be whatever was first in
246        // the list
247        let mut flat_config = FlatConfig {
248            // this unwrap is safe because we just checked
249            origin: self.configs.first().unwrap().to_string(),
250            items: HashMap::new(),
251        };
252        let mut warnings = Vec::new();
253
254        for conf_path in self.configs.iter() {
255            // so this adds some complexity, but it's probably a better user
256            // experience?
257            if seen_configs.contains(conf_path.as_str()) {
258                return Err(ConfpilerError::DuplicateConfig(conf_path.to_string()));
259            } else {
260                seen_configs.insert(conf_path.as_str());
261            }
262
263            // attempt to load every specified config
264            let conf = Config::builder()
265                .add_source(File::with_name(conf_path))
266                .build()?;
267
268            let input = conf.cache.into_table()?;
269
270            let mut out = HashMap::new();
271            flatten_into(
272                &input,
273                &mut out,
274                self.prefix.as_ref(),
275                &self.separator,
276                &self.array_separator,
277            )?;
278            let working_config = FlatConfig {
279                origin: conf_path.to_string(),
280                items: out,
281            };
282
283            let mut working_warnings = flat_config.merge(&working_config);
284            warnings.append(&mut working_warnings);
285        }
286
287        Ok((flat_config, warnings))
288    }
289}
290
291impl Default for FlatConfigBuilder {
292    fn default() -> Self {
293        Self {
294            prefix: None,
295            configs: Vec::new(),
296            separator: Self::DEFAULT_SEPARATOR.to_string(),
297            array_separator: Self::DEFAULT_ARRAY_SEPARATOR.to_string(),
298        }
299    }
300}
301
302/// An enumeration of possible warning values regarding config merging.
303///
304/// These warnings occur as the result of merging two [FlatConfig] instances
305/// together. They are not necessarily errors, but are provided for the caller
306/// to treat as such if they wish.
307///
308/// # Examples
309#[derive(Debug, Clone, Eq, PartialEq)]
310#[non_exhaustive]
311pub enum MergeWarning {
312    /// This variant indicates that we attempted to set a value in a key but it
313    /// already contained that value. This is useful for detecting when a
314    /// configuration file specifies a value when it does not need to, because
315    /// the value it is specifying was already set.
316    ///
317    /// This does not reliably that the _final_ value for a given key was
318    /// unchanged, as merging files `A -> B -> C` where `B` contained the
319    /// redundant value does not mean that `C` did not then change that value
320    /// to something else.
321    RedundantValue {
322        overrider: String,
323        key: String,
324        value: String,
325    },
326}
327
328impl fmt::Display for MergeWarning {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        match self {
331            Self::RedundantValue {
332                ref overrider,
333                ref key,
334                ref value,
335            } => {
336                write!(f, "'{overrider}' is attempting to override '{key}' with '{value}', but the key already contains that value")
337            }
338        }
339    }
340}
341
342pub(crate) fn flatten_into(
343    input: &HashMap<String, Value>,
344    output: &mut HashMap<String, String>,
345    prefix: Option<&String>,
346    separator: &str,
347    array_separator: &str,
348) -> Result<()> {
349    let mut components = Vec::new();
350    if let Some(prefix) = prefix {
351        components.push(prefix.clone());
352    }
353    flatten_into_inner(input, output, separator, array_separator, &mut components)
354}
355
356fn flatten_into_inner(
357    input: &HashMap<String, Value>,
358    output: &mut HashMap<String, String>,
359    separator: &str,
360    array_separator: &str,
361    components: &mut Vec<String>,
362) -> Result<()> {
363    if input.is_empty() {
364        return Ok(());
365    }
366
367    for (key, value) in input.iter() {
368        // convert the current key to uppercase and add it to the list of
369        // components so that we can form names with the current "path"
370        let upper_key = key.to_ascii_uppercase();
371        components.push(upper_key);
372        match &value.kind {
373            // omit these because they have no meaning
374            ValueKind::Nil => {}
375
376            // If we encounter another table, we just need to recurse
377            ValueKind::Table(ref table) => {
378                flatten_into_inner(table, output, separator, array_separator, components)?;
379            }
380
381            // Arrays are only supported if they contain primitive/str types
382            // because what does it actually mean to flatten an array into
383            // separate environment variables? We could do something like
384            // FOO_0 = "a"
385            // FOO_1 = "b"
386            // FOO_2 = "c"
387            // etc.
388            // but consider what the parser consuming said variables would have
389            // to look like? And what does it do when some arbitrary index is
390            // a complex type like an array or a map?
391            //
392            // Instead, it's simpler if we just convert the array into a
393            // sequence-separated string, which limits the kinds of things we
394            // can store in an array
395            ValueKind::Array(ref array) => {
396                let candidate = components.join(separator);
397
398                if output.contains_key(&candidate) {
399                    return Err(ConfpilerError::DuplicateKey(candidate));
400                }
401
402                let val = array
403                    .iter()
404                    .cloned()
405                    .map(|e| e.into_string())
406                    .collect::<std::result::Result<Vec<String>, ConfigError>>()
407                    // TODO: this is actually an assumption about why this would fail - MCL - 2022-02-21
408                    .map_err(|_| ConfpilerError::UnsupportedArray(candidate.clone()))?
409                    .join(array_separator);
410
411                output.insert(candidate, val);
412            }
413
414            // for everything else, we want to add the key/value to the output
415            _ => {
416                let candidate = components.join(separator);
417
418                if output.contains_key(&candidate) {
419                    return Err(ConfpilerError::DuplicateKey(candidate));
420                }
421
422                // this clone might be unnecessary and we could just convert
423                // directly into a string, but I think I want the error to be
424                // raised if the interface changes to not allow arbitrary things
425                // to be converted to string.
426                output.insert(candidate, value.clone().into_string()?);
427            }
428        }
429
430        // we have to remove the key we pushed
431        components.pop();
432    }
433
434    Ok(())
435}
436
437#[cfg(test)]
438mod tests {
439    mod flat_config {
440        use super::super::*;
441
442        #[test]
443        fn builder_yields_a_default_builder() {
444            assert_eq!(FlatConfig::builder(), FlatConfigBuilder::default());
445        }
446
447        #[test]
448        fn merging() {
449            let mut a = FlatConfig {
450                origin: "origin1".to_string(),
451                items: HashMap::from([
452                    ("herp".to_string(), "derp".to_string()),
453                    ("hoof".to_string(), "changeme".to_string()),
454                ]),
455            };
456            let b = FlatConfig {
457                origin: "origin2".to_string(),
458                items: HashMap::from([
459                    ("foo".to_string(), "bar".to_string()),
460                    ("hoof".to_string(), "doof".to_string()),
461                ]),
462            };
463
464            let expected = FlatConfig {
465                origin: "origin1".to_string(),
466                items: HashMap::from([
467                    ("foo".to_string(), "bar".to_string()),
468                    ("hoof".to_string(), "doof".to_string()),
469                    ("herp".to_string(), "derp".to_string()),
470                ]),
471            };
472
473            let warnings = a.merge(&b);
474
475            assert_eq!(a, expected);
476            assert!(warnings.is_empty());
477        }
478
479        #[test]
480        fn merging_when_overriding_with_same_value_generates_warnings() {
481            let mut a = FlatConfig {
482                origin: "origin1".to_string(),
483                items: HashMap::from([
484                    ("herp".to_string(), "derp".to_string()),
485                    ("hoof".to_string(), "changeme".to_string()),
486                ]),
487            };
488            let b = FlatConfig {
489                origin: "origin2".to_string(),
490                items: HashMap::from([
491                    ("foo".to_string(), "bar".to_string()),
492                    ("herp".to_string(), "derp".to_string()),
493                    ("hoof".to_string(), "changeme".to_string()),
494                ]),
495            };
496
497            let expected = FlatConfig {
498                origin: "origin1".to_string(),
499                items: HashMap::from([
500                    ("foo".to_string(), "bar".to_string()),
501                    ("herp".to_string(), "derp".to_string()),
502                    ("hoof".to_string(), "changeme".to_string()),
503                ]),
504            };
505
506            let warnings = a.merge(&b);
507
508            assert_eq!(a, expected);
509
510            assert_eq!(warnings.len(), 2);
511
512            // we're sensitive to ordering here because of the hashing, so just
513            // assert individually
514            assert!(warnings.contains(&MergeWarning::RedundantValue {
515                overrider: "origin2".to_string(),
516                key: "herp".to_string(),
517                value: "derp".to_string(),
518            }));
519
520            assert!(warnings.contains(&MergeWarning::RedundantValue {
521                overrider: "origin2".to_string(),
522                key: "hoof".to_string(),
523                value: "changeme".to_string(),
524            }));
525        }
526    }
527
528    mod flat_config_builder {
529        use super::super::*;
530
531        #[test]
532        fn defaults() {
533            let builder = FlatConfigBuilder::default();
534            assert!(builder.configs.is_empty());
535            assert_eq!(builder.separator, "__".to_string());
536            assert_eq!(builder.array_separator, ",".to_string());
537        }
538
539        #[test]
540        fn adding_configs() {
541            let mut builder = FlatConfigBuilder::default();
542            builder.add_config("foo/bar");
543            builder.add_config("foo/baz");
544
545            let expected = vec!["foo/bar".to_string(), "foo/baz".to_string()];
546
547            assert_eq!(builder.configs, expected);
548        }
549
550        #[test]
551        fn specifying_prefix() {
552            let mut builder = FlatConfigBuilder::default();
553            builder.with_prefix("foo");
554
555            assert_eq!(builder.prefix, Some("FOO".to_string()));
556        }
557
558        #[test]
559        fn specifying_separator() {
560            let mut builder = FlatConfigBuilder::default();
561            builder.with_separator("*");
562
563            assert_eq!(builder.separator, "*".to_string());
564        }
565
566        #[test]
567        fn specifying_array_separator() {
568            let mut builder = FlatConfigBuilder::default();
569            builder.with_array_separator("---");
570
571            assert_eq!(builder.array_separator, "---".to_string());
572        }
573    }
574
575    mod flatten_into {
576        use super::super::*;
577
578        // so this is a PITA to create, but it's probably? Better than trying
579        // to load a real config file from disk. And I have more control over
580        // the types
581        fn valid_input() -> HashMap<String, Value> {
582            let origin = "test".to_string();
583            let input = HashMap::from([
584                (
585                    "foo".to_string(),
586                    Value::new(Some(&origin), ValueKind::Float(10.2)),
587                ),
588                (
589                    "bar".to_string(),
590                    Value::new(Some(&origin), ValueKind::String("Hello".to_string())),
591                ),
592                (
593                    "baz".to_string(),
594                    Value::new(
595                        Some(&origin),
596                        ValueKind::Table(HashMap::from([
597                            (
598                                "herp".to_string(),
599                                Value::new(Some(&origin), ValueKind::Boolean(false)),
600                            ),
601                            (
602                                "derp".to_string(),
603                                Value::new(Some(&origin), ValueKind::I64(15)),
604                            ),
605                            (
606                                "hoof".to_string(),
607                                Value::new(
608                                    Some(&origin),
609                                    ValueKind::Table(HashMap::from([(
610                                        "doof".to_string(),
611                                        Value::new(Some(&origin), ValueKind::I64(999)),
612                                    )])),
613                                ),
614                            ),
615                        ])),
616                    ),
617                ),
618                (
619                    "biz".to_string(),
620                    Value::new(
621                        Some(&origin),
622                        ValueKind::Array(vec![
623                            Value::new(Some(&origin), ValueKind::Boolean(false)),
624                            Value::new(Some(&origin), ValueKind::I64(1111)),
625                            Value::new(Some(&origin), ValueKind::String("Goodbye".to_string())),
626                        ]),
627                    ),
628                ),
629            ]);
630
631            input
632        }
633
634        #[test]
635        fn accepts_empty_input() {
636            let mut out = HashMap::new();
637            let input = HashMap::new();
638
639            let res = flatten_into(&input, &mut out, None, "__", ",");
640
641            assert!(res.is_ok());
642            assert!(out.is_empty());
643        }
644
645        #[test]
646        fn flattens_valid_input() {
647            let mut out = HashMap::new();
648            let input = valid_input();
649
650            let expected: HashMap<String, String> = HashMap::from([
651                ("FOO".to_string(), "10.2".to_string()),
652                ("BAR".to_string(), "Hello".to_string()),
653                ("BAZ__HERP".to_string(), "false".to_string()),
654                ("BAZ__DERP".to_string(), "15".to_string()),
655                ("BAZ__HOOF__DOOF".to_string(), "999".to_string()),
656                ("BIZ".to_string(), "false,1111,Goodbye".to_string()),
657            ]);
658
659            let res = flatten_into(&input, &mut out, None, "__", ",");
660
661            assert!(res.is_ok());
662            assert_eq!(out, expected);
663        }
664
665        #[test]
666        fn supports_prefixing() {
667            let mut out = HashMap::new();
668            let input = valid_input();
669
670            let expected: HashMap<String, String> = HashMap::from([
671                ("PRE__FOO".to_string(), "10.2".to_string()),
672                ("PRE__BAR".to_string(), "Hello".to_string()),
673                ("PRE__BAZ__HERP".to_string(), "false".to_string()),
674                ("PRE__BAZ__DERP".to_string(), "15".to_string()),
675                ("PRE__BAZ__HOOF__DOOF".to_string(), "999".to_string()),
676                ("PRE__BIZ".to_string(), "false,1111,Goodbye".to_string()),
677            ]);
678
679            let prefix = Some("PRE".to_string());
680
681            let res = flatten_into(&input, &mut out, prefix.as_ref(), "__", ",");
682
683            assert!(res.is_ok());
684            assert_eq!(out, expected);
685        }
686
687        #[test]
688        fn uses_the_specified_separators() {
689            let mut out = HashMap::new();
690            let input = valid_input();
691
692            let expected: HashMap<String, String> = HashMap::from([
693                ("FOO".to_string(), "10.2".to_string()),
694                ("BAR".to_string(), "Hello".to_string()),
695                ("BAZ*HERP".to_string(), "false".to_string()),
696                ("BAZ*DERP".to_string(), "15".to_string()),
697                ("BAZ*HOOF*DOOF".to_string(), "999".to_string()),
698                ("BIZ".to_string(), "false 1111 Goodbye".to_string()),
699            ]);
700
701            let res = flatten_into(&input, &mut out, None, "*", " ");
702
703            assert!(res.is_ok());
704            assert_eq!(out, expected);
705        }
706
707        #[test]
708        fn errors_on_duplicate_keys() {
709            let mut out = HashMap::new();
710            let valid = valid_input();
711
712            let mut invalid = valid.clone();
713            invalid.insert(
714                "fOo".to_string(),
715                Value::new(Some(&"test".to_string()), ValueKind::Float(1.0)),
716            );
717
718            let res = flatten_into(&invalid, &mut out, None, "__", ",");
719
720            assert!(res.is_err());
721
722            match res.unwrap_err() {
723                ConfpilerError::DuplicateKey(key) => assert_eq!(key, "FOO".to_string()),
724                e => panic!("unexpected error variant: {}", e),
725            };
726
727            // including duplicates because of nesting
728            let mut invalid = valid.clone();
729            invalid.insert(
730                "baz__herp".to_string(),
731                Value::new(Some(&"test".to_string()), ValueKind::Boolean(true)),
732            );
733
734            let mut out = HashMap::new();
735            let res = flatten_into(&invalid, &mut out, None, "__", ",");
736
737            assert!(res.is_err());
738
739            match res.unwrap_err() {
740                ConfpilerError::DuplicateKey(key) => assert_eq!(key, "BAZ__HERP".to_string()),
741                e => panic!("unexpected error variant: {}", e),
742            };
743        }
744
745        #[test]
746        fn errors_on_unsupported_array() {
747            let mut out = HashMap::new();
748            let valid = valid_input();
749
750            let origin = "test".to_string();
751            let mut invalid = valid.clone();
752            invalid.insert(
753                "biz".to_string(),
754                Value::new(
755                    Some(&"test".to_string()),
756                    ValueKind::Array(vec![
757                        Value::new(Some(&origin), ValueKind::Boolean(false)),
758                        Value::new(Some(&origin), ValueKind::Table(HashMap::new())),
759                        Value::new(Some(&origin), ValueKind::String("Goodbye".to_string())),
760                    ]),
761                ),
762            );
763
764            let res = flatten_into(&invalid, &mut out, None, "__", ",");
765
766            assert!(res.is_err());
767
768            match res.unwrap_err() {
769                ConfpilerError::UnsupportedArray(key) => assert_eq!(key, "BIZ".to_string()),
770                e => panic!("unexpected error variant: {}", e),
771            };
772        }
773    }
774}