Skip to content
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

draper breaks #== on persisted ActiveRecord models, if you aren't using delegate_all, results in NoMethodError on #id #859

Closed
jrochkind opened this issue Jul 11, 2019 · 6 comments · Fixed by #933
Assignees

Comments

@jrochkind
Copy link
Contributor

jrochkind commented Jul 11, 2019

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.

@jrochkind
Copy link
Contributor Author

jrochkind commented Jul 11, 2019

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.

@Alexander-Senko
Copy link
Collaborator

Decorators should not over-ride instance_of? to claim they are instances of their decorated object's class unless delegate_all is being used.

I agree that instance_of? may not be overridden — regardless of whether delegate_all is used or not. Draper decorators don't pretend to be invisible proxies to act like that.

  • There is a maintainer to conceivably review/merge my PR, so time invested in creating it is worthwhile.

Now, there is.

  • 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…

Though I'm quite new here, I can dig through it easily. Unfortunately, core authors seem to be accessible no more.

@Alexander-Senko
Copy link
Collaborator

Starting from 30d209f, I found that kind_of? is overridden as well for some reason 🤔

I'll try to remove both and look for what to be broken.

@jrochkind
Copy link
Contributor Author

That's exciting there is some maintenance energy on this again! Thank you for your work!

I have stopped using Draper in the ensueing 5 years, so no longer have a stake in this.

@Alexander-Senko
Copy link
Collaborator

Alexander-Senko commented Sep 12, 2024

I have stopped using Draper in the ensueing 5 years…

@jrochkind, I'm just wondering to consider what to do with Draper in a perspective: Do you use any alternatives now and, if you do, what advantages do they have over Draper?

@jrochkind
Copy link
Contributor Author

jrochkind commented Sep 12, 2024

I found that embracing ViewComponent has largely eliminated my desire for Draper. But I think different people use Draper in somewhat different ways.

At the time I stopped using it, the advantage of antyhing else was simply that Draper was unmaintained, and that I kept running into weird edge cases causing me problems, like this. I think Draper perhaps required over-complicated implementation to work with Rails how it wanted, making it somewhat fragile.

Alexander-Senko added a commit to Alexander-Senko/draper that referenced this issue Sep 12, 2024
Decorators shouldn't pretend to be instances of the underlying model 
classes for the following reasons:

1. It's terribly wrong to pretend being a class without implementing 
it's interface. It can break third party code.
2. Hacking Ruby core methods can lead to unexpected and hard to 
understand behavior. It should be avoided without a strong reason 
behind.
3. I'd prefer compatibility patches to be narrow-scoped and live in 
there own modules.

Resolves drapergem#859.
Reverts  drapergem#72,
         drapergem#110,
         drapergem#417,
         drapergem#497.
@Alexander-Senko Alexander-Senko linked a pull request Sep 12, 2024 that will close this issue
@Alexander-Senko Alexander-Senko self-assigned this Sep 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants