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}