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