Repository: SergioBenitez/Figment (language: rust)

Issue:
Flexible inputs
Hello! 

### Motivation

We're converting a pre-existing application from config-rs to Figment.  As it stands, our users have a number of pre-existing configuration files that Figment will not accept, because those files:
 * represent booleans as numbers (`1`, `0`)
 * represent booleans as strings (`"true"`, `"false"`)
 * represent numbers as strings (`"5"`, `"10"`).

Previously, all of these representations were accepted.  Thus, we can't convert to Figment without breaking our existing users.

### This PR

This PR adapts the deserialize implementations in Figment to accept these representations as well.

Questions for the reviewer:

1. Would you prefer that this behavior not be the default?  If so, please let me know what kind of option you would like it to be controlled by.

2. I've written more tests and documentation this time, but I'm not sure I have as much documentation as you'd like. Just let me know! :) If you point me to the kind of tests and documentation you would prefer, I'll try to add it.


Discussion / hints:
This is great. Going to make a few edits and then push it back your way for some final comments.
Okay, I've made those edits. It's mostly a simplification of what you'd written. There is one "large" change though, which I need your input for in the comment above.
Hi! I've left some comments and questions.  They may be offbase; you know your codebase better than I do, so feel free to do what you think is best if I'm wrong here. ;)
> Hi! I've left some comments and questions. They may be offbase; you know your codebase better than I do, so feel free to do what you think is best if I'm wrong here. ;)

No, totally on-point! Let me know what you think about the updates here. Feel free to make any changes yourself, of course!
I'm happy with where this is now; I think it's good to merge.
I think there might still be consistency issues with the Env variable parser. Can you take a look at the `Env` provider and ensure that the way it parses values is consistent with this new lossy mechanism? Some tests would be great too. 
Sure, I can give it a shot.  Can you give me a little more clarity on what exactly I should be checking?  I see three ways ahead, and you may see more.

My working guess is that what I should be checking is that whenever an option is set via `APP_XYZ=STR` in Env, we get the same result as if that same option were set via `xyz = "STR"` in a configuration file?

As I'm reading the code, that conversion from "STR" to a `Value` in env happens in `<Env as Provider>::data` where it calls `v::parse()`, which uses `<Value as FromStr>`, which is implemented in `value/parse.rs`.

This makes me think there are a few possibilities for testing/consistency:

## Possibility 1

Write a test to ensure that for all strings `s`:
 
1. If `Value::from_str(s)` is a bool, then `Value::String(s).to_bool_lossy()` is the same bool.
2. If `Value::String(s).to_bool_lossy()` is a bool, then `Value::from_str(s)` is the same bool. 
3. If `Value::from_str(s)` is a number, then `Value::String(s).to_num_lossy()` is the same number.
4. If `Value::String(s).to_num_lossy()` is a number, then `Value::from_str(s)` is the same number. 

But note that property 2 is *not* true: "TRUE" and "on" and "1" will get handled as true by `to_bool_lossy` but not by `Value::from_str`.

So if we're going to take this approach, we'll need to tolerate some inconsistency, or we'll need to cause things like "APP_VAR=1" to get parsed as Value::Bool, which is probably not a great idea.

## Possiblility 2

Check something _other than_ the four properties above.  For example, we could make sure that `Value::from_str(s)` and `Value::String(s)` deserialize into the same booleans and numbers for every `s`. 

This would amount to saying that we allow `Value::from_str(s)` to produce best-guess `Value`s, so long as they eventually deserialize to the same thing as if those values had come in as `Value::String(s)`.

## Possibility 3

Revise `Value::from_str` so that it _always_ converts its string inputs into Value::String, and never Value::Bool or Value::Num.  Allow conversion to bool or num to come later, during deserialization.

This is my favorite option. On the positive side, it would remove redundant parsing code.  It would also make Env more consistent in some cases.  For example, the current Env parser can be lossy if it parses a number that was supposed to be a string, as in "APP_USERNAME=012345".  (IIUC, this will parse to the number `12345` rather than the string "012345". If the application wanted a string here, there is no way to find the leading 0 that was originally there.) 

On the minus side, this _would_ break compatibility with anybody who is currently relying on the `Value`s returned from `Env` to do early conversion into Num and Bool.

