diff --git a/.rubocop.yml b/.rubocop.yml index 0f9ec8a..3f87993 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -43,3 +43,5 @@ Style/StringLiterals: EnforcedStyle: double_quotes Style/SymbolArray: Enabled: false +Style/WordArray: + Enabled: false diff --git a/lib/interactor.rb b/lib/interactor.rb index 2423630..0109494 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -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,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 diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index b3e893b..57f0087 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -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 @@ -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. # diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index 94ad4b2..86f1fb1 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -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 } diff --git a/spec/interactor_spec.rb b/spec/interactor_spec.rb index 05eefdf..d85b8db 100644 --- a/spec/interactor_spec.rb +++ b/spec/interactor_spec.rb @@ -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