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

Allow developers to define #call with arguments for convenience #2

Open
wants to merge 4 commits into
base: modernize
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ Style/StringLiterals:
EnforcedStyle: double_quotes
Style/SymbolArray:
Enabled: false
Style/WordArray:
Enabled: false
26 changes: 25 additions & 1 deletion lib/interactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -163,4 +163,28 @@ def call
# Returns nothing.
def rollback
end

private

# Internal: Determine what keyword arguments (if any) should be passed to the
# "call" instance method when invoking an Interactor. The "call" instance
# method may accept any number of 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
positional_arguments = []
keyword_arguments = {}

method(:call).parameters.each do |(type, name)|
next unless type == :keyreq || type == :key
next unless context.include?(name)

keyword_arguments[name] = context[name]
end

positional_arguments << keyword_arguments if keyword_arguments.any?
positional_arguments
end
end
11 changes: 10 additions & 1 deletion lib/interactor/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def failure?
#
# Raises Interactor::Failure initialized with the Interactor::Context.
def fail!(context = {})
context.each { |key, value| self[key.to_sym] = value }
context.each { |key, value| self[key] = value }
@failure = true
raise Failure, self
end
Expand Down Expand Up @@ -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.
#
Expand Down
20 changes: 20 additions & 0 deletions spec/interactor/context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,26 @@ module Interactor
end
end

describe "#include?" do
it "returns true if the key is found" do
context = Context.build(foo: "bar")

expect(context.include?(:foo)).to eq(true)
end

it "returns true if the symbolized key is found" do
context = Context.build(foo: "bar")

expect(context.include?("foo")).to eq(true)
end

it "returns false if the key is not found" do
context = Context.build(foo: "bar")

expect(context.include?(:hello)).to eq(false)
end
end

describe "#_called" do
let(:context) { Context.build }

Expand Down
62 changes: 62 additions & 0 deletions spec/interactor_spec.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,65 @@
describe Interactor do
include_examples :lint

describe "#call" do
let(:interactor) { Class.new.send(:include, described_class) }

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: "bar", hello: "world")

expect(result.output).to eq("bar")
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

it "raises an error for call definitions with non-keyword arguments" do
interactor.class_eval do
def call(foo)
context.output = foo
end
end

expect { interactor.call(foo: "bar") }.to raise_error(ArgumentError)
end
end
end
end