What do you think?
Possibility 3 is interesting indeed, but I do worry it will break things. Perhaps the best approach is in fact to do nothing at all. After all, these are largely two different things. What we've done here, with this PR, is effectively use the deserializer hints we weren't using before. But this isn't and perhaps shouldn't be related to how a `Value` is parsed from a string.

By the same token, we've now added meaning to certain strings where nothing like it existed before. In particular, the lossy boolean strings on/off, 1/0, yes/no.

I'm a bit concerned about these, and I'm now reminded that we don't have to support this in Figment itself. Specifically, you're always free to write a custom deserializer for your boolean fields that accept the strings you want. In fact, we provide one [that's almost identical](https://docs.rs/figment/latest/figment/util/fn.bool_from_str_or_int.html) to what we've done here in `figment` itself:

```rust
#[derive(Deserialize)]
struct Config {
    #[serde(deserialize_with = "figment::util::bool_from_str_or_int")]
    cli_colors: bool,
}
```

So I believe my real suggestion here is to remove the lossy string -> bool in the deserializer that we've added here. Perhaps even scrap the PR entirely. All of this can be done on the deserialization side without us needing to perhaps unexpectedly do a "dynamic cast" of sorts during deserialization. In fact, this might be particularly concerning as this PR makes it impossible to reject a string that we've now made represent a bool or number. For example, it's impossible to write:

```rust
#[derive(Deserialize)]
struct Config {
    flag: bool,
}
```

Such that `flag` rejects the strings `"on"` and `"off"`.

All of this leads me to believe that while the "lossy" conversions we've added here are useful, perhaps they're best suited as `util` functions like the existing `bool_from_str_or_int` so we can leave the decision to the user. We should also add more documentation to make this clear.
Hm. That approach would work, but the trouble is that we would need to annotate every one of our `bool` and numeric options with `deserialize_with`... and if we forgot one, we would have inconsistent semantics for that option.  (Right now we have dozens of configuration values of each type, and our configuration structures are spread across the several crates that they configure, so it would be easy to forget.)

But I agree that we shouldn't break things.

Maybe instead it would make sense to provide an alternative Deserializer that would make this behavior the default?  If you don't think that's reasonable, I can give it a try.

The implementation would probably look something like:

```
pub struct ConfiguredValueDe<'c, F=DefaultFlavor> {...}
```
where `F` is zero-sized-type that decides how to handle the primitive types.


Fix patch:
diff --git a/src/figment.rs b/src/figment.rs
index fc9daf5..8b2534a 100644
--- a/src/figment.rs
+++ b/src/figment.rs
@@ -4,7 +4,7 @@ use serde::de::Deserialize;
 
 use crate::{Profile, Provider, Metadata};
 use crate::error::{Kind, Result};
-use crate::value::{Value, Map, Dict, Tag, ConfiguredValueDe};
+use crate::value::{Value, Map, Dict, Tag, ConfiguredValueDe, DefaultInterpreter, LossyInterpreter};
 use crate::coalesce::{Coalescible, Order};
 
 /// Combiner of [`Provider`]s for configuration value extraction.
@@ -482,7 +482,64 @@ impl Figment {
     /// });
     /// ```
     pub fn extract<'a, T: Deserialize<'a>>(&self) -> Result<T> {
-        T::deserialize(ConfiguredValueDe::from(self, &self.merged()?))
+        let value = self.merged()?;
+        T::deserialize(ConfiguredValueDe::<'_, DefaultInterpreter>::from(self, &value))
+    }
+
+    /// As [`extract`](Figment::extract_lossy), but interpret numbers and
+    /// booleans more flexibly.
+    ///
+    /// See [`Value::to_bool_lossy`] and [`Value::to_num_lossy`] for a full
+    /// explanation of the imputs accepted.
+    ///
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// use serde::Deserialize;
+    ///
+    /// use figment::{Figment, providers::{Format, Toml, Json, Env}};
+    ///
+    /// #[derive(Debug, PartialEq, Deserialize)]
+    /// struct Config {
+    ///     name: String,
+    ///     numbers: Option<Vec<usize>>,
+    ///     debug: bool,
+    /// }
+    ///
+    /// figment::Jail::expect_with(|jail| {
+    ///     jail.create_file("Config.toml", r#"
+    ///         name = "test"
+    ///         numbers = ["1", "2", "3", "10"]
+    ///     "#)?;
+    ///
+    ///     jail.set_env("config_name", "env-test");
+    ///
+    ///     jail.create_file("Config.json", r#"
+    ///         {
+    ///             "name": "json-test",
+    ///             "debug": "yes"
+    ///         }
+    ///     "#)?;
+    ///
+    ///     let config: Config = Figment::new()
+    ///         .merge(Toml::file("Config.toml"))
+    ///         .merge(Env::prefixed("CONFIG_"))
+    ///         .join(Json::file("Config.json"))
+    ///         .extract_lossy()?;
+    ///
+    ///     assert_eq!(config, Config {
+    ///         name: "env-test".into(),
+    ///         numbers: vec![1, 2, 3, 10].into(),
+    ///         debug: true
+    ///     });
+    ///
+    ///     Ok(())
+    /// });
+    /// ```
+    pub fn extract_lossy<'a, T: Deserialize<'a>>(&self) -> Result<T> {
+        let value = self.merged()?;
+        T::deserialize(ConfiguredValueDe::<'_, LossyInterpreter>::from(self, &value))
     }
 
     /// Deserializes the value at the `key` path in the collected value into
@@ -511,8 +568,43 @@ impl Figment {
     /// });
     /// ```
     pub fn extract_inner<'a, T: Deserialize<'a>>(&self, path: &str) -> Result<T> {
-        T::deserialize(ConfiguredValueDe::from(self, &self.find_value(path)?))
-            .map_err(|e| e.with_path(path))
+        let value = self.find_value(path)?;
+        let de = ConfiguredValueDe::<'_, DefaultInterpreter>::from(self, &value);
+        T::deserialize(de).map_err(|e| e.with_path(path))
+    }
+
+    /// As [`extract`](Figment::extract_lossy), but interpret numbers and
+    /// booleans more flexibly.
+    ///
+    /// See [`Value::to_bool_lossy`] and [`Value::to_num_lossy`] for a full
+    /// explanation of the imputs accepted.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// use figment::{Figment, providers::{Format, Toml, Json}};
+    ///
+    /// figment::Jail::expect_with(|jail| {
+    ///     jail.create_file("Config.toml", r#"
+    ///         numbers = ["1", "2", "3", "10"]
+    ///     "#)?;
+    ///
+    ///     jail.create_file("Config.json", r#"{ "debug": true } "#)?;
+    ///
+    ///     let numbers: Vec<usize> = Figment::new()
+    ///         .merge(Toml::file("Config.toml"))
+    ///         .join(Json::file("Config.json"))
+    ///         .extract_inner_lossy("numbers")?;
+    ///
+    ///     assert_eq!(numbers, vec![1, 2, 3, 10]);
+    ///
+    ///     Ok(())
+    /// });
+    /// ```
+    pub fn extract_inner_lossy<'a, T: Deserialize<'a>>(&self, path: &str) -> Result<T> {
+        let value = self.find_value(path)?;
+        let de = ConfiguredValueDe::<'_, LossyInterpreter>::from(self, &value);
+        T::deserialize(de).map_err(|e| e.with_path(path))
     }
 
     /// Returns an iterator over the metadata for all of the collected values in
diff --git a/src/value/de.rs b/src/value/de.rs
index 05258d4..e3a20ab 100644
--- a/src/value/de.rs
+++ b/src/value/de.rs
@@ -1,28 +1,76 @@
 use std::fmt;
 use std::result;
 use std::cell::Cell;
+use std::marker::PhantomData;
+use std::borrow::Cow;
 
 use serde::Deserialize;
-use serde::de::{self, Deserializer, IntoDeserializer};
-use serde::de::{Visitor, SeqAccess, MapAccess, VariantAccess};
+use serde::de::{self, Deserializer, IntoDeserializer, Visitor};
+use serde::de::{SeqAccess, MapAccess, VariantAccess};
 
 use crate::Figment;
 use crate::error::{Error, Kind, Result};
 use crate::value::{Value, Num, Empty, Dict, Tag};
 
-pub struct ConfiguredValueDe<'c> {
+pub trait Interpreter {
+    fn interpret_as_bool(v: &Value) -> Cow<'_, Value> {
+        Cow::Borrowed(v)
+    }
+
+    fn interpret_as_num(v: &Value) -> Cow<'_, Value> {
+        Cow::Borrowed(v)
+    }
+}
+
+pub struct DefaultInterpreter;
+impl Interpreter for DefaultInterpreter { }
+
+pub struct LossyInterpreter;
+impl Interpreter for LossyInterpreter {
+    fn interpret_as_bool(v: &Value) -> Cow<'_, Value> {
+        v.to_bool_lossy()
+            .map(|b| Cow::Owned(Value::Bool(v.tag(), b)))
+            .unwrap_or(Cow::Borrowed(v))
+    }
+
+    fn interpret_as_num(v: &Value) -> Cow<'_, Value> {
+        v.to_num_lossy()
+            .map(|n| Cow::Owned(Value::Num(v.tag(), n)))
+            .unwrap_or(Cow::Borrowed(v))
+    }
+}
+
+pub struct ConfiguredValueDe<'c, I = DefaultInterpreter> {
     pub config: &'c Figment,
     pub value: &'c Value,
     pub readable: Cell<bool>,
+    _phantom: PhantomData<I>
 }
 
