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

Arbitrary object interfaces (duck types) as parameters? #178

Closed
jdickey opened this issue May 2, 2014 · 10 comments
Closed

Arbitrary object interfaces (duck types) as parameters? #178

jdickey opened this issue May 2, 2014 · 10 comments
Assignees
Milestone

Comments

@jdickey
Copy link

jdickey commented May 2, 2014

Issue #169 touched on this; one very important distinction between ActiveInteraction and its credited inspiration in Mutations is support for duck typing. In Mutations, I can have an attribute whitelist declaration such as

  duck :blog, methods: [:title, :subtitle, :entries] 

and then pass in a bespoke PORO implementing these methods during unit testing, or an ActiveRecord::Base subclass instance in integration test or production. With ActiveInteraction, I have to resort to code in my specs such as

  before :each do
    # @blog = BlogTestDouble.new
    @blog = Blog::Blog.new
    allow(@blog).to receive(:title).and_return 'The Title'
    allow(@blog).to receive(:subtitle).and_return 'Sub Title, Super Entries'
    allow(@blog).to receive(:entries).and_return []
  end

This feels like it makes my tests more brittle because it introduces a dependency on a real live model instance (even with all the stubbing).

@tfausak
Copy link
Collaborator

tfausak commented May 2, 2014

Duck typing is something we've considered. We discussed it in the initial implementation and decided to wait until there was a real need for it. And here it is!

Looking at how Mutations implements it, you can do this now with custom validators. This is obviously not ideal, but it's a workaround.

class DuckInteraction < ActiveInteraction::Base
  model :duck, class: Object, methods: %i[foo bar]
  validate do
    attribute = :duck
    input = inputs[attribute]
    self.class.filters.fetch(attribute).options.fetch(:methods).each do |method|
      unless input.respond_to?(method)
        errors.add_sym(attribute, :invalid_duck, "does not respond to #{method}")
      end
    end
  end
end

I think we could implement a duck filter without too much trouble. I have a couple of reservations, though.

  • Duck typing doesn't tell you anything about the result of the method call. All it says is that the object (probably) won't throw a NoMethodError.
  • Duck typing can't easily be nested. For example, what if I want an object that responds to "foo", which returns an object that responds to "bar"? (Think inputs[:x].foo.bar.)

@AaronLasseigne
Copy link
Owner

Can you provide a bit more information about what you're trying to fix? You mention introducing a dependency on a "real live model". Are you trying to avoid database calls? Have you considered using mock_model?

@jdickey
Copy link
Author

jdickey commented May 2, 2014

@AaronLasseigne No, I didn't know about mock_model; that looks to be Very Useful Indeed.

This came up as I was adapting a class originally written against Mutations to ActiveInteraction. I'm publishing my little interactor-exploration project; you can find the original Mutation-based spec here and class here.

@AaronLasseigne
Copy link
Owner

@tfausak and I were just trying out mock_model and found that it doesn't work because of the type of class check we're doing (see #179).

@jdickey
Copy link
Author

jdickey commented May 2, 2014

Yeah, after a quick read of the Filter source, 179 looks like it could be a real can of worms. (Do you really want to depend on somebody else's code lying to itself and everybody else about what it is?) Duck-type filters would sidestep that whole problem entirely.

Imagine that I use @tfausak's .=== hack on my model. Imagine further that I eventually vendor everything (as I intend to in our real app) for reuse across our applications, and potentially publish the Gem. Imagine the revolver I'd be handing J Random Developer who picks up my model and naively uses it, thereby engaging in a game of Russian roulette where I, and especially he, have no idea how many bullets are in the gun. Hilarity ensues.

@tfausak
Copy link
Collaborator

tfausak commented May 2, 2014

Do you really want to depend on somebody else's code lying to itself and everybody else about what it is?

How is that any worse than relying on duck typing? Somebody else's code could be lying to itself and everybody else about what methods it implements.

class Liar
  def respond_to?(*)
    true
  end
end

Looking at your DummyBlog class, I would add this method to it:

class DummyBlog
  def is_a?(klass)
    klass == Blog || super
  end
end

Then, pending the fix to #179, everything would work fine.

@jdickey
Copy link
Author

jdickey commented May 2, 2014

It's "worse than relying on duck typing" because, with duck typing, you can still write a simple PORO that plays by the rules (is_a? and self.=== included) and lets you specify everything you need to know about the object. With the absolute requirement for an ActiveRecord::Base subclass and nothing else, you're making both your users' and your own lives (as evidenced by Issue 179) harder than they need to be. The problem isn't "dependence on Rails". The problem is "locking out any object that isn't explicitly a Rails (ordinarily database-backed) model instance". Postel's Law has been generally seen as a Good Thing for the last 25 years with good reason.

@AaronLasseigne
Copy link
Owner

With the absolute requirement for an ActiveRecord::Base subclass and nothing else
...
The problem is "locking out any object that isn't explicitly a Rails (ordinarily database-backed) model instance".

I want to clarify a possible point of confusion. The model filter in no way requires the model to be an ActiveRecord::Base. You can absolutely use a PORO.

The model filter currently uses === for it's class equality check. That will respond true to anything that matches that class, is a subclass, or contains an included module.

@jdickey
Copy link
Author

jdickey commented May 2, 2014

Ah. Light dawns, at 4 AM. (thud)

That was not clear from reading the doc and specs; it most certainly should (in conjunction with the fix for 179) address my use case. After turning "should" into "does", I'd like to submit a pull request adding some verbiage to the README (under "How do I define an interaction?", most likely) so that the next guy making this transition doesn't step on the same banana peels I did.

Thanks very much!

@tfausak tfausak added this to the v1.3.0 milestone May 2, 2014
@tfausak tfausak self-assigned this May 2, 2014
This was referenced May 2, 2014
@tfausak
Copy link
Collaborator

tfausak commented May 3, 2014

Thanks for working with us to get to the bottom of this! Version 1.2.1 has been released with a fix for #179. In addition, we've decided to add duck typing to version 1.3.0 via an interface filter. See #182 for an explanation.

You've also encouraged us to make our documentation better. If you want to lend a hand with that, check out #183.

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.

3 participants