Skip to content

Commit

Permalink
feat(config): add config overwrites
Browse files Browse the repository at this point in the history
- Support for overwrites in ConfigBuilder (resolves ScuffleCloud#103)
- Add ManualSource
- Fix doc-tests
- Fix other minor bugs

Co-authored-by: Esdras Amora <[email protected]>
  • Loading branch information
2 people authored and TroyKomodo committed Jul 18, 2023
1 parent 6325951 commit 2d339f7
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 76 deletions.
3 changes: 3 additions & 0 deletions config/config/examples/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ fn parse() -> Result<AppConfig, ConfigError> {
.as_slice(),
)?);

builder.overwrite("logging.level", "TEST")?;
builder.overwrite("logging.json", "off")?;

let config: AppConfig = builder.build()?;

Ok(config)
Expand Down
15 changes: 3 additions & 12 deletions config/config/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub enum ConfigErrorType {
InvalidReference(&'static str),
#[error("deserialized type not supported: {0}")]
DeserializedTypeNotSupported(String),
#[error("serialize: {0}")]
Serialize(#[from] serde_value::SerializerError),
}

#[derive(Debug)]
Expand Down Expand Up @@ -69,6 +71,7 @@ pub enum ErrorSource {
Cli,
Env,
File(String),
Manual,
}

/// General config error
Expand Down Expand Up @@ -176,15 +179,3 @@ impl std::error::Error for ConfigError {
Some(&*self.error)
}
}

pub fn merge_error_opts(
error1: Option<ConfigError>,
error2: Option<ConfigError>,
) -> Option<ConfigError> {
match (error1, error2) {
(Some(err1), Some(err2)) => Some(err1.multi(err2)),
(Some(err), None) => Some(err),
(None, Some(err)) => Some(err),
(None, None) => None,
}
}
9 changes: 7 additions & 2 deletions config/config/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ use crate::{Result, Value};
/// `test.foo[0].bar`
/// is represented as
/// ```
/// # use config::{KeyPathSegment, Value};
/// # fn main() {
/// # let repr =
/// [
/// KeyPathSegment::Map { key: Value::String("test") },
/// KeyPathSegment::Map { key: Value::String("test".to_string()) },
/// KeyPathSegment::Seq { index: 0 },
/// KeyPathSegment::Map { key: Value::String("bar") }
/// KeyPathSegment::Map { key: Value::String("bar".to_string()) }
/// ]
/// # ;
/// # }
/// ```
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct KeyPath {
Expand Down
92 changes: 52 additions & 40 deletions config/config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![doc = include_str!("../README.md")]

use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::collections::{btree_map, BTreeMap, BTreeSet};
use std::iter;
use std::ops::Deref;
use std::sync::Arc;

Expand All @@ -16,6 +17,7 @@ pub use key::*;
pub use primitives::*;
use serde_ignored::Path;
pub use serde_value::Value;
use sources::ManualSource;

/// Config source
///
Expand Down Expand Up @@ -211,6 +213,7 @@ impl<C: Config> Deref for SourceHolder<C> {
/// Use this struct to add sources and build a config.
pub struct ConfigBuilder<C: Config> {
sources: Vec<SourceHolder<C>>,
overwrite: ManualSource<C>,
}

impl<C: Config> Default for ConfigBuilder<C> {
Expand All @@ -219,45 +222,48 @@ impl<C: Config> Default for ConfigBuilder<C> {
}
}

fn merge(first: Value, second: Value) -> (Value, Option<ConfigError>) {
let values = (first, second);
let (Value::Map(mut first), Value::Map(mut second)) = values else {
return (values.0, None);
};

let mut merged = BTreeMap::new();
// Get all unique keys from both maps
let keys = first
.keys()
.chain(second.keys())
.cloned()
.collect::<HashSet<_>>();

let mut error: Option<ConfigError> = None;

for key in keys {
let first = first.remove(&key);
let second = second.remove(&key);

let (value, new_error) = match (first, second) {
(Some(first), Some(second)) => merge(first, second),
(Some(first), None) => (first, None),
(None, Some(second)) => (second, None),
(None, None) => unreachable!(),
};
fn merge(first: Value, second: Value) -> Value {
match (first, second) {
(Value::Map(first), Value::Map(mut second)) => {
for (k1, v1) in first {
match second.entry(k1) {
btree_map::Entry::Vacant(entry) => {
entry.insert(v1);
}
btree_map::Entry::Occupied(entry) => {
let (k2, v2) = entry.remove_entry();
second.insert(k2, merge(v1, v2));
}
}
}
Value::Map(second)
}
(Value::Seq(first), Value::Seq(second)) => {
let mut merged = Vec::with_capacity(std::cmp::max(first.len(), second.len()));
let mut first = first.into_iter();
let mut second = second.into_iter();

loop {
match (first.next(), second.next()) {
(None, None) => break,
(Some(first), Some(second)) => merged.push(merge(first, second)),
(None, Some(second)) => merged.push(second),
(Some(first), None) => merged.push(first),
}
}

error = merge_error_opts(error, new_error);
merged.insert(key, value);
Value::Seq(merged)
}
(first, _) => first,
}

(Value::Map(merged), error)
}

impl<C: Config> ConfigBuilder<C> {
/// Creates a new config builder with no sources.
pub fn new() -> Self {
Self {
sources: Vec::new(),
overwrite: ManualSource::new(),
}
}

Expand Down Expand Up @@ -287,14 +293,23 @@ impl<C: Config> ConfigBuilder<C> {
self.sources.sort_by(|a, b| b.priority.cmp(&a.priority));
}

/// Overwrites a single key.
///
/// This means that all values for this key that come from the added sources will be ignored.
pub fn overwrite<K: Into<KeyPath>, V: serde::Serialize>(
&mut self,
key: K,
value: V,
) -> Result<()> {
self.overwrite.set(key.into(), value)
}

/// Gets a single key by its path.
pub fn get_key(&self, path: impl Into<KeyPath>) -> Result<Option<Value>> {
let key_path = path.into();

let mut iter = self
.sources
.iter()
.map(|s| s.get_key(&key_path))
let mut iter = iter::once(self.overwrite.get_key(&key_path))
.chain(self.sources.iter().map(|s| s.get_key(&key_path)))
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten();
Expand All @@ -303,14 +318,11 @@ impl<C: Config> ConfigBuilder<C> {
return Ok(None);
};

let mut error: Option<ConfigError> = None;
for v in iter {
let (merged, new_error) = merge(value, v);
error = merge_error_opts(error, new_error);
value = merged;
value = merge(value, v);
}

error.map_or(Ok(Some(value)), Err)
Ok(Some(value))
}

/// Parses a single key.
Expand Down
2 changes: 1 addition & 1 deletion config/config/src/sources/file/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! File source
//!
//! ```
//! ```no_run
//! # use config::sources;
//! #
//! # #[derive(config::Config, serde::Deserialize)]
Expand Down
105 changes: 105 additions & 0 deletions config/config/src/sources/manual.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! Manual source
//!
//! A manual source lets you set values manually.
//!
//! ```
//! # use config::{sources, Value};
//! #
//! # #[derive(config::Config, serde::Deserialize)]
//! # struct MyConfig {
//! # // ...
//! # }
//! #
//! # fn main() -> Result<(), config::ConfigError> {
//! let mut builder = config::ConfigBuilder::new();
//! // Create a new ManualSource
//! let mut manual = sources::ManualSource::new();
//! manual.set("test.foo", Value::Bool(true));
//! // Add ManualSource
//! builder.add_source(manual);
//! // Build the final configuration
//! let config: MyConfig = builder.build()?;
//! # Ok(())
//! # }
//! ```
use std::{collections::BTreeMap, marker::PhantomData};

use crate::{
Config, ConfigError, ConfigErrorType, ErrorSource, KeyPath, KeyPathSegment, Result, Source,
Value,
};

use super::utils;

/// Manual source
///
/// Create a new manual source with [`ManualSource::new`](ManualSource::new).
pub struct ManualSource<C: Config> {
value: Option<Value>,
_phantom: PhantomData<C>,
}

fn value_to_value_graph(path: KeyPath, mut value: Value) -> Result<Value> {
for segment in path.into_iter().rev() {
match segment {
KeyPathSegment::Map { key } => {
value = Value::Map(BTreeMap::from([(key, value)]));
}
KeyPathSegment::Seq { index } => {
if index == 0 {
value = Value::Seq(vec![value]);
} else {
return Err(ConfigError::new(ConfigErrorType::ValidationError(
"indices other than 0 not supported when setting values with manual source"
.to_string(),
)));
}
}
}
}
Ok(value)
}

impl<C: Config> Default for ManualSource<C> {
fn default() -> Self {
Self {
value: None,
_phantom: PhantomData,
}
}
}

impl<C: Config> ManualSource<C> {
/// Creates a new manual source.
pub fn new() -> Self {
Self::default()
}

/// Sets a value at the given path.
pub fn set<K: Into<KeyPath>, V: serde::Serialize>(&mut self, path: K, value: V) -> Result<()> {
let path: KeyPath = path.into();
let value = serde_value::to_value(value)
.map_err(Into::into)
.map_err(ConfigError::new)
.map_err(|e| e.with_source(ErrorSource::Manual))?;
let value = C::transform(&path, value_to_value_graph(path.clone(), value)?)?;
if let Some(old_value) = self.value.take() {
self.value = Some(crate::merge(value, old_value));
} else {
self.value = Some(value);
}
Ok(())
}
}

impl<C: Config> Source<C> for ManualSource<C> {
fn get_key(&self, path: &crate::KeyPath) -> crate::Result<Option<Value>> {
match &self.value {
Some(value) => {
utils::get_key::<C>(value, path).map_err(|e| e.with_source(ErrorSource::Manual))
}
None => Ok(None),
}
}
}
2 changes: 2 additions & 0 deletions config/config/src/sources/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
pub mod cli;
pub mod env;
pub mod file;
pub mod manual;

mod utils;

pub use cli::CliSource;
pub use env::EnvSource;
pub use file::FileSource;
pub use manual::ManualSource;
28 changes: 10 additions & 18 deletions config/config_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,25 +105,17 @@ fn impl_config(ast: &syn::DeriveInput) -> syn::Result<TokenStream> {
.unwrap_or_else(|| quote! { <#path as ::config::Config>::graph() });

let add_attrs = {
let env_attr = if let Some(env_attr) = field_env_attr {
if env_attr.skip {
quote! { let key = key.with_skip_env(); }
} else {
quote! {}
}
} else {
quote! {}
};
let env_attr = field_env_attr.and_then(|env_attr| {
env_attr
.skip
.then_some(quote! { let key = key.with_skip_env(); })
});

let cli_attr = if let Some(cli_attr) = field_cli_attr {
if cli_attr.skip {
quote! { let key = key.with_skip_cli(); }
} else {
quote! {}
}
} else {
quote! {}
};
let cli_attr = field_cli_attr.and_then(|cli_attr| {
cli_attr
.skip
.then_some(quote! { let key = key.with_skip_cli(); })
});

quote! {
let key = ::config::Key::new(#type_attr);
Expand Down
Loading

0 comments on commit 2d339f7

Please sign in to comment.