-impl<'c> ConfiguredValueDe<'c> {
+impl<'c, I: Interpreter> ConfiguredValueDe<'c, I> {
     pub fn from(config: &'c Figment, value: &'c Value) -> Self {
-        Self { config, value, readable: Cell::from(true) }
+        Self { config, value, readable: Cell::from(true), _phantom: PhantomData }
+    }
+}
+
+/// Like [`serde::forward_to_deserialize_any`] but applies `$apply` to
+/// `&self` first, then calls `deserialize_any()` on the returned value, and
+/// finally maps any error produced using `$errmap`:
+///   - $apply(&self).deserialize_any(visitor).map_err($errmap)
+macro_rules! apply_then_forward_to_deserialize_any {
+    ( $( $($f:ident),+ => |$this:pat| $apply:expr, $errmap:expr),* $(,)? ) => {
+        $(
+            $(
+                fn $f<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
+                    let $this = &self;
+                    $apply.deserialize_any(visitor).map_err($errmap)
+                }
+            )+
+        )*
     }
 }
 
-impl<'de: 'c, 'c> Deserializer<'de> for ConfiguredValueDe<'c> {
+impl<'de: 'c, 'c, I: Interpreter> Deserializer<'de> for ConfiguredValueDe<'c, I> {
     type Error = Error;
 
     fn deserialize_any<V>(self, v: V) -> Result<V::Value>
