type_is_enum 0.2.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.
@@ -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