One should not have to change more than one place to map a new feature for a plan
Today, to map a new feature, one should:
Create a feature constant
Add feature: NEW_FEATURE on FEATURE_CODES Hash - that is on License
Add NEW_FEATURE => 1 on EE*_FEATURES Hash - also on License (to mimic the legacy pattern we used to fetch on License#add_ons)
Ultimately, we should be able to map those features in a way that only a new Symbol addition on a plan list would be enough.
Follow can? method pattern
One should be able to call can?(user, :feature, project) (wrapping Project#feature_available?) in order to stay consistent with the rest of the permission checking.
Adding the MY_AWESOME_FEATURE = 'GitLab_AwesomeFeature'.freeze and adding it to FEATURE_CODES feels like a redundant action, but it's not really bothering me.
Having a globally scoped licensed_feature_available? (following can? pattern), would help a bit. But isn't it a good thing we now explicitly ask the Project or the Namespace if a feature is available? I feel adding a global method would have too much magic going on once implemented?
I'm a fan of avoiding magic global methods. I don't like a global can? and I wouldn't like a global licensed_feature_available? either.
Seconded. Calling the .feature_available? on an object makes it clear where to look to see what's going on.
In the case of Project & Group it provides the correct context for the feature check which looks nicer than passing it as a parameter.
Perhaps we can use something a little DSL-y? Take the following as a starting-point.
It sure looks nice. And since we'd only need to add a single line for a feature, it would be only 1 conflict at a time instead of the current 3 or 4 .
I would like to see which plan is extended though, something like:
I think we could define Early Adapter like :early_adopter instead of early_adopter: true.
Also, maybe it's an opportunity to define dependencies between features, like @nick.thomas mentioned before Burndown Charts depend on Issue Weights. By defining this somehow we might avoid future mistakes?
eep_featuresdo# can't generate the feature name string automaticallyauditor_userearly_adopter: true,code: 'GitLab_Auditor_User'end
@nick.thomas In which cases can't the feature name string be generated automatically?
Note: Defining the DSL like this would only allows us to create plans that extend each other. But I don't expect this to be a problem in the somewhat near future.
@nick.thomas In which cases can't the feature name string be generated automatically?
@to1ne we need to choose a convention for generating the string from the code, and it's probably a mistake to change any of the existing strings.
If :burndown_carts becomes GitLab_BurndownCharts and :auditor_user becomes GitLab_Auditor_User, then one or the other will need to be manually specified.
Also, if we like to change some of the strings, we might want to do it now, just before the %9.4 merge window is closed (leaving those added in %9.3 alone).
If they appear in any licenses as addons, changing the string will break the license. I guess this isn't a problem for stuff added between %9.3 and %9.4 though.
Couldn't we simplify things and avoid using a DSL by just declaring this on a yml file? We could eager-load this on the GitLab instance load to avoid disk-accesses on runtime.
The one downside I see is that it then almost begs to be modified by unscrupulous users. Obviously, they could modify app/models/license.rb as well, but this somehow feels more likely to happen.
That makes adding a new feature a lot less cumbersome.
We only need to keep a mapping of legacy addon names to feature names ("GitLab_FooBar" => :foo_bar) for the sake of older licenses that include addons in their payload.
We could implement a DSL or yml, but in this case I don't see an obvious benefit over plain old Ruby.
Note that the only real code that we need to properly map are the legacy ones (these have the GitLab_ thing - it's persisted like this on the license, so we need to keep the exact same name):