From 3e0cdd7806d184b37c2d80bed775ff648e71fc76 Mon Sep 17 00:00:00 2001 From: Tom Stuart Date: Sat, 20 Feb 2016 21:02:45 +0000 Subject: [PATCH] Add #respond_to? tests and fix behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation was completely wrong, which I didn’t realise until I tried to add tests around it. This commit adds the tests and simultaneously fixes the code to actually work. A generic implementation of #respond_to_missing? can’t work[1] because answering the question requires knowledge of #and_then’s control flow; for example, Optional only sends the message to the underlying value if it’s not nil, so #respond_to_missing? should return true if the value is nil and ask if it responds to the message otherwise. We have no way of knowing what #and_then’s control flow will look like in general, so we just have to lovingly recreate it in #respond_to_missing? for each monad, which is annoying but necessary. [1] In fact, it *could* work by simply sending the message and reporting whether a NameError is raised, but that would defeat the entire purpose of #respond_to?, namely to tell you whether a message can be sent without actually sending it. --- lib/monads/eventually.rb | 4 ++++ lib/monads/many.rb | 4 ++++ lib/monads/monad.rb | 6 ------ lib/monads/optional.rb | 4 ++++ spec/monads/eventually_spec.rb | 22 ++++++++++++++++++++-- spec/monads/many_spec.rb | 24 ++++++++++++++++++++++-- spec/monads/optional_spec.rb | 24 ++++++++++++++++++++++-- 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/lib/monads/eventually.rb b/lib/monads/eventually.rb index 3db734b..7f328ff 100644 --- a/lib/monads/eventually.rb +++ b/lib/monads/eventually.rb @@ -24,6 +24,10 @@ def and_then(&block) end end + def respond_to_missing?(method_name, include_private = false) + super || run { |value| value.respond_to?(method_name, include_private) } + end + def self.from_value(value) Eventually.new do |success| success.call(value) diff --git a/lib/monads/many.rb b/lib/monads/many.rb index ababb5a..f49a5b5 100644 --- a/lib/monads/many.rb +++ b/lib/monads/many.rb @@ -16,6 +16,10 @@ def and_then(&block) Many.new(values.map(&block).flat_map(&:values)) end + def respond_to_missing?(method_name, include_private = false) + super || values.all? { |value| value.respond_to?(method_name, include_private) } + end + def self.from_value(value) Many.new([value]) end diff --git a/lib/monads/monad.rb b/lib/monads/monad.rb index dc3ed1b..26e0bbd 100644 --- a/lib/monads/monad.rb +++ b/lib/monads/monad.rb @@ -12,12 +12,6 @@ def method_missing(*args, &block) end end - def respond_to_missing?(method_name, include_private = false) - within do |value| - value.respond_to?(method_name, include_private) - end || super - end - private def ensure_monadic_result(&block) diff --git a/lib/monads/optional.rb b/lib/monads/optional.rb index c122d44..f54fec8 100644 --- a/lib/monads/optional.rb +++ b/lib/monads/optional.rb @@ -20,6 +20,10 @@ def and_then(&block) end end + def respond_to_missing?(method_name, include_private = false) + super || value.nil? || value.respond_to?(method_name, include_private) + end + def self.from_value(value) Optional.new(value) end diff --git a/spec/monads/eventually_spec.rb b/spec/monads/eventually_spec.rb index 7353aee..174fd8d 100644 --- a/spec/monads/eventually_spec.rb +++ b/spec/monads/eventually_spec.rb @@ -102,8 +102,6 @@ module Monads it 'forwards any unrecognised message to the block’s value' do expect(value).to receive(:challenge) Eventually.new { |success| success.call(value) }.challenge.run {} - expect { Eventually.new { |success| success.call(value) }.method(:challenge) }.not_to raise_error - expect(Eventually.new { |success| success.call(value) }).to respond_to(:challenge) end it 'returns the message’s result wrapped in an Eventually' do @@ -112,6 +110,26 @@ module Monads expect(@result).to eq response end + context 'when the value responds to the message' do + it 'reports that the Eventually responds to the message' do + expect(Eventually.new { |success| success.call(value) }).to respond_to(:challenge) + end + + it 'allows a Method object to be retrieved' do + expect(Eventually.new { |success| success.call(value) }.method(:challenge)).to be_a(Method) + end + end + + context 'when the value doesn’t respond to the message' do + it 'reports that the Eventually doesn’t respond to the message' do + expect(Eventually.new { |success| success.call(double) }).not_to respond_to(:challenge) + end + + it 'doesn’t allow a Method object to be retrieved' do + expect { Eventually.new { |success| success.call(double) }.method(:challenge) }.to raise_error(NameError) + end + end + context 'when value is Enumerable' do let(:value) { [1, 2, 3] } diff --git a/spec/monads/many_spec.rb b/spec/monads/many_spec.rb index 58e3489..bc14795 100644 --- a/spec/monads/many_spec.rb +++ b/spec/monads/many_spec.rb @@ -87,14 +87,34 @@ module Monads expect(value).to receive(:challenge) end many.challenge - expect { many.method(:challenge) }.not_to raise_error - expect(many).to respond_to(:challenge) end it 'returns the messages’ results wrapped in a Many' do expect(many.challenge.values).to eq responses end + context 'when all of the values respond to the message' do + it 'reports that the Many responds to the message' do + expect(many).to respond_to(:challenge) + end + + it 'allows a Method object to be retrieved' do + expect(many.method(:challenge)).to be_a(Method) + end + end + + context 'when any of the values don’t respond to the message' do + let(:many) { Many.new(values + [double]) } + + it 'reports that the Many doesn’t respond to the message' do + expect(many).not_to respond_to(:challenge) + end + + it 'doesn’t allow a Method object to be retrieved' do + expect { many.method(:challenge) }.to raise_error(NameError) + end + end + context 'when values are Enumerable' do let(:values) { [[1, 2], [3, 5]] } diff --git a/spec/monads/optional_spec.rb b/spec/monads/optional_spec.rb index 120483f..625614d 100644 --- a/spec/monads/optional_spec.rb +++ b/spec/monads/optional_spec.rb @@ -89,14 +89,34 @@ module Monads it 'forwards any unrecognised message to the value' do expect(value).to receive(:challenge) optional.challenge - expect { optional.method(:challenge) }.not_to raise_error - expect(optional).to respond_to(:challenge) end it 'returns the message’s result wrapped in an Optional' do expect(optional.challenge.value).to eq response end + context 'when the value responds to the message' do + it 'reports that the Optional responds to the message' do + expect(optional).to respond_to(:challenge) + end + + it 'allows a Method object to be retrieved' do + expect(optional.method(:challenge)).to be_a(Method) + end + end + + context 'when the value doesn’t respond to the message' do + let(:optional) { Optional.new(double) } + + it 'reports that the Optional doesn’t respond to the message' do + expect(optional).not_to respond_to(:challenge) + end + + it 'doesn’t allow a Method object to be retrieved' do + expect { optional.method(:challenge) }.to raise_error(NameError) + end + end + context 'when value is Enumerable' do let(:value) { [1, 2, 3] }