@@ -114,8 +162,19 @@ impl<'de: 'c, 'c> Deserializer<'de> for ConfiguredValueDe<'c> {
         val
     }
 
+    apply_then_forward_to_deserialize_any! {
+        deserialize_bool =>
+            |de| I::interpret_as_bool(de.value),
+            |e| e.retagged(de.value.tag()).resolved(de.config),
+        deserialize_u8, deserialize_u16, deserialize_u32, deserialize_u64,
+        deserialize_i8, deserialize_i16, deserialize_i32, deserialize_i64,
+        deserialize_f32, deserialize_f64 =>
+            |de| I::interpret_as_num(de.value),
+            |e| e.retagged(de.value.tag()).resolved(de.config),
+    }
+
     serde::forward_to_deserialize_any! {
-        bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str
+        char str
         string seq bytes byte_buf map unit
         ignored_any unit_struct tuple_struct tuple identifier
     }
@@ -348,14 +407,14 @@ impl Value {
         "___figment_value_id", "___figment_value_value"
     ];
 
-    fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>>(
-        de: ConfiguredValueDe<'c>,
+    fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>, I: Interpreter>(
+        de: ConfiguredValueDe<'c, I>,
         visitor: V
     ) -> Result<V::Value> {
         let mut map = Dict::new();
         map.insert(Self::FIELDS[0].into(), de.value.tag().into());
         map.insert(Self::FIELDS[1].into(), de.value.clone());
-        visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::from(de.config, v)))
+        visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::<'_, I>::from(de.config, v)))
     }
 }
 
diff --git a/src/value/magic.rs b/src/value/magic.rs
index 675944b..3e1da38 100644
--- a/src/value/magic.rs
+++ b/src/value/magic.rs
@@ -6,7 +6,7 @@ use std::path::{PathBuf, Path};
 
 use serde::{Deserialize, Serialize, de};
 
