diff options
-rw-r--r-- | .github/workflows/msrv.yml | 8 | ||||
-rw-r--r-- | CHANGELOG.md | 14 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | examples/watch/main.rs | 19 | ||||
-rw-r--r-- | src/builder.rs | 4 | ||||
-rw-r--r-- | src/de.rs | 104 | ||||
-rw-r--r-- | src/file/mod.rs | 3 | ||||
-rw-r--r-- | src/value.rs | 27 | ||||
-rw-r--r-- | tests/env.rs | 4 | ||||
-rw-r--r-- | tests/file_json5.rs | 2 | ||||
-rw-r--r-- | tests/integer_range.rs | 10 | ||||
-rw-r--r-- | tests/unsigned_int.rs | 48 | ||||
-rw-r--r-- | tests/unsigned_int_hm.rs | 46 |
13 files changed, 223 insertions, 74 deletions
diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml index fa9cd47..e069266 100644 --- a/.github/workflows/msrv.yml +++ b/.github/workflows/msrv.yml @@ -111,12 +111,6 @@ jobs: needs: [check] name: Clippy runs-on: ubuntu-latest - strategy: - matrix: - rust: - - stable - - beta - - nightly steps: - name: Checkout sources uses: actions/checkout@v3.0.2 @@ -124,7 +118,7 @@ jobs: - name: Install toolchain uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.rust }} + toolchain: 1.56.1 override: true components: clippy diff --git a/CHANGELOG.md b/CHANGELOG.md index d086c72..daadbc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## 0.13.2 - 2022-08-02 + + - Backport of [#316] to be testing with temp_env. The backport was necessary to + be able to backport the next change. This change shouldn't be user-visible. + - Backport of [#353] to use TryInto for more permissive deserialization of + integers + - Backport of commit [518a3cafa1e62ba7405709e5c508247e328e0a18] from [#362] to + fix tests + +[#316]: https://github.com/mehcode/config-rs/pull/316 +[#353]: https://github.com/mehcode/config-rs/pull/353 +[518a3cafa1e62ba7405709e5c508247e328e0a18]: https://github.com/mehcode/config-rs/commit/518a3cafa1e62ba7405709e5c508247e328e0a18 +[#362]: https://github.com/mehcode/config-rs/pull/362 + ## 0.13.1 - 2022-04-13 - typo in doc comment for ConfigBuilder [#299] @@ -32,7 +32,7 @@ toml = { version = "0.5", optional = true } serde_json = { version = "1.0.2", optional = true } yaml-rust = { version = "0.4", optional = true } rust-ini = { version = "0.18", optional = true } -ron = { version = "0.7", optional = true } +ron = { version = "0.8", optional = true } json5_rs = { version = "0.4", optional = true, package = "json5" } indexmap = { version = "1.7.0", features = ["serde-1"], optional = true} pathdiff = "0.2" @@ -42,12 +42,12 @@ serde_derive = "1.0.8" float-cmp = "0.9" chrono = { version = "0.4", features = ["serde"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "time"]} -warp = "=0.3.1" +warp = "=0.3.2" futures = "0.3.15" reqwest = "0.11.10" serde = "1.0" glob = "0.3" lazy_static = "1" -notify = "^4.0.0" -temp-env = "0.2.0" +notify = "^5.0.0" +temp-env = "0.3.0" diff --git a/examples/watch/main.rs b/examples/watch/main.rs index 801ee4f..ca83572 100644 --- a/examples/watch/main.rs +++ b/examples/watch/main.rs @@ -1,7 +1,8 @@ #![allow(deprecated)] use config::{Config, File}; -use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; +use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; use std::collections::HashMap; +use std::path::Path; use std::sync::mpsc::channel; use std::sync::RwLock; use std::time::Duration; @@ -33,19 +34,29 @@ fn watch() { // Automatically select the best implementation for your platform. // You can also access each implementation directly e.g. INotifyWatcher. - let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2)).unwrap(); + let mut watcher: RecommendedWatcher = Watcher::new( + tx, + notify::Config::default().with_poll_interval(Duration::from_secs(2)), + ) + .unwrap(); // Add a path to be watched. All files and directories at that path and // below will be monitored for changes. watcher - .watch("examples/watch/Settings.toml", RecursiveMode::NonRecursive) + .watch( + Path::new("examples/watch/Settings.toml"), + RecursiveMode::NonRecursive, + ) .unwrap(); // This is a simple loop, but you may want to use more complex logic here, // for example to handle I/O. loop { match rx.recv() { - Ok(DebouncedEvent::Write(_)) => { + Ok(Ok(Event { + kind: notify::event::EventKind::Modify(_), + .. + })) => { println!(" * Settings.toml written; refreshing configuration ..."); SETTINGS.write().unwrap().refresh().unwrap(); show(); diff --git a/src/builder.rs b/src/builder.rs index 6f928c6..df0b728 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -87,6 +87,7 @@ use crate::{config::Config, path::Expression, source::Source, value::Value}; /// let mut builder = ConfigBuilder::<DefaultState>::default(); /// ``` #[derive(Debug, Clone, Default)] +#[must_use] pub struct ConfigBuilder<St: BuilderState> { defaults: Map<Expression, Value>, overrides: Map<Expression, Value>, @@ -201,7 +202,6 @@ impl ConfigBuilder<DefaultState> { /// 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. - #[must_use] pub fn add_source<T>(mut self, source: T) -> Self where T: Source + Send + Sync + 'static, @@ -291,7 +291,6 @@ impl ConfigBuilder<AsyncState> { /// 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. - #[must_use] pub fn add_source<T>(mut self, source: T) -> Self where T: Source + Send + Sync + 'static, @@ -303,7 +302,6 @@ impl ConfigBuilder<AsyncState> { /// 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. - #[must_use] pub fn add_async_source<T>(mut self, source: T) -> Self where T: AsyncSource + Send + Sync + 'static, @@ -1,13 +1,38 @@ use std::collections::VecDeque; +use std::convert::TryInto; use std::iter::Enumerate; use serde::de; use crate::config::Config; -use crate::error::{ConfigError, Result}; +use crate::error::{ConfigError, Result, Unexpected}; use crate::map::Map; use crate::value::{Table, Value, ValueKind}; +macro_rules! try_convert_number { + (signed, $self:expr, $size:literal) => {{ + let num = $self.into_int()?; + num.try_into().map_err(|_| { + ConfigError::invalid_type( + None, + Unexpected::I64(num), + concat!("an signed ", $size, " bit integer"), + ) + })? + }}; + + (unsigned, $self:expr, $size:literal) => {{ + let num = $self.into_uint()?; + num.try_into().map_err(|_| { + ConfigError::invalid_type( + None, + Unexpected::U64(num), + concat!("an unsigned ", $size, " bit integer"), + ) + })? + }}; +} + impl<'de> de::Deserializer<'de> for Value { type Error = ConfigError; @@ -38,49 +63,50 @@ impl<'de> de::Deserializer<'de> for Value { #[inline] fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i8(self.into_int()? as i8) + let num = try_convert_number!(signed, self, "8"); + visitor.visit_i8(num) } #[inline] fn deserialize_i16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i16(self.into_int()? as i16) + let num = try_convert_number!(signed, self, "16"); + visitor.visit_i16(num) } #[inline] fn deserialize_i32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i32(self.into_int()? as i32) + let num = try_convert_number!(signed, self, "32"); + visitor.visit_i32(num) } #[inline] fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - visitor.visit_i64(self.into_int()?) + let num = try_convert_number!(signed, self, "64"); + visitor.visit_i64(num) } #[inline] fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u8(self.into_uint()? as u8) + let num = try_convert_number!(unsigned, self, "8"); + visitor.visit_u8(num) } #[inline] fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u16(self.into_uint()? as u16) + let num = try_convert_number!(unsigned, self, "16"); + visitor.visit_u16(num) } #[inline] fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u32(self.into_uint()? as u32) + let num = try_convert_number!(unsigned, self, "32"); + visitor.visit_u32(num) } #[inline] fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u64(self.into_uint()? as u64) + let num = try_convert_number!(unsigned, self, "u64"); + visitor.visit_u64(num) } #[inline] @@ -345,19 +371,8 @@ impl<'de> de::Deserializer<'de> for Config { where V: de::Visitor<'de>, { - // Deserialize based on the underlying type - match self.cache.kind { - ValueKind::Nil => visitor.visit_unit(), - ValueKind::I64(i) => visitor.visit_i64(i), - ValueKind::I128(i) => visitor.visit_i128(i), - ValueKind::U64(i) => visitor.visit_u64(i), - ValueKind::U128(i) => visitor.visit_u128(i), - ValueKind::Boolean(b) => visitor.visit_bool(b), - ValueKind::Float(f) => visitor.visit_f64(f), - ValueKind::String(s) => visitor.visit_string(s), - ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)), - ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)), - } + // Delegate deserialization to Value + de::Deserializer::deserialize_any(self.cache, visitor) } #[inline] @@ -367,49 +382,50 @@ impl<'de> de::Deserializer<'de> for Config { #[inline] fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i8(self.cache.into_int()? as i8) + let num = try_convert_number!(signed, self.cache, "8"); + visitor.visit_i8(num) } #[inline] fn deserialize_i16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i16(self.cache.into_int()? as i16) + let num = try_convert_number!(signed, self.cache, "16"); + visitor.visit_i16(num) } #[inline] fn deserialize_i32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_i32(self.cache.into_int()? as i32) + let num = try_convert_number!(signed, self.cache, "32"); + visitor.visit_i32(num) } #[inline] fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - visitor.visit_i64(self.cache.into_int()?) + let num = try_convert_number!(signed, self.cache, "64"); + visitor.visit_i64(num) } #[inline] fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u8(self.cache.into_int()? as u8) + let num = try_convert_number!(unsigned, self.cache, "8"); + visitor.visit_u8(num) } #[inline] fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u16(self.cache.into_int()? as u16) + let num = try_convert_number!(unsigned, self.cache, "16"); + visitor.visit_u16(num) } #[inline] fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u32(self.cache.into_int()? as u32) + let num = try_convert_number!(unsigned, self.cache, "32"); + visitor.visit_u32(num) } #[inline] fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> { - // FIXME: This should *fail* if the value does not fit in the requets integer type - visitor.visit_u64(self.cache.into_int()? as u64) + let num = try_convert_number!(unsigned, self.cache, "64"); + visitor.visit_u64(num) } #[inline] diff --git a/src/file/mod.rs b/src/file/mod.rs index 65f3fd6..4067d76 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -20,6 +20,7 @@ pub use self::source::string::FileSourceString; /// /// It supports optional automatic file format discovery. #[derive(Clone, Debug)] +#[must_use] pub struct File<T, F> { source: T, @@ -101,13 +102,11 @@ where F: FileStoredFormat + 'static, T: FileSource<F>, { - #[must_use] pub fn format(mut self, format: F) -> Self { self.format = Some(format); self } - #[must_use] pub fn required(mut self, required: bool) -> Self { self.required = required; self diff --git a/src/value.rs b/src/value.rs index c9536f7..dced53b 100644 --- a/src/value.rs +++ b/src/value.rs @@ -761,15 +761,30 @@ impl<'de> Deserialize<'de> for Value { } #[inline] - fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Value, E> { - // FIXME: This is bad - Ok((value as i64).into()) + fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Value, E> + where + E: ::serde::de::Error, + { + let num: i64 = value.try_into().map_err(|_| { + E::invalid_type(::serde::de::Unexpected::Unsigned(value), &self) + })?; + Ok(num.into()) } #[inline] - fn visit_u128<E>(self, value: u128) -> ::std::result::Result<Value, E> { - // FIXME: This is bad - Ok((value as i128).into()) + fn visit_u128<E>(self, value: u128) -> ::std::result::Result<Value, E> + where + E: ::serde::de::Error, + { + let num: i128 = value.try_into().map_err(|_| { + E::invalid_type( + ::serde::de::Unexpected::Other( + format!("integer `{}` as u128", value).as_str(), + ), + &self, + ) + })?; + Ok(num.into()) } #[inline] diff --git a/tests/env.rs b/tests/env.rs index 2ee67de..3a24bde 100644 --- a/tests/env.rs +++ b/tests/env.rs @@ -191,7 +191,9 @@ fn test_parse_float() { // can't use `matches!` because of float value match config { - TestFloatEnum::Float(TestFloat { float_val }) => assert_eq!(float_val, 42.3), + TestFloatEnum::Float(TestFloat { float_val }) => { + assert!(float_cmp::approx_eq!(f64, float_val, 42.3)) + } } }) } diff --git a/tests/file_json5.rs b/tests/file_json5.rs index 74d06c2..bdeb3ea 100644 --- a/tests/file_json5.rs +++ b/tests/file_json5.rs @@ -84,7 +84,7 @@ fn test_error_parse() { assert_eq!( res.unwrap_err().to_string(), format!( - " --> 2:7\n |\n2 | ok: true␊\n | ^---\n |\n = expected null in {}", + " --> 2:7\n |\n2 | ok: true\n | ^---\n |\n = expected null in {}", path_with_extension.display() ) ); diff --git a/tests/integer_range.rs b/tests/integer_range.rs index 7777ef2..e80a2f2 100644 --- a/tests/integer_range.rs +++ b/tests/integer_range.rs @@ -13,8 +13,14 @@ fn wrapping_u16() { .build() .unwrap(); - let port: u16 = c.get("settings.port").unwrap(); - assert_eq!(port, 464); + // FIXME: Can't compare ConfigError, because Unexpected are private. + let _port_error = c.get::<u16>("settings.port").unwrap_err(); + /* + assert!(matches!( + Err(ConfigError::invalid_type(None, config::Unexpected::U64(66000), "an unsigned 16 bit integer"),) + port_error + )); + */ } #[test] diff --git a/tests/unsigned_int.rs b/tests/unsigned_int.rs new file mode 100644 index 0000000..e870c8a --- /dev/null +++ b/tests/unsigned_int.rs @@ -0,0 +1,48 @@ +#![cfg(feature = "preserve_order")] + +extern crate indexmap; + +#[derive(serde::Deserialize, Eq, PartialEq, Debug)] +struct Container<T> { + inner: T, +} + +#[derive(serde::Deserialize, Eq, PartialEq, Debug)] +struct Unsigned { + unsigned: u16, +} + +impl Default for Unsigned { + fn default() -> Self { + Self { unsigned: 128 } + } +} + +impl From<Unsigned> for config::ValueKind { + fn from(unsigned: Unsigned) -> Self { + let mut properties = indexmap::IndexMap::new(); + properties.insert( + "unsigned".to_string(), + config::Value::from(unsigned.unsigned), + ); + + Self::Table(properties) + } +} + +#[test] +fn test_deser_unsigned_int() { + let container = Container { + inner: Unsigned::default(), + }; + + let built = config::Config::builder() + .set_default("inner", Unsigned::default()) + .unwrap() + .build() + .unwrap() + .try_deserialize::<Container<Unsigned>>() + .unwrap(); + + assert_eq!(container, built); +} diff --git a/tests/unsigned_int_hm.rs b/tests/unsigned_int_hm.rs new file mode 100644 index 0000000..ab2a60c --- /dev/null +++ b/tests/unsigned_int_hm.rs @@ -0,0 +1,46 @@ +#![cfg(not(feature = "preserve_order"))] + +#[derive(serde::Deserialize, Eq, PartialEq, Debug)] +struct Container<T> { + inner: T, +} + +#[derive(serde::Deserialize, Eq, PartialEq, Debug)] +struct Unsigned { + unsigned: u16, +} + +impl Default for Unsigned { + fn default() -> Self { + Self { unsigned: 128 } + } +} + +impl From<Unsigned> for config::ValueKind { + fn from(unsigned: Unsigned) -> Self { + let mut properties = std::collections::HashMap::new(); + properties.insert( + "unsigned".to_string(), + config::Value::from(unsigned.unsigned), + ); + + Self::Table(properties) + } +} + +#[test] +fn test_deser_unsigned_int_hm() { + let container = Container { + inner: Unsigned::default(), + }; + + let built = config::Config::builder() + .set_default("inner", Unsigned::default()) + .unwrap() + .build() + .unwrap() + .try_deserialize::<Container<Unsigned>>() + .unwrap(); + + assert_eq!(container, built); +} |