Skip to content

Commit

Permalink
feature(has_instance_proxy): delegate to target missing proxy methods
Browse files Browse the repository at this point in the history
  • Loading branch information
marian13 committed Jan 12, 2024
1 parent e40b9b8 commit 6be8d3b
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ module HasInstanceProxy
module Entities
class InstanceProxy
##
# @api private
#
# @param target [Object] Can be any type.
# @return [void]
#
Expand All @@ -15,13 +17,17 @@ def initialize(target:)
end

##
# @api public
#
# @return [Object] Can be any type.
#
def instance_proxy_target
@__convenient_service_instance_proxy_target__
end

##
# @api public
#
# @param other [Object] Can be any type.
# @return [Boolean, nil]
#
Expand All @@ -35,6 +41,33 @@ def ==(other)

true
end

private

##
# @see https://thoughtbot.com/blog/always-define-respond-to-missing-when-overriding
# @see https://blog.marc-andre.ca/2010/11/15/methodmissing-politely
# @see https://stackoverflow.com/a/3304683/12201472
#
# @param method_name [Symbol, String]
# @param include_private [Boolean]
# @return [Boolean]
#
# @internal
# IMPORTANT: `respond_to_missing?` is like `initialize`. It is always `private`.
# - https://ruby-doc.org/core-2.7.0/Object.html#method-i-respond_to_missing-3F
# - https://github.com/ruby/spec/blob/master/language/def_spec.rb#L65
#
def respond_to_missing?(method_name, include_private = false)
instance_proxy_target.respond_to?(method_name, include_private)
end

##
# @return [Object] Can be any type.
#
def method_missing(...)
instance_proxy_target.public_send(...)
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@

# rubocop:disable RSpec/NestedGroups, RSpec/MultipleMemoizedHelpers
RSpec.describe ConvenientService::Common::Plugins::HasInstanceProxy::Entities::InstanceProxy do
include ConvenientService::RSpec::Matchers::DelegateTo

let(:instance_proxy) { described_class.new(target: target) }
let(:target) { "foo" }
let(:target) { target_klass.new }

let(:target_klass) do
Class.new do
def foo(*args, **kwargs, &block)
[__method__, args, kwargs, block]
end
end
end

example_group "instance methods" do
describe "#instance_proxy_target" do
Expand All @@ -19,6 +29,86 @@
expect(instance_proxy.instance_proxy_target).to eq(instance_proxy.instance_variable_get(:@__convenient_service_instance_proxy_target__))
end
end

example_group "comparison" do
describe "#==" do
let(:instance_proxy) { described_class.new(target: target) }

context "when `other` has different class" do
let(:other) { 42 }

it "returns `nil`" do
expect(instance_proxy == other).to be_nil
end
end

context "when `other` has different `target`" do
let(:other) { described_class.new(target: 42) }

it "returns `false`" do
expect(instance_proxy == other).to eq(false)
end
end

context "when `other` has same attributes" do
let(:other) { described_class.new(target: target) }

it "returns `true`" do
expect(instance_proxy == other).to eq(true)
end
end
end
end
end

example_group "private instance methods" do
describe "#respond_to_missing?" do
let(:method_name) { :foo }
let(:include_private) { false }
let(:instance_proxy) { instance_proxy_klass.new(target: target) }

let(:instance_proxy_klass) do
Class.new(described_class) do
##
# IMPORTANT: `respond_to_missing?` is always private, which is enforced by Ruby. That is why this wrapper is used.
# - https://ruby-doc.org/core-2.7.0/Object.html#method-i-respond_to_missing-3F
# - https://github.com/ruby/spec/blob/master/language/def_spec.rb#L65
#
def respond_to_missing_public?(...)
respond_to_missing?(...)
end
end
end

it "is private" do
##
# NOTE: Depending on the `did_you_mean` version, an additional line may be added to the exception message, which is why the `with_message` string is replaced by regex.
#
expect { instance_proxy.respond_to_missing?(method_name, include_private) }
.to raise_error(NoMethodError)
.with_message(/private method `respond_to_missing\?' called for #{target}/)
end

specify do
expect { instance_proxy.respond_to_missing_public?(method_name, include_private) }
.to delegate_to(instance_proxy.instance_proxy_target, :respond_to?)
.with_arguments(method_name, include_private)
.and_return_its_value
end
end

describe "#method_missing" do
let(:args) { [:foo] }
let(:kwargs) { {foo: :bar} }
let(:block) { proc { :foo } }

specify do
expect { instance_proxy.foo(*args, **kwargs, &block) }
.to delegate_to(instance_proxy.instance_proxy_target, :public_send)
.with_arguments(:foo, *args, **kwargs, &block)
.and_return_its_value
end
end
end
end
# rubocop:enable RSpec/NestedGroups, RSpec/MultipleMemoizedHelpers

0 comments on commit 6be8d3b

Please sign in to comment.