good 0.1.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/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +234 -0
- data/Rakefile +1 -0
- data/good.gemspec +23 -0
- data/lib/good.rb +71 -0
- data/spec/good_spec.rb +199 -0
- metadata +103 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Rafer Hazen
|
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,234 @@
|
|
1
|
+
# Good
|
2
|
+
|
3
|
+
2 little things that make writing good Ruby programs a little easier.
|
4
|
+
|
5
|
+
1. `Good::Value` is a class generator for simple, pleasant [Value objects](http://en.wikipedia.org/wiki/Value_object).
|
6
|
+
|
7
|
+
2. `Good::Record` is a class generator for simple, pleasant [Record objects](http://en.wikipedia.org/wiki/Record_(computer_science) "Record Objects"). They're a lot like `Struct`.
|
8
|
+
|
9
|
+
Both are used the same way same way, like this:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class Person < Good::Value.new(:name, :age)
|
13
|
+
end
|
14
|
+
```
|
15
|
+
|
16
|
+
or like this if you prefer:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
Person = Good::Value.new(:name, :age)
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
Now, we can create a `Person`:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
person = Person.new(:name => "Mrs. Betty Slocombe", :age => 46)
|
27
|
+
```
|
28
|
+
|
29
|
+
and ask it about itself:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
person.name # => "Mrs. Betty Slocombe"
|
33
|
+
person.age # => 46
|
34
|
+
```
|
35
|
+
|
36
|
+
`Good::Value` objects are immutable:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
person.name = "Captain Stephen Peacock" #=> NoMethodError: undefined method `name=' for ...
|
40
|
+
```
|
41
|
+
|
42
|
+
But don't worry, you can still get what you want:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
person = person.merge(:name => "Captain Stephen Peacock")
|
46
|
+
person.name # => "Captain Stephen Peacock"
|
47
|
+
```
|
48
|
+
|
49
|
+
Most of the time immutable is good. If you don't want that though, try
|
50
|
+
`Good::Record`:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class Person < Good::Record.new(:name, :age)
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
Now we can mutate the object:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
person = Person.new(:name => "Mrs. Betty Slocombe", :age => 46)
|
61
|
+
person.age = 30
|
62
|
+
person.age # => 30
|
63
|
+
```
|
64
|
+
|
65
|
+
Except for mutability `Good::Value` and `Good::Record` have the same interface.
|
66
|
+
|
67
|
+
Don't forget, `Good::Value` and `Good::Record` are just regular Ruby objects,
|
68
|
+
so they get to have methods just like everybody else:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
Person < Good::Value.new(:name, :age)
|
72
|
+
def introduction
|
73
|
+
"My name is #{name} and I'm #{age} years old"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
Also, classes created with `Good::Value` and `Good::Record` have reasonable
|
79
|
+
implmentations of `#==`, `#eql?` and `#hash`.
|
80
|
+
|
81
|
+
## Bonus Features
|
82
|
+
|
83
|
+
You can ask `Good::Value` and `Good::Record` a little about their structure:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
person.new(:name => "Miss Brahms", :age => 30)
|
87
|
+
|
88
|
+
Person::MEMBERS # => [:name, :age]
|
89
|
+
person.members # => [:name, :age]
|
90
|
+
person.values # => ["Miss Brahms", 30]
|
91
|
+
person.attributes # => {:name => "Miss Mrahms", :age => 30}
|
92
|
+
```
|
93
|
+
|
94
|
+
You can call `Person.coerce` to coerce input to a `Person` in the follwoing
|
95
|
+
ways:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
# from a Hash (creates a new Person)
|
99
|
+
Person.coerce(:name => "Mr. Ernest Grainge") # => #<Person:0x007fbe9121d048 @name="Mr. Ernest Grainge">
|
100
|
+
|
101
|
+
# from a Person (returns the input unmodified)
|
102
|
+
person = Person.new(:name => "Mr. Cuthbert Rumbold")
|
103
|
+
Person.coerce(person) # => #<Person:0x007fbe920270f8 @name="Mr. Cuthbert Rumbold">
|
104
|
+
|
105
|
+
# from something wrong
|
106
|
+
Person.coerce("WRONG") # => TypeError: Unable to coerce String into Person
|
107
|
+
```
|
108
|
+
|
109
|
+
`.coerce` is particularly useful at code boundaries. It allows clients to pass
|
110
|
+
options as a hash if they want to, while allowing you to use the type you
|
111
|
+
expected confidently (because blatantly incorrect values raise a `TypeError`).
|
112
|
+
|
113
|
+
## Motivation
|
114
|
+
|
115
|
+
Why does the world need this?
|
116
|
+
|
117
|
+
### `Good` vs Regular Ruby Objects
|
118
|
+
|
119
|
+
Creating value classes is a good idea. Properly used, they make testing easier,
|
120
|
+
help with separation of concerns and make interfaces more apparent. In Ruby, we
|
121
|
+
like to do stuff with as little ceremony as possible. So what's wrong with a
|
122
|
+
regular class:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class Person
|
126
|
+
attr_accessor :age, :name
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
Nothing, really. The only problem is that, in order to get an object that's
|
131
|
+
easy to work with, you'll probably want to implement `#initialize`, `#==`,
|
132
|
+
`#eql?` and `#hash`. This isn't really so bad, but if you want to quickly create
|
133
|
+
a number of these classes, the boilerplate code gets heavy pretty quickly. Plus
|
134
|
+
you'll probably do it wrong the first time (I certainly did).
|
135
|
+
|
136
|
+
It's worth noting that `Good` in no way seeks to become the foundation of your
|
137
|
+
domain model. The second a class outgrows it's `Good::Value` or `Good::Record`
|
138
|
+
roots, by all means you should remove `Good` from the picture and rely on pure
|
139
|
+
Ruby classes instead. `Good` helps you get started quickly by making a
|
140
|
+
particular pattern easy, but when your classes get more mature, it's time for
|
141
|
+
`Good` to go.
|
142
|
+
|
143
|
+
### `Good` vs `Hash`
|
144
|
+
|
145
|
+
In general, passing hashes around in your application is a bad idea, unless the
|
146
|
+
data they contain is truly unstructured. Since you can't add methods to hashes
|
147
|
+
(unless you subclass them, which is perhaps its own variety of bad idea), a
|
148
|
+
little bit of the logic to deal with these "structured" hashes gets spread
|
149
|
+
around a lot of places.
|
150
|
+
|
151
|
+
With a hash it can also be hard to figure out exactly what it is expected to
|
152
|
+
contain. In many cases the passing of a hash with specific expectations about
|
153
|
+
its contents is an indication that you're missing a class. Hopefully, prudent
|
154
|
+
application of `Good::Value` and `Good::Record` will allow you extract that
|
155
|
+
class more quickly, with minimal extra work.
|
156
|
+
|
157
|
+
However, this is not to say that you should not use a hash at the boundary
|
158
|
+
between client and library, or between various modules in your system. For
|
159
|
+
example, say we've got an `Authenticator` class that takes a user's credentials:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
class Authenticator
|
163
|
+
def initialize(credentials)
|
164
|
+
username = credentials[:username]
|
165
|
+
password = credentials[:password]
|
166
|
+
end
|
167
|
+
|
168
|
+
def authentic?
|
169
|
+
...
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
This is a perfectly reasonable interface for a client to use:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
authenticator = Authenticator.new({
|
178
|
+
:username => "m.grace@gracebrothers.com",
|
179
|
+
:password => "rUbngS3rvd"
|
180
|
+
})
|
181
|
+
|
182
|
+
login if authenticator.authentic?
|
183
|
+
```
|
184
|
+
|
185
|
+
The following implementation maintains the same interface for the client and
|
186
|
+
adds very little code:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class Authenticator
|
190
|
+
Credentials = Good::Value.new(:username, :password)
|
191
|
+
|
192
|
+
def initialize(credentials)
|
193
|
+
@credentials = Credentials.coerce(credentials)
|
194
|
+
end
|
195
|
+
|
196
|
+
def authentic?
|
197
|
+
...
|
198
|
+
end
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
Say now that the the Authenticator needs to pass the user's credentials to
|
203
|
+
another component (to log the attempt, for example), we are now in the enviable
|
204
|
+
position of having an object, with a well defined interface to pass around -
|
205
|
+
not a hash with implicit assumptions about its contents. Further, because of
|
206
|
+
the `.coerce` method we can now accept a hash at the boundary or a fully formed
|
207
|
+
`Credentials` object, it makes no difference to the `Authenticator`.
|
208
|
+
|
209
|
+
This evolulution seems fairly common. To solve an immediate problem, a new
|
210
|
+
`Good::Value` class is created inside the namespace of an existing class, which
|
211
|
+
is at first desirable because it does not inflict this abstraction externally.
|
212
|
+
Then, as the class begins to interact with other compontents in the system,
|
213
|
+
this previously internal class can be made external and evolved into it's own
|
214
|
+
fully fledged domain object (perhaps shedding `Good` in the process). When you
|
215
|
+
start with a hash, it can be harder to spot the "missing" class.
|
216
|
+
|
217
|
+
## Installation
|
218
|
+
|
219
|
+
Add this line to your application's Gemfile:
|
220
|
+
|
221
|
+
gem 'good'
|
222
|
+
|
223
|
+
And then execute:
|
224
|
+
|
225
|
+
$ bundle
|
226
|
+
|
227
|
+
Or install it yourself as:
|
228
|
+
|
229
|
+
$ gem install good
|
230
|
+
|
231
|
+
## Tests
|
232
|
+
|
233
|
+
bundle && bundle exec rake
|
234
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/good.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'good'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "good"
|
8
|
+
spec.version = Good::VERSION
|
9
|
+
spec.authors = ["Rafer Hazen"]
|
10
|
+
spec.email = ["rafer@ralua.com"]
|
11
|
+
spec.summary = %q{Good::Value and Good::Record}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "rspec", "~> 2.0 "
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/good.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
class Good
|
2
|
+
VERSION = "0.1.0"
|
3
|
+
|
4
|
+
class Value
|
5
|
+
def self.new(*members, &block)
|
6
|
+
Good.generate(false, *members, &block)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Record
|
11
|
+
def self.new(*members, &block)
|
12
|
+
Good.generate(true, *members, &block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.generate(mutable, *members, &block)
|
17
|
+
Class.new do
|
18
|
+
|
19
|
+
mutable ? attr_accessor(*members) : attr_reader(*members)
|
20
|
+
|
21
|
+
const_set(:MEMBERS, members.dup.freeze)
|
22
|
+
|
23
|
+
def self.coerce(coercable)
|
24
|
+
case coercable
|
25
|
+
when self then coercable
|
26
|
+
when Hash then new(coercable)
|
27
|
+
else raise TypeError, "Unable to coerce #{coercable.class} into #{self}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method(:initialize) do |attributes={}|
|
32
|
+
if mutable
|
33
|
+
attributes.each { |k, v| send("#{k}=", v) }
|
34
|
+
else
|
35
|
+
attributes.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def attributes
|
40
|
+
{}.tap { |h| self.class::MEMBERS.each { |m| h[m] = send(m) } }
|
41
|
+
end
|
42
|
+
|
43
|
+
def members
|
44
|
+
self.class::MEMBERS.dup
|
45
|
+
end
|
46
|
+
|
47
|
+
def values
|
48
|
+
self.class::MEMBERS.map { |m| send(m) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def merge(attributes={})
|
52
|
+
self.class.new(self.attributes.merge(attributes))
|
53
|
+
end
|
54
|
+
|
55
|
+
def ==(other)
|
56
|
+
other.is_a?(self.class) && attributes == other.attributes
|
57
|
+
end
|
58
|
+
|
59
|
+
def eql?(other)
|
60
|
+
self == other
|
61
|
+
end
|
62
|
+
|
63
|
+
def hash
|
64
|
+
attributes.hash
|
65
|
+
end
|
66
|
+
|
67
|
+
class_eval(&block) if block
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
data/spec/good_spec.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
shared_examples :good do
|
4
|
+
before do
|
5
|
+
class Person < described_class.new(:name, :age)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
Object.send(:remove_const, :Person)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#initialize" do
|
14
|
+
it "accepts values via hash in the constructor" do
|
15
|
+
person = Person.new(:name => "Bob")
|
16
|
+
expect(person.name).to eq("Bob")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "accepts string keys" do
|
20
|
+
person = Person.new("name" => "Bob")
|
21
|
+
expect(person.name).to eq("Bob")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "allows 0 argument construction" do
|
25
|
+
person = Person.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#==" do
|
30
|
+
it "is true if all the parameters are ==" do
|
31
|
+
bob_1 = Person.new(:name => "Bob", :age => 50)
|
32
|
+
bob_2 = Person.new(:name => "Bob", :age => 50)
|
33
|
+
|
34
|
+
expect(bob_1).to eq(bob_2)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "is false if any attributes are not #==" do
|
38
|
+
bob = Person.new(:name => "Bob", :age => 50)
|
39
|
+
ted = Person.new(:name => "Ted", :age => 50)
|
40
|
+
|
41
|
+
expect(bob).not_to eq(ted)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "is false if the other object is not of the same class" do
|
45
|
+
bob = Person.new(:name => "Bob", :age => 50)
|
46
|
+
alien_bob = described_class.new(:name, :age).new(:name => "Bob", :age => 50)
|
47
|
+
|
48
|
+
expect(bob).not_to eq(alien_bob)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#eql" do
|
53
|
+
it "is true if all the parameters are ==" do
|
54
|
+
bob_1 = Person.new(:name => "Bob", :age => 50)
|
55
|
+
bob_2 = Person.new(:name => "Bob", :age => 50)
|
56
|
+
|
57
|
+
expect(bob_1).to eql(bob_2)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "is false if any attributes are not #==" do
|
61
|
+
bob = Person.new(:name => "Bob", :age => 50)
|
62
|
+
ted = Person.new(:name => "Ted", :age => 50)
|
63
|
+
|
64
|
+
expect(bob).not_to eql(ted)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "is false if the other object is not of the same class" do
|
68
|
+
bob = Person.new(:name => "Bob", :age => 50)
|
69
|
+
alien_bob = Struct.new(:name, :age).new("Bob", 50)
|
70
|
+
|
71
|
+
expect(bob).not_to eql(alien_bob)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#hash" do
|
76
|
+
it "is stable" do
|
77
|
+
bob_1 = Person.new(:name => "Bob")
|
78
|
+
bob_2 = Person.new(:name => "Bob")
|
79
|
+
|
80
|
+
expect(bob_1.hash).to eq(bob_2.hash)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "varies with the parameters" do
|
84
|
+
bob = Person.new(:name => "Bob", :age => 50)
|
85
|
+
ted = Person.new(:name => "Ted", :age => 50)
|
86
|
+
|
87
|
+
expect(bob.hash).not_to eql(ted.hash)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "::MEMBERS" do
|
92
|
+
it "is the list of member variables" do
|
93
|
+
expect(Person::MEMBERS).to eq([:name, :age])
|
94
|
+
end
|
95
|
+
|
96
|
+
it "is frozen" do
|
97
|
+
expect { Person::MEMBERS << :height }.to raise_error(/can't modify frozen/)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#members" do
|
102
|
+
it "is the list of member variables" do
|
103
|
+
person = Person.new
|
104
|
+
expect(person.members).to eq([:name, :age])
|
105
|
+
end
|
106
|
+
|
107
|
+
it "is modifiable without affecting the original members" do
|
108
|
+
person = Person.new
|
109
|
+
person.members << :height
|
110
|
+
expect(person.members).to eq([:name, :age])
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "#values" do
|
115
|
+
it "is the list of values (in the same order as the #members)" do
|
116
|
+
person = Person.new(:age => 50, :name => "BOB")
|
117
|
+
expect(person.values).to eq(["BOB", 50])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "#attributes" do
|
122
|
+
it "is a hash of the attributes (with symbol keys)" do
|
123
|
+
person = Person.new(:name => "Bob", :age => 50)
|
124
|
+
expect(person.attributes).to eq(:name => "Bob", :age => 50)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "#merge" do
|
129
|
+
it "returns an object with the given properties modified" do
|
130
|
+
young = Person.new(:name => "Bob", :age => 50)
|
131
|
+
old = young.merge(:age => 51)
|
132
|
+
|
133
|
+
expect(old.name).to eq("Bob")
|
134
|
+
expect(old.age).to eq(51)
|
135
|
+
end
|
136
|
+
|
137
|
+
it "does not mutate the old object" do
|
138
|
+
person = Person.new(:name => "Bob", :age => 50)
|
139
|
+
person.merge(:age => 51)
|
140
|
+
|
141
|
+
expect(person.age).to eq(50)
|
142
|
+
end
|
143
|
+
|
144
|
+
it "accepts 0 arguments" do
|
145
|
+
person = Person.new
|
146
|
+
expect(person.merge).not_to be(person)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe ".coerce" do
|
151
|
+
it "returns the input unmodified if it is already an instance of the struct" do
|
152
|
+
person = Person.new
|
153
|
+
expect(Person.coerce(person)).to be(person)
|
154
|
+
end
|
155
|
+
|
156
|
+
it "initializes a new instance if the input is a hash" do
|
157
|
+
person = Person.coerce({:name => "Bob"})
|
158
|
+
expect(person).to eq(Person.new(:name => "Bob"))
|
159
|
+
end
|
160
|
+
|
161
|
+
it "raises a TypeError otherwise" do
|
162
|
+
expect { Person.coerce("15 lbs of squirrel fur") }.to raise_error(TypeError)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "block construction" do
|
167
|
+
let(:car_klass) do
|
168
|
+
described_class.new(:wheels) do
|
169
|
+
def drive
|
170
|
+
"Driving with all #{wheels} wheels!"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
it "allows definition of methods" do
|
176
|
+
car = car_klass.new(:wheels => 4)
|
177
|
+
expect(car.drive).to eq("Driving with all 4 wheels!")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe Good::Value do
|
183
|
+
include_examples(:good)
|
184
|
+
|
185
|
+
it "is immutable" do
|
186
|
+
person = Person.new
|
187
|
+
expect { person.name = "Bob" }.to raise_error(NoMethodError)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
describe Good::Record do
|
192
|
+
include_examples(:good)
|
193
|
+
|
194
|
+
it "is mutable" do
|
195
|
+
person = Person.new
|
196
|
+
expect { person.name = "Bob" }.to change { person.name }.to("Bob")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: good
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Rafer Hazen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-05-15 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.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.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.5'
|
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: '1.5'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description:
|
63
|
+
email:
|
64
|
+
- rafer@ralua.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE.txt
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- good.gemspec
|
75
|
+
- lib/good.rb
|
76
|
+
- spec/good_spec.rb
|
77
|
+
homepage: ''
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 1.8.23
|
99
|
+
signing_key:
|
100
|
+
specification_version: 3
|
101
|
+
summary: Good::Value and Good::Record
|
102
|
+
test_files:
|
103
|
+
- spec/good_spec.rb
|