Skip to content

Commit

Permalink
Implement delegate_method supporting with_private
Browse files Browse the repository at this point in the history
  • Loading branch information
pr0d1r2 committed Oct 3, 2024
1 parent 43d14fb commit 9c62dbb
Showing 1 changed file with 86 additions and 5 deletions.
91 changes: 86 additions & 5 deletions lib/shoulda/matchers/independent/delegate_method_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,27 @@ module Independent
# should delegate_method(:plan).to(:subscription).allow_nil
# end
#
# @return [DelegateMethodMatcher]
# ##### with_private
#
# Use `with_private` if the delegation accounts for the fact that your
# delegation is private. (This is mostly intended as an analogue to
# the `private` option that Rails' `delegate` helper takes.)
#
# class Account
# delegate :plan, to: :subscription, private: true
# end
#
# # RSpec
# describe Account do
# it { should delegate_method(:plan).to(:subscription).with_private }
# end
#
# # Minitest
# class PageTest < Minitest::Test
# should delegate_method(:plan).to(:subscription).with_private
# end
#
# @return [DelegateMethodMatcher]
def delegate_method(delegating_method)
DelegateMethodMatcher.new(delegating_method).in_context(self)
end
Expand All @@ -187,6 +206,7 @@ def initialize(delegating_method)
@delegate_object_reader_method = nil
@delegated_arguments = []
@expects_to_allow_nil_delegate_object = false
@expects_private_delegation = false
end

def in_context(context)
Expand All @@ -202,14 +222,19 @@ def matches?(subject)
subject_has_delegating_method? &&
subject_has_delegate_object_reader_method? &&
subject_delegates_to_delegate_object_correctly? &&
subject_handles_nil_delegate_object?
subject_handles_nil_delegate_object? &&
subject_handles_private_delegation?
end

def description
string =
"delegate #{formatted_delegating_method_name} to the " +
"#{formatted_delegate_object_reader_method_name} object"

if expects_private_delegation?
string << ' privately'
end

if delegated_arguments.any?
string << " passing arguments #{delegated_arguments.inspect}"
end
Expand Down Expand Up @@ -254,6 +279,11 @@ def allow_nil
self
end

def with_private
@expects_private_delegation = true
self
end

def build_delegating_method_prefix(prefix)
case prefix
when true, nil then delegate_object_reader_method
Expand All @@ -264,14 +294,19 @@ def build_delegating_method_prefix(prefix)
def failure_message
message = "Expected #{class_under_test} to #{description}.\n\n"

if failed_to_allow_nil_delegate_object?
if failed_to_allow_nil_delegate_object? || failed_to_handle_private_delegation?
message << formatted_delegating_method_name(include_module: true)
message << ' did delegate to '
message << formatted_delegate_object_reader_method_name
end

if failed_to_allow_nil_delegate_object?
message << ' when it was non-nil, but it failed to account '
message << 'for when '
message << formatted_delegate_object_reader_method_name
message << ' *was* nil.'
elsif failed_to_handle_private_delegation?
message << ", but 'private: true' is missing."
else
message << 'Method calls sent to '
message << formatted_delegate_object_reader_method_name(
Expand Down Expand Up @@ -322,6 +357,10 @@ def expects_to_allow_nil_delegate_object?
@expects_to_allow_nil_delegate_object
end

def expects_private_delegation?
@expects_private_delegation
end

def formatted_delegate_method(options = {})
formatted_method_name_for(delegate_method, options)
end
Expand Down Expand Up @@ -367,7 +406,11 @@ def delegate_object_received_call_with_delegated_arguments?
end

def subject_has_delegating_method?
subject.respond_to?(delegating_method)
if expects_private_delegation?
!subject.respond_to?(delegating_method) && subject.respond_to?(delegating_method, true)
else
subject.respond_to?(delegating_method)
end
end

def subject_has_delegate_object_reader_method?
Expand All @@ -381,7 +424,11 @@ def ensure_delegate_object_has_been_specified!
end

def subject_delegates_to_delegate_object_correctly?
call_delegating_method_with_delegate_method_returning(delegate_object)
if expects_private_delegation?
privately_call_delegating_method_with_delegate_method_returning(delegate_object)
else
call_delegating_method_with_delegate_method_returning(delegate_object)
end

if delegated_arguments.any?
delegate_object_received_call_with_delegated_arguments?
Expand Down Expand Up @@ -411,11 +458,37 @@ def subject_handles_nil_delegate_object?
end
end

def subject_handles_private_delegation?
@subject_handled_private_delegation =
if expects_private_delegation?
begin
call_delegating_method_with_delegate_method_returning(delegate_object)
true
rescue Module::DelegationError
false
rescue NoMethodError => e
if e.message =~
/private method `#{delegating_method}' called for/
true
else
raise e
end
end
else
true
end
end

def failed_to_allow_nil_delegate_object?
expects_to_allow_nil_delegate_object? &&
!@subject_handled_nil_delegate_object
end

def failed_to_handle_private_delegation?
expects_private_delegation? &&
!@subject_handled_private_delegation
end

def call_delegating_method_with_delegate_method_returning(value)
register_subject_double_collection_to(value)

Expand All @@ -424,6 +497,14 @@ def call_delegating_method_with_delegate_method_returning(value)
end
end

def privately_call_delegating_method_with_delegate_method_returning(value)
register_subject_double_collection_to(value)

Doublespeak.with_doubles_activated do
subject.__send__(delegating_method, *delegated_arguments)
end
end

def register_subject_double_collection_to(returned_value)
double_collection =
Doublespeak.double_collection_for(subject.singleton_class)
Expand Down

0 comments on commit 9c62dbb

Please sign in to comment.