Steal equalizer from veritas
This commit is contained in:
parent
fb29d3927d
commit
74a7477e1c
4 changed files with 397 additions and 0 deletions
125
lib/mutant/support/equalizer.rb
Normal file
125
lib/mutant/support/equalizer.rb
Normal 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
|
138
spec/unit/mutant/equalizer/class_method/new_spec.rb
Normal file
138
spec/unit/mutant/equalizer/class_method/new_spec.rb
Normal 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
|
49
spec/unit/mutant/equalizer/methods/eql_spec.rb
Normal file
49
spec/unit/mutant/equalizer/methods/eql_spec.rb
Normal 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
|
85
spec/unit/mutant/equalizer/methods/equal_value_spec.rb
Normal file
85
spec/unit/mutant/equalizer/methods/equal_value_spec.rb
Normal 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
|
Loading…
Reference in a new issue