From d769f4de322ce0b40bf04681c826ea7d73681325 Mon Sep 17 00:00:00 2001 From: ouchinao Date: Wed, 8 Jul 2026 02:10:20 +0900 Subject: [PATCH] Raise a helpful ArgumentError for non-hash context Passing a non-hash argument to `.call` previously failed with a cryptic "undefined method `each_pair'" error. Context.build now raises an ArgumentError explaining that keyword arguments were likely intended. nil and any object responding to #each_pair are still accepted. Closes #178 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/interactor/context.rb | 9 ++++++++- spec/interactor/context_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index 1402e60..742c6c5 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -52,11 +52,18 @@ class Context < OpenStruct # # => 2170969340 # # Returns the Interactor::Context. + # Raises ArgumentError if the argument is not nil, an Interactor::Context + # or an object responding to each_pair (like a Hash). def self.build(context = {}) if self === context context - else + elsif context.nil? || context.respond_to?(:each_pair) new(context) + else + raise ArgumentError, + "Expected a Hash or an object responding to #each_pair, " \ + "got #{context.class}. Did you mean to pass keyword arguments? " \ + "(e.g. MyInteractor.call(foo: \"bar\"))" end end diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index d990479..c3fd708 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -27,6 +27,31 @@ module Interactor } end + it "builds an empty context if nil is given" do + context = Context.build(nil) + + expect(context).to be_a(Context) + expect(context.send(:table)).to eq({}) + end + + it "accepts any object responding to #each_pair" do + hash_like = double(:hash_like) + allow(hash_like).to receive(:each_pair) do |&block| + {foo: "bar"}.each_pair(&block) + end + + context = Context.build(hash_like) + + expect(context).to be_a(Context) + expect(context.foo).to eq("bar") + end + + it "raises a helpful error when given a non-hash argument" do + expect { + Context.build("baz") + }.to raise_error(ArgumentError, /String/) + end + it "preserves an already built context" do context1 = Context.build(foo: "bar") context2 = Context.build(context1)