equivalence 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in equivalence.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ernie Miller
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Equivalence
2
+
3
+ Because implementing object equality wasn't easy enough already.
4
+
5
+ Do your objects recognize their equals? If you have complete control over how
6
+ your objects are used, maybe you don't care. If you're writing code for others
7
+ to reuse, though, your code might be leaving your users perplexed.
8
+
9
+ Consider the following situation:
10
+
11
+ ```ruby
12
+ class Awesomeness
13
+ def initialize(level, description)
14
+ @level = level
15
+ @description = description
16
+ end
17
+
18
+ def declare_awesomeness
19
+ puts "My awesomeness level is #{@level} (#{@description})!"
20
+ end
21
+ end
22
+
23
+ awesome1 = Awesomeness.new(10, 'really awesome')
24
+ awesome2 = Awesomeness.new(10, 'really awesome')
25
+ awesome1.declare_awesomeness
26
+ # => "My awesomeness level is 10 (really awesome)!"
27
+ awesome2.declare_awesomeness
28
+ # => "My awesomeness level is 10 (really awesome)!"
29
+ [awesome1, awesome2].uniq.size # => 2
30
+ awesome1 == awesome2 # => false
31
+ ```
32
+
33
+ Surprised? You shouldn't be. Ruby's default implementation of object equality
34
+ considers objects equal only if they are the same object, *not* if they have the
35
+ same contents.
36
+
37
+ This probably isn't what you want for your Awesomeness class. To get equality
38
+ behaving as you'd expect, you need to do the following:
39
+
40
+ ```ruby
41
+ class Awesomeness
42
+ attr_reader :level, :description
43
+
44
+ def hash
45
+ [@level, @description].hash
46
+ end
47
+
48
+ def eql?(other)
49
+ self.class == other.class &&
50
+ self.level == other.level &&
51
+ self.description == other.description
52
+ end
53
+ alias :== :eql?
54
+ end
55
+ ```
56
+
57
+ Implementing the `==` method gets your comparison to return true, as expected,
58
+ and implementing `hash` and `eql?` gets `Array#uniq` to behave as expected, and
59
+ also lets you use your values as Hash keys in a way that works properly with
60
+ `Hash#[]`, `Hash#[]=`, `Hash#merge` and the like.
61
+
62
+ Have more instance variables? You'll need to add them to the `hash` and `eql?`
63
+ methods. Have other custom objects as instance variables? They'll need to
64
+ implement these methods, too.
65
+
66
+ It can get to feel a lot like busy work, and let's face it, if we liked doing
67
+ busy work, we'd be using Java.
68
+
69
+ ## Installation
70
+
71
+ Add this line to your application's Gemfile:
72
+
73
+ gem 'equivalence'
74
+
75
+ And then execute:
76
+
77
+ $ bundle
78
+
79
+ Or install it yourself as:
80
+
81
+ $ gem install equivalence
82
+
83
+ ## Usage
84
+
85
+ ### Basic
86
+
87
+ ```ruby
88
+ class MySpiffyClass
89
+ extend Equivalence
90
+ equivalence :@my, :@instance, :@variables # , [...]
91
+ # Your spiffy class implementation
92
+ end
93
+ ```
94
+
95
+ You'll get the equality methods we "painstakingly" added above, without all that
96
+ pesky typing. If you don't implement reader methods (as above), Equivalence will
97
+ create some for you, with `protected` access (meaning only other objects within
98
+ MySpiffyClass's class hierarchy will be able to call them), since they're
99
+ necessary for the `eql?` method to work. Defining your own readers? No problem,
100
+ Equivalence won't mess with them.
101
+
102
+ Let's re-visit the example from above.
103
+
104
+ ```ruby
105
+ class Awesomeness
106
+ extend Equivalence
107
+ equivalence :@level, :@description
108
+
109
+ def initialize(level, description)
110
+ @level = level
111
+ @description = description
112
+ end
113
+
114
+ def declare_awesomeness
115
+ puts "My awesomeness level is #{@level} (#{@description})!"
116
+ end
117
+ end
118
+
119
+ awesome1 = Awesomeness.new(10, 'really awesome')
120
+ awesome2 = Awesomeness.new(10, 'really awesome')
121
+ [awesome1, awesome2].uniq.size # => 1
122
+ awesome1 == awesome2 # => true
123
+ ```
124
+
125
+ Less hassle, same result.
126
+
127
+ ### "Advanced" (if there is such a thing, for such a simple library)
128
+
129
+ Maybe your attribute readers aren't named the same as your instance variables,
130
+ because you like to confuse people. Or maybe, your readers are lazy-loading
131
+ certain instance variables or doing some casting of Fixnums to Strings. In that
132
+ case, you'll want your `hash` method to be defined with calls to the methods
133
+ instead of accessing the ivars directly, to get the expected results. Just omit
134
+ the leading @ in each parameter, like so:
135
+
136
+ ```ruby
137
+ equivalence :level, :description
138
+ ```
139
+
140
+ ## Contributing
141
+
142
+ 1. Fork it
143
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
144
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
145
+ 4. Push to the branch (`git push origin my-new-feature`)
146
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec) do |rspec|
6
+ rspec.rspec_opts = ['--backtrace']
7
+ end
8
+
9
+ task :default => :spec
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/equivalence/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ernie Miller"]
6
+ gem.email = ["ernie@erniemiller.org"]
7
+ gem.description = %q{Implement object equality by extending a module and calling a macro. Now you have no excuse for not doing it.}
8
+ gem.summary = %q{Because implementing object equality wasn't easy enough already.}
9
+ gem.homepage = "http://github.com/ernie/equivalence"
10
+
11
+ gem.add_development_dependency 'rspec', '~> 2.11.0'
12
+ gem.add_development_dependency 'rake'
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.name = "equivalence"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = Equivalence::VERSION
20
+ end
@@ -0,0 +1,3 @@
1
+ module Equivalence
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,51 @@
1
+ require "equivalence/version"
2
+
3
+ module Equivalence
4
+
5
+ private
6
+
7
+ def equivalence(*args)
8
+ raise ArgumentError, 'At least one attribute is required.' if args.empty?
9
+ args.map!(&:to_s)
10
+ method_names = args.map { |arg| arg.sub /^@/, '' }
11
+
12
+ __define_equivalence_hash_method(args)
13
+ __define_equivalence_attribute_readers(method_names)
14
+ __define_equivalence_equality_methods(method_names)
15
+ end
16
+
17
+ def __define_equivalence_hash_method(ivar_or_method_names)
18
+ # Method names might be keywords. We'll want to prefix them with "self"
19
+ ivar_or_method_names = ivar_or_method_names.map do |name|
20
+ name.start_with?('@') ? name : "self.#{name}"
21
+ end
22
+
23
+ class_eval <<-EVAL, __FILE__, __LINE__
24
+ def hash
25
+ [#{ivar_or_method_names.join(', ')}].hash
26
+ end
27
+ EVAL
28
+ end
29
+
30
+ def __define_equivalence_attribute_readers(method_names)
31
+ method_names.each do |method|
32
+ unless method_defined?(method)
33
+ class_eval <<-EVAL, __FILE__, __LINE__
34
+ attr_reader :#{method} unless private_method_defined?(:#{method})
35
+ protected :#{method}
36
+ EVAL
37
+ end
38
+ end
39
+ end
40
+
41
+ def __define_equivalence_equality_methods(method_names)
42
+ class_eval <<-EVAL, __FILE__, __LINE__
43
+ def eql?(other)
44
+ self.class == other.class &&
45
+ #{method_names.map {|m| "self.#{m} == other.#{m}"}.join(" &&\n")}
46
+ end
47
+ alias :== :eql?
48
+ EVAL
49
+ end
50
+
51
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe Equivalence do
4
+
5
+ it 'requires at least one attribute as an argument' do
6
+ expect {
7
+ klass = Class.new do
8
+ extend Equivalence
9
+ equivalence
10
+ end
11
+ }.to raise_error ArgumentError
12
+ end
13
+
14
+ it 'accepts method names as arguments' do
15
+ klass = Class.new do
16
+ extend Equivalence
17
+ attr_accessor :var1, :var2
18
+ equivalence :var1, :var2
19
+ end
20
+ k1 = klass.new
21
+ k1.var1 = 1
22
+ k1.var2 = 2
23
+ k2 = klass.new
24
+ k2.var1 = 1
25
+ k2.var2 = 2
26
+ [k1, k2].uniq.should have(1).item
27
+ end
28
+
29
+ it 'accepts instance variable names as arguments' do
30
+ klass = Class.new do
31
+ extend Equivalence
32
+ attr_accessor :var1, :var2
33
+ equivalence :var1, :var2
34
+ end
35
+ k1 = klass.new
36
+ k1.var1 = 1
37
+ k1.var2 = 2
38
+ k2 = klass.new
39
+ k2.var1 = 1
40
+ k2.var2 = 2
41
+ [k1, k2].uniq.should have(1).item
42
+ end
43
+
44
+ it 'creates a valid hash method if a keyword is used' do
45
+ klass = Class.new do
46
+ extend Equivalence
47
+ attr_accessor :alias
48
+ equivalence :alias
49
+ end
50
+ k1 = klass.new
51
+ k1.alias = 'bob'
52
+ k2 = klass.new
53
+ k2.alias = 'bob'
54
+ [k1, k2].uniq.should have(1).item
55
+ end
56
+
57
+ it 'defines protected attribute readers if not already defined' do
58
+ klass = Class.new do
59
+ extend Equivalence
60
+ equivalence :@var
61
+ def initialize(var)
62
+ @var = var
63
+ end
64
+ end
65
+ klass.protected_method_defined?(:var).should be_true
66
+ end
67
+
68
+ it 'does not alter access of already-accessible methods' do
69
+ klass = Class.new do
70
+ extend Equivalence
71
+ attr_reader :var
72
+ equivalence :@var
73
+ def initialize(var)
74
+ @var = var
75
+ end
76
+ end
77
+ klass.public_method_defined?(:var).should be_true
78
+ klass.protected_method_defined?(:var).should be_false
79
+ end
80
+
81
+ it 'does not overwrite a private reader method, but makes it protected' do
82
+ # Not that it's likely that you're going to call equivalence in the order
83
+ # shown here. Still, better safe than sorry. What you do *after* you call
84
+ # equivalence is your problem, but we don't want to "unexpectedly" overwrite
85
+ # anything.
86
+ klass = Class.new do
87
+ extend Equivalence
88
+ def initialize(var)
89
+ @var = var
90
+ end
91
+ private
92
+ def var
93
+ 'zomg'
94
+ end
95
+ equivalence :@var
96
+ end
97
+ klass.protected_method_defined?(:var).should be_true
98
+ klass.private_method_defined?(:var).should be_false
99
+ klass.new(1).send(:var).should eq 'zomg'
100
+ end
101
+
102
+ end
103
+
@@ -0,0 +1 @@
1
+ require 'equivalence'
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: equivalence
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ernie Miller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.11.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.11.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Implement object equality by extending a module and calling a macro.
47
+ Now you have no excuse for not doing it.
48
+ email:
49
+ - ernie@erniemiller.org
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - .travis.yml
56
+ - Gemfile
57
+ - LICENSE
58
+ - README.md
59
+ - Rakefile
60
+ - equivalence.gemspec
61
+ - lib/equivalence.rb
62
+ - lib/equivalence/version.rb
63
+ - spec/equivalence/equivalence_spec.rb
64
+ - spec/spec_helper.rb
65
+ homepage: http://github.com/ernie/equivalence
66
+ licenses: []
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 1.8.24
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: Because implementing object equality wasn't easy enough already.
89
+ test_files:
90
+ - spec/equivalence/equivalence_spec.rb
91
+ - spec/spec_helper.rb