From 49f935abe56be4f85f06dab0363a6919ac4df813 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 26 Jun 2021 15:22:55 +0200 Subject: [PATCH 01/12] Refactor: Extract setting of cache to helper fn Signed-off-by: Matthias Beyer --- src/source.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/source.rs b/src/source.rs index dc5f3b5f..bf2ed87e 100644 --- a/src/source.rs +++ b/src/source.rs @@ -17,18 +17,22 @@ pub trait Source: Debug { fn collect_to(&self, cache: &mut Value) -> Result<()> { self.collect()? .iter() - .for_each(|(key, val)| match path::Expression::from_str(key) { - // Set using the path - Ok(expr) => expr.set(cache, val.clone()), - - // Set diretly anyway - _ => path::Expression::Identifier(key.clone()).set(cache, val.clone()), - }); + .for_each(|(key, val)| set_value(cache, key, val)); Ok(()) } } +fn set_value(cache: &mut Value, key: &String, value: &Value) { + match path::Expression::from_str(key) { + // Set using the path + Ok(expr) => expr.set(cache, value.clone()), + + // Set diretly anyway + _ => path::Expression::Identifier(key.clone()).set(cache, value.clone()), + } +} + impl Clone for Box { fn clone(&self) -> Box { self.clone_into_box() From ddc41bf5d295402499fffa2f78b75583310b5c0c Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 26 Jun 2021 15:29:26 +0200 Subject: [PATCH 02/12] Refactor: Import Result and use it instead of specifying full type path Signed-off-by: Matthias Beyer --- src/builder.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 627ba2a6..a64040a5 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,7 +1,8 @@ use std::str::FromStr; use std::{collections::HashMap, iter::IntoIterator}; -use crate::{config::Config, error, path::Expression, source::Source, value::Value}; +use crate::error::Result; +use crate::{config::Config, path::Expression, source::Source, value::Value}; /// A configuration builder /// @@ -72,7 +73,7 @@ impl ConfigBuilder { /// # Errors /// /// Fails if `Expression::from_str(key)` fails. - pub fn set_default(mut self, key: S, value: T) -> error::Result + pub fn set_default(mut self, key: S, value: T) -> Result where S: AsRef, T: Into, @@ -100,7 +101,7 @@ impl ConfigBuilder { /// # Errors /// /// Fails if `Expression::from_str(key)` fails. - pub fn set_override(mut self, key: S, value: T) -> error::Result + pub fn set_override(mut self, key: S, value: T) -> Result where S: AsRef, T: Into, @@ -118,7 +119,7 @@ impl ConfigBuilder { /// # 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 { + pub fn build(self) -> Result { Self::build_internal(self.defaults, self.overrides, &self.sources) } @@ -130,7 +131,7 @@ impl ConfigBuilder { /// # 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 { + pub fn build_cloned(&self) -> Result { Self::build_internal(self.defaults.clone(), self.overrides.clone(), &self.sources) } @@ -138,7 +139,7 @@ impl ConfigBuilder { defaults: HashMap, overrides: HashMap, sources: &[Box], - ) -> error::Result { + ) -> Result { let mut cache: Value = HashMap::::new().into(); // Add defaults From bbb61a66f755e1907739409d4500009e82064274 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 26 Jun 2021 15:23:33 +0200 Subject: [PATCH 03/12] Add doc to fn Signed-off-by: Matthias Beyer --- src/source.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/source.rs b/src/source.rs index bf2ed87e..647d9488 100644 --- a/src/source.rs +++ b/src/source.rs @@ -14,6 +14,7 @@ pub trait Source: Debug { /// a HashMap. fn collect(&self) -> Result>; + /// Collects all configuration properties to a provided cache. fn collect_to(&self, cache: &mut Value) -> Result<()> { self.collect()? .iter() From 48e4a66ab3dc30729c2212703d5b6992201a4028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Kot?= Date: Sat, 26 Jun 2021 15:23:22 +0200 Subject: [PATCH 04/12] Add AsyncSource trait This patch adds the AsyncSource trait, the interface for providing async source functionality for this crate. Signed-off-by: Matthias Beyer Reviewed-by: Matthias Beyer --- Cargo.toml | 1 + src/lib.rs | 1 + src/source.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 35b1ab33..5d0db610 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ ini = ["rust-ini"] json5 = ["json5_rs"] [dependencies] +async-trait = "0.1.50" lazy_static = "1.0" serde = "1.0.8" nom = "6" diff --git a/src/lib.rs b/src/lib.rs index 60d70127..00526c40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,6 +69,7 @@ pub use crate::config::Config; pub use crate::env::Environment; pub use crate::error::ConfigError; pub use crate::file::{File, FileFormat, FileSourceFile, FileSourceString}; +pub use crate::source::AsyncSource; pub use crate::source::Source; pub use crate::value::Value; pub use crate::value::ValueKind; diff --git a/src/source.rs b/src/source.rs index 647d9488..8711e776 100644 --- a/src/source.rs +++ b/src/source.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use std::fmt::Debug; use std::str::FromStr; +use async_trait::async_trait; + use crate::error::*; use crate::path; use crate::value::{Value, ValueKind}; @@ -34,6 +36,45 @@ fn set_value(cache: &mut Value, key: &String, value: &Value) { } } +/// Describes a generic _source_ of configuration properties capable of using an async runtime. +/// +/// At the moment this library does not implement it, although it allows using its implementations +/// within builders. Due to the scattered landscape of asynchronous runtimes, it is impossible to +/// cater to all needs with one implementation. Also, this trait might be most useful with remote +/// configuration sources, reachable via the network, probably using HTTP protocol. Numerous HTTP +/// libraries exist, making it even harder to find one implementation that rules them all. +/// +/// For those reasons, it is left to other crates to implement runtime-specific or proprietary +/// details. +/// +/// It is advised to use `async_trait` crate while implementing this trait. +/// +/// See examples for sample implementation. +#[async_trait] +pub trait AsyncSource: Debug + Sync { + // Sync is supertrait due to https://docs.rs/async-trait/0.1.50/async_trait/index.html#dyn-traits + + /// Collects all configuration properties available from this source and return + /// a HashMap as an async operations. + async fn collect(&self) -> Result>; + + /// Collects all configuration properties to a provided cache. + async fn collect_to(&self, cache: &mut Value) -> Result<()> { + self.collect() + .await? + .iter() + .for_each(|(key, val)| set_value(cache, key, val)); + + Ok(()) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.to_owned() + } +} + impl Clone for Box { fn clone(&self) -> Box { self.clone_into_box() From c708d44985805b31b60697643ca77fd64315330e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Kot?= Date: Sat, 26 Jun 2021 15:31:45 +0200 Subject: [PATCH 05/12] Reimplement the Config building mechanism This patch rewrites the Config building mechanism using special objects for tracking the config building state. Transitions between states are done on the fly as required. This is required so that the async sources can be stored inside the configuration building objects, while keeping out the expenses in the non-async case, so a user of the crate has only to pay for what they are using (no async means no overhead for that). Signed-off-by: Matthias Beyer Reviewed-by: Matthias Beyer --- src/builder.rs | 232 ++++++++++++++++++++++++++++++++++++++++++++----- src/config.rs | 6 +- src/lib.rs | 3 +- 3 files changed, 216 insertions(+), 25 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index a64040a5..bb88f445 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use std::{collections::HashMap, iter::IntoIterator}; use crate::error::Result; +use crate::source::AsyncSource; use crate::{config::Config, path::Expression, source::Source, value::Value}; /// A configuration builder @@ -22,15 +23,24 @@ use crate::{config::Config, path::Expression, source::Source, value::Value}; /// 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. /// +/// # Sync and async builder +/// +/// [`ConfigBuilder`] uses type parameter to keep track of builder state. +/// +/// In [`DefaultState`] builder only supports [`Source`]s +/// +/// In [`AsyncState`] it supports both [`Source`]s and [`AsyncSource`]s at the price of building using `async fn`. +/// /// # Examples /// /// ```rust /// # use config::*; /// # use std::error::Error; /// # fn main() -> Result<(), Box> { -/// let mut builder = ConfigBuilder::default() +/// let mut builder = Config::builder() /// .set_default("default", "1")? /// .add_source(File::new("config/settings", FileFormat::Json)) +/// // .add_async_source(...) /// .set_override("override", "1")?; /// /// match builder.build() { @@ -45,12 +55,15 @@ use crate::{config::Config, path::Expression, source::Source, value::Value}; /// # } /// ``` /// +/// If any [`AsyncSource`] is used, the builder will transition to [`AsyncState`]. +/// In such case, it is required to _await_ calls to [`build`](Self::build) and its non-consuming sibling. +/// /// Calls can be not chained as well /// ```rust /// # use std::error::Error; /// # use config::*; /// # fn main() -> Result<(), Box> { -/// let mut builder = ConfigBuilder::default(); +/// let mut builder = Config::builder(); /// 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)); @@ -58,22 +71,86 @@ use crate::{config::Config, path::Expression, source::Source, value::Value}; /// # Ok(()) /// # } /// ``` +/// +/// Calling [`Config::builder`](Config::builder) yields builder in the default state. +/// If having an asynchronous state as the initial state is desired, _turbofish_ notation needs to be used. +/// ```rust +/// # use config::{*, builder::AsyncState}; +/// let mut builder = ConfigBuilder::::default(); +/// ``` +/// +/// If for some reason acquiring builder in default state is required without calling [`Config::builder`](Config::builder) +/// it can also be achieved. +/// ```rust +/// # use config::{*, builder::DefaultState}; +/// let mut builder = ConfigBuilder::::default(); +/// ``` #[derive(Debug, Clone, Default)] -pub struct ConfigBuilder { +pub struct ConfigBuilder { defaults: HashMap, overrides: HashMap, + state: St, +} + +/// Represents [`ConfigBuilder`] state. +pub trait BuilderState {} + +/// Represents data specific to builder in default, sychronous state, without support for async. +#[derive(Debug, Default)] +pub struct DefaultState { sources: Vec>, } -impl ConfigBuilder { +/// The asynchronous configuration builder. +/// +/// Similar to a [`ConfigBuilder`] it maintains a set of defaults, a set of sources, and overrides. +/// +/// Defaults do not override anything, sources override defaults, and overrides override anything else. +/// Within those three groups order of adding them at call site matters - entities added later take precedence. +/// +/// For more detailed description and examples see [`ConfigBuilder`]. +/// [`AsyncConfigBuilder`] is just an extension of it that takes async functions into account. +/// +/// To obtain a [`Config`] call [`build`](AsyncConfigBuilder::build) or [`build_cloned`](AsyncConfigBuilder::build_cloned) +/// +/// # Example +/// Since this library does not implement any [`AsyncSource`] an example in rustdocs cannot be given. +/// Detailed explanation about why such a source is not implemented is in [`AsyncSource`]'s documentation. +/// +/// Refer to [`ConfigBuilder`] for similar API sample usage or to the examples folder of the crate, where such a source is implemented. +#[derive(Debug, Clone, Default)] +pub struct AsyncConfigBuilder { + defaults: HashMap, + overrides: HashMap, + sources: Vec, +} + +/// Represents data specific to builder in asychronous state, with support for async. +#[derive(Debug, Default)] +pub struct AsyncState { + sources: Vec, +} + +#[derive(Debug, Clone)] +enum SourceType { + Sync(Box), + Async(Box), +} + +impl BuilderState for DefaultState {} +impl BuilderState for AsyncState {} + +impl ConfigBuilder { + // operations allowed in any state + /// Set a default `value` at `key` /// - /// This value can be overwritten by any [`Source`] or override. + /// This value can be overwritten by any [`Source`], [`AsyncSource`] or override. /// /// # Errors /// /// Fails if `Expression::from_str(key)` fails. - pub fn set_default(mut self, key: S, value: T) -> Result + pub fn set_default(mut self, key: S, value: T) -> Result> where S: AsRef, T: Into, @@ -83,25 +160,14 @@ impl ConfigBuilder { 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(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`] + /// This function sets an overwrite value. It will not be altered by any default, [`Source`] nor [`AsyncSource`] /// /// # Errors /// /// Fails if `Expression::from_str(key)` fails. - pub fn set_override(mut self, key: S, value: T) -> Result + pub fn set_override(mut self, key: S, value: T) -> Result> where S: AsRef, T: Into, @@ -110,6 +176,44 @@ impl ConfigBuilder { .insert(Expression::from_str(key.as_ref())?, value.into()); Ok(self) } +} + +impl ConfigBuilder { + // operations allowed in sync state + + /// 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(mut self, source: T) -> Self + where + T: Source + Send + Sync + 'static, + { + self.state.sources.push(Box::new(source)); + self + } + + /// Registers new [`AsyncSource`] in this builder and forces transition to [`AsyncState`]. + /// + /// Calling this method does not invoke any I/O. [`AsyncSource`] is only saved in internal register for later use. + pub fn add_async_source(self, source: T) -> ConfigBuilder + where + T: AsyncSource + Send + Sync + 'static, + { + let async_state = ConfigBuilder { + state: AsyncState { + sources: self + .state + .sources + .into_iter() + .map(|s| SourceType::Sync(s)) + .collect(), + }, + defaults: self.defaults, + overrides: self.overrides, + }; + + async_state.add_async_source(source) + } /// Reads all registered [`Source`]s. /// @@ -120,7 +224,7 @@ impl ConfigBuilder { /// 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) -> Result { - Self::build_internal(self.defaults, self.overrides, &self.sources) + Self::build_internal(self.defaults, self.overrides, &self.state.sources) } /// Reads all registered [`Source`]s. @@ -132,7 +236,11 @@ impl ConfigBuilder { /// 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) -> Result { - Self::build_internal(self.defaults.clone(), self.overrides.clone(), &self.sources) + Self::build_internal( + self.defaults.clone(), + self.overrides.clone(), + &self.state.sources, + ) } fn build_internal( @@ -158,3 +266,85 @@ impl ConfigBuilder { Ok(Config::new(cache)) } } + +impl ConfigBuilder { + // operations allowed in async state + + /// 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(mut self, source: T) -> ConfigBuilder + where + T: Source + Send + Sync + 'static, + { + self.state.sources.push(SourceType::Sync(Box::new(source))); + self + } + + /// Registers new [`AsyncSource`] in this builder. + /// + /// Calling this method does not invoke any I/O. [`AsyncSource`] is only saved in internal register for later use. + pub fn add_async_source(mut self, source: T) -> ConfigBuilder + where + T: AsyncSource + Send + Sync + 'static, + { + self.state.sources.push(SourceType::Async(Box::new(source))); + self + } + + /// Reads all registered defaults, [`Source`]s, [`AsyncSource`]s and overrides. + /// + /// 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 async fn build(self) -> Result { + Self::build_internal(self.defaults, self.overrides, &self.state.sources).await + } + + /// Reads all registered defaults, [`Source`]s, [`AsyncSource`]s and overrides. + /// + /// 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 async fn build_cloned(&self) -> Result { + Self::build_internal( + self.defaults.clone(), + self.overrides.clone(), + &self.state.sources, + ) + .await + } + + async fn build_internal( + defaults: HashMap, + overrides: HashMap, + sources: &[SourceType], + ) -> Result { + let mut cache: Value = HashMap::::new().into(); + + // Add defaults + for (key, val) in defaults.into_iter() { + key.set(&mut cache, val); + } + + for source in sources.iter() { + match source { + SourceType::Sync(source) => source.collect_to(&mut cache)?, + SourceType::Async(source) => source.collect_to(&mut cache).await?, + } + } + + // 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 3d80aeb8..b9c64b48 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fmt::Debug; -use crate::builder::ConfigBuilder; +use crate::builder::{ConfigBuilder, DefaultState}; use serde::de::Deserialize; use serde::ser::Serialize; @@ -44,8 +44,8 @@ impl Config { } /// Creates new [`ConfigBuilder`] instance - pub fn builder() -> ConfigBuilder { - ConfigBuilder::default() + pub fn builder() -> ConfigBuilder { + ConfigBuilder::::default() } /// Merge in a configuration property source. diff --git a/src/lib.rs b/src/lib.rs index 00526c40..87c82bc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ extern crate ron; #[cfg(feature = "json5")] extern crate json5_rs; -mod builder; +pub mod builder; mod config; mod de; mod env; @@ -64,6 +64,7 @@ mod ser; mod source; mod value; +pub use crate::builder::AsyncConfigBuilder; pub use crate::builder::ConfigBuilder; pub use crate::config::Config; pub use crate::env::Environment; From f4de34eefe95cead9527557abcfddc2648d30278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Kot?= Date: Sat, 26 Jun 2021 15:32:44 +0200 Subject: [PATCH 06/12] Add example implementation using async source Signed-off-by: Matthias Beyer Reviewed-by: Matthias Beyer --- Cargo.toml | 4 ++ examples/async_source/main.rs | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 examples/async_source/main.rs diff --git a/Cargo.toml b/Cargo.toml index 5d0db610..f72389ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,7 @@ json5_rs = { version = "0.3", optional = true, package = "json5" } serde_derive = "1.0.8" float-cmp = "0.8" chrono = { version = "0.4", features = ["serde"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "time"]} +warp = "0.3.1" +futures = "0.3.15" +reqwest = "0.11.3" diff --git a/examples/async_source/main.rs b/examples/async_source/main.rs new file mode 100644 index 00000000..979f8292 --- /dev/null +++ b/examples/async_source/main.rs @@ -0,0 +1,75 @@ +use std::{collections::HashMap, error::Error}; + +use config::{builder::AsyncState, AsyncSource, ConfigBuilder, ConfigError, FileFormat}; + +use async_trait::async_trait; +use futures::{select, FutureExt}; +use warp::Filter; + +// Example below presents sample configuration server and client. +// +// Server serves simple configuration on HTTP endpoint. +// Client consumes it using custom HTTP AsyncSource built on top of reqwest. + +#[tokio::main] +async fn main() -> Result<(), Box> { + select! { + r = run_server().fuse() => r, + r = run_client().fuse() => r + } +} + +async fn run_server() -> Result<(), Box> { + let service = warp::path("configuration").map(|| r#"{ "value" : 123 }"#); + + println!("Running server on localhost:5001"); + + warp::serve(service).bind(([127, 0, 0, 1], 5001)).await; + + Ok(()) +} + +async fn run_client() -> Result<(), Box> { + // Good enough for an example to allow server to start + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + let config = ConfigBuilder::::default() + .add_async_source(HttpSource { + uri: "http://localhost:5001/configuration".into(), + format: FileFormat::Json, + }) + .build() + .await?; + + println!("Config value is {}", config.get::("value")?); + + Ok(()) +} + +// Actual implementation of AsyncSource can be found below + +#[derive(Debug)] +struct HttpSource { + uri: String, + format: FileFormat, +} + +impl HttpSource { + async fn call(&self) -> Result { + reqwest::get(&self.uri).await?.text().await + } +} + +#[async_trait] +impl AsyncSource for HttpSource { + async fn collect(&self) -> Result, ConfigError> { + self.call() + .await + .map_err(|e| ConfigError::Foreign(Box::new(e))) + .and_then(|text| { + self.format + .parse(Some(&self.uri), &text) + .map_err(|e| ConfigError::Foreign(e)) + }) + } +} From f8cef64efbcabc399c075788e2a835b9a3267c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Kot?= Date: Sat, 26 Jun 2021 15:32:57 +0200 Subject: [PATCH 07/12] Add test for async builder Signed-off-by: Matthias Beyer Reviewed-by: Matthias Beyer --- tests/Settings.json | 1 + tests/async_builder.rs | 153 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 tests/async_builder.rs diff --git a/tests/Settings.json b/tests/Settings.json index c8b72c57..001948dd 100644 --- a/tests/Settings.json +++ b/tests/Settings.json @@ -1,5 +1,6 @@ { "debug": true, + "debug_json": true, "production": false, "arr": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "place": { diff --git a/tests/async_builder.rs b/tests/async_builder.rs new file mode 100644 index 00000000..5d7d6ba6 --- /dev/null +++ b/tests/async_builder.rs @@ -0,0 +1,153 @@ +use async_trait::async_trait; +use config::*; +use std::{env, fs, path, str::FromStr}; +use tokio::{fs::File, io::AsyncReadExt}; + +#[derive(Debug)] +struct AsyncFile { + path: String, + format: FileFormat, +} + +/// This is a test only implementation to be used in tests +impl AsyncFile { + pub fn new(path: String, format: FileFormat) -> Self { + AsyncFile { path, format } + } +} + +#[async_trait] +impl AsyncSource for AsyncFile { + async fn collect(&self) -> Result, ConfigError> { + let mut path = env::current_dir().unwrap(); + let local = path::PathBuf::from_str(&self.path).unwrap(); + + path.extend(local.into_iter()); + + let path = match fs::canonicalize(path) { + Ok(path) => path, + Err(e) => return Err(ConfigError::Foreign(Box::new(e))), + }; + + let text = match File::open(path).await { + Ok(mut file) => { + let mut buffer = String::default(); + match file.read_to_string(&mut buffer).await { + Ok(_read) => buffer, + Err(e) => return Err(ConfigError::Foreign(Box::new(e))), + } + } + Err(e) => return Err(ConfigError::Foreign(Box::new(e))), + }; + + self.format + .parse(Some(&self.path), &text) + .map_err(|e| ConfigError::Foreign(e)) + } +} + +#[tokio::test] +async fn test_single_async_file_source() { + let config = Config::builder() + .add_async_source(AsyncFile::new( + "tests/Settings.json".to_owned(), + FileFormat::Json, + )) + .build() + .await + .unwrap(); + + assert_eq!(true, config.get::("debug").unwrap()); +} + +#[tokio::test] +async fn test_two_async_file_sources() { + let config = Config::builder() + .add_async_source(AsyncFile::new( + "tests/Settings.json".to_owned(), + FileFormat::Json, + )) + .add_async_source(AsyncFile::new( + "tests/Settings.toml".to_owned(), + FileFormat::Toml, + )) + .build() + .await + .unwrap(); + + assert_eq!("Torre di Pisa", config.get::("place.name").unwrap()); + assert_eq!(true, config.get::("debug_json").unwrap()); + assert_eq!(1, config.get::("place.number").unwrap()); +} + +#[tokio::test] +async fn test_sync_to_async_file_sources() { + let config = Config::builder() + .add_source(config::File::new("tests/Settings", FileFormat::Json)) + .add_async_source(AsyncFile::new( + "tests/Settings.toml".to_owned(), + FileFormat::Toml, + )) + .build() + .await + .unwrap(); + + assert_eq!("Torre di Pisa", config.get::("place.name").unwrap()); + assert_eq!(1, config.get::("place.number").unwrap()); +} + +#[tokio::test] +async fn test_async_to_sync_file_sources() { + let config = Config::builder() + .add_async_source(AsyncFile::new( + "tests/Settings.toml".to_owned(), + FileFormat::Toml, + )) + .add_source(config::File::new("tests/Settings", FileFormat::Json)) + .build() + .await + .unwrap(); + + assert_eq!("Torre di Pisa", config.get::("place.name").unwrap()); + assert_eq!(1, config.get::("place.number").unwrap()); +} + +#[tokio::test] +async fn test_async_file_sources_with_defaults() { + let config = Config::builder() + .set_default("place.name", "Tower of London") + .unwrap() + .set_default("place.sky", "blue") + .unwrap() + .add_async_source(AsyncFile::new( + "tests/Settings.toml".to_owned(), + FileFormat::Toml, + )) + .build() + .await + .unwrap(); + + assert_eq!("Torre di Pisa", config.get::("place.name").unwrap()); + assert_eq!("blue", config.get::("place.sky").unwrap()); + assert_eq!(1, config.get::("place.number").unwrap()); +} + +#[tokio::test] +async fn test_async_file_sources_with_overrides() { + let config = Config::builder() + .set_override("place.name", "Tower of London") + .unwrap() + .add_async_source(AsyncFile::new( + "tests/Settings.toml".to_owned(), + FileFormat::Toml, + )) + .build() + .await + .unwrap(); + + assert_eq!( + "Tower of London", + config.get::("place.name").unwrap() + ); + assert_eq!(1, config.get::("place.number").unwrap()); +} From f0f212ed7fd5364a2e4e4c67ef4b8a0b31106ade Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 19 Jun 2021 13:26:38 +0200 Subject: [PATCH 08/12] On 1.44.0, only test tests This is required because the examples pull in the "reqwest" crate, which depends on "socket2" and this crate fails to build if the "const fn" feature is not present (which wasn't on 1.44.0). Hence, we only run the tests in the 1.44.0 job, but do not compile the examples. Signed-off-by: Matthias Beyer --- .github/workflows/msrv.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml index 17bed76e..9fdd28e9 100644 --- a/.github/workflows/msrv.yml +++ b/.github/workflows/msrv.yml @@ -59,10 +59,18 @@ jobs: override: true - name: Run cargo test - if: matrix.rust != 'nightly' + if: matrix.rust != 'nightly' && matrix.rust != '1.44.0' + uses: actions-rs/cargo@v1 + with: + command: test + + - name: Run cargo test (nightly) + if: matrix.rust == '1.44.0' + continue-on-error: true uses: actions-rs/cargo@v1 with: command: test + args: --tests - name: Run cargo test (nightly) if: matrix.rust == 'nightly' From 1a7ca3c95d180f315bf68319f576470301848890 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 22 Jun 2021 21:47:42 +0200 Subject: [PATCH 09/12] Bump MSRV to 1.46.0 Because one of our dependencies (namingly `socket2`) uses match in a const fn, which is stabilized in rust 1.46.0, we bump to this version as MSRV for this crate. Signed-off-by: Matthias Beyer Tested-by: Matthias Beyer --- .github/workflows/msrv.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml index 9fdd28e9..23e80471 100644 --- a/.github/workflows/msrv.yml +++ b/.github/workflows/msrv.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: rust: - - 1.44.0 + - 1.46.0 - stable - beta - nightly @@ -44,7 +44,7 @@ jobs: strategy: matrix: rust: - - 1.44.0 + - 1.46.0 - stable - beta - nightly @@ -59,13 +59,13 @@ jobs: override: true - name: Run cargo test - if: matrix.rust != 'nightly' && matrix.rust != '1.44.0' + if: matrix.rust != 'nightly' && matrix.rust != '1.46.0' uses: actions-rs/cargo@v1 with: command: test - name: Run cargo test (nightly) - if: matrix.rust == '1.44.0' + if: matrix.rust == '1.46.0' continue-on-error: true uses: actions-rs/cargo@v1 with: From 55ba43de725338eb0062d1bcf949e0e79e4ebce4 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 26 Jun 2021 17:37:23 +0200 Subject: [PATCH 10/12] Fix clippy: Use &str instead of &String as argument Signed-off-by: Matthias Beyer --- src/source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source.rs b/src/source.rs index 8711e776..7781fc10 100644 --- a/src/source.rs +++ b/src/source.rs @@ -26,7 +26,7 @@ pub trait Source: Debug { } } -fn set_value(cache: &mut Value, key: &String, value: &Value) { +fn set_value(cache: &mut Value, key: &str, value: &Value) { match path::Expression::from_str(key) { // Set using the path Ok(expr) => expr.set(cache, value.clone()), From 18f01fd6f628f74ec6a340c8f554995148616f2f Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 26 Jun 2021 17:37:40 +0200 Subject: [PATCH 11/12] Fix clippy: Use to_string() instead of clone() Signed-off-by: Matthias Beyer --- src/source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source.rs b/src/source.rs index 7781fc10..831e4c4c 100644 --- a/src/source.rs +++ b/src/source.rs @@ -32,7 +32,7 @@ fn set_value(cache: &mut Value, key: &str, value: &Value) { Ok(expr) => expr.set(cache, value.clone()), // Set diretly anyway - _ => path::Expression::Identifier(key.clone()).set(cache, value.clone()), + _ => path::Expression::Identifier(key.to_string()).set(cache, value.clone()), } } From 33c6432dcb007b68008599a24d19fb724ebaf1ca Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 29 Jun 2021 19:57:22 +0200 Subject: [PATCH 12/12] Simplify example impl With this simplification, we save a bit of code on one side, but also showcase that errors from custom AsyncSource implementations are possible because the ConfigError type provides a variant for it. Signed-off-by: Matthias Beyer Tested-by: Matthias Beyer --- examples/async_source/main.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/async_source/main.rs b/examples/async_source/main.rs index 979f8292..10befe05 100644 --- a/examples/async_source/main.rs +++ b/examples/async_source/main.rs @@ -54,16 +54,13 @@ struct HttpSource { format: FileFormat, } -impl HttpSource { - async fn call(&self) -> Result { - reqwest::get(&self.uri).await?.text().await - } -} - #[async_trait] impl AsyncSource for HttpSource { async fn collect(&self) -> Result, ConfigError> { - self.call() + reqwest::get(&self.uri) + .await + .map_err(|e| ConfigError::Foreign(Box::new(e)))? // error conversion is possible from custom AsyncSource impls + .text() .await .map_err(|e| ConfigError::Foreign(Box::new(e))) .and_then(|text| {