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 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 ok for a little while. Later, though, we might want to add a little logic around the `Job`'s status, something like:
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 job notifier class or something
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 this implementing both in a single class becomes an [SRP][http://en.wikipedia.org/wiki/Single_responsibility_principle] violation.
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`):
@@ -1,3 +1,3 @@
1
1
  module EnumeratedType
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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, /private method `new' called/)
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, /private method `declare' called/)
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, /duplicate name/)
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, /can't modify frozen/)
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, "Property name 'name' is not allowed")
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.1.2
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: 2013-12-12 00:00:00.000000000 Z
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: -1744150559039667923
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: -1744150559039667923
102
+ hash: -1874679306341587618
103
103
  requirements: []
104
104
  rubyforge_project:
105
105
  rubygems_version: 1.8.26