diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2021-05-08 18:23:00 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-08 18:23:00 +0200 |
commit | 266f504d9f23e192c03ef486f58a678847249b60 (patch) | |
tree | 815674a16f873fb1dd52d111f9e2de7c6757724f | |
parent | 79883ff7f8ab246c8993f6276d97e5670271cb8c (diff) | |
parent | c4d296482db823221f16d7b26d498810cb26ba59 (diff) |
Merge pull request #196 from szarykott/builder
Create the ConfigBuilder
33 files changed, 2045 insertions, 202 deletions
diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..627ba2a --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,159 @@ +use std::str::FromStr; +use std::{collections::HashMap, iter::IntoIterator}; + +use crate::{config::Config, error, path::Expression, source::Source, value::Value}; + +/// A configuration builder +/// +/// It registers ordered sources of configuration to later build consistent [`Config`] from them. +/// Configuration sources it defines are defaults, [`Source`]s and overrides. +/// +/// Defaults are alaways loaded first and can be overwritten by any of two other sources. +/// Overrides are always loaded last, thus cannot be overridden. +/// Both can be only set explicitly key by key in code +/// using [`set_default`](Self::set_default) or [`set_override`](Self::set_override). +/// +/// An intermediate category, [`Source`], set groups of keys at once implicitly using data coming from external sources +/// like files, environment variables or others that one implements. Defining a [`Source`] is as simple as implementing +/// a trait for a struct. +/// +/// Adding sources, setting defaults and overrides does not invoke any I/O nor builds a config. +/// It happens on demand when [`build`](Self::build) (or its alternative) is called. +/// Therefore all errors, related to any of the [`Source`] will only show up then. +/// +/// # Examples +/// +/// ```rust +/// # use config::*; +/// # use std::error::Error; +/// # fn main() -> Result<(), Box<dyn Error>> { +/// let mut builder = ConfigBuilder::default() +/// .set_default("default", "1")? +/// .add_source(File::new("config/settings", FileFormat::Json)) +/// .set_override("override", "1")?; +/// +/// match builder.build() { +/// Ok(config) => { +/// // use your config +/// }, +/// Err(e) => { +/// // something went wrong +/// } +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// Calls can be not chained as well +/// ```rust +/// # use std::error::Error; +/// # use config::*; +/// # fn main() -> Result<(), Box<dyn Error>> { +/// let mut builder = ConfigBuilder::default(); +/// builder = builder.set_default("default", "1")?; +/// builder = builder.add_source(File::new("config/settings", FileFormat::Json)); +/// builder = builder.add_source(File::new("config/settings.prod", FileFormat::Json)); +/// builder = builder.set_override("override", "1")?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ConfigBuilder { + defaults: HashMap<Expression, Value>, + overrides: HashMap<Expression, Value>, + sources: Vec<Box<dyn Source + Send + Sync>>, +} + +impl ConfigBuilder { + /// Set a default `value` at `key` + /// + /// This value can be overwritten by any [`Source`] or override. + /// + /// # Errors + /// + /// Fails if `Expression::from_str(key)` fails. + pub fn set_default<S, T>(mut self, key: S, value: T) -> error::Result<ConfigBuilder> + where + S: AsRef<str>, + T: Into<Value>, + { + self.defaults + .insert(Expression::from_str(key.as_ref())?, value.into()); + Ok(self) + } + + /// Registers new [`Source`] in this builder. + /// + /// Calling this method does not invoke any I/O. [`Source`] is only saved in internal register for later use. + pub fn add_source<T>(mut self, source: T) -> Self + where + T: Source + Send + Sync + 'static, + { + self.sources.push(Box::new(source)); + self + } + + /// Set an override + /// + /// This function sets an overwrite value. It will not be altered by any default or [`Source`] + /// + /// # Errors + /// + /// Fails if `Expression::from_str(key)` fails. + pub fn set_override<S, T>(mut self, key: S, value: T) -> error::Result<ConfigBuilder> + where + S: AsRef<str>, + T: Into<Value>, + { + self.overrides + .insert(Expression::from_str(key.as_ref())?, value.into()); + Ok(self) + } + + /// Reads all registered [`Source`]s. + /// + /// This is the method that invokes all I/O operations. + /// For a non consuming alternative see [`build_cloned`](Self::build_cloned) + /// + /// # Errors + /// If source collection fails, be it technical reasons or related to inability to read data as `Config` for different reasons, + /// this method returns error. + pub fn build(self) -> error::Result<Config> { + Self::build_internal(self.defaults, self.overrides, &self.sources) + } + + /// Reads all registered [`Source`]s. + /// + /// Similar to [`build`](Self::build), but it does not take ownership of `ConfigBuilder` to allow later reuse. + /// Internally it clones data to achieve it. + /// + /// # Errors + /// If source collection fails, be it technical reasons or related to inability to read data as `Config` for different reasons, + /// this method returns error. + pub fn build_cloned(&self) -> error::Result<Config> { + Self::build_internal(self.defaults.clone(), self.overrides.clone(), &self.sources) + } + + fn build_internal( + defaults: HashMap<Expression, Value>, + overrides: HashMap<Expression, Value>, + sources: &[Box<dyn Source + Send + Sync>], + ) -> error::Result<Config> { + let mut cache: Value = HashMap::<String, Value>::new().into(); + + // Add defaults + for (key, val) in defaults.into_iter() { + key.set(&mut cache, val); + } + + // Add sources + sources.collect_to(&mut cache)?; + + // Add overrides + for (key, val) in overrides.into_iter() { + key.set(&mut cache, val); + } + + Ok(Config::new(cache)) + } +} diff --git a/src/config.rs b/src/config.rs index 2b6c5b6..3d80aeb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::fmt::Debug; +use crate::builder::ConfigBuilder; use serde::de::Deserialize; use serde::ser::Serialize; @@ -35,23 +36,41 @@ impl Default for Config { } impl Config { + pub(crate) fn new(value: Value) -> Self { + Config { + cache: value, + ..Default::default() + } + } + + /// Creates new [`ConfigBuilder`] instance + pub fn builder() -> ConfigBuilder { + ConfigBuilder::default() + } + /// Merge in a configuration property source. + #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] pub fn merge<T>(&mut self, source: T) -> Result<&mut Config> where T: 'static, T: Source + Send + Sync, { self.sources.push(Box::new(source)); + + #[allow(deprecated)] self.refresh() } /// Merge in a configuration property source. + #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] pub fn with_merged<T>(mut self, source: T) -> Result<Self> where T: 'static, T: Source + Send + Sync, { self.sources.push(Box::new(source)); + + #[allow(deprecated)] self.refresh()?; Ok(self) } @@ -61,6 +80,7 @@ impl Config { /// /// Configuration is automatically refreshed after a mutation /// operation (`set`, `merge`, `set_default`, etc.). + #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] pub fn refresh(&mut self) -> Result<&mut Config> { self.cache = { let mut cache: Value = HashMap::<String, Value>::new().into(); @@ -85,11 +105,14 @@ impl Config { } /// Set a default `value` at `key` + #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] pub fn set_default<T>(&mut self, key: &str, value: T) -> Result<&mut Config> where T: Into<Value>, { self.defaults.insert(key.parse()?, value.into()); + + #[allow(deprecated)] self.refresh() } @@ -101,14 +124,18 @@ impl Config { /// # Warning /// /// Errors if config is frozen + #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] pub fn set<T>(&mut self, key: &str, value: T) -> Result<&mut Config> where T: Into<Value>, { self.overrides.insert(key.parse()?, value.into()); + + #[allow(deprecated)] self.refresh() } + #[deprecated(since = "0.12.0", note = "please use 'ConfigBuilder' instead")] pub fn set_once(&mut self, key: &str, value: Value) -> Result<()> { let expr: path::Expression = key.parse()?; @@ -51,6 +51,7 @@ extern crate ini; #[cfg(feature = "ron")] extern crate ron; +mod builder; mod config; mod de; mod env; @@ -61,6 +62,7 @@ mod ser; mod source; mod value; +pub use crate::builder::ConfigBuilder; pub use crate::config::Config; pub use crate::env::Environment; pub use crate::error::ConfigError; @@ -27,6 +27,8 @@ impl ConfigSerializer { ))) } }; + + #[allow(deprecated)] self.output.set(&key, value.into())?; Ok(()) } diff --git a/src/source.rs b/src/source.rs index 2b0eb30..dc5f3b5 100644 --- a/src/source.rs +++ b/src/source.rs @@ -55,6 +55,26 @@ impl Source for Vec<Box<dyn Source + Send + Sync>> { } } +impl Source for [Box<dyn Source + Send + Sync>] { + fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> { + Box::new(self.to_owned()) + } + + fn collect(&self) -> Result<HashMap<String, Value>> { + let mut cache: Value = HashMap::<String, Value>::new().into(); + + for source in self { + source.collect_to(&mut cache)?; + } + + if let ValueKind::Table(table) = cache.kind { + Ok(table) + } else { + unreachable!(); + } + } +} + impl<T> Source for Vec<T> where T: Source + Sync + Send, diff --git a/tests/datetime.rs b/tests/datetime.rs index 471da47..2b0c22d 100644 --- a/tests/datetime.rs +++ b/tests/datetime.rs @@ -14,8 +14,8 @@ use chrono::{DateTime, TimeZone, Utc}; use config::*; fn make() -> Config { - Config::default() - .merge(File::from_str( + Config::builder() + .add_source(File::from_str( r#" { "json_datetime": "2017-05-10T02:14:53Z" @@ -23,22 +23,19 @@ fn make() -> Config { "#, FileFormat::Json, )) - .unwrap() - .merge(File::from_str( + .add_source(File::from_str( r#" yaml_datetime: 2017-06-12T10:58:30Z "#, FileFormat::Yaml, )) - .unwrap() - .merge(File::from_str( + .add_source(File::from_str( r#" toml_datetime = 2017-05-11T14:55:15Z "#, FileFormat::Toml, )) - .unwrap() - .merge(File::from_str( + .add_source(File::from_str( r#" { "hjson_datetime": "2017-05-10T02:14:53Z" @@ -46,15 +43,13 @@ fn make() -> Config { "#, FileFormat::Hjson, )) - .unwrap() - .merge(File::from_str( + .add_source(File::from_str( r#" ini_datetime = 2017-05-10T02:14:53Z "#, FileFormat::Ini, )) - .unwrap() - .merge(File::from_str( + .add_source(File::from_str( r#" ( ron_datetime: "2021-04-19T11:33:02Z" @@ -62,8 +57,8 @@ fn make() -> Config { "#, FileFormat::Ron, )) + .build() .unwrap() - .clone() } #[test] diff --git a/tests/env.rs b/tests/env.rs index 6ce82c7..560ec3b 100644 --- a/tests/env.rs +++ b/tests/env.rs @@ -105,11 +105,13 @@ fn test_parse_int() { env::set_var("INT_VAL", "42"); let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - config.set("tag", "Int").unwrap(); - - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Int") + .unwrap() + .add_source(environment) + .build() + .unwrap(); let config: TestIntEnum = config.try_into().unwrap(); @@ -135,11 +137,13 @@ fn test_parse_float() { env::set_var("FLOAT_VAL", "42.3"); let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Float").unwrap(); - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Float") + .unwrap() + .add_source(environment) + .build() + .unwrap(); let config: TestFloatEnum = config.try_into().unwrap(); @@ -168,11 +172,13 @@ fn test_parse_bool() { env::set_var("BOOL_VAL", "true"); let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Bool").unwrap(); - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Bool") + .unwrap() + .add_source(environment) + .build() + .unwrap(); let config: TestBoolEnum = config.try_into().unwrap(); @@ -202,11 +208,13 @@ fn test_parse_off_int() { env::set_var("INT_VAL_1", "42"); let environment = Environment::new().try_parsing(false); - let mut config = Config::default(); - config.set("tag", "Int").unwrap(); - - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Int") + .unwrap() + .add_source(environment) + .build() + .unwrap(); env::remove_var("INT_VAL_1"); @@ -231,11 +239,13 @@ fn test_parse_off_float() { env::set_var("FLOAT_VAL_1", "42.3"); let environment = Environment::new().try_parsing(false); - let mut config = Config::default(); - - config.set("tag", "Float").unwrap(); - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Float") + .unwrap() + .add_source(environment) + .build() + .unwrap(); env::remove_var("FLOAT_VAL_1"); @@ -260,11 +270,13 @@ fn test_parse_off_bool() { env::set_var("BOOL_VAL_1", "true"); let environment = Environment::new().try_parsing(false); - let mut config = Config::default(); - - config.set("tag", "Bool").unwrap(); - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Bool") + .unwrap() + .add_source(environment) + .build() + .unwrap(); env::remove_var("BOOL_VAL_1"); @@ -289,11 +301,13 @@ fn test_parse_int_fail() { env::set_var("INT_VAL_2", "not an int"); let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - config.set("tag", "Int").unwrap(); - - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Int") + .unwrap() + .add_source(environment) + .build() + .unwrap(); env::remove_var("INT_VAL_2"); @@ -318,11 +332,13 @@ fn test_parse_float_fail() { env::set_var("FLOAT_VAL_2", "not a float"); let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Float").unwrap(); - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Float") + .unwrap() + .add_source(environment) + .build() + .unwrap(); env::remove_var("FLOAT_VAL_2"); @@ -347,11 +363,13 @@ fn test_parse_bool_fail() { env::set_var("BOOL_VAL_2", "not a bool"); let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - - config.set("tag", "Bool").unwrap(); - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "Bool") + .unwrap() + .add_source(environment) + .build() + .unwrap(); env::remove_var("BOOL_VAL_2"); @@ -375,11 +393,13 @@ fn test_parse_string() { env::set_var("STRING_VAL", "test string"); let environment = Environment::new().try_parsing(true); - let mut config = Config::default(); - config.set("tag", "String").unwrap(); - - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "String") + .unwrap() + .add_source(environment) + .build() + .unwrap(); let config: TestStringEnum = config.try_into().unwrap(); @@ -409,11 +429,13 @@ fn test_parse_off_string() { env::set_var("STRING_VAL_1", "test string"); let environment = Environment::new().try_parsing(false); - let mut config = Config::default(); - - config.set("tag", "String").unwrap(); - config.merge(environment).unwrap(); + let config = Config::builder() + .set_default("tag", "String") + .unwrap() + .add_source(environment) + .build() + .unwrap(); let config: TestStringEnum = config.try_into().unwrap(); diff --git a/tests/errors.rs b/tests/errors.rs index cb7f637..54bbe95 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -10,17 +10,17 @@ use std::path::PathBuf; use config::*; fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Toml)) - .unwrap(); - - c + Config::builder() + .add_source(File::new("tests/Settings", FileFormat::Toml)) + .build() + .unwrap() } #[test] fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Toml)); + let res = Config::builder() + .add_source(File::new("tests/Settings-invalid", FileFormat::Toml)) + .build(); let path: PathBuf = ["tests", "Settings-invalid.toml"].iter().collect(); @@ -121,9 +121,13 @@ inner: test: ABC "#; - let mut cfg = Config::default(); - cfg.merge(File::from_str(CFG, FileFormat::Yaml)).unwrap(); - let e = cfg.try_into::<Outer>().unwrap_err(); + let e = Config::builder() + .add_source(File::from_str(CFG, FileFormat::Yaml)) + .build() + .unwrap() + .try_into::<Outer>() + .unwrap_err(); + if let ConfigError::Type { key: Some(path), .. } = e diff --git a/tests/file.rs b/tests/file.rs index 0680c2a..c282691 100644 --- a/tests/file.rs +++ b/tests/file.rs @@ -6,16 +6,18 @@ use config::*; #[test] fn test_file_not_required() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/NoSettings", FileFormat::Yaml).required(false)); + let res = Config::builder() + .add_source(File::new("tests/NoSettings", FileFormat::Yaml).required(false)) + .build(); assert!(res.is_ok()); } #[test] fn test_file_required_not_found() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/NoSettings", FileFormat::Yaml)); + let res = Config::builder() + .add_source(File::new("tests/NoSettings", FileFormat::Yaml)) + .build(); assert!(res.is_err()); assert_eq!( @@ -26,8 +28,9 @@ fn test_file_required_not_found() { #[test] fn test_file_auto() { - let mut c = Config::default(); - c.merge(File::with_name("tests/Settings-production")) + let c = Config::builder() + .add_source(File::with_name("tests/Settings-production")) + .build() .unwrap(); assert_eq!(c.get("debug").ok(), Some(false)); @@ -36,8 +39,9 @@ fn test_file_auto() { #[test] fn test_file_auto_not_found() { - let mut c = Config::default(); - let res = c.merge(File::with_name("tests/NoSettings")); + let res = Config::builder() + .add_source(File::with_name("tests/NoSettings")) + .build(); assert!(res.is_err()); assert_eq!( @@ -48,8 +52,10 @@ fn test_file_auto_not_found() { #[test] fn test_file_ext() { - let mut c = Config::default(); - c.merge(File::with_name("tests/Settings.json")).unwrap(); + let c = Config::builder() + .add_source(File::with_name("tests/Settings.json")) + .build() + .unwrap(); assert_eq!(c.get("debug").ok(), Some(true)); assert_eq!(c.get("production").ok(), Some(false)); diff --git a/tests/file_hjson.rs b/tests/file_hjson.rs index 4ef48ac..4002a90 100644 --- a/tests/file_hjson.rs +++ b/tests/file_hjson.rs @@ -35,11 +35,10 @@ struct Settings { } fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Hjson)) - .unwrap(); - - c + Config::builder() + .add_source(File::new("tests/Settings", FileFormat::Hjson)) + .build() + .unwrap() } #[test] @@ -68,8 +67,9 @@ fn test_file() { #[test] fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Hjson)); + let res = Config::builder() + .add_source(File::new("tests/Settings-invalid", FileFormat::Hjson)) + .build(); let path: PathBuf = ["tests", "Settings-invalid.hjson"].iter().collect(); diff --git a/tests/file_ini.rs b/tests/file_ini.rs index 437c0d9..332d3ea 100644 --- a/tests/file_ini.rs +++ b/tests/file_ini.rs @@ -28,10 +28,10 @@ struct Settings { } fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Ini)) - .unwrap(); - c + Config::builder() + .add_source(File::new("tests/Settings", FileFormat::Ini)) + .build() + .unwrap() } #[test] @@ -56,8 +56,9 @@ fn test_file() { #[test] fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Ini)); + let res = Config::builder() + .add_source(File::new("tests/Settings-invalid", FileFormat::Ini)) + .build(); let path: PathBuf = ["tests", "Settings-invalid.ini"].iter().collect(); diff --git a/tests/file_json.rs b/tests/file_json.rs index bd27572..4563e42 100644 --- a/tests/file_json.rs +++ b/tests/file_json.rs @@ -35,11 +35,10 @@ struct Settings { } fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Json)) - .unwrap(); - - c + Config::builder() + .add_source(File::new("tests/Settings", FileFormat::Json)) + .build() + .unwrap() } #[test] @@ -68,8 +67,9 @@ fn test_file() { #[test] fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Json)); + let res = Config::builder() + .add_source(File::new("tests/Settings-invalid", FileFormat::Json)) + .build(); let path_with_extension: PathBuf = ["tests", "Settings-invalid.json"].iter().collect(); @@ -85,8 +85,8 @@ fn test_error_parse() { #[test] fn test_json_vec() { - let c = Config::default() - .merge(File::from_str( + let c = Config::builder() + .add_source(File::from_str( r#" { "WASTE": ["example_dir1", "example_dir2"] @@ -94,8 +94,8 @@ fn test_json_vec() { "#, FileFormat::Json, )) - .unwrap() - .clone(); + .build() + .unwrap(); let v = c.get_array("WASTE").unwrap(); let mut vi = v.into_iter(); diff --git a/tests/file_ron.rs b/tests/file_ron.rs index 1f1ede2..d60c890 100644 --- a/tests/file_ron.rs +++ b/tests/file_ron.rs @@ -36,11 +36,10 @@ struct Settings { } fn make() -> Config { - let mut c = Config::default(); - c.merge(File::new("tests/Settings", FileFormat::Ron)) - .unwrap(); - - c + Config::builder() + .add_source(File::new("tests/Settings", FileFormat::Ron)) + .build() + .unwrap() } #[test] @@ -70,8 +69,9 @@ fn test_file() { #[test] fn test_error_parse() { - let mut c = Config::default(); - let res = c.merge(File::new("tests/Settings-invalid", FileFormat::Ron)); + let res = Config::builder() + .add_source(File::new("tests/Settings-invalid", FileFormat::Ron)) + .build(); let path_with_extension: PathBuf = ["tests", "Settings-invalid.ron"].iter().collect(); |