-
Notifications
You must be signed in to change notification settings - Fork 180
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
Implement automatic predicate method generation for ActiveHash::Enum #321
Conversation
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 think we need to make this opt-in behavior (and not default behavior) because people may have already implemented their own methods that, depending on when the Enum module is included, might be clobbered during an upgrade.
I'd love to hear your thoughts on how to handle that case.
lib/enum/enum.rb
Outdated
return if @enum_accessors.blank? | ||
|
||
@records&.each do |record| | ||
enum_vallue = @enum_accessors.map { |name| record.attributes[name] }.join("_").downcase.gsub(/\W+/, '_').gsub(/^_|_$/, '') |
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.
Did this subexpression come from somewhere else? Why not use something like Active Support's #camelize
instead of this?
Also, how is this implementation going to handle collisions (e.g., two values like "FooBar" and "foo_bar" that would create the same method name)? How does rails handle collisions? I would like to see some test coverage for a collision case as well
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 enum_value was derived from this section.
Lines 48 to 49 in a192385
constant.gsub!(/\W+/, "_") | |
constant.gsub!(/^_|_$/, '') |
Also, how is this implementation going to handle collisions (e.g., two values like "FooBar" and "foo_bar" that would create the same method name)?
I try it.
A DuplicateEnumAccessor error occurs when the set_constant method is executed.
Line 39 in a192385
raise DuplicateEnumAccessor, "#{constant} already defined for #{self.class}" unless const_get(constant, false) == record |
And We have also added a test for cases where the method is already defined by itself.
00c1497
@kbrock wdyt of this feature |
@flavorjones I had started to throw some code together to see how I would implement it. I'm glad this PR came through before I got too far. Not really a fan of this, but I appreciate that we all have our own preferred styles. https://api.rubyonrails.org/v5.1/classes/ActiveRecord/Enum.html Docs have: class Conversation < ActiveRecord::Base
enum status: [ :active, :archived ]
end
conversation.active? # => true
conversation.status # => "active" A quick implementation would probably be like: def enum columns
methods = []
columns.each do |column, values|
values.each do |value|
methods << "def #{value}? ; #{column} == #{value.inspect} ; end"
end
end
instance_eval { methods.uniq.join(";") }
end So then that begs to ask, why do we define a constant for each class in enum? This feels very different from active record. I want to accept this, but feel my negativity coming through. I do not understand why I'm negative though. It seems relatively sound. Tangent: the while undefining a constant when the field is removed distracts me. Don't think we should be supporting delete/insert/update type behavior. again, not related though |
Thank you for the insightful comment!
I agree with this. I personally prefer the following style, where when calling class PublicStatus < ActiveHash::Base
include ActiveHash::Enum
self.data = [
{ id: 1, name: "Publish", type: "published" },
{ id: 2, name: "Draft", type: "drafted" },
{ id: 3, name: "Archive", type: "archived" }
]
enum_accessor :type
# No need to manually define published?, drafted?, archived? methods
end Do you prefer the approach of not using class PublicStatus < ActiveHash::Base
include ActiveHash::Enum
self.data = [
{ id: 1, name: "Publish", type: "published" },
{ id: 2, name: "Draft", type: "drafted" },
{ id: 3, name: "Archive", type: "archived" }
]
enum type: [:published, :drafted, :archived]
end I'm currently debating the implementation approach. |
@hatsu38 Thank you for your logical thoughts. - enum_accessor :type
+ enum type: [:published, :drafted, :archived]
The current implementation to add constants is a little troubling. I don't like that it keeps adding constants to the class for every record added. I would like to avoid adding different methods to the class for every record added. I'd prefer to make as many changes all at once. If we agree that |
@kbrock Thanks you! The implementation with enum type: [:published, :drafted, :archived] is closer to Rails, making it easier to understand for those who are new to ActiveHash. After actually trying it out, it feels pretty good to write! I think I'll proceed with this approach 🙆 |
Also, though my opinion has changed over the years, I think I'm in camp |
spec/enum/enum_spec.rb
Outdated
|
||
enum_accessor :name | ||
|
||
enum action: { like: 'LIKE', comment: 'COMMENT', follow: 'FOLLOW', mention: 'MENTION' } |
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 are cases where I want to use uppercase letters in the enum values.
To support this, I'd like to implement functionality similar to Rails' enum, which allows for both simple array definitions and more complex hash-based definitions.
For example, I want to support both of these use cases:
enum status: [:draft, :published, :archived]
enum action: { like: 'LIKE', comment: 'COMMENT', follow: 'FOLLOW', mention: 'MENTION' }
This approach would provide greater flexibility in defining enums, similar to how it's done in Active Record (as documented here: https://api.rubyonrails.org/v7.1/classes/ActiveRecord/Enum.html).
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.
good find. this looks great
I wasn't sure how to use instance_eval, so I tried using class_eval instead. |
lib/enum/enum.rb
Outdated
columns.each do |column, values| | ||
values = values.zip(values.map(&:to_s)).to_h if values.is_a?(Array) | ||
values.each do |method, value| | ||
method_definitions << "def #{method}?; #{column} == '#{value}'; 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.
method_definitions << "def #{method}?; #{column} == '#{value}'; end" | |
method_definitions << "def #{method}?; #{column} == #{value.inspect}; end" |
Will value.inspect
work?
I'd prefer to use that if it buys us enumeration with columns that contain integers.
Strings are most important, integers are a nice to have, and symbols are a very distant nice to have. I think we get all 3 with inspect
UPDATE:
Looking at your referenced link, I see that they tried to cover numbers and symbols as well.
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 have decided to go with your suggestion!
5cab8ac
bundle exec rspec spec/enum/enum_spec.rb
...........
Finished in 0.01806 seconds (files took 0.49792 seconds to load)
11 examples, 0 failures
This looks great. I tried do an injection attack here and was not able to get anything through. The closes I can come is: # this returns true for all values:
x="a'||true||'"
instance_eval("def a; 'z' == '#{x}' ; end") ; a
# => true
# this creates a file named yy
"x' ; `touch yy` ; '"
instance_eval("def a; 'z' == '#{x}' ; end") ; a
# => true
# not able to exploit `inspect` but still looking a way to inject something.
x=:"a||true"
instance_eval("def a; 'z' == #{x.inspect} ; end") ; a
# => false @flavorjones you good to go on this one? |
- Implement `enum` method to support both array and hash-style definitions - Allow predicate methods generation for enum values - Support Rails-like enum syntax: `enum status: [:draft, :published, :archived]` - Support custom value mapping: `enum action: { like: 'LIKE', comment: 'COMMENT', ... }` - Add comprehensive tests for both enum definition styles This enhancement allows ActiveHash::Enum to handle enum definitions similar to Rails, improving compatibility and ease of use. It provides a more flexible way to define and use enums in ActiveHash models.
5cab8ac
to
01344c6
Compare
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've pushed a commit that sets the line number for eval
and uses frozen strings -- these are Rails project conventions that I think are good.
I've also rebased onto master.
01344c6
to
59e79da
Compare
Whoops, force-pushed a fix to the frozen strings comment. |
Setting the line number properly helps with stack walkbacks and debugging.
59e79da
to
2173c30
Compare
class_eval <<~METHOD, __FILE__, __LINE__ + 1 | ||
# frozen_string_literal: true | ||
def #{method}? | ||
#{column} == #{value.inspect} | ||
end | ||
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.
Great call on fronzen_string_literal
and the line number.
I had taken my inspiration from https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/class/attribute.rb#L119-L120
They only call class_eval
once, hence the method_definitions
, and they use the caller location. But this should work fine.
Interestingly, enum uses def_method
.
^ more of a note to future self.
close #319
Description
This PR addresses the issue of repetitive predicate method definitions in ActiveHash when using enum types. It implements an automatic generation of status check methods based on the defined enum values, simplifying the usage of ActiveHash::Enum.
Changes
define_enum_methods
toActiveHash::Enum::Methods
moduleenum_accessor
is calledExample
Before:
After:
Testing
Added unit tests to verify the automatic generation of predicate methods
Tested with various enum value names to ensure proper sanitization