-
Notifications
You must be signed in to change notification settings - Fork 36
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
Flexible inputs #97
Flexible inputs #97
Conversation
2abaa6d
to
4c9258a
Compare
When a bool is expected, we now accept the numbers 1 and 0, and the case-insensitive strings "true", "false", "on", "off", "yes", "no", "1", and "0". When a number is expected, we now accept a string that parses to a number. New Value::to_flexible_{bool,num} functions. For many configuration formats, it's convenient to accept the string `"true"` is if it were the boolean `true`, or the string `"7"` as if it were the number `7`. This is part of making that possible.
This is great. Going to make a few edits and then push it back your way for some final comments. |
4c9258a
to
7c98fae
Compare
src/value/value.rs
Outdated
@@ -563,6 +717,27 @@ impl Num { | |||
Num::F64(v) => Actual::Float(v as f64), | |||
} | |||
} | |||
|
|||
// /// Given a number, return the narrowest representation of that number. | |||
// fn compact(self) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, to_num_lossy()
used this method to get the lowest fitting integer type. I've now changed it to use usize
or isize
irrespective of the narrowest size -- this is commensurate with how values are parsed from environment variables (see value/parse.rs
). I'm wondering, however, if the compacting behavior was important for applications I can't think of at the moment. And if it is, then we should change both to to_lossy()
impl and the value parser.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't 100% remember why I had this method; I think it should be okay to leave values in a wider form, unless I am also missing something. Perhaps I wrote a test that behaved badly when the result was truly u128?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to choose the smallest representation, I think that's okay. I don't know if there's any precedence for doing this or not. Deserialization libraries like json
and toml
simply choose a representation like i64
and use it for everything.
Whatever we do, we should be consistent everywhere. I think the way to go here is to implement FromStr
for Num
and have that be the canonical implementation. I'll do that now and compact in it, and we can figure out if that's the most sensible thing or not.
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. |
src/value/value.rs
Outdated
@@ -563,6 +717,27 @@ impl Num { | |||
Num::F64(v) => Actual::Float(v as f64), | |||
} | |||
} | |||
|
|||
// /// Given a number, return the narrowest representation of that number. | |||
// fn compact(self) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't 100% remember why I had this method; I think it should be okay to leave values in a wider form, unless I am also missing something. Perhaps I wrote a test that behaved badly when the result was truly u128?
src/value/value.rs
Outdated
if let Ok(n) = s.parse::<usize>() { | ||
Some(n.into()) | ||
} else if let Ok(n) = s.parse::<isize>() { | ||
Some(n.into()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand right, using only usize
/isize
here means that 128-bit values can't be configured as strings (and neither can 64-bit values on 32-bit systems). That seems like a surprising behavior for users.
In other words,
# this works
x = 1
# and this works
x = "1"
# and this works
x = 300000000000000000000
# but this doesn't work
x = "300000000000000000000"
That seems like it's going to confuse somebody at some point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true, but this is the way the value parser works right now. My point is not to say it should work this way, but that all conversions from strings to Value
s of any kind in the library should all work the same. Any reasonable and consistent behavior is okay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense; and I like the new approach. It seems to simplify things in a few ways.
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 |
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 As I'm reading the code, that conversion from "STR" to a This makes me think there are a few possibilities for testing/consistency: Possibility 1Write a test to ensure that for all strings
But note that property 2 is not true: "TRUE" and "on" and "1" will get handled as true by 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 2Check something other than the four properties above. For example, we could make sure that This would amount to saying that we allow Possibility 3Revise 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 On the minus side, this would break compatibility with anybody who is currently relying on the 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 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 to what we've done here in #[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: #[derive(Deserialize)]
struct Config {
flag: bool,
} Such that All of this leads me to believe that while the "lossy" conversions we've added here are useful, perhaps they're best suited as |
Hm. That approach would work, but the trouble is that we would need to annotate every one of our 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:
where |
(These are taken from their final forms in SergioBenitez#97.)
I have opened a new PR, #100, to show what I mean. |
(These are taken from their final forms in SergioBenitez#97.)
This commit adds `Figment::extract_{inner_}lossy()`, variants of the existing methods that convert string representations of booleans and integers into their boolean and integer forms. The original string form is lost and is not directly recoverable. Methods that performs the same conversion are added to `Value` types: * `Value::to_num_lossy()` * `Value::to_bool_lossy()` * `Num::to_u128_lossy()` * `Num::from_str()` Closes SergioBenitez#97. Co-authored-by: Sergio Benitez <[email protected]>
This commit adds `Figment::extract_{inner_}lossy()`, variants of the existing methods that convert string representations of booleans and integers into their boolean and integer forms. The original string form is lost and is not directly recoverable. Methods that performs the same conversion are added to `Value` types: * `Value::to_num_lossy()` * `Value::to_bool_lossy()` * `Num::to_u128_lossy()` * `Num::from_str()` Closes SergioBenitez#97. Co-authored-by: Sergio Benitez <[email protected]>
This commit adds `Figment::extract_{inner_}lossy()`, variants of the existing methods that convert string representations of booleans and integers into their boolean and integer forms. The original string form is lost and is not directly recoverable. Methods that performs the same conversion are added to `Value` types: * `Value::to_num_lossy()` * `Value::to_bool_lossy()` * `Num::to_u128_lossy()` * `Num::from_str()` Closes SergioBenitez#97. Co-authored-by: Sergio Benitez <[email protected]>
This commit adds `Figment::extract_{inner_}lossy()`, variants of the existing methods that convert string representations of booleans and integers into their boolean and integer forms. The original string form is lost and is not directly recoverable. Methods that performs the same conversion are added to `Value` types: * `Value::to_num_lossy()` * `Value::to_bool_lossy()` * `Num::to_u128_lossy()` * `Num::from_str()` Closes SergioBenitez#97. Co-authored-by: Sergio Benitez <[email protected]>
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:
1
,0
)"true"
,"false"
)"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:
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.
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.