diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml index 95f1e0ae..b2528071 100644 --- a/.github/workflows/msrv.yml +++ b/.github/workflows/msrv.yml @@ -81,6 +81,37 @@ jobs: command: test args: --all-features + test-feature: + needs: [check] + name: Test Suite, only some features enabled + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + feature: + - json + - yaml + - hjson + - ini + - json5 + - preserve_order + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + + - name: Run cargo test ${{ matrix.feature }} + uses: actions-rs/cargo@v1 + with: + command: test + args: --no-default-features --features ${{ matrix.feature }} + fmt: needs: [check] name: Rustfmt diff --git a/examples/async_source/main.rs b/examples/async_source/main.rs index 005a4737..d8068445 100644 --- a/examples/async_source/main.rs +++ b/examples/async_source/main.rs @@ -1,72 +1,84 @@ -use std::error::Error; +#[cfg(feature = "json")] +mod example { + use std::error::Error; -use config::{builder::AsyncState, AsyncSource, ConfigBuilder, ConfigError, FileFormat, Map}; + use config::{builder::AsyncState, AsyncSource, ConfigBuilder, ConfigError, FileFormat, Map}; -use async_trait::async_trait; -use futures::{select, FutureExt}; -use warp::Filter; + use async_trait::async_trait; + 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. + // 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 }"#); + pub async fn run_server() -> Result<(), Box> { + let service = warp::path("configuration").map(|| r#"{ "value" : 123 }"#); - println!("Running server on localhost:5001"); + println!("Running server on localhost:5001"); - warp::serve(service).bind(([127, 0, 0, 1], 5001)).await; + warp::serve(service).bind(([127, 0, 0, 1], 5001)).await; - Ok(()) -} + 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; + pub 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?; + 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")?); + println!("Config value is {}", config.get::("value")?); - Ok(()) -} + Ok(()) + } -// Actual implementation of AsyncSource can be found below + // Actual implementation of AsyncSource can be found below + + #[derive(Debug)] + struct HttpSource { + uri: String, + format: FileFormat, + } + + #[async_trait] + impl AsyncSource for HttpSource { + async fn collect(&self) -> Result, ConfigError> { + 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| { + self.format + .parse(Some(&self.uri), &text) + .map_err(|e| ConfigError::Foreign(e)) + }) + } + } -#[derive(Debug)] -struct HttpSource { - uri: String, - format: FileFormat, } -#[async_trait] -impl AsyncSource for HttpSource { - async fn collect(&self) -> Result, ConfigError> { - 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| { - self.format - .parse(Some(&self.uri), &text) - .map_err(|e| ConfigError::Foreign(e)) - }) +#[cfg(feature = "json")] +#[tokio::main] +async fn main() -> Result<(), Box> { + use futures::{select, FutureExt}; + select! { + r = example::run_server().fuse() => r, + r = example::run_client().fuse() => r } } + +#[cfg(not(feature = "json"))] +fn main() { + println!("This example needs the 'json' feature enabled"); +} + diff --git a/src/builder.rs b/src/builder.rs index 7a26151e..2189df20 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -34,44 +34,46 @@ use crate::{config::Config, path::Expression, source::Source, value::Value}; /// /// # Examples /// -/// ```rust -/// # use config::*; -/// # use std::error::Error; -/// # fn main() -> Result<(), Box> { -/// 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() { -/// Ok(config) => { -/// // use your config -/// }, -/// Err(e) => { -/// // something went wrong -/// } -/// } -/// # Ok(()) -/// # } -/// ``` -/// -/// 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 = 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)); -/// builder = builder.set_override("override", "1")?; -/// # Ok(()) -/// # } -/// ``` +#[cfg_attr(feature = "feature", doc = r##" +```rust +# use config::*; +# use std::error::Error; +# fn main() -> Result<(), Box> { +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() { + Ok(config) => { + // use your config + }, + Err(e) => { + // something went wrong + } +} +# Ok(()) +# } +``` + +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 = 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)); +builder = builder.set_override("override", "1")?; +# 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. diff --git a/src/ser.rs b/src/ser.rs index c7e08e17..1a005088 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -699,6 +699,9 @@ impl ser::SerializeStructVariant for StringKeySerializer { mod test { use super::*; + use serde_derive::Serialize; + use serde_derive::Deserialize; + #[test] fn test_struct() { #[derive(Debug, Serialize, Deserialize, PartialEq)] diff --git a/tests/async_builder.rs b/tests/async_builder.rs index b0aa0f4f..00ad85dc 100644 --- a/tests/async_builder.rs +++ b/tests/async_builder.rs @@ -46,6 +46,7 @@ impl AsyncSource for AsyncFile { } } +#[cfg(feature = "json")] #[tokio::test] async fn test_single_async_file_source() { let config = Config::builder() @@ -60,6 +61,7 @@ async fn test_single_async_file_source() { assert_eq!(true, config.get::("debug").unwrap()); } +#[cfg(all(feature = "json", feature = "toml"))] #[tokio::test] async fn test_two_async_file_sources() { let config = Config::builder() @@ -80,6 +82,7 @@ async fn test_two_async_file_sources() { assert_eq!(1, config.get::("place.number").unwrap()); } +#[cfg(all(feature = "toml", feature = "json"))] #[tokio::test] async fn test_sync_to_async_file_sources() { let config = Config::builder() @@ -96,6 +99,7 @@ async fn test_sync_to_async_file_sources() { assert_eq!(1, config.get::("place.number").unwrap()); } +#[cfg(all(feature = "toml", feature = "json"))] #[tokio::test] async fn test_async_to_sync_file_sources() { let config = Config::builder() @@ -112,6 +116,7 @@ async fn test_async_to_sync_file_sources() { assert_eq!(1, config.get::("place.number").unwrap()); } +#[cfg(feature = "toml")] #[tokio::test] async fn test_async_file_sources_with_defaults() { let config = Config::builder() @@ -132,6 +137,7 @@ async fn test_async_file_sources_with_defaults() { assert_eq!(1, config.get::("place.number").unwrap()); } +#[cfg(feature = "toml")] #[tokio::test] async fn test_async_file_sources_with_overrides() { let config = Config::builder() diff --git a/tests/errors.rs b/tests/errors.rs index 60f11211..7075dad3 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -117,40 +117,20 @@ fn error_with_path() { inner: Inner, } const CFG: &str = r#" -inner: - test: ABC -"#; + inner.test = "ABC" + "#; let e = Config::builder() - .add_source(File::from_str(CFG, FileFormat::Yaml)) + .add_source(File::from_str(CFG, FileFormat::Toml)) .build() .unwrap() .try_into::() .unwrap_err(); - if let ConfigError::Type { - key: Some(path), .. - } = e - { + if let ConfigError::Type { key: Some(path), .. } = e { assert_eq!(path, "inner.test"); } else { panic!("Wrong error {:?}", e); } } -#[test] -fn test_error_root_not_table() { - match Config::builder() - .add_source(File::from_str(r#"false"#, FileFormat::Json5)) - .build() - { - Ok(_) => panic!("Should not merge if root is not a table"), - Err(e) => match e { - ConfigError::FileParse { cause, .. } => assert_eq!( - "invalid type: boolean `false`, expected a map", - format!("{}", cause) - ), - _ => panic!("Wrong error: {:?}", e), - }, - } -} diff --git a/tests/file_json5.rs b/tests/file_json5.rs index a768bba0..4af3556e 100644 --- a/tests/file_json5.rs +++ b/tests/file_json5.rs @@ -94,3 +94,20 @@ fn test_error_parse() { ) ); } + +#[test] +fn test_error_root_not_table() { + match Config::builder() + .add_source(File::from_str(r#"false"#, FileFormat::Json5)) + .build() + { + Ok(_) => panic!("Should not merge if root is not a table"), + Err(e) => match e { + ConfigError::FileParse { cause, .. } => assert_eq!( + "invalid type: boolean `false`, expected a map", + format!("{}", cause) + ), + _ => panic!("Wrong error: {:?}", e), + }, + } +} diff --git a/tests/legacy/errors.rs b/tests/legacy/errors.rs index c0ce234d..5ded0a8d 100644 --- a/tests/legacy/errors.rs +++ b/tests/legacy/errors.rs @@ -113,18 +113,15 @@ fn error_with_path() { struct Outer { inner: Inner, } + const CFG: &str = r#" -inner: - test: ABC -"#; + inner.test = "ABC" + "#; let mut cfg = Config::default(); - cfg.merge(File::from_str(CFG, FileFormat::Yaml)).unwrap(); + cfg.merge(File::from_str(CFG, FileFormat::Toml)).unwrap(); let e = cfg.try_into::().unwrap_err(); - if let ConfigError::Type { - key: Some(path), .. - } = e - { + if let ConfigError::Type { key: Some(path), .. } = e { assert_eq!(path, "inner.test"); } else { panic!("Wrong error {:?}", e); diff --git a/tests/legacy/set.rs b/tests/legacy/set.rs index 7a3e3a3e..843ebd6c 100644 --- a/tests/legacy/set.rs +++ b/tests/legacy/set.rs @@ -77,7 +77,7 @@ fn test_set_arr_path() { assert_eq!(c.get("items[2]").ok(), Some("George".to_string())); } -#[cfg(feature = "toml")] +#[cfg(feature = "json")] #[test] fn test_set_capital() { let mut c = Config::default(); diff --git a/tests/set.rs b/tests/set.rs index 4c827d8e..cac53589 100644 --- a/tests/set.rs +++ b/tests/set.rs @@ -75,7 +75,7 @@ fn test_set_arr_path() { assert_eq!(config.get("items[2]").ok(), Some("George".to_string())); } -#[cfg(feature = "toml")] +#[cfg(feature = "json")] #[test] fn test_set_capital() { let config = Config::builder()