-
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
Changes from 4 commits
0d6ab9f
822c2a4
cf6b273
a0b9688
27af274
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -140,7 +140,7 @@ def run | |
# Raises Interactor::Failure if the context is failed. | ||
def run! | ||
with_hooks do | ||
call | ||
call(*arguments_for_call) | ||
context.called!(self) | ||
end | ||
rescue | ||
|
@@ -163,4 +163,30 @@ def call | |
# Returns nothing. | ||
def rollback | ||
end | ||
|
||
private | ||
|
||
# Internal: Determine what arguments (if any) should be passed to the "call" | ||
# instance method when invoking an Interactor. The "call" instance method may | ||
# accept any combination of positional and keyword arguments. This method | ||
# will extract values from the context in order to populate those arguments | ||
# 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 commentThe 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 commentThe 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 commentThe 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. 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. 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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, great catch! Maybe when we There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 😃 |
||
positional_arguments = [] | ||
keyword_arguments = {} | ||
|
||
method(:call).parameters.each do |(type, name)| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think should be here: |
||
next unless context.include?(name) | ||
|
||
case type | ||
when :req, :opt then positional_arguments << context[name] | ||
when :keyreq, :key then keyword_arguments[name] = context[name] | ||
end | ||
end | ||
|
||
positional_arguments << keyword_arguments if keyword_arguments.any? | ||
positional_arguments | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
@failure = true | ||
raise Failure, self | ||
end | ||
|
@@ -158,6 +158,15 @@ def rollback! | |
@rolled_back = true | ||
end | ||
|
||
# Public: Check for the presence of a given key in the context. This does | ||
# not check whether the value is truthy, just whether the key is set to any | ||
# value at all. | ||
# | ||
# Returns true if the key is found or false otherwise. | ||
def include?(key) | ||
table.include?(key.to_sym) | ||
end | ||
|
||
# Internal: An Array of successfully called Interactor instances invoked | ||
# against this Interactor::Context instance. | ||
# | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,213 @@ | ||
describe Interactor do | ||
include_examples :lint | ||
|
||
describe "#call" do | ||
let(:interactor) { Class.new.send(:include, described_class) } | ||
|
||
context "positional arguments" do | ||
it "accepts required positional arguments" do | ||
interactor.class_eval do | ||
def call(foo) | ||
context.output = foo | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz", hello: "world") | ||
|
||
expect(result.output).to eq("baz") | ||
end | ||
|
||
it "accepts optional positional arguments" do | ||
interactor.class_eval do | ||
def call(foo = "bar") | ||
context.output = foo | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz", hello: "world") | ||
|
||
expect(result.output).to eq("baz") | ||
end | ||
|
||
it "assigns absent positional arguments" do | ||
interactor.class_eval do | ||
def call(foo = "bar") | ||
context.output = foo | ||
end | ||
end | ||
|
||
result = interactor.call(hello: "world") | ||
|
||
expect(result.output).to eq("bar") | ||
end | ||
|
||
it "raises an error for missing positional arguments" do | ||
interactor.class_eval do | ||
def call(foo) | ||
context.output = foo | ||
end | ||
end | ||
|
||
expect { interactor.call(hello: "world") }.to raise_error(ArgumentError) | ||
end | ||
end | ||
|
||
context "keyword arguments" do | ||
it "accepts required keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo:) | ||
context.output = foo | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz", hello: "world") | ||
|
||
expect(result.output).to eq("baz") | ||
end | ||
|
||
it "accepts optional keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo: "bar") | ||
context.output = foo | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz", hello: "world") | ||
|
||
expect(result.output).to eq("baz") | ||
end | ||
|
||
it "assigns absent keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo: "bar") | ||
context.output = foo | ||
end | ||
end | ||
|
||
result = interactor.call(hello: "world") | ||
|
||
expect(result.output).to eq("bar") | ||
end | ||
|
||
it "raises an error for missing keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo:) | ||
context.output = foo | ||
end | ||
end | ||
|
||
expect { interactor.call(hello: "world") }.to raise_error(ArgumentError) | ||
end | ||
end | ||
|
||
context "combination arguments" do | ||
it "accepts required positional with required keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo, hello:) | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz", hello: "world") | ||
|
||
expect(result.output).to eq(["baz", "world"]) | ||
end | ||
|
||
it "accepts required positional with optional keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo, hello: "there") | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz", hello: "world") | ||
|
||
expect(result.output).to eq(["baz", "world"]) | ||
end | ||
|
||
it "accepts required positional and assigns absent keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo, hello: "there") | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz") | ||
|
||
expect(result.output).to eq(["baz", "there"]) | ||
end | ||
|
||
it "accepts optional positional with required keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo = "bar", hello:) | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz", hello: "world") | ||
|
||
expect(result.output).to eq(["baz", "world"]) | ||
end | ||
|
||
it "accepts optional positional with optional keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo = "bar", hello: "there") | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz", hello: "world") | ||
|
||
expect(result.output).to eq(["baz", "world"]) | ||
end | ||
|
||
it "accepts optional positional and assigns absent keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo = "bar", hello: "there") | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call(foo: "baz") | ||
|
||
expect(result.output).to eq(["baz", "there"]) | ||
end | ||
|
||
it "assigns absent positional and accepts required keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo = "bar", hello:) | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call(hello: "world") | ||
|
||
expect(result.output).to eq(["bar", "world"]) | ||
end | ||
|
||
it "assigns absent positional and accepts optional keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo = "bar", hello: "there") | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call(hello: "world") | ||
|
||
expect(result.output).to eq(["bar", "world"]) | ||
end | ||
|
||
it "assigns absent positional and absent keyword arguments" do | ||
interactor.class_eval do | ||
def call(foo = "bar", hello: "there") | ||
context.output = [foo, hello] | ||
end | ||
end | ||
|
||
result = interactor.call | ||
|
||
expect(result.output).to eq(["bar", "there"]) | ||
end | ||
end | ||
end | ||
end |
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.