-use crate::{Error, value::{ConfiguredValueDe, MapDe, Tag}};
+use crate::{Error, value::{ConfiguredValueDe, Interpreter, MapDe, Tag}};
 
 /// Marker trait for "magic" values. Primarily for use with [`Either`].
 pub trait Magic: for<'de> Deserialize<'de> {
@@ -16,8 +16,8 @@ pub trait Magic: for<'de> Deserialize<'de> {
     /// The fields of the pseudo-structure. The last one should be the value.
     #[doc(hidden)] const FIELDS: &'static [&'static str];
 
-    #[doc(hidden)] fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>>(
-        de: ConfiguredValueDe<'c>,
+    #[doc(hidden)] fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>, I: Interpreter>(
+        de: ConfiguredValueDe<'c, I>,
         visitor: V
     ) -> Result<V::Value, Error>;
 }
@@ -177,8 +177,8 @@ impl Magic for RelativePathBuf {
         "___figment_relative_path"
     ];
 
-    fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>>(
-        de: ConfiguredValueDe<'c>,
+    fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>, I: Interpreter>(
+        de: ConfiguredValueDe<'c, I>,
         visitor: V
     ) -> Result<V::Value, Error> {
         // If we have this struct with a non-empty metadata_path, use it.
@@ -186,7 +186,8 @@ impl Magic for RelativePathBuf {
         if let Some(d) = de.value.as_dict() {
             if let Some(mpv) = d.get(Self::FIELDS[0]) {
                 if mpv.to_empty().is_none() {
-                    return visitor.visit_map(MapDe::new(d, |v| ConfiguredValueDe::from(config, v)));
+                    let map_de = MapDe::new(d, |v| ConfiguredValueDe::<I>::from(config, v));
+                    return visitor.visit_map(map_de);
                 }
             }
         }
@@ -204,7 +205,7 @@ impl Magic for RelativePathBuf {
         // If we have this struct with no metadata_path, still use the value.
         let value = de.value.find_ref(Self::FIELDS[1]).unwrap_or(&de.value);
         map.insert(Self::FIELDS[1].into(), value.clone());
-        visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::from(config, v)))
+        visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::<I>::from(config, v)))
     }
 }
 
@@ -406,7 +407,7 @@ impl RelativePathBuf {
 //     ) -> Result<V::Value, Error>{
 //         let mut map = crate::value::Map::new();
 //         map.insert(Self::FIELDS[0].into(), de.config.profile().to_string().into());
-//         visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::from(de.config, v)))
+//         visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::<I>::from(de.config, v)))
 //     }
 // }
 //
@@ -572,8 +573,8 @@ impl<T: for<'de> Deserialize<'de>> Magic for Tagged<T> {
         "___figment_tagged_tag" , "___figment_tagged_value"
     ];
 
-    fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>>(
-        de: ConfiguredValueDe<'c>,
+    fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>, I: Interpreter>(
+        de: ConfiguredValueDe<'c, I>,
         visitor: V
     ) -> Result<V::Value, Error>{
         let config = de.config;
@@ -584,7 +585,7 @@ impl<T: for<'de> Deserialize<'de>> Magic for Tagged<T> {
             if let Some(tagv) = dict.get(Self::FIELDS[0]) {
                 if let Ok(false) = tagv.deserialize::<Tag>().map(|t| t.is_default()) {
                     return visitor.visit_map(MapDe::new(dict, |v| {
-                        ConfiguredValueDe::from(config, v)
+                        ConfiguredValueDe::<I>::from(config, v)
                     }));
                 }
             }
@@ -594,7 +595,7 @@ impl<T: for<'de> Deserialize<'de>> Magic for Tagged<T> {
         let value = de.value.find_ref(Self::FIELDS[1]).unwrap_or(&de.value);
         map.insert(Self::FIELDS[0].into(), de.value.tag().into());
         map.insert(Self::FIELDS[1].into(), value.clone());
-        visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::from(config, v)))
+        visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::<I>::from(config, v)))
     }
 }
 
diff --git a/src/value/mod.rs b/src/value/mod.rs
index e360508..2738629 100644
--- a/src/value/mod.rs
+++ b/src/value/mod.rs
@@ -14,6 +14,7 @@ mod escape;
 pub mod magic;
 
 pub(crate) use {self::ser::*, self::de::*};
+
 pub use tag::Tag;
 pub use value::{Value, Map, Num, Dict, Empty};
 pub use uncased::{Uncased, UncasedStr};
