Class: Safer::Protocol

Inherits:
Object
  • Object
show all
Defined in:
lib/safer/protocol.rb

Overview

Named set of class- and instance- methods, with associated numbers of arguments. Call create_from_class or create_from_instance to create a new Protocol object. Call class_conforms? or instance_conforms? to verify that a Class or instance of the class (respectively) conforms to a protocol.

Usage

In the example, we define a class with a single instance variable, initialized from an argument to #initialize. We must verify that the object corresponding to this argument implements a particular protocol.

class YourClass
  # Define a class or module containing all the class- and instance-
  # methods that you require from objects passed into your methods.  Here,
  # we expect the +Class+ object to implement a +foo+ method that takes
  # three arguments, and for instances to implement a +bar+ method that
  # takes one required argument, followed by any number of optional
  # arguments:
  class FooInterface
    # Note that we have a good place to document what these interfaces
    # mean, in standard RDoc fashion.
    def self.foo(arg1, arg2, arg3)
    end
    def bar(arg1, *rest)
    end
  end

  # Derive a Safer::Protocol object from this class:
  FOOPROTOCOL = Safer::Protocol.create_from_class(FooProtocol)
  # Note: we could also do this step as follows:
  # FOOPROTOCOL = Safer::Protocol.create_from_instance(FooProtocol.new)

  # Our #initialize method takes an object that should conform to
  # FOOPROTOCOL.
  def initialize(obj)
    # verify that 'obj' conforms to FOOPROTOCOL.  If it does not, a
    # Safer::Protocol::Error::InstanceError exception will be raised.
    FOOPROTOCOL.instance_conforms?(obj)
    @obj = obj
  end

  # Now, functions can assume that @obj implements FOOPROTOCOL.
  def quux
    @obj.class.foo(self, 2, 3)
    @obj.bar(@obj)
  end
  # including client functions.
  attr_reader :obj
end

class Foo
  # implements YourClass::FOOPROTOCOL
  def self.foo(arg1, arg2, arg3)
    return arg1 + arg2 + arg3
  end
  def bar(arg1, *rest)
    "#{arg1.class.to_s}:#{rest.inspect}"
  end
end
# should succeed.
a = YourClass.new(Foo.new)
# should raise a Safer::Protocol::Error::InstanceError.
b = YourClass.new(15)

Rationale

It is often the case that we expect objects passed to our methods to follow some usage protocol. There are two ways this is usually accomplished in Ruby:

  1. We can ensure that objects are of a particular type, by calling Object.kind_of? and related functions, disabling features or signaling error when the object is of the wrong type. If the object is of a particular type, then we can reasonably expect that the object will conform to that type’s interface.

  2. We can verify that objects respond to desired interfaces using Object#respond_to? and related functions, disabling features or signaling error when these objects do not support certain interfaces.

The first method cannot always be used, as there exist circumstances in which the inheritance hierarchy cannot (or should not be used to) predict if an object can be used in a particular way. (For example, the object hierarchy does not determine if an object is serializable – there is no base class for which all sub-classes are serializable, and only sub-classes are serializable.) So we must sometimes rely on directly verifying that an object supports a set of interfaces.

Safer::Protocol is intended to improve the maintainability of code that must directly verify that objects implement desired interfaces. A Protocol object is derived from a class definition that includes the set of methods for which the Protocol should test, and provides a simple means of testing whether another class object (or instance objects) implement that set of methods. If the method set is not fully supported, a report object (Safer::Protocol::Error) is generated describing the differences between the object under test and the Protocol’s requirements.

Safer::Protocol is also intended to make it easier to document the interfaces required from a Protocol-implementing object. Protocol objects are derived from Class objects, so one can document the interfaces required by the protocol by documenting the Class definition.

Defined Under Namespace

Classes: Error, Match, Signature, Violation

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, class_signature, instance_signature) ⇒ Protocol

Create a Protocol object. Used internally.

name

Value for name field object.

class_signature

Value for class_signature field.

instance_signature

Value for instance_signature field.



398
399
400
401
402
403
404
# File 'lib/safer/protocol.rb', line 398

def initialize(name, class_signature, instance_signature)
  self.safer_protocol__name = name
  self.safer_protocol__class_signature = class_signature
  self.safer_protocol__instance_signature = instance_signature
  self.safer_protocol__match_class = Safer::Protocol::Match.new(self.method(:violations_from_class))
  self.safer_protocol__match_instance = Safer::Protocol::Match.new(self.method(:violations_from_instance))
