-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
enum traits do not use prefix #1452
Comments
Hello, it looks like it is not due to FactoryBot's implementation but ActiveRecord's. ActiveRecord doesn't itself include suffix and prefix in the list of enum's values: class Book < ApplicationRecord
enum language: %i[english japanese], _prefix: :in
end
Book.languages # => {"english"=>0, "japanese"=>1} So a solution would be to change ActiveRecord's enum definition but forcing to use prefix and suffix in the list of enum's values. But I don't think this is the way for this feature: What do you think? |
Hi, I recently ran into this problem and decided to take a look at it. I wonder if a PR with a somewhat dirty solution like i attached here would be accepted? Of course it needs some cleaning, and better error handling, and a couple of specs. In lib/factory_bot/enum.rb we can redefine the def enum_values(klass)
return @values if @values
# Fetching AcitveRecord::Enums
enum = klass.send(@attribute_name.to_s.pluralize)
# it will return here if there is no _prefix or _suffix on the enum.
return enum if klass.respond_to?(enum.keys.first)
enum_key = enum.keys.sample.to_s
all_methods = klass.send(:methods)
all_methods.select!{ |met| # find all the ones with a prefix or a suffix
met.match?(/_#{enum_key}|#{enum_key}_/)
}
all_methods.select! { |met| # all the enum scopes have a matching `not_...`
all_methods.include?("not_#{met}".to_sym)
}
if all_methods.size > 1
# either register/print a warning, or raise "oopsie, I crashed."
return {}
# right now im a bit out of ideas ..., but I can improve this part.
# terrible idea nr 1: compare `klass.method(met).source` with the following
# singleton_class.define_method(name) do |*args|
# scope = all._exec_scope(*args, &body)
# scope = scope.extending(extension) if extension
# scope
# end
end
prefix, suffix = all_methods.first.to_s.split("#{enum_key}", 2).map(&:presence)
enum.transform_keys { |key| "#{prefix}#{key}#{suffix}" }
# maybe we should do some checks here that all the methods we expect from a enum is actually existing? with `klass.new.respons_to?("#{prefix}#{key}#{suffix}!" )`
end |
I love that you are exploring this, but it seems difficult to reliably get the right method like this. For example:
Not if the enum has the There's also a ton of methods on ActiveRecord objects so this wouldn't necessarily be efficient. There's a sort of ugly and probably brittle way to narrow down to the methods we care about: relevant_methods = klass.ancestors.filter_map do |ancestor|
if ancestor.class.name == "ActiveRecord::Enum::EnumMethods"
ancestor.instance_methods(false)
end
end.flatten But I'm not sure we could merge that since it could change in Rails at any time. |
Another idea, but this time through using a monkey_patch, this will at least be more stable. Then we can lookup in In my perspective, it would make sense if rails exposed something like this name in How do you like the monkey_patch? # class Car < ApplicationRecord
# enum _prefix: "123", name: %i[derp_pimper killer_frank], seats: %i[four five], _suffix: "asd"
# end
# class Role < ApplicationRecord
# string_enum :resource, %i[valida unit global], _prefix: "hasdawd", _suffix: "dash"
# string_enum :name, %i[sjappa blings dings], _prefix: "bar"
# end
class ActiveRecordEnumRecorder
class << self
def add(klass, attribute, name)
@enums ||= {}
@enums[klass] ||= {}
@enums[klass][attribute] = name
end
def enums
@enums
end
end
end
module ActiveRecord
module Enum
alias original_enum enum
def enum(*args, **kwargs)
kwargs
.except(:_prefix, :_suffix, :_scopes, :_default)
.each do |attribute_name, values|
ActiveRecordEnumRecorder.add(self.to_s, attribute_name, [kwargs[:_prefix], attribute_name, kwargs[:_suffix]].compact.join("_"))
end
original_enum(*args, **kwargs)
end
end
end
puts Role.last
puts Car.last
pp ActiveRecordEnumRecorder.enums
# => {
# "Role"=>{:resource=>"hasdawd_resource_dash", :name=>"bar_name"},
# "Car"=>{:name=>"123_name_asd", :seats=>"123_seats_asd"}
# }
pp Car.names
# => {"derp_pimper"=>0, "killer_frank"=>1}
pp Role.resources
# => {"valida"=>"valida", "unit"=>"unit", "global"=>"global"}
pp Role.names
# => {"sjappa"=>"sjappa", "blings"=>"blings", "dings"=>"dings"} |
@composerinteralia, WDYT about the monkey patch solution? |
Similar feelings to the other solutions so far. It's clever, but worries me a bit. Patching code in tests only can introduce subtle differences between tests and production. I'm also not sure we can guarantee that patch loads before any models with enums are loaded. I poked around in Rails to see if there's an obvious way to expose the enum method names, but I'm finding it hard to justify the change beyond my wanting it for factory_bot. |
Hi sorry I've dropped out of this for a while, I think my initial thinking on this was to not touch active record (or rely on anything within it) as that just feels like a recipe for things breaking in the future. I think i was imagining (at least for now) a "hook" that could be implemented within the factory to alert it to the prefix or suffix. This of course would end up with having to explicitly define the enum in the factory again (of which the goal was to not have to do) but given that this is a special situation, i don't think a nice factory api that handles it would a be big deal. I was thinking maybe something like:
This would allow |
That approach makes sense to me. I think we could add |
this is a go at adding it to |
Hey, I think the approach in #1513, making users of factorybot define the enum in the factory as well as in the model, is sort of like, we don't want to write dirty code in our project so y'all have to do dirty code in your projects(duplicate your code). #1513 makes perfect sense as an improvement if you are using factory bot without active_record. But I dislike this as a solution for working with active_record :( Question, if someone implements an api to extract _suffix and _prefix for enums in active_record, would you still like to merge #1513? I belive rails-team will be open for having an API exposing this for their enums, I can make a proposal to implement said API and post it on the rails project for the rails-team to look at it around 10th of October. |
I think #1513 is still worthwhile even if we do find a way to make automatic trait generation take the prefix and suffix into consideration. Some folks might have automatic trait generation turned off but still want to add enum traits
It's not really that I don't want dirty code in factory_bot (I'm sure there's plenty of it!), it's that I want to make sure the library is stable. Monkey patching or otherwise using Rails internal APIs means that Rails can change those at any time without warning, and a minor Rails bump could suddenly cause the test suite to fail. That's not a tradeoff I'm willing to make.
The interface in #1513 is related to, but separate from Rails. It doesn't use any additional Rails APIs, so it wouldn't necessarily need to change if Rails changes. If Rails completely changes the way enums work, maybe we'd reconsider our interface, but that seems unlikely and we'd at least get some warning via deprecations and major releases.
That would be great! If Rails exposes the prefix and suffix, we'll use them! I looked into this, but didn't see a way of doing it beyond storing extra information that might only be useful to factory_bot. |
Description
When defining enums with a prefix or suffix the trait that is automatically generated does not take into account the prefix or suffix.
Reproduction Steps
Expected behavior
I would expect the trait to correspond to the predicate or bang methods defined with the enum.
in the above examples an instance of Foo would have methods
bang_baz?
andbang_baz!
.Actual behavior
The traits are defined from only the enum values. So in the above example the traits would be used:
System configuration
factory_bot version: 6.1.0
rails version: 5.2.4.4
ruby version: 2.5.5
The text was updated successfully, but these errors were encountered: