-
Notifications
You must be signed in to change notification settings - Fork 215
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
Allow developers to define #call with arguments for convenience #135
Conversation
If the "call" instance method accepts arguments, those arguments will be automatically assigned from the provided context, matching on name. This works for both positional and keyword arguments. If an argument is specified but no matching value is provided in the context, an ArgumentError is raised.
Required keyword arguments appeared in Ruby 2.1, after the introduction of keyword arguments in Ruby 2.0.
This also improves performance of the Interactor#arguments_for_call method by not duplicating the table of data held internally by the context. As a happy side effect, this adds the Interactor::Context#include? method which may be helpful for developers.
…by not interacting directly with OpenStruct#modifiable.
67824df
to
a0b9688
Compare
@hedgesky Would you mind taking a look at this? Thank you! |
Yeah, I'm watching the repo 😄 |
@@ -121,7 +121,7 @@ def failure? | |||
# | |||
# Raises Interactor::Failure initialized with the Interactor::Context. | |||
def fail!(context = {}) | |||
context.each { |key, value| modifiable[key.to_sym] = value } | |||
context.each { |key, value| self[key] = value } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
lib/interactor.rb
Outdated
# based on their names. | ||
# | ||
# Returns an Array of arguments to be applied as an argument list. | ||
def arguments_for_call # rubocop:disable Metrics/MethodLength |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really cool implementation!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll end up backing out the positional argument support. It feels a little too magical in hindsight. Plus, I think there's a bug that I haven't tested for yet that could misplace positional arguments. The case I'm wondering about is:
class MyInteractor
include Interactor
def call(foo, hello = "world")
context.output = [foo, hello]
end
end
result = MyInteractor.call(hello: "Anton")
result.output # This should raise an ArgumentError, but might be ["Anton", "world"]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only thing that bothers me is that supporting only keywords arguments may be confusing. #call
will not act like a standard Ruby method, but will have its own rules instead. There would be additional lines in Readme, things to keep in memory, things to explain to a newcomer into a project-which-uses-interactors, etc.
I thought about alternative syntax:
class MyInteractor
include Interactor
required_arguments :user, :order
optional_arguments :email, send_notification: true
def call
end
end
On the one hand, keyword arguments do the same and in more expressive way and they are already familiar.
On the other hand, this familiarity may become a bad thing in our case. E.g, if I can write:
def call(hello:)
end
MyInteractor.call(hello: "Steve")
why couldn't I write
def call(name)
end
MyInteractor.call("Steve")
? And here magic falls apart.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed positional argument support. I think it just muddied the waters and could be achieved just as well with keyword arguments.
As for the last two code examples in your comment above, I don't feel like the arguments accepted by the call
class method needs to necessarily agree 100% with the arguments accepted by the call
instance method, but scrapping positional argument support certainly does make it more consistent between the two, which I like. What are your thoughts, @hedgesky?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I'd rather raise an exception for positional arguments. It would help avoiding such situations (just checked this in call-with-keywords
branch):
class A
include Interactor
def call(foo, bar: "")
puts "Foo is #{foo}"
puts "Bar is #{bar}"
end
end
A.call(bar: 1)
# Foo is {:bar=>1}
# Bar is
Also this would draw a clear line between approaches we support and ones we don't.
@laserlemon, I'm open to discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, great catch! Maybe when we include Interactor
, we can check the call
instance method's argument signature to make sure it's either all keywords or nothing at all. That way we can raise an error at load time rather than at run time. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, aborting execution on load would be perfect. But I guess it's possible only if we force users to include Interactor
after defining call
, which is inconvenient:
module M
def self.included(base)
print "From #{base.name}: "
puts base.instance_methods.grep(/call/).inspect
end
end
class A
include M
def call(foo)
end
end
class B
def call(foo)
end
include M
end
# From A: []
# From B: [:call]
However, with runtime approach, method with wrong signature will raise an error at its very first run. I hope even those who don't write automated tests check their code manually at least once before deploying to production 😃
There were edge cases that weren't yet covered by the specs that would fail for combinations of positional and keyword arguments depending on what's available in the context. For example: class MyInteractor include Interactor def call(a, b: "bears") context.output = [a, b] end end MyInteractor.call(b: "beets").output # => [{ b: "beets" }, "bears"] Plus, this simplifies the interface by giving the developer one choice rather than multiple competing choices that achieve the same thing.
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec| | |||
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) | |||
spec.test_files = spec.files.grep(/^spec/) | |||
|
|||
spec.required_ruby_version = ">= 2.0" | |||
spec.required_ruby_version = ">= 2.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A big thank-you for maintaining this value.
positional_arguments = [] | ||
keyword_arguments = {} | ||
|
||
method(:call).parameters.each do |(type, name)| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think should be here: self.class.instance_method(:call)
instead of method(:call)
, because for some reason method(:call) returns the parameters from the class method "call", and not from the instance method "call" . I know this because i actually tried this and the only way i could make it work was to use self.class.instance_method
.
Or maybe i am missing something.
@laserlemon This seems like a nice improvement, but an issue yet to be fixed as per @bogdanRada's comment? |
Any updates on this? |
Pinging it again. Any updates on this? |
Pinging it again, again. :) |
+1 for Custom DSL instead of positional arguments or even keyword-only
#140 for reference. |
If the
call
instance method accepts arguments, those arguments will be automatically assigned from the provided context, matching on name. This works for both positional and keyword arguments. If an argument is specified but no matching value is provided in the context, anArgumentError
is raised.