class T::InterfaceWrapper

Wraps an object, exposing only the methods defined on a given class/module. The idea is that, in the absence of a static type checker that would prevent you from calling non-Bar methods on a variable of type Bar, we can use these wrappers as a way of enforcing it at runtime.

Once we ship static type checking, we should get rid of this entirely.

Public Class Methods

dynamic_cast(obj, mod) click to toggle source

“Cast” an object to another type. If `obj` is an InterfaceWrapper, returns the the wrapped object if that matches `type`. Otherwise, returns `obj` if it matches `type`. Otherwise, returns nil.

@param obj [Object] object to cast @param mod [Module] type to cast `obj` to

@example

if (impl = T::InterfaceWrapper.dynamic_cast(iface, MyImplementation))
  impl.do_things
end
# File lib/types/interface_wrapper.rb, line 123
def self.dynamic_cast(obj, mod)
  if obj.is_a?(T::InterfaceWrapper)
    target_obj = obj.__target_obj_DO_NOT_USE
    target_obj.is_a?(mod) ? target_obj : nil
  elsif obj.is_a?(mod)
    obj
  else
    nil
  end
end
new(target_obj, interface_mod) click to toggle source
# File lib/types/interface_wrapper.rb, line 43
def initialize(target_obj, interface_mod)
  if target_obj.is_a?(T::InterfaceWrapper)
    # wrapped_dynamic_cast should guarantee this never happens.
    raise "Unexpected: wrapping a wrapper. Please report to #dev-productivity."
  end

  if !target_obj.is_a?(interface_mod)
    # wrapped_dynamic_cast should guarantee this never happens.
    raise "Unexpected: `is_a?` failed. Please report to #dev-productivity."
  end

  if target_obj.class == interface_mod
    # wrapped_dynamic_cast should guarantee this never happens.
    raise "Unexpected: exact class match. Please report to #dev-productivity."
  end

  @target_obj = target_obj
  @interface_mod = interface_mod
  self_methods = self.class.self_methods

  # If perf becomes an issue, we can define these on an anonymous subclass, and keep a cache
  # so we only need to do it once per unique `interface_mod`
  T::Utils.methods_excluding_object(interface_mod).each do |method_name|
    if self_methods.include?(method_name)
      raise "interface_mod has a method that conflicts with #{self.class}: #{method_name}"
    end

    define_singleton_method(method_name) do |*args, &blk|
      target_obj.send(method_name, *args, &blk)
    end

    if target_obj.singleton_class.public_method_defined?(method_name)
      # no-op, it's already public
    elsif target_obj.singleton_class.protected_method_defined?(method_name)
      singleton_class.send(:protected, method_name)
    elsif target_obj.singleton_class.private_method_defined?(method_name)
      singleton_class.send(:private, method_name)
    else
      raise "This should never happen. Report to #dev-productivity"
    end
  end
end
self_methods() click to toggle source
# File lib/types/interface_wrapper.rb, line 155
def self.self_methods
  @self_methods ||= self.instance_methods(false).to_set
end
wrap_instance(obj, interface_mod) click to toggle source
# File lib/types/interface_wrapper.rb, line 24
def self.wrap_instance(obj, interface_mod)
  wrapper = wrapped_dynamic_cast(obj, interface_mod)
  if wrapper.nil?
    raise "#{obj.class} cannot be cast to #{interface_mod}"
  end
  wrapper
end
wrap_instances(arr, interface_mod) click to toggle source
# File lib/types/interface_wrapper.rb, line 39
def self.wrap_instances(arr, interface_mod)
  arr.map {|instance| self.wrap_instance(instance, interface_mod)}
end
wrapped_dynamic_cast(obj, mod) click to toggle source

Like dynamic_cast, but puts the result in its own wrapper if necessary.

@param obj [Object] object to cast @param mod [Module] type to cast `obj` to

# File lib/types/interface_wrapper.rb, line 138
def self.wrapped_dynamic_cast(obj, mod)
  # Avoid unwrapping and creating an equivalent wrapper.
  if obj.is_a?(T::InterfaceWrapper) && obj.__interface_mod_DO_NOT_USE == mod
    return obj
  end

  cast_obj = dynamic_cast(obj, mod)
  if cast_obj.nil?
    nil
  elsif cast_obj.class == mod
    # Nothing to wrap, they want the full class
    cast_obj
  else
    new(cast_obj, mod)
  end
end

Public Instance Methods

__interface_mod_DO_NOT_USE() click to toggle source

Prefixed because we're polluting the namespace of the interface we're wrapping, and we don't want anyone else (besides wrapped_dynamic_cast) calling it.

# File lib/types/interface_wrapper.rb, line 108
def __interface_mod_DO_NOT_USE # rubocop:disable Naming/MethodName
  @interface_mod
end
__target_obj_DO_NOT_USE() click to toggle source

Prefixed because we're polluting the namespace of the interface we're wrapping, and we don't want anyone else (besides dynamic_cast) calling it.

# File lib/types/interface_wrapper.rb, line 102
def __target_obj_DO_NOT_USE # rubocop:disable Naming/MethodName
  @target_obj
end
is_a?(other) click to toggle source
# File lib/types/interface_wrapper.rb, line 90
def is_a?(other)
  if !other.is_a?(Module)
    raise TypeError.new("class or module required")
  end

  # This makes is_a? return true for T::InterfaceWrapper (and its ancestors),
  # as well as for @interface_mod and its ancestors.
  self.class <= other || @interface_mod <= other
end
kind_of?(other) click to toggle source
# File lib/types/interface_wrapper.rb, line 86
def kind_of?(other)
  is_a?(other)
end