-
Notifications
You must be signed in to change notification settings - Fork 116
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
Specify handling of empty config property #407
Conversation
A config property can be configured with an empty value (the empty String `""`). | ||
Such config property will be interpreted by the Config API as follows: | ||
|
||
* `Config.getValue(key, String.class)` returns `""` (empty String) |
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 disagree strongly with this behavior. This makes it impossible to have a configuration source which overrides a present value with a non-present value.
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.
yes, this is against the possibility to detect 'nils'. This would break other features, like to 'unconfigure' values.
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.
The spec never mentions anything about "unconfiguring" the properties.
My understanding of the spec is that the configsource only allows to overrides values, not to "delete/unconfigure" them.
If a lower configsource defines a=foo
and I override that with a higher configsource to be a=
(empty value). I expect the Config API to take my value and returns ``.
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.
Probably we never wrote this down but I only explained this in the past. The original handing in DeltaSpike was to have an empty string 'erase' any configured value in a lower ordinal ConfigSource. This turned out to be quite useful. Especially now with Optional<T>
it is otherwise possible to come back to the orElse. Do you have a better idea?
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.
The spec never mentions anything about "unconfiguring" the properties.
My understanding of the spec is that the configsource only allows to overrides values, not to "delete/unconfigure" them.
This to me is a mistake and we should amend the specification.
If a lower configsource defines
a=foo
and I override that with a higher configsource to bea=
(empty value). I expect the Config API to take my value and returns ``.
As I stated elsewhere, there is no possible meaningful distinction between empty and missing. At best it encourages poor behavior and UX.
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 agree with @dmlloyd proposal to modify the Optional case:
Input string | Output type | Current rules | Better rules |
---|---|---|---|
"" | Optional | Optional.of("") | Optional.empty() |
This would make it possible to use the Optional<T>
to "unconfigure" the value and use the orElse
.
Edit: I agree with david's latest change to treat an empty string as a missing config value.
If I understand you correctly, If I have a lower config source with a=foo
that I want to configure, I'd have to provide a higher config source with a=
.
In my code, I then have to take into account all the use cases:
a
is not configured anywhere,Config.getValue(String)
throwsNoSuchElementException
a
is configured in a config source,Config.getValue(String)
returnfoo
a
is configured in a config source and "unconfigured" in a higher config source with an empty string. What doesConfig.getValue()
return?- `` (empty string that I specified)
null
as David is proposing- throws a
NoSuchElementException
(current rule and it is consistent with "unconfiguring" the property)
Such config property will be interpreted by the Config API as follows: | ||
|
||
* `Config.getValue(key, String.class)` returns `""` (empty String) | ||
* `Config.getValue(key, String[].class)` returns a 0-sized `String[]` array |
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.
Having an empty string be an empty string is implicitly contradictory with this requirement. If ,
returns an array of [ "", "" ]
then the empty string must return an array of [ "" ]
. The only logical and consistent solution is to have arrays (and lists) ignore empty strings and have an empty string be identical to "not present".
* `Config.getValue(key, X.class)` | ||
** if X is an array type, it returns a 0-sized array of the given type `X` | ||
** else it returns either an object of the type `X` (if it can be converted from an empty string) or throws a `IllegalArgumentException` | ||
* `Config.getOptionalValue(key, String.class)` returns `Optional.of("")` |
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.
Then this should be Optional.empty()
.
** if X is an array type, it returns a 0-sized array of the given type `X` | ||
** else it returns either an object of the type `X` (if it can be converted from an empty string) or throws a `IllegalArgumentException` | ||
* `Config.getOptionalValue(key, String.class)` returns `Optional.of("")` | ||
* `Config.getOptionalValue(key, X[].class)` returns an `Optional` of a 0-sized array of the given type `X` |
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.
And this should also be Optional.empty()
.
Here is my basic problem with the current and proposed rules. An empty string is sometimes considered non-present, and sometimes considered a present empty string. This inconsistency causes a few problems:
It would be far, far more sensible to always treat all empty values as an explicit "not present", which allows us to have much more consistent rules.
Updated with the method name |
@dmlloyd thanks for the table, it clearly identifies all the use cases (and should serve as the basic of a TCK test) I thinks the following ones are not accurate wrt to the current rules (and disagree with the
I have no issue with changing the rule so that a missing |
Unfortunately this just cannot be made to work consistently, especially when custom converters are involved. If an empty string is an empty string, then arrays created from empty strings must be
Ah you're right, that was my mistake. I'll update the table accordingly. |
@dmlloyd with your latest changes, I approved your changes. It was bothering me to treat an This also is consistent with @struberg comment about unconfiguring properties. However, this will change the behaviour of existing applications and must bump the MicroProfile Configuration to 2.0. |
* [Spec] add section explaining how the Config API handles a config property with missing or empty value * [Spec] mention that it is possible to set an empty value to reset a property from a lower-ordinal config source. * [TCK] add EmptyConfigPropertyTest to test all cases of empty config property Signed-off-by: Jeff Mesnil <jmesnil@redhat.com>
86caeae
to
0aeb8cd
Compare
@dmlloyd I've integrated your table in the spec: https://github.com/eclipse/microprofile-config/pull/407/files#diff-8faa438f11f150bc9d369db23f9044a1R120 @struberg I've added a note about resetting a property by setting it to an empty string in a higher-ordinal config source: https://github.com/eclipse/microprofile-config/pull/407/files#diff-8faa438f11f150bc9d369db23f9044a1R113 and I've based the TCK tests on the spec tables to check all these use cases |
This PR break compatibility as it now treats an empty string as a missing property and must require to bump the major version of the spec to 2.0 |
Since people are definitely using a missing value to signify that a default should be used, I think it may be worth adding a provision to the spec for making a "default values" configuration source like we do in Quarkus which has bottom priority. It might also be a good idea to make it non-enumerable. But this is something that can be discussed (there are pros and cons). |
In your table, you have,
But what happens when the user specifies,
It seems that for consistency with the previous two example, you would need to remove the empty string value from the middle of this list as well. However, removing empty strings that the user has explicitly specified, whether at the beginning, end, or middle, seems wrong to me. If you do that, how do they go about specifying an empty string when they actually want one? Also, I want to point out that we're currently trying to write a spec that lets the user specify an empty list value in MicroProfile Config. I opened #397 specifically for this because the MP Config spec was unclear on whether to interpret |
Exactly, you'd get a two-element list. This is also useful for expression cases like this: foo.my.property=something,${foo.other.property:},something-else
They can't have one under the current rules. And note that under the current SmallRye implementation, trailing empty segments were already ignored and have been all along. If a user wants to explicitly have empty strings, then we need a quoting mechanism. But I think this would be a design/UX error in 90% of cases.
The user would always get an empty list for an empty value; this would not get rid of that. This gets rid of the logic that would result in a |
That means I misinterpreted the meaning of
Or, maybe I don't understand what you mean by missing. I've been assuming that you mean that the key/value pair is missing entirely. But if you only meant that the value is missing but the key is present, then this is fine. |
Right. But I don't think you do need to make that distinction - I suspect what you're really trying to determine is whether the user explicitly wants no contexts to be propagated, or whether the default should be used (whatever that may be) - if I'm wrong about that, please correct me. The spec still has a hole here that I want to fix as a part of this change. In Quarkus for example the default configuration values exist as a config source. So if the user does not give a key, calling /**
* Return the resolved property value with the specified type for the
* specified property name from the underlying {@link ConfigSource ConfigSources},
* returning a default value if the property name is not found in any of them.
*
* If this method gets used very often then consider to locally store the configured value.
*
* @param <T>
* The property type
* @param propertyName
* The configuration propertyName.
* @param propertyType
* The type into which the resolve property value should get converted
* @param defVal
* The default value to use if no configuration source contains the configuration property name, or {@code null} to indicate that the property should be treated as non-present
* @return the resolved property value as an object of the requested type.
* @throws java.lang.IllegalArgumentException if the property cannot be converted to the specified type.
* @throws java.util.NoSuchElementException if the property isn't present in the configuration.
*/
<T> T getValue(String name, Class<T> aClass, T defVal); This could also be extended to This basically solves the problem of needing to fall back to a default if the key is not specified, while also allowing the user to specify no value explicitly which would override the default. |
@jmesnil tells me that the new |
@njr-11 @dmlloyd I still feel you are talking about two slightly different scenarios. So assuming I have a config From MP CP perspective (as it is designed now), having a config key equals empty value is perfectly fine and it doesn't mean "unconfigure" or "use default"; it translates into an empty string array. |
I would propose that you get an empty array, as shown in the chart, but OTOH with optional values I think it's probably better to get an
You're right.
So I guess it's a technical question about We would have to beef up the language around converters and what they are expected to do if we go with the second option, to make it clear that they should return |
Regardless of which approach is standardized here, we have a requirement for the user to be able to specify a size 0 (empty) array, the purpose of which is well summarized in your earlier comment,
If there is a conflicting request asking for the ability to 'unconfigure' a property back to its defaults and there is trouble deciding between whether an empty string value should be used for that vs the size 0 (empty) array, would it be possible to solve the problem by introducing a constant to be used instead for one of the these? For example, the UNCONFIGURED_VALUE constant in org.eclipse.microprofile.config.inject.ConfigProperty which is currently available but lacks JavaDoc explaining its purpose? Then you could do,
as a very natural way of specifying that you want to override a config property such that is no longer configured and thus ends up with its default value. |
The problem I'm trying to solve here is that when a property has a default value, the user can't configure it to an empty or missing value. In other words, I think the concept of a default value could be decoupled completely from the concept of a missing value. So here's another table covering just these problem cases:
The * indicates that you can change the rules with custom converters. |
wow. This is a long conversation. I don't agree with the suggestion though. I would like to discuss this in more details on a hangout. In the past we were required to differentiate empty value with nulls. Now, some of you suggested it is wrong to differentiate them. I am not convinced. By the way, the spec does not state an empty string means erasing a value. It is DeltaSpike feature not MP Config. When we try to support erasing a property, we might use a different approach. By the way, the issue was raised to clarify the behavior on how to convert to an array.
|
Correct.
To turn this around, can you come up with a valid reason to make them be different? I contend that such reasons either result in usability deficits or else are hypothetical (i.e. not real-world), or else simply don't necessitate differentiation. On the other hand there are definite concrete real-world advantages to keep them the same, as I've outlined above. How would you decide which approach is best, if not by comparing use cases?
Correct, I'm proposing changing the spec in a slightly incompatible way, based on our experiences with Quarkus.
The current implementation gives two empty values in this case AFAICT.
What do you mean by "full table expansion"? |
Sorry, I'm confused about what you're proposing because I cannot figure out what suggestion by @jmesnil you are referring to. Are you suggesting that the proposal can be accepted as it currently stands? Or is there some case you would add or change? |
What I mentioned is not in this PR. It was in one of @jmesnil comments. if a different type such as int or Integer is specified, a Conversion Exception should be thrown. I think we have discussed this long enough. I suggest to validate this PR to reflect the above. |
Unfortunately this introduces quite a lot of chaos into the implementation. Proposed rules:
Your rules:
Can you explain why these rules are better? It seems like every time we talk, you agree that the proposed rules make sense, yet you keep coming back to these rules which are more complex and yet cover fewer use cases AFAICT. |
Also you keep talking about consensus, but have you noticed that we have reached many (conflicting) consensuses? It all depends on which subset of people you have in the group, and whether they understand what is being proposed. Those opposed have largely assumed that the proposal doesn't solve some use case, whereas the use case chart above would easily have answered these questions. The way it is continually described consistently has conflated "what" (adding support for overriding values with empty, without sacrificing any known use case) with "how" (the specific treatment of optional and empty string, which changes the way some use cases are executed). So, imagine my frustration when you say a decision has been reached. If you are expressing that you've reached a point of frustration because of the length of this process, I can only say that it's a natural result of the combination of a general lack of interest (one way or another) and confusion about what is being proposed. It is frankly necessary that both of these problems be solved in order to proceed rationally. |
The distinction for 1) only makes sense if the config sources can make the distinction. And if they can do it, they either have to do the conversion themselves or they need to pass this information to upper levels. If we do the conversion without knowing whether the value is set to empty or not set at all, the non-default conversion can be done in a custom converter. If we expect that ConfigSources do the conversion, then it's up to each ConfigSource how the conversion is done. Though we can specify what it means when a ConfigSource returns null and "". We can also specify how the default ConfigSources treat If we want to cover most cases with a simple solution and allow extending it to all other cases, we should provide all the information to converters so that a custom converter can decide what to do with an empty/null value. A possible solution:
If we go with the simpler default (an empty value leads to "missing") as described by @dmlloyd in #407 (comment), a simple converter could return empty strings instead:
Alternatively, we can specify a special confg prefix which would trigger this behavior automatically even just for specific config keys. E.g. if the app specifies |
So, my question is: regardless of the final default solution, is there something that prevents using a custom converter to switch to the other behavior or even customize it completely? If not, I prefer the behavior that is simpler to specify. Here I agree with @dmlloyd (#407 (comment)) that returning null is easier to specify than returning "" and throwing exceptions for incompatible types. |
Ondro, there's a lot of confusion in this thread so let me lay it out simply. The solution you propose is very close to what I have proposed. This proposal is really about two things: adding the ability to unset properties, and aligning on exactly one representation for empty values for all specification-provided converters. All other use cases which are supported today would remain possible and supported. In terms of UX it is proposed to do it this way:
The following additional behavioral changes are necessary:
I would also suggest that to ensure a uniform behavior, the converter for a given type should always be interrogated to determine what to do when a value is empty by passing it an empty string. This would give user-provided converters complete control over what they return. However many converters today will fail with an Converters should be specified to never be given a One thing that is not yet clear is how to apply the new consistent rules to |
I agree that looking at this from the perspective of user experience is a good approach to take. The part of the user experience that stands out as missing here is how the user sets properties to an empty list. I have a MicroProfile spec that instructs users to specify an empty list as the property value in MP Config when they want to override the implementation's default built-in value with empty. It is a completely different meaning from 'missing', which would mean to accept the default built-in value. In current versions of MP Config, this is possible with the empty string value. The proposal under this issue which seems to be gaining consensus involves repurposing the empty string value, but without any clear way for the user to specify that they really wanted an empty list value. I suspect the intent here is that when you want empty list, write your own converter that re-interprets the value in that way, but that is an awful user experience. The user who configures the property and the 'user' which is the code that accepts the configured value are two completely different users. A well-designed solution shouldn't require that they have prearranged knowledge of eachother's interpretation of values to make this work. Even if that approach were considered acceptable, it involves swapping away one provided function (ability to unset) to get another (set to empty list). If unset is as important as those behind this issue are claiming it to be, then those ought to be two completely distinct things, so that a user who has the ability to specify an empty list value for one property simultaneously has the ability to unset a different property of the same type. |
I'm going to stop you right there because this is the point where people seem to have confusion. You've made an implementation assumption that detecting a missing value as empty is how one would know to use a default value, but that is not correct. Using the config accessor API you should specify what the default value is in the case (either as a string or as the actual target object type) where the default value is something other than empty. This way you would simply read the property and use the result as-is: if it's empty, that's because the user intended it to be empty. If it's the default value, either the user didn't specify the property or else the default was used (both cases should be treated with one code path if possible). If it's some other value, then that value should be used. No branching is necessary. In the less common event that the default value is not representable, you would have to use (and test for) a sentinel default value object instead in the implementation. But I expect this case to be less common than a simple default value. |
I'm not intending to make any implementation assumptions, especially not the one stated. Not making implementation assumptions means that we don't know if the user who consumes the value will do so via the ConfigAccessor or other MP Config API methods like Config.getValue or Config.getOptionalValue. When the user invokes I know this discussion has gone on for a while, and I'm torn between continuing to provide helpful - if that's even the right word - feedback on the user perspective here, or whether to just remain quiet until a solution complete with JavaDoc and everything else is finished, and then assess how it fits into our spec usage in MicroProfile. |
Right, but those would be the wrong methods to call if you have a default value, unless you follow the (recommended) practice of using a low-priority configuration source for your default values. (The reason this is recommended is due to property expansion: most of our users seem to expect that their expressions can refer to values that have defined defaults, and get the default value if the value isn't defined, but this is only achievable using a configuration source for default values).
Agreed that making this dependent on implementation would be a bad user experience. Users would call this method if empty is not an acceptable value.
Saying that the user must call one of these two methods is in fact assuming an implementation which cannot meet the use cases. To meet these kinds of use cases, the config accessor API becomes useful. However I also would propose adding methods to <T> T getValueWithDefault(String key, Class<T> type, T defaultValue);
<T> T getValueWithStringDefault(String key, Class<T> type, String defaultValue);
<T> T getValueWithDefault(String key, Converter<T> converter, T defaultValue);
<T> T getValueWithStringDefault(String key, Converter<T> converter, String defaultValue);
<T> Optional<T> getOptionalValueWithDefault(String key, Class<T> type, T defaultValue);
<T> Optional<T> getOptionalValueWithStringDefault(String key, Class<T> type, String defaultValue);
<T> Optional<T> getOptionalValueWithDefault(String key, Converter<T> converter, T defaultValue);
<T> Optional<T> getOptionalValueWithStringDefault(String key, Converter<T> converter, String defaultValue); Note that by allowing the converter to provide a custom empty behavior, all of the
It's always worth the one-time investment to get things right IMO. However I will say that I'm on vacation right now so my responses may be slow. |
SmallRye Config is currently intentionally non-compliant with MP Config when it comes to handling empty strings. The proposal to change MP Config spec is at [1]. [1] eclipse/microprofile-config#407
SmallRye Config is currently intentionally non-compliant with MP Config when it comes to handling empty strings. The proposal to change MP Config spec is at [1]. [1] eclipse/microprofile-config#407
SmallRye Config is currently intentionally non-compliant with MP Config when it comes to handling empty strings. The proposal to change MP Config spec is at [1]. [1] eclipse/microprofile-config#407
smallrye-config has been splitted in multiple artifacts. * added io.smallrye:smallrye-config-common and io.smallrye.config.smallrye-config-source-file-system dependencies * DirConfigSource class has been renamed to FileSystemConfigSource * exclude tests from MicroProfile Config TCK that are challenged by smallrye-config (see eclipse/microprofile-config#407) JIRA: https://issues.jboss.org/browse/WFLY-12687
smallrye-config has been splitted in multiple artifacts. * added io.smallrye:smallrye-config-common and io.smallrye.config.smallrye-config-source-file-system dependencies * DirConfigSource class has been renamed to FileSystemConfigSource * exclude tests from MicroProfile Config TCK that are challenged by smallrye-config (see eclipse/microprofile-config#407) JIRA: https://issues.jboss.org/browse/WFLY-12687
SmallRyeConfig getValue doesn't allow the provided configured value to be empty, therefore use getOptionalValue instead. See also: eclipse/microprofile-config#407 Fixes #303
SmallRyeConfig getValue doesn't allow the provided configured value to be empty, therefore use getOptionalValue instead. See also: eclipse/microprofile-config#407 Fixes #303
SmallRyeConfig `getValue` doesn't allow the provided configured value to be empty, therefore use `getOptionalValue` instead. See also: eclipse/microprofile-config#407 Fixes #303
property with missing or empty value
property from a lower-ordinal config source.
property
Signed-off-by: Jeff Mesnil jmesnil@redhat.com