Class: Matchi::BeAnInstanceOf

Inherits:
Object
  • Object
show all
Defined in:
lib/matchi/be_an_instance_of.rb

Overview

Type/class matcher with enhanced class checking.

This matcher aims to provide a more reliable way to check if an object is an exact instance of a specific class (not a subclass). While not foolproof, it uses a more robust method to get the actual class of an object that helps resist common attempts at type checking manipulation.

Examples:

Basic usage

require "matchi/be_an_instance_of"

matcher = Matchi::BeAnInstanceOf.new(String)
matcher.match? { "foo" }  # => true
matcher.match? { :foo }   # => false

Enhanced class checking in practice

# Consider a class that attempts to masquerade as String by overriding
# common type checking methods:
class MaliciousString
  def class
    ::String
  end

  def instance_of?(klass)
    self.class == klass
  end

  def is_a?(klass)
    "".is_a?(klass)  # Delegates to a real String
  end

  def kind_of?(klass)
    is_a?(klass)     # Maintains Ruby's kind_of? alias for is_a?
  end
end

obj = MaliciousString.new
obj.class                                             # => String
obj.is_a?(String)                                     # => true
obj.kind_of?(String)                                  # => true
obj.instance_of?(String)                              # => true

# Using our enhanced checking approach:
matcher = Matchi::BeAnInstanceOf.new(String)
matcher.match? { obj }                                # => false

Instance Method Summary collapse

Constructor Details

#initialize(expected) ⇒ BeAnInstanceOf

Initialize the matcher with (the name of) a class or module.

Examples:

require "matchi/be_an_instance_of"

Matchi::BeAnInstanceOf.new(String)
Matchi::BeAnInstanceOf.new("String")
Matchi::BeAnInstanceOf.new(:String)

Parameters:

  • expected (Class, #to_s)

    The expected class name

Raises:

  • (ArgumentError)

    if the class name doesn’t start with an uppercase letter



60
61
62
63
64
65
66
# File 'lib/matchi/be_an_instance_of.rb', line 60

def initialize(expected)
  @expected = String(expected)
  return if /\A[A-Z]/.match?(@expected)

  raise ::ArgumentError,
        "expected must start with an uppercase letter (got: #{@expected})"
end

Instance Method Details

#match?Boolean

Securely checks if the yielded object is an instance of the expected class.

This method uses a specific Ruby reflection technique to get the true class of an object, bypassing potential method overrides:

  1. ::Object.instance_method(:class) retrieves the original, unoverridden ‘class’ method from the Object class

  2. .bind_call(obj) binds this original method to our object and calls it, ensuring we get the real class regardless of method overrides

This approach is more reliable than obj.class because it uses Ruby’s method binding mechanism to call the original implementation directly. While not completely foolproof, it provides better protection against type check spoofing than using regular method calls which can be overridden.

Examples:

Basic class check

matcher = Matchi::BeAnInstanceOf.new(String)
matcher.match? { "test" }        # => true
matcher.match? { StringIO.new }  # => false

Yield Returns:

  • (Object)

    the actual value to check

Returns:

  • (Boolean)

    true if the object’s actual class is exactly the expected class

Raises:

  • (ArgumentError)

    if no block is provided

See Also:



94
95
96
97
98
99
# File 'lib/matchi/be_an_instance_of.rb', line 94

def match?
  raise ::ArgumentError, "a block must be provided" unless block_given?

  actual_class = ::Object.instance_method(:class).bind_call(yield)
  expected_class == actual_class
end

#to_sString

Returns a string representing the matcher.

Returns:

  • (String)

    a human-readable description of the matcher



104
105
106
# File 'lib/matchi/be_an_instance_of.rb', line 104

def to_s
  "be an instance of #{@expected}"
end