I want to create an object that acts as a specific class, such as Fixnum, but isn't an instance of that class nor its subclasses.
There are various use-cases for this. In the Fixnum case, I want to define a more specific integer type that is essentially a Fixnum but implements some extra logic, too. I can't subclass Fixnum itself because immediate types such as Fixnum and Symbol can't be subclassed.
Another use-case is mocking in automated tests: sometimes you want to create an object that acts like a certain class (usually a model instance) but is for technical reasons not an instance of that exact class.
Here's how to create a specific integer type that delegates all methods to an internally stored fixnum:
require 'delegate'
require 'forwardable'
# integer representing a page number
class PageNumber < DelegateClass(Integer)
extend Forwardable
def initialize(value, name)
@name = name
super(value)
end
def inspect
"#{@name} #{to_i}"
end
alias_method :to_i, :__getobj__
def_delegators :to_i, :instance_of?, :kind_of?, :is_a?
end
This object can pass is_a?
and similar checks:
page = PageNumber.new(1, "page")
page.is_a? Fixnum #=> true
But nothing I do can make it pass the Module#===
type check:
# my problem:
Fixnum === page #=> false
The fact that my object fails this check is very unfortunate, since the ===
method is used internally in case
statements:
case page
when Fixnum
# it will never get here
when String
# ...
else
# ...
end
My question is how can I create a mock type that passes the ===
check without augmenting the ===
methods on built-in classes?
If we are speaking about the MRI1, the answer is simple: you cannot.
The Module#===
method is effectively an alias of rb_obj_is_kind_of
C API method. The implementation of latter is so short that I will paste it here:
VALUE
rb_obj_is_kind_of(VALUE obj, VALUE c)
{
VALUE cl = CLASS_OF(obj);
/* Type checking of `c' omitted */
while (cl) {
if (cl == c || RCLASS_M_TBL(cl) == RCLASS_M_TBL(c))
return Qtrue;
cl = RCLASS_SUPER(cl);
}
return Qfalse;
}
As you can see, this method traverses the ancestors of the object being examined, and compares them in two ways: first, it checks if the ancestor is the same as the module which was passed, and then, it checks if they have same method table.
The latter check is required because included modules in Ruby are seemingly inserted in the inheritance chain, but as one module may be included in several other ones, it's not the real module which is inserted into the chain, but a proxy object, which has its constant and method tables pointing to the original module.
For example, let's look at Object's ancestors:
ruby-1.9.2-p136 :001 > Object.ancestors
=> [Object, Kernel, BasicObject]
ruby-1.9.2-p136 :002 > Object.ancestors.map { |mod| Object.new.is_a? mod }
=> [true, true, true]
Here, the Object
and BasicObject
will be successfully compared by the first check, and Kernel
by the second one.
Even if you'll try to make (with a C extension) a proxy object which will try to trick the rb_obj_is_kind_of
method, it will need to have the same method table as a real Fixnum
, which would effectively include all Fixnum
's methods.
This is a hackish solution that I warned against in my question:
Fixnum === page #=> false
Numeric.extend Module.new {
def ===(obj)
obj.instance_of?(PageNumber) or super
end
}
Fixnum === page #=> true
It solves the problem but raises a question is it safe to do? I can't think of any drawbacks of this method from the top of my mind but since we're messing with a very important method here it might not be something we'd want to do.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With