enumerated_type 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +17 -4
- data/lib/enumerated_type/version.rb +1 -1
- data/lib/enumerated_type.rb +10 -0
- data/test/enumerated_type_spec.rb +36 -5
- metadata +4 -4
data/README.md
CHANGED
@@ -27,10 +27,10 @@ class Job
|
|
27
27
|
end
|
28
28
|
```
|
29
29
|
|
30
|
-
At first pass this seems fine. Any code that needs to act based on a job's status has to have magic symbols (i.e. `job.status == :success`), but maybe that's
|
30
|
+
At first pass this seems fine. Any code that needs to act based on a job's status has to have magic symbols (i.e. `job.status == :success`), but maybe that's OK for a little while. Later, though, we might want to add a little logic around the `Job`'s status, something like:
|
31
31
|
|
32
32
|
```ruby
|
33
|
-
# In a
|
33
|
+
# In something like a JobNotifier class
|
34
34
|
if job.status == :failure or job.status == :success
|
35
35
|
# Email user to let them know about their job
|
36
36
|
end
|
@@ -75,9 +75,9 @@ end
|
|
75
75
|
|
76
76
|
There are some advantages to this approach:
|
77
77
|
|
78
|
-
1. The list of all the possible statuses lives in *only one place*. I think it's way easier to look at the `JobStatus` class and see what the possible statuses are than it is to hunt through the `Job` class Looking for symbols assigned to `@status
|
78
|
+
1. The list of all the possible statuses lives in *only one place*. I think it's way easier to look at the `JobStatus` class and see what the possible statuses are than it is to hunt through the `Job` class Looking for symbols assigned to `@status` (or, even worse, to look *outside* the `Job` class looking for assignments to Job#status).
|
79
79
|
2. It's now possible to interact with the list of all legal statuses programmatically (perhaps in an admin console that has a drop down for of all statuses so that they may be manually updated).
|
80
|
-
3. We can separate behavior (methods) that apply to the enumerated types from the classes that include one of these types (`Job` in our example). Although the status handling code in our version of `Job` was fairly simple, it often becomes clear that handling the status of a job is a very separate concern from the actual processing of a `Job`, and
|
80
|
+
3. We can separate behavior (methods) that apply to the enumerated types from the classes that include one of these types (`Job` in our example). Although the status handling code in our version of `Job` was fairly simple, it often becomes clear that handling the status of a job is a very separate concern from the actual processing of a `Job`, and thus implementing both in a single class becomes a [Single Responsibility Principle](http://en.wikipedia.org/wiki/Single_responsibility_principle) violation.
|
81
81
|
4. By separating the `JobStatus` behavior from the `Job`, we're able to more easily respond to change in requirements around the `JobStatus` in the future. Perhaps at some point we'll want to transform `JobStatus` into a state machine where only certain transitions are allowed, or include code that audits the change of state, etc.
|
82
82
|
|
83
83
|
"But all did was explain enumerated types, the kind that are present all over the place in other languages." Yep. That is correct. The only downside of the approach shown is that you have to re-create very similar, one-off implementations of an enumerated type. The `EnumeratedType` gem is just a clean and simple Ruby implementation of a well known concept.
|
@@ -162,6 +162,19 @@ end
|
|
162
162
|
JobStatus::SUCCESS.message # => "Your job has completed"
|
163
163
|
```
|
164
164
|
|
165
|
+
Coerce from other types:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
JobStatus.coerce(:pending) # => #<JobStatus:pending>
|
169
|
+
JobStatus.coerce("pending") # => #<JobStatus:pending>
|
170
|
+
JobStatus.coerce(JobStatus::PENDING) # => #<JobStatus:pending>
|
171
|
+
JobStatus.coerce(nil) # => raises a TypeError
|
172
|
+
JobStatus.coerce(1) # => raises a TypeError
|
173
|
+
JobStatus.coerce(:wrong) # => raises an ArgumentError
|
174
|
+
```
|
175
|
+
|
176
|
+
`.coerce` is particularly useful for scrubbing parameters, allowing you to succinctly assert that arguments are valid for your `EnumeratedType`, while also broadening the range of types that can be used as input. Using `.coerce` at the boundaries of your code allows clients the freedom to pass in full fledged `EnumeratedType` objects, symbols or even strings, and allows you to use the `.coerce`d input with confidence (i.e without any type or validity checking beyond the call to `.coerce`).
|
177
|
+
|
165
178
|
## Development
|
166
179
|
|
167
180
|
To run the tests (assuming you have already run `gem install bundler`):
|
data/lib/enumerated_type.rb
CHANGED
@@ -56,6 +56,16 @@ module EnumeratedType
|
|
56
56
|
map(&:name).include?(name)
|
57
57
|
end
|
58
58
|
|
59
|
+
def coerce(coercable)
|
60
|
+
case
|
61
|
+
when coercable.class == self then coercable
|
62
|
+
when coercable.respond_to?(:to_sym) then self[coercable.to_sym]
|
63
|
+
when coercable.respond_to?(:to_str) then self[coercable.to_str.to_sym]
|
64
|
+
else
|
65
|
+
raise TypeError, "#{coercable.inspect} cannot be coerced into a #{self.name}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
59
69
|
private
|
60
70
|
|
61
71
|
def declare(name, options = {})
|
@@ -14,7 +14,7 @@ describe EnumeratedType do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
it "privatizes the constructor" do
|
17
|
-
lambda { Gender.new }.must_raise(NoMethodError
|
17
|
+
lambda { Gender.new }.must_raise(NoMethodError)
|
18
18
|
end
|
19
19
|
|
20
20
|
it "is enumerable" do
|
@@ -64,12 +64,12 @@ describe EnumeratedType do
|
|
64
64
|
|
65
65
|
describe ".declare" do
|
66
66
|
it "is private" do
|
67
|
-
lambda { Gender.declare }.must_raise(NoMethodError
|
67
|
+
lambda { Gender.declare }.must_raise(NoMethodError)
|
68
68
|
end
|
69
69
|
|
70
70
|
it "requires the name to be unique" do
|
71
71
|
duplicate_name = Gender.first.name
|
72
|
-
lambda { Gender.send(:declare, duplicate_name) }.must_raise(ArgumentError
|
72
|
+
lambda { Gender.send(:declare, duplicate_name) }.must_raise(ArgumentError)
|
73
73
|
end
|
74
74
|
|
75
75
|
it "produces frozen instances" do
|
@@ -81,7 +81,7 @@ describe EnumeratedType do
|
|
81
81
|
end
|
82
82
|
|
83
83
|
it "freezes properties" do
|
84
|
-
lambda { Gender::MALE.planet.replace("pluto") }.must_raise(RuntimeError
|
84
|
+
lambda { Gender::MALE.planet.replace("pluto") }.must_raise(RuntimeError)
|
85
85
|
end
|
86
86
|
|
87
87
|
it "does not expose public setters for properties" do
|
@@ -96,7 +96,7 @@ describe EnumeratedType do
|
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
99
|
-
name_property_definition.must_raise(ArgumentError
|
99
|
+
name_property_definition.must_raise(ArgumentError)
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
@@ -121,6 +121,37 @@ describe EnumeratedType do
|
|
121
121
|
end
|
122
122
|
end
|
123
123
|
|
124
|
+
describe ".coerce" do
|
125
|
+
it "returns the correct type if given a recognized symbol" do
|
126
|
+
Gender.coerce(:female).must_equal Gender::FEMALE
|
127
|
+
end
|
128
|
+
|
129
|
+
it "returns the correct type if given a recognized string" do
|
130
|
+
Gender.coerce("female").must_equal Gender::FEMALE
|
131
|
+
end
|
132
|
+
|
133
|
+
it "returns the correct type if given something that responds to #to_str" do
|
134
|
+
stringlike = Object.new
|
135
|
+
def stringlike.to_str
|
136
|
+
"female"
|
137
|
+
end
|
138
|
+
|
139
|
+
Gender.coerce(stringlike).must_equal Gender::FEMALE
|
140
|
+
end
|
141
|
+
|
142
|
+
it "returns the object unmodified if given an instance of the enumerated type" do
|
143
|
+
Gender.coerce(Gender::FEMALE).must_equal Gender::FEMALE
|
144
|
+
end
|
145
|
+
|
146
|
+
it "raises a TypeError if given something that isn't coercable" do
|
147
|
+
lambda { Gender.coerce(Object.new) }.must_raise(TypeError)
|
148
|
+
end
|
149
|
+
|
150
|
+
it "raises a ArgumentError if given something coercable but not recognized" do
|
151
|
+
lambda { Gender.coerce(:neuter) }.must_raise(ArgumentError)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
124
155
|
describe "#inspect" do
|
125
156
|
it "looks reasonable" do
|
126
157
|
Gender::FEMALE.inspect.must_equal "#<Gender:female>"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: enumerated_type
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2014-01-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -90,7 +90,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
90
|
version: '0'
|
91
91
|
segments:
|
92
92
|
- 0
|
93
|
-
hash: -
|
93
|
+
hash: -1874679306341587618
|
94
94
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
95
|
none: false
|
96
96
|
requirements:
|
@@ -99,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
99
|
version: '0'
|
100
100
|
segments:
|
101
101
|
- 0
|
102
|
-
hash: -
|
102
|
+
hash: -1874679306341587618
|
103
103
|
requirements: []
|
104
104
|
rubyforge_project:
|
105
105
|
rubygems_version: 1.8.26
|