end

Class Method Details

._array_to_table(array, &block) ⇒ Object

Utility function used during Protocol operations. Given an Array, construct a Hash with the values in the Array as keys. If no block is provided, then the values for all keys in the Hash will be true. If a block is provided, the block is called for each value in the Array, and its return value will be stored as the value for that element in the Hash. If the block returns nil, then the element will not be stored in the resulting Hash.



112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/safer/protocol.rb', line 112

def self._array_to_table(array, &block)
  if ! block
    block = proc do |h, el| true ; end
  end
  array.inject(Hash.new) do |h, el|
    newval = block.call(h, el)
    if newval
      h[el] = newval
    end
    h
  end
end

.create_from_class(klass, base = Object) ⇒ Object

Given a Class object klass and a Class object base, create a Protocol object representing the methods implemented in klass that are not present in base.

klass

Class object from which to derive a Protocol.

base

Class object describing routines that will be implemented by klass, but which should not be included in the returned Protocol.



467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'lib/safer/protocol.rb', line 467

def self.create_from_class(klass, base = Object)
  class_signature = Signature.create(klass, :methods, :method) do
    |method_name|
    ! Class.respond_to?(method_name)
  end
  baseinstance_table = self._array_to_table(base.instance_methods)
  instance_signature = Signature.create(klass, :instance_methods, :instance_method) do
    |method_name|
    ! baseinstance_table.has_key?(method_name)
  end

  self.new(klass.to_s, class_signature, instance_signature)
end

.create_from_instance(instance, baseclass = Object) ⇒ Object

Given an instance object instance and a Class object baseclass, create a Protocol object representing the methods implemented in klass that are not present in base.



485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/safer/protocol.rb', line 485

def self.create_from_instance(instance, baseclass = Object)
  class_signature = Signature.create(instance.class, :methods, :method) do
    |method_name|
    ! Class.method_defined?(method_name)
  end
  baseinstance_table = self._array_to_table(base.instance_methods)
  instance_signature = Signature.create(instance, :methods, :method) do
    |method_name|
    ! baseinstance_table.has_key?(method_name)
  end

  self.new(instance.class.to_s, class_signature, instance_signature)
end

Instance Method Details

#class_conforms?(klass) ⇒ Boolean

Check that a Class object conforms to this protocol.

Returns:

  • (Boolean)


441
442
443
444
445
446
447
# File 'lib/safer/protocol.rb', line 441

def class_conforms?(klass)
  violations = self.violations_from_class(klass)
  if violations
    raise violations
  end
  true
end

#instance_conforms?(instance) ⇒ Boolean

Check that an instance object conforms to this protocol.

Returns:

  • (Boolean)


451
452
453
454
455
456
457
# File 'lib/safer/protocol.rb', line 451

def instance_conforms?(instance)
  violations = self.violations_from_instance(instance)
  if violations
    raise violations
  end
  true
end

#selfObject

:attr_reader: match_instance Object provides === operation matching when #violations_from_instance would return no violations.



370
# File 'lib/safer/protocol.rb', line 370

Safer::IVar.export_reader(self, :name)

#violations_from_class(klass) ⇒ Object

Given a Class object, find the protocol methods that are not supported by a class, or its instances.

returns

If protocol violations are found, returns a Safer::Protocol::Error::ClassError object describing the violations. Otherwise, returns nil.



412
413
414
415
416
417
418
419
420
421
# File 'lib/safer/protocol.rb', line 412

def violations_from_class(klass)
  class_violations = self.safer_protocol__class_signature.find_violations(klass, :method)
  instance_violations = self.safer_protocol__instance_signature.find_violations(klass, :instance_method)

  if class_violations || instance_violations
    Error::ClassError.new(klass, self, class_violations, instance_violations)
  else
    nil
  end
end

#violations_from_instance(instance) ⇒ Object

Given an instance object, find the protocol methods that are not supported by the instance, or its class.

returns

If protocol violations are found, returns a Safer::Protocol::Error::InstanceError object describing the violations. Otherwise, returns nil.



429
430
431
432
433
434
435
436
437
# File 'lib/safer/protocol.rb', line 429

def violations_from_instance(instance)
  class_violations = self.safer_protocol__class_signature.find_violations(instance.class, :method)
  instance_violations = self.safer_protocol__instance_signature.find_violations(instance, :method)
  if class_violations || instance_violations
    Error::InstanceError.new(instance, self, class_violations, instance_violations)
  else
    nil
  end
end