-
Notifications
You must be signed in to change notification settings - Fork 274
breaking change: enums case insensitive by default #1419
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
breaking change: enums case insensitive by default #1419
Conversation
|
|
||
| **skip_migration_check** | ||
| **Default:** ``false`` | ||
| **association_model_namespace** |
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 took a moment to rearrange the Configuration::Variables section of the docs to be in alphabetical order (it seemed to be randomly ordered before)
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.
👍
| # => CYPHER: "MATCH (result_media:`StoredFile`) WHERE (result_media.type <> 0)" | ||
| By default, every ``enum`` property will be defined as ``unique``, to improve query performances. If you want to disable this, simply pass ``_index: false`` to ``enum``: | ||
| By default, every ``enum`` property will require you to add an associated index to improve query performance. If you want to disable this, simply pass ``_index: false`` to ``enum``: |
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 updated the enum _index option docs
| end | ||
| Sometimes it is desirable to have a default value for an ``enum`` property. To acheive this, you can simply define a property with the same name which defines a default value: | ||
| Sometimes it is desirable to have a default value for an ``enum`` property. To acheive this, you can simply pass the ``_default`` option when defining the enum: |
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 was outdated
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.
Hrmm, interesting. I actually changed this recently because default wasn't working, but it might just be that I didn't know about the _default option. I think we tried default, so at some point in the future it might be good to have the underscored and non-underscored version of all of these options work
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.
You're totally right. A _default option is whitelisted in the source and I assumed it worked. Just testing though, I see it does not work. I'll change it back to the old documentation.
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.
Wait no, the _default option does work. There's just a bug that if someone submits an enum param that isn't among the list of options, then the record is persisted with the first option. It should either be persisted with the _default option or it should raise an exception. Do you have an opinion either way?
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, regarding _default vs default, personally, I don't really like the idea of accepting both. More code would need to be added to make sure only one option was given and it feels like clutter. Given that this is already a breaking change, now might be the time to remove _ from all these options.
This being said, I'm happy to do it your way if that's what you decide. It would probably involve adding a normalize_options_listfunction to both normalize the new options and spit out depreciation warnings.
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.
So looking into persistence a bit more. Currently, if a _default option isn't set, and a submitted value doesn't match a permitted value, then the node will be persisted with a value of 0.
This is a bug because if someone has defined their enums using a hash like enum property: {one: 'ONE', two: 'TWO'} and execute Model.create(property: 'TEST'), then the object will be persisted with property = 0 when the only allowed values are 'ONE' or 'TWO'. It's a little hard to diagnose because the Neo4jrb object shows property = nil (but the db has property = 0).
This is what I think the fix should be:
- If
_defaultisn't specified, and a given param doesn't match one of the values in the enum set, an exception should be raised. It could be anArgumentError, but since these happen for a variety of reasons, I'm thinking a specialInvalidEnumValueError(which could inheritArgumentError) would be better (allowing someone to rescue that, specifically). - If
_defaultis specified, and a given param doesn't match one of the keys in the enum set (including ifparam = nil), then the_defaultvalue is used as ifModel.create(property: default_value)had been called (i.e.default_valuewill be matched against the enum keys to determine what value should be persisted to the db).
The problem with the above, is if someone wants property to have a default value, but only if Model.create is called with a property key (i.e. they don't want every Model object to be persisted with a property value, but, if it is persisted with a property value, the default value should be x). This scenario could be solved manually by their rescuing InvalidEnumValueError, or we could provide a new option for it (default_if_present). I'm kinda thinking that we should just let people deal with that scenario themselves, as this is really a params issue and folks should validate their params so Model.create is never be called with an invalid option (and if it was, then that indicates something has gone wrong and its appropriate for an error to be raised).
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.
Yeah, fair enough about supporting both. Let's stick with the underscore for now because it's what ActiveRecord does (even though it's really strange...)
Really good catch about it defaulting to 0 in all cases. That's pretty ugly...
One thing that I thought of: if we have a _default we should raise an exception if that isn't in the list of enum options.
I like 1), though regarding 2) I think that if you specify a _default and you give a value that isn't in the list of enums it should raise an exception.
In the end, I think you're absolutely right. If they give something that's not in the params list we should raise an exception. I definitely like the idea of having an InvalidEnumValueError which inherits from ArgumentError so that users can account for this.
Just checking in really quickly (still working through the backlog of issues and PRs, don't worry ;) ). Let me know if I missed anything here
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.
Will do. And no rush! I've thrown a lot at you this week 👍 .
Codecov Report
@@ Coverage Diff @@
## master #1419 +/- ##
==========================================
- Coverage 96.87% 96.81% -0.07%
==========================================
Files 205 205
Lines 12457 12511 +54
==========================================
+ Hits 12068 12112 +44
- Misses 389 399 +10
Continue to review full report at Codecov.
|
|
Is someone able to describe the use case for this change beyond it being "nice to do"? |
|
@brandon-zeeb-nw, in languages with an enum type, convention is for enum variables be given in screaming case (which is case insensitive / only one case). Currently, if you want your neo4jrb enums to accept screaming case params, you need to define the enums in screaming case This change allows you to define For folks who reject this change, a global config is available for them to change the default back to case sensitive. Personally, I would describe the current behavior of allowing case sensitive enums to be a bug. |
|
@brandon-zeeb-nw this being said, do you have a use case for case sensitive enums? |
|
thanks @thefliik, that helps. I fully support the idea of sticking to conventions, but I'm not sure this is the way. This feels more like a mapping problem to me, to map the ruby conventions into a normalized form and back. Case insensitivity is a way around it, Imo. That said I do fully support the spirit of the change. |
|
I thought about simply normalizing the method names and not the enums themselves, but enums should be case insensitive, per convention (well, all caps). This strategy encourages developers to use enums that fall in line with other languages--especially useful if 3rd party clients will be sending the params and you can't guarantee their formatting. |
|
@thefliik I had a somewhat incomplete view of the angle on this change, I retract my objection, I think your change is pretty legit. Thanks for working with me. |
|
@brandon-zeeb-nw for sure! |
cheerfulstoic
left a comment
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 a few comments. Looks great otherwise, thanks!
| end | ||
| Sometimes it is desirable to have a default value for an ``enum`` property. To acheive this, you can simply define a property with the same name which defines a default value: | ||
| Sometimes it is desirable to have a default value for an ``enum`` property. To acheive this, you can simply pass the ``_default`` option when defining the enum: |
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.
Hrmm, interesting. I actually changed this recently because default wasn't working, but it might just be that I didn't know about the _default option. I think we tried default, so at some point in the future it might be good to have the underscored and non-underscored version of all of these options work
| enum type: [:image, :video, :unknown], _default: :video | ||
| end | ||
| By default, enum setters are `case insensitive` (in the example below, ``Media.create(type: 'VIDEO').type == :video``). If you wish to disable this for a specific enum, pass the ``_case_sensitive: true`` option. if you wish to change the global default for ``_case_sensitive`` to ``true``, use Neo4jrb's ``enums_case_sensitive`` config option (detailed in the :ref:`configuration-variables` section). |
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.
Love the global option, thanks!
|
|
||
| **skip_migration_check** | ||
| **Default:** ``false`` | ||
| **association_model_namespace** |
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.
👍
lib/neo4j/shared/enum.rb
Outdated
| def normalize_key_list(enum_keys) | ||
| def normalize_key_list(enum_keys, options) | ||
| case_sensitive = options[:_case_sensitive] | ||
| case_sensitive = Neo4j::Config.enums_case_sensitive if case_sensitive.nil? |
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.
You could do
case_sensitive = options[:_case_sensitive] || Neo4j::Config.enums_case_sensitiveor
options.fetch(:_case_sensitive, Neo4j::Config.enums_case_sensitive)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 difference between the two is that the first would fall back to Neo4j::Config.enums_case_sensitive if you get a nil value from options[:_case_sensitive] regardless of if there was no key or if there was a key but it just happened to be nil. In the second one if options[:_case_sensitive] is set to nil then the result will be nil because fetch only falls back to the default when the key isn't set in the hash.
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'll update it to use fetch, the first one doesn't quite work because if options[:_case_sensitive] = false and Neo4j::Config.enums_case_sensitive = true, then case_sensitive = true, when it should equal false. Because case_sensitive is only used in unless case_sensitive, it's ok for case_sensitive to be nil in the unlikely (incorrect) event that someone sets _case_sensitive: nil locally (though the way it is currently ensures that case_sensitive is never nil).
lib/neo4j/shared/enum.rb
Outdated
| def build_enum_options(enum_keys, options = {}) | ||
| enum_options = build_property_options(enum_keys, options) | ||
| enum_options[:case_sensitive] = options[:_case_sensitive] | ||
| enum_options |
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.
Maybe a bit cleaner:
build_property_options(enum_keys, options).tap do |enum_options|
enum_options[:case_sensitive] = options[:_case_sensitive]
end
spec/e2e/enum_spec.rb
Outdated
| expect(StoredFile.type_formats).to eq(Mpeg: 0, Png: 1) | ||
| expect(UploaderRel.origins).to eq(disk: 0, web: 1) | ||
| end | ||
| end |
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.
There should be a spec somewhere is this file which tests that the ArgumentError gets raised when a non-lowercase option is passed to enum. Probably you can dynamically, in the spec, call StoredFile.enum something: [:value1, :Value1]
|
Oh, also! The build is failing because If you run |
|
Ya, I have |
|
That's certainly odd... |
|
Well I confirmed that It's definitely not a |
| extend ActiveSupport::Concern | ||
|
|
||
| class ConflictingEnumMethodError < Neo4j::Error; end | ||
| class InvalidEnumValueError < Neo4j::InvalidParameterError; end |
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'm inheriting from InvalidParameterError rather than ArgumentError. When I suggested ArgumentError, I didn't know about InvalidParameterError. Seeing as we're talking about passing an invalid enum value through a param, this seemed more appropriate. Let me know if you prefer ArgumentError though.
|
I just resolved a conflict in the I went with the |
|
Let me know if you're done with this PR and I'll take another look! |
Ooopss!! The change from I'm not sure where I made that mistake, but, excluding it, this PR should be finished. Let me know if I need to change those numbers back to |
cheerfulstoic
left a comment
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'm really happy with this, thanks! I have a few changes which I think are pretty minor and then I'd be happy to merge
|
|
||
| return unless @options[:case_sensitive].nil? | ||
|
|
||
| @options[:case_sensitive] = Neo4j::Config.enums_case_sensitive |
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.
Instead of these two lines, what about just:
@options[:case_sensitive] ||= Neo4j::Config.enums_case_sensitiveThere 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 doesn't work because if @options[:case_sensitive] == false and Neo4j::Config.enums_case_sensitive == true, then @options[:case_sensitive] will be set to true when it should remain false (i.e. local settings should always take precedence over global settings). As it stands, global settings only kick in if @options[:case_sensitive].nil?.
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.
Ah, right, good point ;)
| file.save! | ||
| expect(StoredFile.as(:f).pluck('f.type')).to eq([2]) | ||
| expect(file.reload.type).to eq(:video) | ||
| end |
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.
You could test the configuration by doing:
context 'global enums_case_sensitive config is set to true' do
let_config(:enums_case_sensitive) { true }
# spec here testing, for example, that you can't set `type` to `VIdeO`
endWe define the let_config helper here:
https://github.com/neo4jrb/neo4j/blob/master/spec/spec_helper.rb#L135
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.
Oh thats cool
spec/e2e/enum_spec.rb
Outdated
| it 'respects local _case_sensitive option' do | ||
| file = StoredFile.new | ||
| file.type_format = :png | ||
| expect { file.save! }.to raise_error(Neo4j::Shared::Enum::InvalidEnumValueError) |
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.
For a stronger assertion this could be:
expect { file.save! }.to raise_error(Neo4j::Shared::Enum::InvalidEnumValueError, 'Value passed to an enum property must match one of the enum keys')
spec/e2e/enum_spec.rb
Outdated
| it "raises error if value doesn't match an enum key" do | ||
| file = StoredFile.new | ||
| file.type = :audio | ||
| expect { file.save! }.to raise_error Neo4j::Shared::Enum::InvalidEnumValueError |
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.
For a stronger assertion this could be:
expect { file.save! }.to raise_error(Neo4j::Shared::Enum::InvalidEnumValueError, 'Case-insensitive (downcased) value passed to an enum property must match one of the enum keys')|
Ok, I think its done. Let me know if there's anything else though. |
|
Excellent, thanks! |
|
Yes, both of those would be backwards-compatible. I'm not sure if #1428 is backwards compatible or not. Depends on whether the current behavior is considered a bug (though, even if its a bug, its so pervasive fixing it might be considered a breaking change). |
Enums are now by default case insensitive. To make things easy for everyone, I've added a global config option
enums_case_sensitiveif people want to revert to the old standard. This being said, this is still a breaking change to Neo4jrb. I've also updated the docs.Fixes #1418
This pull introduces/changes:
_case_sensitive: truewhen defining an enum.config.neo4j.enums_case_sensitive = truePings:
@cheerfulstoic
@subvertallchris