Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test features individually #234

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
31 changes: 31 additions & 0 deletions .github/workflows/msrv.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
120 changes: 66 additions & 54 deletions examples/async_source/main.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Error>> {
select! {
r = run_server().fuse() => r,
r = run_client().fuse() => r
}
}

async fn run_server() -> Result<(), Box<dyn Error>> {
let service = warp::path("configuration").map(|| r#"{ "value" : 123 }"#);
pub async fn run_server() -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
// 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<dyn Error>> {
// Good enough for an example to allow server to start
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;

let config = ConfigBuilder::<AsyncState>::default()
.add_async_source(HttpSource {
uri: "http://localhost:5001/configuration".into(),
format: FileFormat::Json,
})
.build()
.await?;
let config = ConfigBuilder::<AsyncState>::default()
.add_async_source(HttpSource {
uri: "http://localhost:5001/configuration".into(),
format: FileFormat::Json,
})
.build()
.await?;

println!("Config value is {}", config.get::<String>("value")?);
println!("Config value is {}", config.get::<String>("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<Map<String, config::Value>, 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<Map<String, config::Value>, 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<dyn std::error::Error>> {
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");
}

78 changes: 40 additions & 38 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Error>> {
/// 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<dyn Error>> {
/// 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<dyn Error>> {
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<dyn Error>> {
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.
Expand Down
3 changes: 3 additions & 0 deletions src/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
6 changes: 6 additions & 0 deletions tests/async_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ impl AsyncSource for AsyncFile {
}
}

#[cfg(feature = "json")]
#[tokio::test]
async fn test_single_async_file_source() {
let config = Config::builder()
Expand All @@ -60,6 +61,7 @@ async fn test_single_async_file_source() {
assert_eq!(true, config.get::<bool>("debug").unwrap());
}

#[cfg(all(feature = "json", feature = "toml"))]
#[tokio::test]
async fn test_two_async_file_sources() {
let config = Config::builder()
Expand All @@ -80,6 +82,7 @@ async fn test_two_async_file_sources() {
assert_eq!(1, config.get::<i32>("place.number").unwrap());
}

#[cfg(all(feature = "toml", feature = "json"))]
#[tokio::test]
async fn test_sync_to_async_file_sources() {
let config = Config::builder()
Expand All @@ -96,6 +99,7 @@ async fn test_sync_to_async_file_sources() {
assert_eq!(1, config.get::<i32>("place.number").unwrap());
}

#[cfg(all(feature = "toml", feature = "json"))]
#[tokio::test]
async fn test_async_to_sync_file_sources() {
let config = Config::builder()
Expand All @@ -112,6 +116,7 @@ async fn test_async_to_sync_file_sources() {
assert_eq!(1, config.get::<i32>("place.number").unwrap());
}

#[cfg(feature = "toml")]
#[tokio::test]
async fn test_async_file_sources_with_defaults() {
let config = Config::builder()
Expand All @@ -132,6 +137,7 @@ async fn test_async_file_sources_with_defaults() {
assert_eq!(1, config.get::<i32>("place.number").unwrap());
}

#[cfg(feature = "toml")]
#[tokio::test]
async fn test_async_file_sources_with_overrides() {
let config = Config::builder()
Expand Down
28 changes: 4 additions & 24 deletions tests/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Outer>()
.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),
},
}
}
17 changes: 17 additions & 0 deletions tests/file_json5.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
}
}
Loading