-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
FlexibleConfig #2757
FlexibleConfig #2757
Conversation
* Add tests for Setting and RangedNumberValueValidator
* Add tests for unbounded and semi-bounded RangedNumberValueValidators
* Added `FlexibleConfig.Key<T>` which stores `T.class` for `Setting<T>` in the `settingMap` * Added package-private static method `Setting.cast` which converts a raw `Setting` instance to a `Setting<T>`
Can one of the admins please verify this patch? |
* Added tests for Setting.setValueValidator and Setting subscribers
A few questions I'd like reviewers to answer:
|
* Fixed `FlexibleConfig.get` to perform a cast instead of creating a new instance. Removed now unnecessary `Setting.cast` method. * Added tests for each method in `FlexibleConfig`
@GooeyHub: ok to test @eviltak: thanks for all the work and consideration you've put into this topic :-) Poking @emanuele3d and @flo for feedback |
Hooray Jenkins reported success with all tests good! |
@eviltak: this PR might need some more patience than usual unfortunately. With the other folks interested in architecture we are currently looking at some high-level concepts that have been mentioned a number of time and never fully discussed. Configs is one of those concept and it's not quite clear yet if we are even going in the right direction with issue #2668 and therefore this PR. So, you might have to wait a bit longer than usual to get a proper review. |
@emanuele3d that's alright. Any other things (that you mentioned) that you think I can help out in? |
Heya @eviltak - you can check out some of the related architecture talk over here if you're interested: http://forum.terasology.org/threads/config-context-and-security.1761/ A ton of it is beyond the initial Config work which is sort of a phase 1 and the first thing we hope to get done, with this PR the focal point. So far the main solid point I've taken to have some impact on this PR is the idea of moving to use It might also give us a nice key naming convention simply consisting of a module name + object, with the module name possibly relating to an actual module or something else when appropriate (global rendering config or other stuff - more of a category there) The idea behind that is covered in the thread: in short it would allow easy extraction of Config+Context+Injection into its own little external library at some point, without having to depend on On the type stuff and UI auto-building then my brain is still trying to digest that. I think I get what you mean but stuff like Thanks for sticking with this so far, and apologies for any missteps I may make in trying to understand all this, I really don't get enough exposure to fancy Java these days :-) |
@Cervator Switching to The reason for creating a separate generic The reason for making |
I understand you reasons @eviltak.
Your approach is a virtuous one but not one that I support. I will write
more about it later.
|
Our Florian is @flo, like listed over in the Reviewers section in the sidebar. Dunno who that temsa is :-) Thanks for the further examples! Is that getting into how you'd prepare
Is that what I do like it when in existing code you supply a |
@Cervator You got that right. In my example, I wanted to show how my approach makes the compiler point out an error in the choice of type instead of a runtime exception (I'm sorry I didn't make that clear, updated the comment). |
So if I understand it right that's then also what gets in the way of easy looping. You could loop over the map, but not easily get the type of what you were looking at. A user wanting to get a single typed object would already know what they want, much like when we retrieve something from the CoreRegistry. |
A few more thoughts: I think I am seeing better what @emanuele3d is getting at. The very beginning of the original issue was very focused on the details for every individual config item, like the UI hint for instance. The types would be very limited as there just aren't that many - generally you have numbers, strings, and booleans (which sometimes may be in lists for stuff like radio buttons or checkboxes). The discussion spiraled to all kinds of more advanced (and mostly unrelated) topics which introduced plenty of confusion and got us off track somewhat. Whitelists, security, all kinds of fancy stuff. That led to the forum thread to try splitting stuff apart again so we could just focus on plain config again here. While the code here is super cool and I think I understand better what it does now it sort of ends up almost reimplementing Rather than having an exceedingly focused config object we store in a Hopefully I'm on track here and making some sort of sense. Maybe other config frameworks are leaking in, from situations where you really only have one sort of container of arbitrary objects that could contain config, but are classes in their own right. While here we have a split between the high level |
Just wanted to inform you all I am reviewing the code. Hopefully I will finish the review tonight (CET). |
@Cervator The reason I'm going for a generic approach is type safety and flexibility. The following types of values were found by a script that I wrote in the
Instead of having 9 (and possibly more for other Going with using separate implementations of a Regarding the similarity between the implementations 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.
@eviltak, thank you for your work. This PR is an excellent starting point.
Generally speaking, the main problem is that you built a Ferrari when all we needed was a relatively simple semi-truck. Fine engineering, no doubts about that, and I congratulate you on your java proficiency. But we need something simpler and more in line with the original design even though that design might appear less robust to you.
So, lots of changes, most minor, but some big ones too.
In regard to your last comment and the relationship between FlexibleConfigs/Settings and Contexts, I agree with your assessment. Cervator and I did discuss this at length about 24 hours ago and I think we are all a bit clearer about it - i.e. as you say, Contexts shouldn't be used to store Setting instances directly although it would make sense to me to use them to store FlexibleConfig instances, via sub-classes or, better perhaps, through a ConfigList instance.
|
||
assertEquals(50, config.get(key1).getValue().intValue()); | ||
assertEquals(25d, config.get(key2).getValue(), 0.00001d); | ||
} |
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.
This test is meant to test the get method. While I understand that you are using the test to document the workflow, the assertions test the value in each setting. What you should test here is that the setting instance stored earlier is exactly the same as the retrieved one. Incidentally you can show how a Setting instance can be initialized, but I wouldn't go further. Although I'm sure some test purists would prefer you used a MockSetting class so that you can be sure no error can originate from it.
All other tests should be reconsidered in light of this. More complete workflow examples can be added to the javadocs of methods and classes.
import java.util.Map; | ||
|
||
public class FlexibleConfig { | ||
private Map<Key<?>, Setting> settingMap; |
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.
This needs to go back toward the original design:
private Map<SimpleUri, Setting> settingMap;
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 can change the Key
and Setting
class to use SimpleUri
instead, but if we stop using Key
as the map key type, we won't get typed Setting<T>
instances from FlexibleConfig.get
. We'll just get the raw type Setting
.
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, that's ok with me.
|
||
// Maybe throw an exception? | ||
if (has(key)) | ||
return null; |
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.
Let's return a boolean. True if the setting has been added, false if the setting can't be added because one setting with the same uri is already in the config. This makes it consistent with the behavior of remove.
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.
Also, let's make sure we do not allow null keys.
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 return the Key
because the Key
is created by the FlexibleConfig
itself, and needs to be used to access the setting from FlexibleConfig.get
. Useful in cases like:
Key<Float> key = flexibleConfig.add(someFloatSetting);
// ...
Setting<Float> setting = flexibleConfig.get(key);
Of course, if keys are going to be manually created (like in a static helper class), I can create an overload of add
: boolean add(Key<V>, Setting<V>)
that returns true or false showing whether the Key
was added. What do you think I should do with the existing add(Setting)
method?
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 key is the SimpleUri stored in the setting, so we don't need the config to create it and return it.
Of course we need to make sure that the map is aware of the fact that
new SimpleUri("module:setting") == new SimpleUri("module:setting")
that is, SimpleUris are considered identical if their content is identical.
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 presume the SimpleUri.equals
method handles that.
Setting setting = get(id); | ||
|
||
if (setting == null || setting.hasSubscribers()) | ||
return false; |
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.
Before we return false, let's issue two separate logger warnings depending if the key isn't in the map or if the given setting still has subscribers.
public Setting(ResourceUrn id, T defaultValue, SettingValueValidator<T> valueValidator) { | ||
this.id = id; | ||
// Precomputing for performance | ||
this.idString = id.toString(); |
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.
No need: settings shouldn't be retrieved continuously in hot code anyway. That's why we make them subscribable: only when something happens to the setting we react. In those circumstances a small performance loss is acceptable.
|
||
if (setting == null) return null; | ||
|
||
// Possibly raise an exception? |
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.
Once keys are SimpleUri this method should be as simple as:
return settingMap.get(key);
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.
Same as above: if we stop using Key
as the map key type, we won't get typed Setting<T>
instances from FlexibleConfig.get
. We'll just get the raw type Setting
.
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.
Yep, I confirm that's ok with me, user will have to know what type it is and cast it.
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.
Alright. One thing though: Making the user cast it will cause loads of unchecked cast warnings. Instead, I propose to make the get
method perform the cast for them and suppress the unchecked cast warning for it. Instead of Setting<Integer> setting = (Setting<Integer>) config.get(uri)
polluting the codebase with unchecked cast warnings, we can have either Setting<Integer> setting = config.get(uri, Integer.class)
or Setting<Integer> setting = config.get<Integer>(uri)
. Since we don't have a need for the Class (unless you want to return null instead of raising an exception if the types are invalid), I propose the second method.
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 understand the idea of confining the warning to the get method. It's a good idea. Can you show me how you'd write the get method for this form:
Setting<Integer> setting = config.get(uri, Integer.class)
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.
It would be defined as
<V> Setting<V> get(SimpleUri uri, Class<V> valueClass) {
return (Setting<V>) settingMap.get(uri);
}
Since the Class<V>
will not be used (unless you want to return null
instead of causing a class cast exception when the types do not match), the parameter can be removed. That's why I prefer config.get<Integer>(uri)
instead of this signature. Plus, get<Integer>(uri)
makes it more clear than get(uri, Integer.class)
that the fetched type will be a Setting<Integer>
, since the class parameter could be used for absolutely anything.
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.
Ok @eviltak: let's proceed with option 2 and let's see how that looks like in the code.
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.
My inexperience with Java got to me again 😛 Turns out that the get
type argument is inferred from the type of the variable to be assigned. It simply is Setting<Integer> setting = config.get(uri);
Simple, isn't it? Although we can't specify the type argument directly in the method like in C#: config.get<Integer>(uri)
does not work. The result must be assigned to a local variable for it to work.
this.subscribers = Lists.newArrayList(); | ||
} | ||
|
||
public Class<? extends SettingValueValidator> getValidatorClass() { |
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 think we need this method.
|
||
this.valueValidator = valueValidator; | ||
|
||
this.subscribers = Lists.newArrayList(); |
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.
Perhaps we can initialize this only when the first subscriber arrives and we can set it to null when the last one has been unsubscribed. Just a minor memory saving for those settings that aren't being actively monitored.
return value; | ||
} | ||
|
||
public boolean setValue(T 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.
Let's rename the input variable to newValue
.
private String name; | ||
|
||
private String description; | ||
private SettingValueValidator<T> valueValidator; |
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.
Let's rename this variable to just validator
. In the context of this class we know it validates the value.
By the way @eviltak, feel free to hold on changing the test files. You can wait until the other classes have stabilized. |
* `SimpleUri` replaces `ResourceUrn` as the id type for `Setting`.
@emanuele3d Please review when possible. |
Hooray Jenkins reported success with all tests good! |
3 similar comments
Hooray Jenkins reported success with all tests good! |
Hooray Jenkins reported success with all tests good! |
Hooray Jenkins reported success with all tests good! |
Review in progress.... |
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.
Just three small changes, two of which had fallen through the cracks! @eviltak: YOU CAN DO THIS! 😄
@Test | ||
public void testContains() throws Exception { | ||
SimpleUri id = new SimpleUri("engine-tests:TestSetting"); | ||
|
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.
As per earlier feedback, this empty line and the identical one in the next test should be removed, the idea being the line above and the line below, together, they setup the test and should be grouped, while the assert line at the end, on its own, executes the test.
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.
As you seem to have missed this and a few other items of feedback, you might want to adopt a more robust workflow:
- if you just adopt the suggestion, just react with a thumb-up to it - after you have done the change in your local code.
- if you don't want to adopt a suggestion or it needs further discussion leave a comment about it: we will discuss things further and the end result should be a thumb-up from you - again, after local changes - or from me if I retract the suggestion.
Either way you can then be sure that an item of feedback has been dealt with and it gets easy to check large-ish number of past feedback items in the main PR page.
config.add(setting); | ||
|
||
setting.subscribe(propertyChangeEvent -> { | ||
}); |
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.
No thanks, it's ugly and greatly offends my sense of code aesthetics. :)
Besides, you let a machine reformat the code for you???? Shocking. How can an algorithm possibly put poetry in the layout of a piece of code??? Only a person can! (cough) At least for a few more years until Deep Learning has been unleashed on this issue too... 😨
@eviltak: additional question for you. A couple of FlexibleConfigs-related issues (#2893, #2894) are in the works to determine where to go next, after FCs get merged in the codebase. @vampcat seem to have a bit of free time available and would be happy to take care of them or at least help. You have given a lot of time to this PR and it's only fair we ask you first: would you like to continue being the coding lead on this effort? Do you have enough time and energy to proceed with other FC-related issues? I'd be happy for you to do so. Alternatively you could help with the review process, which tends to be somewhat less time-consuming compared to coding but just as important. Let us know! |
@emanuele3d I'm more than fine with others pitching in. If @vampcat (or anyone else, for that matter) is ready to tackle those issues, they are free to do so. I may not have time to actually implement the changes, but I can and will post recommendations and answer/pose any questions if needed. I'd love to help with the review process, if needed. |
- removed a few unnecessary empty lines / unified a previously split line
Hooray Jenkins reported success with all tests good! |
@Cervator: this PR is finally good to go. At 236 conversations (with this one), I believe it's the new record? 😊 @eviltak: thank you for your work and your persistence with this. I believe when Flexible Configs serialization is in place FCs will slowly become widespread in many parts of Terasology's codebase and your work will greatly pay off. Regarding future FCs-related efforts, especially on Serialization, I then recommend for @vampcat to lead the charge. I will then be close behind leading/coordinating the review process and eviltak will take part in it in the measure that his schedule and level of interest will allow. Again, @eviltak, thank you. |
Thanks @eviltak and reviewers! Merged at last :-) Especially appreciated are the new unit tests, ran them and all appeared well. This is definitely another of our record PRs, there are a few though and they each tend to excel in one or more unique ways :D Looking forward to more work in this area, as well as maybe a small chapter in a tutorial somewhere. |
How do I use this? |
I need something for #2901, even if it's just experimental. |
@SoniEx2 FlexibleConfig hasn't been integrated into the engine yet (i.e. there are no ways to create, store or retrieve a FlexibleConfig), so using it in its current state is out of the question. This PR only deals with the implementation, and the integration is going to be covered in future issues and PRs. I suggest you go with the typical hard coded class in the |
Closes #2668. Implements a
FlexibleConfig
andSetting
class which allows for an extensible configuration system which does not have hard-coded settings.Testing
org.terasology.config.flexible
package)Outstanding before merging
Setting
FlexibleConfig
RangedValueValidator
Setting
SettingImpl
FlexibleConfig
FlexibleConfigImpl