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 +17 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +146 -0
- data/Rakefile +9 -0
- data/equivalence.gemspec +20 -0
- data/lib/equivalence/version.rb +3 -0
- data/lib/equivalence.rb +51 -0
- data/spec/equivalence/equivalence_spec.rb +103 -0
- data/spec/spec_helper.rb +1 -0
- metadata +91 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
data/equivalence.gemspec
ADDED
@@ -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
|
data/lib/equivalence.rb
ADDED
@@ -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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|