type_is_enum 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,49 @@
1
+ # ------------------------------------------------------------
2
+ # RSpec
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+
7
+ namespace :spec do
8
+
9
+ desc 'Run all unit tests'
10
+ RSpec::Core::RakeTask.new(:unit) do |task|
11
+ task.rspec_opts = %w(--color --format documentation --order default)
12
+ task.pattern = 'unit/**/*_spec.rb'
13
+ end
14
+
15
+ task all: [:unit]
16
+ end
17
+
18
+ desc 'Run all tests'
19
+ task spec: 'spec:all'
20
+
21
+ # ------------------------------------------------------------
22
+ # Coverage
23
+
24
+ desc 'Run all unit tests with coverage'
25
+ task :coverage do
26
+ ENV['COVERAGE'] = 'true'
27
+ Rake::Task['spec:unit'].execute
28
+ end
29
+
30
+ # ------------------------------------------------------------
31
+ # RuboCop
32
+
33
+ require 'rubocop/rake_task'
34
+ RuboCop::RakeTask.new
35
+
36
+ # ------------------------------------------------------------
37
+ # TODOs
38
+
39
+ desc 'List TODOs (from spec/todo.rb)'
40
+ RSpec::Core::RakeTask.new(:todo) do |task|
41
+ task.rspec_opts = %w(--color --format documentation --order default)
42
+ task.pattern = 'todo.rb'
43
+ end
44
+
45
+ # ------------------------------------------------------------
46
+ # Defaults
47
+
48
+ desc 'Run unit tests, check test coverage, run acceptance tests, check code style'
49
+ task default: [:coverage, :rubocop]
@@ -0,0 +1,3 @@
1
+ module TypeIsEnum
2
+ Dir.glob(File.expand_path('../type_is_enum/*.rb', __FILE__)).sort.each(&method(:require))
3
+ end
@@ -0,0 +1,137 @@
1
+ # A Ruby implementation of Joshua Bloch's
2
+ # [typesafe enum pattern](http://www.oracle.com/technetwork/java/page1-139488.html#replaceenums)
3
+ module TypeIsEnum
4
+ # Base class for typesafe enum classes.
5
+ class Enum
6
+ include Comparable
7
+
8
+ class << self
9
+
10
+ # Returns an array of the enum instances in declaration order
11
+ # @return [Array<self>] All instances of this enum, in declaration order
12
+ def to_a
13
+ as_array.dup
14
+ end
15
+
16
+ # Returns the number of enum instances
17
+ # @return [Integer] the number of instances
18
+ def size
19
+ as_array ? as_array.length : 0
20
+ end
21
+
22
+ # Iterates over the set of enum instances
23
+ # @yield [self] Each instance of this enum, in declaration order
24
+ # @return [Array<self>] All instances of this enum, in declaration order
25
+ def each(&block)
26
+ to_a.each(&block)
27
+ end
28
+
29
+ # Iterates over the set of enum instances
30
+ # @yield [self, Integer] Each instance of this enum, in declaration order,
31
+ # with its ordinal index
32
+ # @return [Array<self>] All instances of this enum, in declaration order
33
+ def each_with_index(&block)
34
+ to_a.each_with_index(&block)
35
+ end
36
+
37
+ # Iterates over the set of enum instances
38
+ # @yield [self] Each instance of this enum, in declaration order
39
+ # @return [Array] An array containing the result of applying `&block`
40
+ # to each instance of this enum, in instance declaration order
41
+ def map(&block)
42
+ to_a.map(&block)
43
+ end
44
+
45
+ # Looks up an enum instance based on its key
46
+ # @param key [Symbol] the key to look up
47
+ # @return [self, nil] the corresponding enum instance, or nil
48
+ def find_by_key(key)
49
+ by_key[key]
50
+ end
51
+
52
+ # Looks up an enum instance based on its ordinal
53
+ # @param ord [Integer] the ordinal to look up
54
+ # @return [self, nil] the corresponding enum instance, or nil
55
+ def find_by_ord(ord)
56
+ return nil if ord < 0 || ord > size
57
+ as_array[ord]
58
+ end
59
+
60
+ private
61
+
62
+ def add(key, *args, &block)
63
+ fail TypeError, "#{key} is not a symbol" unless key.is_a?(Symbol)
64
+ obj = new(*args)
65
+ obj.instance_variable_set :@key, key
66
+ obj.instance_variable_set :@ord, size
67
+ class_exec(obj) do |instance|
68
+ register(instance)
69
+ instance.instance_eval(&block) if block_given?
70
+ end
71
+ end
72
+
73
+ def by_key
74
+ @by_key ||= {}
75
+ end
76
+
77
+ def as_array
78
+ @as_array ||= []
79
+ end
80
+
81
+ def valid_key(instance)
82
+ key = instance.key
83
+ if find_by_key(key)
84
+ warn("ignoring redeclaration of #{name}::#{key} (source: #{caller[4]})")
85
+ nil
86
+ else
87
+ key
88
+ end
89
+ end
90
+
91
+ def register(instance)
92
+ key = valid_key(instance)
93
+ return unless key
94
+
95
+ const_set(key.to_s, instance)
96
+ by_key[key] = instance
97
+ as_array << instance
98
+ end
99
+ end
100
+
101
+ # The symbol key for the enum instance
102
+ # @return [Symbol] the key
103
+ attr_reader :key
104
+
105
+ # The ordinal of the enum instance, in declaration order
106
+ # @return [Integer] the ordinal
107
+ attr_reader :ord
108
+
109
+ # Compares two instances of the same enum class based on their declaration order
110
+ # @param other [self] the enum instance to compare
111
+ # @return [Integer, nil] -1 if this value precedes `other`; 0 if the two are
112
+ # the same enum instance; 1 if this value follows `other`; `nil` if `other`
113
+ # is not an instance of this enum class
114
+ def <=>(other)
115
+ ord <=> other.ord if self.class == other.class
116
+ end
117
+
118
+ # Generates a Fixnum hash value for this enum instance
119
+ # @return [Fixnum] the hash value
120
+ def hash
121
+ @hash ||= begin
122
+ result = 17
123
+ result = 31 * result + self.class.hash
124
+ result = 31 * result + ord
125
+ result.is_a?(Fixnum) ? result : result.hash
126
+ end
127
+ end
128
+
129
+ def name
130
+ key.to_s
131
+ end
132
+
133
+ def to_s
134
+ "#{self.class}::#{key} [#{ord}]"
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,10 @@
1
+ module TypeIsEnum
2
+ # The name of this gem
3
+ NAME = 'type_is_enum'
4
+
5
+ # The version of this gem
6
+ VERSION = '0.2.0'
7
+
8
+ # The copyright notice for this gem
9
+ COPYRIGHT = 'Copyright (c) 2016 The Regents of the University of California, Andrew Shcheglov'
10
+ end
@@ -0,0 +1,9 @@
1
+ module TypeIsEnum
2
+ class ValueEnum < Enum
3
+ def initialize(value)
4
+ @value = value
5
+ end
6
+
7
+ attr_reader :value
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ inherit_from: ../.rubocop.yml
2
+
3
+ Metrics/MethodLength:
4
+ Enabled: false
5
+
6
+ Metrics/ModuleLength:
7
+ Enabled: false
8
+
9
+ Style/ClassAndModuleChildren:
10
+ Enabled: false
@@ -0,0 +1,31 @@
1
+ # ------------------------------------------------------------
2
+ # SimpleCov setup
3
+
4
+ if ENV['COVERAGE']
5
+ require 'simplecov'
6
+ require 'simplecov-console'
7
+
8
+ SimpleCov.minimum_coverage 100
9
+ SimpleCov.start do
10
+ add_filter '/spec/'
11
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
12
+ SimpleCov::Formatter::HTMLFormatter,
13
+ SimpleCov::Formatter::Console,
14
+ ]
15
+ end
16
+ end
17
+
18
+ # ------------------------------------------------------------
19
+ # Rspec configuration
20
+
21
+ require 'rspec'
22
+
23
+ RSpec.configure do |config|
24
+ config.raise_errors_for_deprecations!
25
+ config.mock_with :rspec
26
+ end
27
+
28
+ # ------------------------------------------------------------
29
+ # TypesafeEnum
30
+
31
+ require 'type_is_enum'
@@ -0,0 +1,367 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ class Suit < TypeIsEnum::Enum
5
+ add :CLUBS
6
+ add :DIAMONDS
7
+ add :HEARTS
8
+ add :SPADES
9
+ end
10
+
11
+ class Tarot < TypeIsEnum::ValueEnum
12
+ add :CUPS, 'Cups'
13
+ add :COINS, 'Coins'
14
+ add :WANDS, 'Wands'
15
+ add :SWORDS, 'Swords'
16
+ end
17
+
18
+ class RGBColor < TypeIsEnum::ValueEnum
19
+ add :RED, :red
20
+ add :GREEN, :green
21
+ add :BLUE, :blue
22
+ end
23
+
24
+ class Scale < TypeIsEnum::ValueEnum
25
+ add :DECA, 10
26
+ add :HECTO, 100
27
+ add :KILO, 1_000
28
+ add :MEGA, 1_000_000
29
+ end
30
+
31
+ class Car < TypeIsEnum::Enum
32
+ def initialize(price, coolness)
33
+ @price = price
34
+ @coolness = coolness
35
+ end
36
+
37
+ attr_reader :price, :coolness
38
+
39
+ add :Audi, 25_000, 4
40
+ add :Mercedes, 30_000, 6
41
+ add :Toyota, 10_000, 2
42
+ end
43
+
44
+ module TypeIsEnum
45
+ describe Enum do
46
+
47
+ it 'allows custom constructors with multiple arguments' do
48
+ expect(Car.to_a).to contain_exactly(
49
+ have_attributes(price: 25_000, coolness: 4),
50
+ have_attributes(price: 30_000, coolness: 6),
51
+ have_attributes(price: 10_000, coolness: 2)
52
+ )
53
+ end
54
+
55
+ it 'has a name that is a key as string' do
56
+ expect(Car.to_a.map(&:name)).to eq(['Audi', 'Mercedes', 'Toyota'])
57
+ end
58
+
59
+ describe '::add' do
60
+ it ' adds a constant enum value' do
61
+ enum = Suit::CLUBS
62
+ expect(enum).to be_a(Suit)
63
+ end
64
+
65
+ it 'insists symbols be symbols' do
66
+ expect do
67
+ class ::StringKeys < ValueEnum
68
+ add 'spades', 'spades'
69
+ end
70
+ end.to raise_error(TypeError)
71
+ expect(::StringKeys.to_a).to be_empty
72
+ end
73
+
74
+ it 'insists symbols be uppercase' do
75
+ expect do
76
+ class ::LowerCaseKeys < ValueEnum
77
+ add :spades, 'spades'
78
+ end
79
+ end.to raise_error(NameError)
80
+ expect(::LowerCaseKeys.to_a).to be_empty
81
+ end
82
+
83
+ it 'disallows nil keys' do
84
+ expect do
85
+ class ::NilKeys < ValueEnum
86
+ add nil, 'nil'
87
+ end
88
+ end.to raise_error(TypeError)
89
+ expect(::NilKeys.to_a).to be_empty
90
+ end
91
+
92
+ it 'allows, but ignores redeclaration of identical instances' do
93
+ class ::IdenticalInstances < ValueEnum
94
+ add :SPADES, 'spades'
95
+ end
96
+ expect(::IdenticalInstances).to receive(:warn).with(a_string_matching(/ignoring redeclaration of IdenticalInstances::SPADES/))
97
+ class ::IdenticalInstances < ValueEnum
98
+ add :SPADES, 'spades'
99
+ end
100
+ expect(::IdenticalInstances.to_a).to eq([::IdenticalInstances::SPADES])
101
+ end
102
+
103
+ it 'is private' do
104
+ expect { Tarot.add(:PENTACLES) }.to raise_error(NoMethodError)
105
+ end
106
+ end
107
+
108
+ describe '::to_a' do
109
+ it 'returns the values as an array' do
110
+ expect(Suit.to_a).to eq([Suit::CLUBS, Suit::DIAMONDS, Suit::HEARTS, Suit::SPADES])
111
+ end
112
+ it 'returns a copy' do
113
+ array = Suit.to_a
114
+ array.clear
115
+ expect(array.empty?).to eq(true)
116
+ expect(Suit.to_a).to eq([Suit::CLUBS, Suit::DIAMONDS, Suit::HEARTS, Suit::SPADES])
117
+ end
118
+ end
119
+
120
+ describe '::size' do
121
+ it 'returns the number of enum instnaces' do
122
+ expect(Suit.size).to eq(4)
123
+ end
124
+ end
125
+
126
+ describe '::each' do
127
+ it 'iterates the enum values' do
128
+ expected = [Suit::CLUBS, Suit::DIAMONDS, Suit::HEARTS, Suit::SPADES]
129
+ index = 0
130
+ Suit.each do |s|
131
+ expect(s).to be(expected[index])
132
+ index += 1
133
+ end
134
+ end
135
+ end
136
+
137
+ describe '::each_with_index' do
138
+ it 'iterates the enum values with indices' do
139
+ expected = [Suit::CLUBS, Suit::DIAMONDS, Suit::HEARTS, Suit::SPADES]
140
+ Suit.each_with_index do |s, index|
141
+ expect(s).to be(expected[index])
142
+ end
143
+ end
144
+ end
145
+
146
+ describe '::map' do
147
+ it 'maps enum values' do
148
+ all_keys = Suit.map(&:key)
149
+ expect(all_keys).to eq([:CLUBS, :DIAMONDS, :HEARTS, :SPADES])
150
+ end
151
+ end
152
+
153
+ describe '#<=>' do
154
+ it 'orders enum instances' do
155
+ Suit.each_with_index do |s1, i1|
156
+ Suit.each_with_index do |s2, i2|
157
+ expect(s1 <=> s2).to eq(i1 <=> i2)
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ describe '#==' do
164
+ it 'returns true for identical instances, false otherwise' do
165
+ Suit.each do |s1|
166
+ Suit.each do |s2|
167
+ identical = s1.equal?(s2)
168
+ expect(s1 == s2).to eq(identical)
169
+ end
170
+ end
171
+ end
172
+
173
+ it 'reports instances as unequal to nil and other objects' do
174
+ a_nil_value = nil
175
+ Suit.each do |s|
176
+ expect(s.nil?).to eq(false)
177
+ expect(s == a_nil_value).to eq(false)
178
+ Tarot.each do |t|
179
+ expect(s == t).to eq(false)
180
+ expect(t == s).to eq(false)
181
+ end
182
+ end
183
+ end
184
+
185
+ it 'survives marshalling' do
186
+ Suit.each do |s1|
187
+ dump = Marshal.dump(s1)
188
+ s2 = Marshal.load(dump)
189
+ expect(s1 == s2).to eq(true)
190
+ expect(s2 == s1).to eq(true)
191
+ end
192
+ end
193
+ end
194
+
195
+ describe '#!=' do
196
+ it 'returns false for identical instances, true otherwise' do
197
+ Suit.each do |s1|
198
+ Suit.each do |s2|
199
+ different = !s1.equal?(s2)
200
+ expect(s1 != s2).to eq(different)
201
+ end
202
+ end
203
+ end
204
+
205
+ it 'reports instances as unequal to nil and other objects' do
206
+ a_nil_value = nil
207
+ Suit.each do |s|
208
+ expect(s != a_nil_value).to eq(true)
209
+ Tarot.each do |t|
210
+ expect(s != t).to eq(true)
211
+ expect(t != s).to eq(true)
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ describe '#hash' do
218
+ it 'gives consistent values' do
219
+ Suit.each do |s1|
220
+ Suit.each do |s2|
221
+ expect(s1.hash == s2.hash).to eq(s1 == s2)
222
+ end
223
+ end
224
+ end
225
+
226
+ it 'returns different values for different types' do
227
+ class Suit2 < ValueEnum
228
+ add :CLUBS, 'Clubs'
229
+ add :DIAMONDS, 'Diamonds'
230
+ add :HEARTS, 'Hearts'
231
+ add :SPADES, 'Spades'
232
+ end
233
+
234
+ Suit.each do |s1|
235
+ s2 = Suit2.find_by_key(s1.key)
236
+ expect(s1.hash == s2.hash).to eq(false)
237
+ end
238
+ end
239
+
240
+ it 'survives marshalling' do
241
+ Suit.each do |s1|
242
+ dump = Marshal.dump(s1)
243
+ s2 = Marshal.load(dump)
244
+ expect(s2.hash).to eq(s1.hash)
245
+ end
246
+ end
247
+
248
+ it 'always returns a Fixnum' do
249
+ Suit.each do |s1|
250
+ expect(s1.hash).to be_a(Fixnum)
251
+ end
252
+ end
253
+ end
254
+
255
+ describe '#eql?' do
256
+ it 'is consistent with #hash' do
257
+ Suit.each do |s1|
258
+ Suit.each do |s2|
259
+ expect(s1.eql?(s2)).to eq(s1.hash == s2.hash)
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ describe '#key' do
266
+ it 'returns the symbol key of the enum instance' do
267
+ expected = [:CLUBS, :DIAMONDS, :HEARTS, :SPADES]
268
+ Suit.each_with_index do |s, index|
269
+ expect(s.key).to eq(expected[index])
270
+ end
271
+ end
272
+ end
273
+
274
+ describe '#ord' do
275
+ it 'returns the ord value of the enum instance' do
276
+ Suit.each_with_index do |s, index|
277
+ expect(s.ord).to eq(index)
278
+ end
279
+ end
280
+ end
281
+
282
+ describe '#to_s' do
283
+ it 'provides an informative string' do
284
+ aggregate_failures 'informative string' do
285
+ [Suit, Tarot, RGBColor, Scale].each do |ec|
286
+ ec.each do |ev|
287
+ result = ev.to_s
288
+ [ec.to_s, ev.key, ev.ord].each do |info|
289
+ expect(result).to include("#{info}")
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ describe '::find_by_key' do
298
+ it 'maps symbol keys to enum instances' do
299
+ keys = [:CLUBS, :DIAMONDS, :HEARTS, :SPADES]
300
+ expected = Suit.to_a
301
+ keys.each_with_index do |k, index|
302
+ expect(Suit.find_by_key(k)).to be(expected[index])
303
+ end
304
+ end
305
+
306
+ it 'returns nil for invalid keys' do
307
+ expect(Suit.find_by_key(:WANDS)).to be_nil
308
+ end
309
+ end
310
+
311
+ describe '::find_by_ord' do
312
+ it 'maps ordinal indices to enum instances' do
313
+ Suit.each do |s|
314
+ expect(Suit.find_by_ord(s.ord)).to be(s)
315
+ end
316
+ end
317
+
318
+ it 'returns nil for negative indices' do
319
+ expect(Suit.find_by_ord(-1)).to be_nil
320
+ end
321
+
322
+ it 'returns nil for out-of-range indices' do
323
+ expect(Suit.find_by_ord(Suit.size)).to be_nil
324
+ end
325
+
326
+ it 'returns nil for invalid indices' do
327
+ expect(Suit.find_by_ord(100)).to be_nil
328
+ end
329
+ end
330
+
331
+ it 'supports case statements' do
332
+ def pip(suit)
333
+ case suit
334
+ when Suit::CLUBS
335
+ '♣'
336
+ when Suit::DIAMONDS
337
+ '♦'
338
+ when Suit::HEARTS
339
+ '♥'
340
+ when Suit::SPADES
341
+ '♠'
342
+ else
343
+ fail "unknown suit: #{self}"
344
+ end
345
+ end
346
+
347
+ expect(Suit.map { |s| pip(s) }).to eq(%w(♣ ♦ ♥ ♠))
348
+ end
349
+
350
+ it 'supports "inner class" methods' do
351
+ class Operation < ValueEnum
352
+ add(:PLUS, '+') do
353
+ def eval(x, y)
354
+ x + y
355
+ end
356
+ end
357
+ add(:MINUS, '-') do
358
+ def eval(x, y)
359
+ x - y
360
+ end
361
+ end
362
+ end
363
+
364
+ expect(Operation.map { |op| op.eval(39, 23) }).to eq([39 + 23, 39 - 23])
365
+ end
366
+ end
367
+ end