equatable-0.6.1/0000755000175000017500000000000013611565275013052 5ustar boutilboutilequatable-0.6.1/tasks/0000755000175000017500000000000013611565275014177 5ustar boutilboutilequatable-0.6.1/tasks/spec.rake0000644000175000017500000000033313611565275015774 0ustar boutilboutil# encoding: utf-8 begin require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**{,/*/**}/*_spec.rb' end rescue LoadError $stderr.puts("Cannot load rspec task.") end equatable-0.6.1/tasks/coverage.rake0000644000175000017500000000032213611565275016633 0ustar boutilboutil# encoding: utf-8 desc 'Measure code coverage' task :coverage do begin original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' Rake::Task['spec'].invoke ensure ENV['COVERAGE'] = original end end equatable-0.6.1/tasks/console.rake0000644000175000017500000000031213611565275016501 0ustar boutilboutildesc 'Load gem inside irb console' task :console do require 'irb' require 'irb/completion' require File.join(__FILE__, '../../lib/equatable') ARGV.clear IRB.start end task :c => %w[ console ] equatable-0.6.1/spec/0000755000175000017500000000000013611565275014004 5ustar boutilboutilequatable-0.6.1/spec/spec_helper.rb0000644000175000017500000000204013611565275016616 0ustar boutilboutil# frozen_string_literal: true if RUBY_VERSION > '1.9' and (ENV['COVERAGE'] || ENV['TRAVIS']) require 'simplecov' require 'coveralls' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ] SimpleCov.start do command_name 'spec' add_filter 'spec' end end require 'equatable' RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true end # Limits the available syntax to the non-monkey patched syntax that is recommended. config.disable_monkey_patching! # This setting enables warnings. It's recommended, but in some cases may # be too noisy due to issues in dependencies. config.warnings = true if config.files_to_run.one? config.default_formatter = 'doc' end config.profile_examples = 2 config.order = :random Kernel.srand config.seed end equatable-0.6.1/spec/equatable/0000755000175000017500000000000013611565275015747 5ustar boutilboutilequatable-0.6.1/spec/equatable/subclass_spec.rb0000644000175000017500000000247513611565275021135 0ustar boutilboutil# frozen_string_literal: true RSpec.describe Equatable, 'subclass' do let(:name) { 'Value' } context 'when subclass' do let(:value) { 11 } let(:klass) { ::Class.new do include Equatable attr_reader :value def initialize(value) @value = value end end } let(:subclass) { ::Class.new(klass) } subject { subclass.new(value) } before { allow(klass).to receive(:name).and_return(name) } it { expect(subclass.superclass).to eq(klass) } it { is_expected.to respond_to(:value) } describe '#inspect' do it { expect(subject.inspect).to eql('#') } end describe '#eql?' do context 'when objects are similar' do let(:other) { subject.dup } it { expect(subject.eql?(other)).to eql(true) } end context 'when objects are different' do let(:other) { double('other') } it { expect(subject.eql?(other)).to eql(false) } end end describe '#==' do context 'when objects are similar' do let(:other) { subject.dup } it { expect(subject == other).to eql(true) } end context 'when objects are different' do let(:other) { double('other') } it { expect(subject == other).to eql(false) } end end end end equatable-0.6.1/spec/equatable/include_spec.rb0000644000175000017500000000776413611565275020747 0ustar boutilboutil# frozen_string_literal: true RSpec.describe Equatable, '#include' do let(:name) { 'Value' } let(:object) { described_class } context 'without attributes' do let(:klass) { ::Class.new } subject { klass.new } before { allow(klass).to receive(:name).and_return(name) klass.send(:include, object) } it { is_expected.to respond_to(:compare?) } it { is_expected.to be_instance_of(klass) } it 'has no attribute names' do expect(klass.comparison_attrs).to eq([]) end describe '#inspect' do it { expect(subject.inspect).to eql('#') } end describe '#hash' do it { expect(subject.hash).to eql([klass].hash) } end describe '#eql?' do context 'when objects are similar' do let(:other) { subject.dup } it { expect(subject.eql?(other)).to eql(true) } end context 'when objects are different' do let(:other) { double('other') } it { expect(subject.eql?(other)).to eql(false) } end end describe '#==' do context 'when objects are similar' do let(:other) { subject.dup } it { expect(subject == other).to eql(true) } end context 'when objects are different' do let(:other) { double('other') } it { expect(subject == other).to eql(false) } end end context 'equivalence relation' do let(:other) { subject.dup } let(:another) { other.dup } it 'is not equal to nil reference' do expect(subject.eql?(nil)).to eql(false) end it 'is reflexive' do expect(subject.eql?(subject)).to eql(true) end it 'is symmetric' do expect(subject.eql?(other)).to eql( other.eql?(subject) ) end it 'is transitive' do expect(subject.eql?(other) && other.eql?(another)).to eql(subject.eql?(another)) end end end context 'with attributes' do let(:value) { 11 } let(:klass) { ::Class.new do include Equatable attr_reader :value def initialize(value) @value = value end end } before { allow(klass).to receive(:name).and_return(name) } subject { klass.new(value) } it 'dynamically defines #hash method' do expect(klass.method_defined?(:hash)).to eql(true) end it 'dynamically defines #inspect method' do expect(klass.method_defined?(:inspect)).to eql(true) end it { is_expected.to respond_to(:compare?) } it { is_expected.to respond_to(:eql?) } it 'has comparison attribute names' do expect(klass.comparison_attrs).to eq([:value]) end describe '#eql?' do context 'when objects are similar' do let(:other) { subject.dup } it { expect(subject.eql?(other)).to eql(true) } end context 'when objects are different' do let(:other) { double('other') } it { expect(subject.eql?(other)).to eql(false) } end end describe '#==' do context 'when objects are similar' do let(:other) { subject.dup } it { expect(subject == other).to eql(true) } end context 'when objects are different' do let(:other) { double('other') } it { expect(subject == other).to eql(false) } end end describe '#inspect' do it { expect(subject.inspect).to eql('#') } end describe '#hash' do it { expect(subject.hash).to eql( ([klass] + [value]).hash) } end context 'equivalence relation' do let(:other) { subject.dup } let(:another) { other.dup } it 'is not equal to nil reference' do expect(subject.eql?(nil)).to eql(false) end it 'is reflexive' do expect(subject.eql?(subject)).to eql(true) end it 'is symmetric' do expect(subject.eql?(other)).to eql( other.eql?(subject) ) end it 'is transitive' do expect(subject.eql?(other) && other.eql?(another)).to eql(subject.eql?(another)) end end end end equatable-0.6.1/spec/equatable/equal_spec.rb0000644000175000017500000000316513611565275020422 0ustar boutilboutil# frozen_string_literal: true RSpec.describe Equatable, '#==' do let(:name) { 'Value' } let(:value) { 11 } let(:super_klass) { ::Class.new do include Equatable attr_reader :value def initialize(value) @value = value end end } let(:klass) { Class.new(super_klass) } let(:object) { klass.new(value) } subject { object == other } context 'with the same object' do let(:other) { object } it { is_expected.to eql(true) } it 'is symmetric' do is_expected.to eql(other == object) end end context 'with an equivalent object' do let(:other) { object.dup } it { is_expected.to eql(true) } it 'is symmetric' do is_expected.to eql(other == object) end end context 'with an equivalent object of a subclass' do let(:other) { ::Class.new(klass).new(value) } it { is_expected.to eql(true) } it 'is not symmetric' do # LSP, any equality for type should work for subtype but # not the other way is_expected.not_to eql(other == object) end end context 'with an equivalent object of a superclass' do let(:other) { super_klass.new(value) } it { is_expected.to eql(false) } it 'is not symmetric' do is_expected.not_to eql(other == object) end end context 'with an object with a different interface' do let(:other) { Object.new } it { is_expected.to eql(false) } end context 'with an object of another class' do let(:other) { Class.new.new } it { is_expected.to eql(false) } it 'is symmetric' do is_expected.to eql(other == object) end end end equatable-0.6.1/spec/equatable/eql_spec.rb0000644000175000017500000000167713611565275020102 0ustar boutilboutil# frozen_string_literal: true RSpec.describe Equatable, '#eql?' do let(:name) { 'Value' } let(:value) { 11 } let(:klass) { ::Class.new do include Equatable attr_reader :value def initialize(value) @value = value end end } let(:object) { klass.new(value) } subject { object.eql?(other) } context 'with the same object' do let(:other) { object } it { is_expected.to eql(true) } it 'is symmetric' do is_expected.to eql(other.eql?(object)) end end context 'with an equivalent object' do let(:other) { object.dup } it { is_expected.to eql(true) } it 'is symmetric' do is_expected.to eql(other.eql?(object)) end end context 'with an equivalent object of a subclass' do let(:other) { ::Class.new(klass).new(value) } it { is_expected.to eql(false) } it 'is symmetric' do is_expected.to eql(other.eql?(object)) end end end equatable-0.6.1/lib/0000755000175000017500000000000013611565275013620 5ustar boutilboutilequatable-0.6.1/lib/equatable/0000755000175000017500000000000013611565275015563 5ustar boutilboutilequatable-0.6.1/lib/equatable/version.rb0000644000175000017500000000011013611565275017565 0ustar boutilboutil# frozen_string_literal: true module Equatable VERSION = '0.6.1' end equatable-0.6.1/lib/equatable.rb0000644000175000017500000000657013611565275016120 0ustar boutilboutil# frozen_string_literal: true require 'equatable/version' # Make it easy to define equality and hash methods. module Equatable # Hook into module inclusion. # # @param [Module] base # the module or class including Equatable # # @return [self] # # @api private def self.included(base) super base.extend(self) base.class_eval do include Methods define_methods end end # Holds all attributes used for comparison. # # @return [Array] # # @api private attr_reader :comparison_attrs # Objects that include this module are assumed to be value objects. # It is also assumed that the only values that affect the results of # equality comparison are the values of the object's attributes. # # @param [Array] *args # # @return [undefined] # # @api public def attr_reader(*args) super comparison_attrs.concat(args) end # Copy the comparison_attrs into the subclass. # # @param [Class] subclass # # @api private def inherited(subclass) super subclass.instance_variable_set(:@comparison_attrs, comparison_attrs.dup) end private # Define all methods needed for ensuring object's equality. # # @return [undefined] # # @api private def define_methods define_comparison_attrs define_compare define_hash define_inspect end # Define class instance #comparison_attrs as an empty array. # # @return [undefined] # # @api private def define_comparison_attrs instance_variable_set('@comparison_attrs', []) end # Define a #compare? method to check if the receiver is the same # as the other object. # # @return [undefined] # # @api private def define_compare define_method(:compare?) do |comparator, other| klass = self.class attrs = klass.comparison_attrs attrs.all? do |attr| other.respond_to?(attr) && send(attr).send(comparator, other.send(attr)) end end end # Define a hash method that ensures that the hash value is the same for # the same instance attributes and their corresponding values. # # @api private def define_hash define_method(:hash) do klass = self.class attrs = klass.comparison_attrs ([klass] + attrs.map { |attr| send(attr) }).hash end end # Define an inspect method that shows the class name and the values for the # instance's attributes. # # @return [undefined] # # @api private def define_inspect define_method(:inspect) do klass = self.class name = klass.name || klass.inspect attrs = klass.comparison_attrs "#<#{name}#{attrs.map { |attr| " #{attr}=#{send(attr).inspect}" }.join}>" end end # The equality methods module Methods # Compare two objects for equality based on their value # and being an instance of the given class. # # @param [Object] other # the other object in comparison # # @return [Boolean] # # @api public def eql?(other) instance_of?(other.class) && compare?(__method__, other) end # Compare two objects for equality based on their value # and being a subclass of the given class. # # @param [Object] other # the other object in comparison # # @return [Boolean] # # @api public def ==(other) other.is_a?(self.class) && compare?(__method__, other) end end # Methods end # Equatable equatable-0.6.1/examples/0000755000175000017500000000000013611565275014670 5ustar boutilboutilequatable-0.6.1/examples/point.rb0000644000175000017500000000154413611565275016352 0ustar boutilboutil$:.unshift File.join(File.dirname(__FILE__), '../lib') require 'equatable' class Point include Equatable attr_reader :x, :y def initialize(x, y) @x, @y = x, y end end class ColorPoint < Point attr_reader :color def initialize(x, y, color) super(x, y) @color = color end end point_1 = Point.new(1, 1) point_2 = Point.new(1, 1) point_3 = Point.new(2, 1) puts point_1 == point_2 puts point_1.hash == point_2.hash puts point_1.eql?(point_2) puts point_1.equal?(point_2) puts point_1 == point_3 puts point_1.hash == point_3.hash puts point_1.eql?(point_3) puts point_1.equal?(point_3) puts point_1.inspect point = Point.new(1, 1) color_point = ColorPoint.new(1, 1, :red) puts 'Subtypes' puts point == color_point puts color_point == point puts point.hash == color_point.hash puts point.eql?(color_point) puts point.equal?(color_point) equatable-0.6.1/equatable.gemspec0000644000175000017500000000305713611565275016367 0ustar boutilboutillib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'equatable/version' Gem::Specification.new do |spec| spec.name = "equatable" spec.version = Equatable::VERSION spec.authors = ["Piotr Murach"] spec.email = ["me@piotrmurach.com"] spec.summary = %q{Extends Ruby objects with equality comparison and inspection methods.} spec.description = %q{Extends Ruby objects with equality comparison and inspection methods. By including this module, a class indicates that its instances have explicit general contracts for `hash`, `==` and `eql?` methods.} spec.homepage = "https://github.com/piotrmurach/equatable" spec.license = "MIT" if spec.respond_to?(:metadata=) spec.metadata = { "allowed_push_host" => "https://rubygems.org", "bug_tracker_uri" => "#{spec.homepage}/issues", "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md", "documentation_uri" => "https://www.rubydoc.info/gems/equatable", "homepage_uri" => spec.homepage, "source_code_uri" => spec.homepage } end spec.files = Dir['{lib,spec,examples}/**/*.rb'] spec.files += Dir['tasks/*', 'equatable.gemspec'] spec.files += Dir['README.md', 'CHANGELOG.md', 'LICENSE.txt', 'Rakefile'] spec.require_paths = ["lib"] spec.required_ruby_version = '>= 1.8.7' spec.add_development_dependency 'bundler', '>= 1.5.0' spec.add_development_dependency 'rspec', '~> 3.1' spec.add_development_dependency 'rake' end equatable-0.6.1/Rakefile0000644000175000017500000000025713611565275014523 0ustar boutilboutil# encoding: utf-8 require "bundler/gem_tasks" FileList['tasks/**/*.rake'].each { |task| import task } task :default => [:spec] desc 'Run all specs' task :ci => %w[ spec ] equatable-0.6.1/README.md0000644000175000017500000000760113611565275014335 0ustar boutilboutil# Equatable [![Gem Version](https://badge.fury.io/rb/equatable.svg)][gem] [![Build Status](https://secure.travis-ci.org/piotrmurach/equatable.svg?branch=master)][travis] [![Build status](https://ci.appveyor.com/api/projects/status/lsb02nm0g4c6guiu?svg=true)][appveyor] [![Code Climate](https://codeclimate.com/github/piotrmurach/equatable/badges/gpa.svg)][codeclimate] [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/equatable/badge.svg)][coverage] [![Inline docs](http://inch-ci.org/github/piotrmurach/equatable.svg?branch=master)][inchpages] [gem]: http://badge.fury.io/rb/equatable [travis]: http://travis-ci.org/piotrmurach/equatable [appveyor]: https://ci.appveyor.com/project/piotrmurach/equatable [codeclimate]: https://codeclimate.com/github/piotrmurach/equatable [coverage]: https://coveralls.io/github/piotrmurach/equatable [inchpages]: http://inch-ci.org/github/piotrmurach/equatable Allows ruby objects to implement equality comparison and inspection methods. By including this module, a class indicates that its instances have explicit general contracts for `hash`, `==` and `eql?` methods. Specifically `eql?` contract requires that it implements an equivalence relation. By default each instance of the class is equal only to itself. This is a right behaviour when you have distinct objects. However, it is the responsibility of any class to clearly define their equality. Failure to do so may prevent instances to behave as expected when for instance `Array#uniq` is invoked or when they are used as `Hash` keys. ## Installation Add this line to your application's Gemfile: gem 'equatable' And then execute: $ bundle Or install it yourself as: $ gem install equatable ## Usage It is assumed that your objects are value objects and the only values that affect equality comparison are the ones specified by your attribute readers. Each attribute reader should be a significant field in determining objects values. ```ruby class Point include Equatable attr_reader :x, :y def initialize(x, y) @x, @y = x, y end end point_1 = Point.new(1, 1) point_2 = Point.new(1, 1) point_3 = Point.new(1, 2) point_1 == point_2 # => true point_1.hash == point_2.hash # => true point_1.eql?(point_2) # => true point_1.equal?(point_2) # => false point_1 == point_3 # => false point_1.hash == point_3.hash # => false point_1.eql?(point_3) # => false point_1.equal?(point_3) # => false point_1.inspect # => "#" ``` ## Attributes It is important that the attribute readers should allow for performing deterministic computations on class instances. Therefore you should avoid specifying attributes that depend on unreliable resources like IP address that require network access. ## Subtypes **Equatable** ensures that any important property of a type holds for its subtypes. However, please note that adding an extra attribute reader to a subclass will violate the equivalence contract, namely, the superclass will be equal to the subclass but reverse won't be true. For example: ```ruby class ColorPoint < Point attr_reader :color def initialize(x, y, color) super(x, y) @color = color end end point = Point.new(1, 1) color_point = ColorPoint.new(1, 1, :red) point == color_point # => true color_point == point # => false point.hash == color_point.hash # => false point.eql?(color_point) # => false point.equal?(color_point) # => false ``` The `ColorPoint` class demonstrates that extending a class with extra value property does not preserve the `equals` contract. ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request ## Copyright Copyright (c) 2012 Piotr Murach. See LICENSE for further details. equatable-0.6.1/LICENSE.txt0000644000175000017500000000205413611565275014676 0ustar boutilboutilCopyright (c) 2012 Piotr Murach MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.equatable-0.6.1/CHANGELOG.md0000644000175000017500000000147713611565275014674 0ustar boutilboutil# Change log ## [v0.6.1] - 2019-06-26 ### Added * Add license key to gemspec ## [v0.6.0] - 2019-06-16 ### Added * Add dev dependencies to gemspec ### Changed * Change to limit Ruby >= 1.8.7 * Change gemspec to load files directly instead of git ## [v0.1.0] - 2012-12-09 * Initial implementation and release [v0.6.1]: https://github.com/piotrmurach/equatable/compare/v0.6.0...v0.6.1 [v0.6.0]: https://github.com/piotrmurach/equatable/compare/v0.5.0...v0.6.0 [v0.5.0]: https://github.com/piotrmurach/equatable/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/piotrmurach/equatable/compare/v0.3.0...v0.4.0 [v0.3.0]: https://github.com/piotrmurach/equatable/compare/v0.2.0...v0.3.0 [v0.2.0]: https://github.com/piotrmurach/equatable/compare/v0.1.0...v0.2.0 [v0.1.0]: https://github.com/piotrmurach/equatable/compare/v0.1.0