lib/functional/protocol_info.rb
require 'functional/synchronization'
module Functional
# An immutable object describing a single protocol and capable of building
# itself from a block. Used by {Functional#SpecifyProtocol}.
#
# @see Functional::Protocol
class ProtocolInfo < Synchronization::Object
# The symbolic name of the protocol
attr_reader :name
# Process a protocol specification block and build a new object.
#
# @param [Symbol] name the symbolic name of the protocol
# @yield self to the given specification block
# @return [Functional::ProtocolInfo] the new info object, frozen
#
# @raise [ArgumentError] when name is nil or an empty string
# @raise [ArgumentError] when no block given
def initialize(name, &specification)
raise ArgumentError.new('no block given') unless block_given?
raise ArgumentError.new('no name given') if name.nil? || name.empty?
super
@name = name.to_sym
@info = Info.new({}, {}, [])
self.instance_eval(&specification)
@info.each_pair{|col, _| col.freeze}
@info.freeze
ensure_ivar_visibility!
self.freeze
end
# The instance methods expected by this protocol.
#
# @return [Hash] a frozen hash of all instance method names and their
# expected arity for this protocol
def instance_methods
@info.instance_methods
end
# The class methods expected by this protocol.
#
# @return [Hash] a frozen hash of all class method names and their
# expected arity for this protocol
def class_methods
@info.class_methods
end
# The constants expected by this protocol.
#
# @return [Array] a frozen list of the constants expected by this protocol
def constants
@info.constants
end
# Does the given module/class/object satisfy this protocol?
#
# @return [Boolean] true if the target satisfies this protocol else false
def satisfies?(target)
satisfies_constants?(target) &&
satisfies_instance_methods?(target) &&
satisfies_class_methods?(target)
end
private
# Data structure for encapsulating the protocol info data.
# @!visibility private
Info = Struct.new(:instance_methods, :class_methods, :constants)
# Does the target satisfy the constants expected by this protocol?
#
# @param [target] target the module/class/object to interrogate
# @return [Boolean] true when satisfied else false
def satisfies_constants?(target)
clazz = target.is_a?(Module) ? target : target.class
@info.constants.all?{|constant| clazz.const_defined?(constant) }
end
# Does the target satisfy the instance methods expected by this protocol?
#
# @param [target] target the module/class/object to interrogate
# @return [Boolean] true when satisfied else false
def satisfies_instance_methods?(target)
@info.instance_methods.all? do |method, arity|
if target.is_a? Module
target.method_defined?(method) && check_arity?(target.instance_method(method), arity)
else
target.respond_to?(method) && check_arity?(target.method(method), arity)
end
end
end
# Does the target satisfy the class methods expected by this protocol?
#
# @param [target] target the module/class/object to interrogate
# @return [Boolean] true when satisfied else false
def satisfies_class_methods?(target)
clazz = target.is_a?(Module) ? target : target.class
@info.class_methods.all? do |method, arity|
break false unless clazz.respond_to? method
method = clazz.method(method)
check_arity?(method, arity)
end
end
# Does the given method have the expected arity? Returns true
# if the arity of the method is `-1` (variable length argument list
# with no required arguments), when expected is `nil` (indicating any
# arity is acceptable), or the arity of the method exactly matches the
# expected arity.
#
# @param [Method] method the method object to interrogate
# @param [Fixnum] expected the expected arity
# @return [Boolean] true when an acceptable match else false
#
# @see http://www.ruby-doc.org/core-2.1.2/Method.html#method-i-arity Method#arity
def check_arity?(method, expected)
arity = method.arity
expected.nil? || arity == -1 || expected == arity
end
#################################################################
# DSL methods
# Specify an instance method.
#
# @param [Symbol] name the name of the method
# @param [Fixnum] arity the required arity
def instance_method(name, arity = nil)
arity = arity.to_i unless arity.nil?
@info.instance_methods[name.to_sym] = arity
end
# Specify a class method.
#
# @param [Symbol] name the name of the method
# @param [Fixnum] arity the required arity
def class_method(name, arity = nil)
arity = arity.to_i unless arity.nil?
@info.class_methods[name.to_sym] = arity
end
# Specify an instance reader attribute.
#
# @param [Symbol] name the name of the attribute
def attr_reader(name)
instance_method(name, 0)
end
# Specify an instance writer attribute.
#
# @param [Symbol] name the name of the attribute
def attr_writer(name)
instance_method("#{name}=".to_sym, 1)
end
# Specify an instance accessor attribute.
#
# @param [Symbol] name the name of the attribute
def attr_accessor(name)
attr_reader(name)
attr_writer(name)
end
# Specify a class reader attribute.
#
# @param [Symbol] name the name of the attribute
def class_attr_reader(name)
class_method(name, 0)
end
# Specify a class writer attribute.
#
# @param [Symbol] name the name of the attribute
def class_attr_writer(name)
class_method("#{name}=".to_sym, 1)
end
# Specify a class accessor attribute.
#
# @param [Symbol] name the name of the attribute
def class_attr_accessor(name)
class_attr_reader(name)
class_attr_writer(name)
end
# Specify a constant.
#
# @param [Symbol] name the name of the constant
def constant(name)
@info.constants << name.to_sym
end
end
end