From 3607df526c1a3877bff9a3532111bf9b4a4fc539 Mon Sep 17 00:00:00 2001 From: Steve Richert Date: Fri, 31 Mar 2017 13:56:27 -0400 Subject: [PATCH] Allow developers to define #call with arguments for convenience 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. --- lib/interactor.rb | 30 +++++- spec/interactor_spec.rb | 210 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 1 deletion(-) diff --git a/lib/interactor.rb b/lib/interactor.rb index 2423630..69edce0 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,32 @@ 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 + positional_arguments, keyword_arguments = [], {} + available_context_keys = context.to_h.keys + + method(:call).parameters.each do |(type, name)| + next unless available_context_keys.include?(name) + + case type + when :req, :opt + positional_arguments << context[name] + when :keyreq, :key + keyword_arguments[name] = context[name] + end + end + + positional_arguments << keyword_arguments if keyword_arguments.any? + positional_arguments + end end diff --git a/spec/interactor_spec.rb b/spec/interactor_spec.rb index 05eefdf..2153bfc 100644 --- a/spec/interactor_spec.rb +++ b/spec/interactor_spec.rb @@ -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