Steal equalizer from veritas

This commit is contained in:
Markus Schirp 2012-08-29 13:40:48 +02:00
parent fb29d3927d
commit 74a7477e1c
4 changed files with 397 additions and 0 deletions

View file

@ -0,0 +1,125 @@
# encoding: utf-8
module Mutant
# Define equality, equivalence and inspection methods
class Equalizer < Module
# Initialize an Equalizer with the given keys
#
# Will use the keys with which it is initialized to define #cmp?,
# #hash, and #inspect
#
# @param [Array<Symbol>] *keys
#
# @return [undefined]
#
# @api private
def initialize(*keys)
@keys = Immutable.freeze_object(keys)
define_methods
include_comparison_methods
end
private
# Define the equalizer methods based on #keys
#
# @return [undefined]
#
# @api private
def define_methods
define_cmp_method
define_hash_method
define_inspect_method
end
# Define an #cmp? method based on the instance's values identified by #keys
#
# @return [undefined]
#
# @api private
def define_cmp_method
keys = @keys
define_method(:cmp?) do |comparator, other|
keys.all? { |key| send(key).send(comparator, other.send(key)) }
end
private :cmp?
end
# Define a #hash method based on the instance's values identified by #keys
#
# @return [undefined]
#
# @api private
def define_hash_method
keys = @keys
define_method(:hash) do
keys.map { |key| send(key).hash }.reduce(self.class.hash, :^)
end
end
# Define an inspect method that reports the values of the instance's keys
#
# @return [undefined]
#
# @api private
def define_inspect_method
keys = @keys
define_method(:inspect) do
klass = self.class
name = klass.name || klass.inspect
"#<#{name}#{keys.map { |key| " #{key}=#{send(key).inspect}" }.join}>"
end
end
# Include the #eql? and #== methods
#
# @return [undefined]
#
# @api private
def include_comparison_methods
module_eval do
include Methods, Immutable
memoize :hash
end
end
# The comparison methods
module Methods
# Compare the object with other object for equality
#
# @example
# object.eql?(other) # => true or false
#
# @param [Object] other
# the other object to compare with
#
# @return [Boolean]
#
# @api public
def eql?(other)
instance_of?(other.class) and cmp?(__method__, other)
end
# Compare the object with other object for equivalency
#
# @example
# object == other # => true or false
#
# @param [Object] other
# the other object to compare with
#
# @return [Boolean]
#
# @api public
def ==(other)
other = coerce(other) if respond_to?(:coerce, true)
return false unless self.class <=> other.class
cmp?(__method__, other)
end
end # module Methods
end # class Equalizer
end # module Mutant

View file

@ -0,0 +1,138 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Equalizer, '.new' do
let(:object) { described_class }
let(:name) { 'User' }
let(:klass) { ::Class.new }
context 'with no keys' do
subject { object.new }
before do
# specify the class #name method
klass.stub(:name).and_return(name)
klass.send(:include, subject)
end
let(:instance) { klass.new }
it { should be_instance_of(object) }
it 'defines #hash and #inspect methods dynamically' do
subject.public_instance_methods(false).map(&:to_s).should =~ %w[ hash inspect ]
end
describe '#eql?' do
context 'when the objects are similar' do
let(:other) { instance.dup }
it { instance.eql?(other).should be(true) }
end
context 'when the objects are different' do
let(:other) { stub('other') }
it { instance.eql?(other).should be(false) }
end
end
describe '#==' do
context 'when the objects are similar' do
let(:other) { instance.dup }
it { (instance == other).should be(true) }
end
context 'when the objects are different' do
let(:other) { stub('other') }
it { (instance == other).should be(false) }
end
end
describe '#hash' do
it { instance.hash.should eql(klass.hash) }
it 'memoizes the hash code' do
instance.hash.should eql(instance.memoized(:hash))
end
end
describe '#inspect' do
it { instance.inspect.should eql('#<User>') }
end
end
context 'with keys' do
subject { object.new(*keys) }
let(:keys) { [ :first_name ].freeze }
let(:first_name) { 'John' }
let(:instance) { klass.new(first_name) }
let(:klass) do
::Class.new do
attr_reader :first_name
def initialize(first_name)
@first_name = first_name
end
end
end
before do
# specify the class #inspect method
klass.stub(:name).and_return(nil)
klass.stub(:inspect).and_return(name)
klass.send(:include, subject)
end
it { should be_instance_of(object) }
it 'defines #hash and #inspect methods dynamically' do
subject.public_instance_methods(false).map(&:to_s).should =~ %w[ hash inspect ]
end
describe '#eql?' do
context 'when the objects are similar' do
let(:other) { instance.dup }
it { instance.eql?(other).should be(true) }
end
context 'when the objects are different' do
let(:other) { stub('other') }
it { instance.eql?(other).should be(false) }
end
end
describe '#==' do
context 'when the objects are similar' do
let(:other) { instance.dup }
it { (instance == other).should be(true) }
end
context 'when the objects are different' do
let(:other) { stub('other') }
it { (instance == other).should be(false) }
end
end
describe '#hash' do
it { instance.hash.should eql(klass.hash ^ first_name.hash) }
it 'memoizes the hash code' do
instance.hash.should eql(instance.memoized(:hash))
end
end
describe '#inspect' do
it { instance.inspect.should eql('#<User first_name="John">') }
end
end
end

View file

@ -0,0 +1,49 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Equalizer::Methods, '#eql?' do
subject { object.eql?(other) }
let(:object) { described_class.new }
let(:described_class) do
Class.new do
include Mutant::Equalizer::Methods
def cmp?(comparator, other)
!!(comparator and other)
end
end
end
context 'with the same object' do
let(:other) { object }
it { should be(true) }
it 'is symmetric' do
should eql(other.eql?(object))
end
end
context 'with an equivalent object' do
let(:other) { object.dup }
it { should be(true) }
it 'is symmetric' do
should eql(other.eql?(object))
end
end
context 'with an equivalent object of a subclass' do
let(:other) { Class.new(described_class).new }
it { should be(false) }
it 'is symmetric' do
should eql(other.eql?(object))
end
end
end

View file

@ -0,0 +1,85 @@
# encoding: utf-8
require 'spec_helper'
describe Mutant::Equalizer::Methods, '#==' do
subject { object == other }
let(:object) { described_class.new(true) }
let(:described_class) do
Class.new do
include Mutant::Equalizer::Methods
attr_reader :boolean
def initialize(boolean)
@boolean = boolean
end
def cmp?(comparator, other)
boolean.send(comparator, other.boolean)
end
end
end
context 'with the same object' do
let(:other) { object }
it { should be(true) }
it 'is symmetric' do
should eql(other == object)
end
end
context 'with an equivalent object' do
let(:other) { object.dup }
it { should be(true) }
it 'is symmetric' do
should eql(other == object)
end
end
context 'with an equivalent object of a subclass' do
let(:other) { Class.new(described_class).new(true) }
it { should be(true) }
it 'is symmetric' do
should eql(other == object)
end
end
context 'with an object of another class' do
let(:other) { Class.new.new }
it { should be(false) }
it 'is symmetric' do
should eql(other == object)
end
end
context 'with an equivalent object after coercion' do
let(:other) { Object.new }
before do
# declare a private #coerce method
described_class.class_eval do
def coerce(other)
self.class.new(!!other)
end
private :coerce
end
end
it { should be(true) }
it 'is not symmetric' do
should_not eql(other == object)
end
end
end