diff --git a/.rubocop.yml b/.rubocop.yml index 8338bf2..080edff 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,7 @@ # This should always correspond to the required Ruby version specified in the # gemspec. AllCops: - TargetRubyVersion: 2.0 + TargetRubyVersion: 2.1 # TODO: What should we do here? Style/FrozenStringLiteralComment: @@ -43,3 +43,5 @@ Style/StringLiterals: EnforcedStyle: double_quotes Style/SymbolArray: Enabled: false +Style/WordArray: + Enabled: false diff --git a/.travis.yml b/.travis.yml index 46b42c6..423fc3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ cache: bundler language: ruby matrix: allow_failures: - - rvm: "2.0" - rvm: ruby-head notifications: webhooks: @@ -16,7 +15,6 @@ notifications: urls: - http://buildlight.collectiveidea.com/ rvm: - - "2.0" - "2.1" - "2.2" - "2.3.4" diff --git a/interactor.gemspec b/interactor.gemspec index e6844b0..b226a50 100644 --- a/interactor.gemspec +++ b/interactor.gemspec @@ -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" spec.add_development_dependency "bundler", "~> 1.9" spec.add_development_dependency "rake", "~> 10.4" 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 cb269d2..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| modifiable[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 eb8b905..f93fd68 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -164,6 +164,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