drapergem / draper

Decorators/View-Models for Rails Applications

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.