draper breaks #== on persisted ActiveRecord models, if you aren't using delegate_all, results in NoMethodError on #id
jrochkind opened this issue · comments
If you aren't using delegate_all
in your decorator, draper breaks calling #== to compare an ActiveRecord model to a Decorator decorating an instance of the same class as the model.
Why does this matter, why would you ever do it? Not sure, but rspec is trying to do it when reporting a spec failure.
Reproduction
# table must exist
class Widget < ApplicationRecord
end
class WidgetDecorator < Draper::Decorator
end
widget = Widget.create!
other_widget = Widget.create!
widget_decorator = WidgetDecorator.new(other_widget)
widget == widget_decorator
Will raise:
*** NoMethodError Exception: undefined method `id' for #<WidgetDecorator:0x00007fbadcf65060>
Why does it happen?
widget#==
is from Draper::Decorator::Equality. The first thing it does is call super
.
The super method is ActiveRecord::Core#==
That method first calls super
(and gets false from BasicObject#==). Then it checks to see if widget_decorator.instance_of?(widget.class)
. Because draper overrides instance_of on decorators, it says that widget_decorator
is an instance of Widget
.
Then ActiveRecord will try to compare widget_decorator.id to widget.id and get the NoMethodException, because WidgetDecorator
does not delegate_all
(or specifically delegate id
), so widget_decorator has no #id
.
Why does it matter, why would you do this?
I'm not totally sure, but in my actual rspec suite, a failing spec triggered something in rspec to try to compare a Widget to a WidgetDecorator, raising and interrupting rspec before it even got to give me details of failing test spec.
I'm not totally sure what's going on, but here's an actual exception backtrace. (This one involves LazyHelpers because that's in my actual app, but LazyHelpers is NOT necessary to reproduce the problem).
stack trace
34: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/exe/rspec:4:in `' 33: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/runner.rb:45:in `invoke' 32: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/runner.rb:71:in `run' 31: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/runner.rb:87:in `run' 30: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/runner.rb:110:in `run_specs' 29: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/reporter.rb:76:in `report' 28: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/reporter.rb:166:in `finish' 27: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/reporter.rb:186:in `close_after' 26: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/reporter.rb:170:in `block in finish' 25: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/reporter.rb:200:in `notify' 24: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/reporter.rb:200:in `each' 23: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/reporter.rb:201:in `block in notify' 22: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/base_text_formatter.rb:32:in `dump_failures' 21: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/notifications.rb:113:in `fully_formatted_failed_examples' 20: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/notifications.rb:113:in `each_with_index' 19: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/notifications.rb:113:in `each' 18: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/notifications.rb:114:in `block in fully_formatted_failed_examples' 17: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/notifications.rb:200:in `fully_formatted' 16: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:78:in `fully_formatted' 15: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:86:in `fully_formatted_lines' 14: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:240:in `formatted_message_and_backtrace' 13: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:72:in `colorized_formatted_backtrace' 12: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:41:in `formatted_backtrace' 11: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:46:in `formatted_cause' 10: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:101:in `final_exception' 9: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:99:in `final_exception' 8: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:99:in `include?' 7: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:99:in `==' 6: from /Users/jrochkind/.gem/ruby/2.6.3/gems/rspec-core-3.8.0/lib/rspec/core/formatters/exception_presenter.rb:99:in `==' 5: from /Users/jrochkind/.gem/ruby/2.6.3/gems/draper-3.1.0/lib/draper/decorator.rb:169:in `==' 4: from /Users/jrochkind/.gem/ruby/2.6.3/gems/draper-3.1.0/lib/draper/decoratable/equality.rb:16:in `test' 3: from /Users/jrochkind/.gem/ruby/2.6.3/gems/draper-3.1.0/lib/draper/decoratable/equality.rb:8:in `==' 2: from /Users/jrochkind/.gem/ruby/2.6.3/gems/activerecord-5.2.3/lib/active_record/core.rb:426:in `==' 1: from /Users/jrochkind/.gem/ruby/2.6.3/gems/draper-3.1.0/lib/draper/lazy_helpers.rb:7:in `method_missing' /Users/jrochkind/.gem/ruby/2.6.3/gems/draper-3.1.0/lib/draper/lazy_helpers.rb:10:in `rescue in method_missing': undefined method `id' for # (NoMethodError)
How to fix it?
I have no idea, it's the result of a strange interaction of several odd meta-programming functions in both Draper and ActiveRecord. Pretty unclear how to cleanly fix it.
In general, overriding instance_of?
to say that the decorator is an ActiveRecord, when if it doesn't use delegate_all
it doesn't really have the API/type of an ActiveRecord... is a very dangerous thing to do.
But it's probably there for a reason? Or perhaps you should only get the instance_of
override if you have delegate_all
-- that would fix this issue, not sure if it would break something else.
Workaround?
If you aren't using delegate_all
, you probably still want to delegate :id
just to avoid messing things up in this case, or any other case that might use ==
where the operands include a Decorator and an ActiveRecord.
That's what I did, tested, it resolves the issue.
The draper README examples of not using delegate_all
and selectively delegating methods do not show delegating :id
-- but you probably always should to avoid this issue.
If there are any currently active Draper maintainers, it would be lovely to get some feedback!
As I think about it, it seems clear the 'right' answer is that Decorators should not over-ride instance_of to claim they are instances of their decorated object's class unless delegate_all
is being used. If you aren't delegating all to the real object, you really don't want to tell everyone you are an instance of that object's class. It could cause all sorts of problems.
I could prepare a PR to do that, but before investing that time it would be awesome to get feedback:
-
Someone who knows more about Draper than me might be able to say that's a non-starter and is likely to break other things (although only for people not using delegate_all, which I'm guessing is maybe an unloved use case that at least in this case is already broken)
-
There is a maintainer to conceivably review/merge my PR, so time invested in creating it is worthwhile.