diff --git a/src/value/value.rs b/src/value/value.rs
index 41ad9b3..25655cd 100644
--- a/src/value/value.rs
+++ b/src/value/value.rs
@@ -1,9 +1,10 @@
-use std::str::Split;
 use std::collections::BTreeMap;
+use std::num::{ParseFloatError, ParseIntError};
+use std::str::{FromStr, Split};
 
 use serde::Serialize;
 
-use crate::value::{Tag, ValueSerializer};
+use crate::value::{Tag, ValueSerializer, magic::Either};
 use crate::error::{Error, Actual};
 
 /// An alias to the type of map used in [`Value::Dict`].
@@ -280,6 +281,94 @@ impl Value {
         self.to_num()?.to_f64()
     }
 
+    /// Converts `self` to a `bool` if it is a [`Value::Bool`], or if it is a
+    /// [`Value::String`] or a [`Value::Num`] with a boolean interpretation.
+    ///
+    /// The case-insensitive strings "true", "yes", "1", and "on", and the
+    /// signed or unsigned integers `1` are interpreted as `true`.
+    ///
+    /// The case-insensitive strings "false", "no", "0", and "off", and the
+    /// signed or unsigned integers `0` are interpreted as false.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use figment::value::Value;
+    ///
+    /// let value = Value::from(true);
+    /// assert_eq!(value.to_bool_lossy(), Some(true));
+    ///
+    /// let value = Value::from(1);
+    /// assert_eq!(value.to_bool_lossy(), Some(true));
+    ///
+    /// let value = Value::from("YES");
+    /// assert_eq!(value.to_bool_lossy(), Some(true));
+    ///
+    /// let value = Value::from(false);
+    /// assert_eq!(value.to_bool_lossy(), Some(false));
+    ///
+    /// let value = Value::from(0);
+    /// assert_eq!(value.to_bool_lossy(), Some(false));
+    ///
+    /// let value = Value::from("no");
+    /// assert_eq!(value.to_bool_lossy(), Some(false));
+    ///
+    /// let value = Value::from("hello");
+    /// assert_eq!(value.to_bool_lossy(), None);
+    /// ```
+    pub fn to_bool_lossy(&self) -> Option<bool> {
+        match self {
+            Value::Bool(_, b) => Some(*b),
+            Value::Num(_, num) => match num.to_u128_lossy() {
+                Some(0) => Some(false),
+                Some(1) => Some(true),
+                _ => None
+            }
+            Value::String(_, s) => {
+                const TRUE: &[&str] = &["true", "yes", "1", "on"];
+                const FALSE: &[&str] = &["false", "no", "0", "off"];
+
+                if TRUE.iter().any(|v| uncased::eq(v, s)) {
+                    Some(true)
+                } else if FALSE.iter().any(|v| uncased::eq(v, s)) {
+                    Some(false)
+                } else {
+                    None
+                }
+            },
+            _ => None,
+        }
+    }
+
+    /// Converts `self` to a [`Num`] if it is a [`Value::Num`] or if it is a
+    /// [`Value::String`] that parses as a `usize` ([`Num::USize`]), `isize`
+    /// ([`Num::ISize`]), or `f64` ([`Num::F64`]), in that order of precendence.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use figment::value::{Value, Num};
+    ///
+    /// let value = Value::from(7_i32);
+    /// assert_eq!(value.to_num_lossy(), Some(Num::I32(7)));
+    ///
+    /// let value = Value::from("7");
+    /// assert_eq!(value.to_num_lossy(), Some(Num::U8(7)));
+    ///
+    /// let value = Value::from("-7000");
+    /// assert_eq!(value.to_num_lossy(), Some(Num::I16(-7000)));
+    ///
+    /// let value = Value::from("7000.5");
+    /// assert_eq!(value.to_num_lossy(), Some(Num::F64(7000.5)));
+    /// ```
+    pub fn to_num_lossy(&self) -> Option<Num> {
+        match self {
+            Value::Num(_, num) => Some(*num),
+            Value::String(_, s) => s.parse().ok(),
+            _ => None,
+        }
+    }
+
     /// Converts `self` into the corresponding [`Actual`].
     ///
     /// See also [`Num::to_actual()`] and [`Empty::to_actual()`], which are
@@ -398,6 +487,37 @@ macro_rules! impl_from_for_value {
     )*)
 }
 
+macro_rules! try_convert {
+    ($n:expr => $($T:ty),*) => {$(
+        if let Ok(n) = <$T as std::convert::TryFrom<_>>::try_from($n) {
+            return Ok(n.into());
+        }
+    )*}
+}
+
+impl FromStr for Num {
+    type Err = Either<ParseIntError, ParseFloatError>;
+
+    fn from_str(string: &str) -> Result<Self, Self::Err> {
+        let string = string.trim();
+        if string.contains('.') {
+            if string.len() <= (f32::DIGITS as usize + 1) {
+                Ok(string.parse::<f32>().map_err(Either::Right)?.into())
+            } else {
+                Ok(string.parse::<f64>().map_err(Either::Right)?.into())
+            }
+        } else if string.starts_with('-') {
+            let int = string.parse::<i128>().map_err(Either::Left)?;
+            try_convert![int => i8, i16, i32, i64];
+            Ok(int.into())
+        } else {
+            let uint = string.parse::<u128>().map_err(Either::Left)?;
+            try_convert![uint => u8, u16, u32, u64];
+            Ok(uint.into())
+        }
+    }
+}
+
 impl_from_for_value! {
     String: String, char: Char, bool: Bool,
     u8: Num, u16: Num, u32: Num, u64: Num, u128: Num, usize: Num,
@@ -484,6 +604,38 @@ impl Num {
         })
     }
 
+    /// Converts `self` into a `u128` if it is non-negative, even if `self` is
+    /// of a signed variant.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use figment::value::Num;
+    ///
+    /// let num: Num = 123u8.into();
+    /// assert_eq!(num.to_u128_lossy(), Some(123));
+    ///
+    /// let num: Num = 123i8.into();
+    /// assert_eq!(num.to_u128_lossy(), Some(123));
+    /// ```
+    pub fn to_u128_lossy(self) -> Option<u128> {
+        Some(match self {
+            Num::U8(v) => v as u128,
+            Num::U16(v) => v as u128,
+            Num::U32(v) => v as u128,
+            Num::U64(v) => v as u128,
+            Num::U128(v) => v as u128,
+            Num::USize(v) => v as u128,
+            Num::I8(v) if v >= 0 => v as u128,
+            Num::I16(v) if v >= 0 => v as u128,
+            Num::I32(v) if v >= 0 => v as u128,
+            Num::I64(v) if v >= 0 => v as u128,
+            Num::I128(v) if v >= 0 => v as u128,
+            Num::ISize(v) if v >= 0 => v as u128,
+            _ => return None,
+        })
+    }
+
     /// Converts `self` into an `i128` if `self` is a signed `Value::Num`
     /// variant.
     ///


Test patch:
diff --git a/tests/lossy_values.rs b/tests/lossy_values.rs
new file mode 100644
index 0000000..7ea38e9
--- /dev/null
+++ b/tests/lossy_values.rs
@@ -0,0 +1,28 @@
+use serde::Deserialize;
+use figment::{Figment, providers::{Toml, Format}};
+
+#[derive(Debug, Deserialize, PartialEq)]
+struct Config {
+    bs: Vec<bool>,
+    u8s: Vec<u8>,
+    i32s: Vec<i32>,
+    f64s: Vec<f64>,
+}
+
+static TOML: &str = r##"
+    u8s = [1, 2, 3, "4", 5, "6"]
+    i32s = [-1, -2, 3, "-4", 5, "6"]
+    f64s = [1, "2", -3, -4.5, "5.0", "-6.0"]
+    bs = [true, false, "true", "false", "YES", "no", "on", "OFF", "1", "0", 1, 0]
+"##;
+
+#[test]
+fn lossy_values() {
+    let config: Config = Figment::from(Toml::string(TOML)).extract_lossy().unwrap();
+    assert_eq!(&config.u8s, &[ 1, 2, 3, 4, 5, 6 ]);
+    assert_eq!(&config.i32s, &[-1, -2, 3, -4, 5, 6]);
+    assert_eq!(&config.f64s, &[1.0, 2.0, -3.0, -4.5, 5.0, -6.0]);
+    assert_eq!(&config.bs, &[
+        true, false, true, false, true, false, true, false, true, false, true, false
+    ]);
+}