ruby-enum 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Enum
5
+ ##
6
+ # Adds a method to an enum class that allows for exhaustive matching on a value.
7
+ #
8
+ # @example
9
+ # class Color
10
+ # include Ruby::Enum
11
+ # include Ruby::Enum::Case
12
+ #
13
+ # define :RED, :red
14
+ # define :GREEN, :green
15
+ # define :BLUE, :blue
16
+ # define :YELLOW, :yellow
17
+ # end
18
+ #
19
+ # Color.case(Color::RED, {
20
+ # [Color::RED, Color::GREEN] => -> { "red or green" },
21
+ # Color::BLUE => -> { "blue" },
22
+ # Color::YELLOW => -> { "yellow" },
23
+ # })
24
+ #
25
+ # Reserves the :else key for a default case:
26
+ # Color.case(Color::RED, {
27
+ # [Color::RED, Color::GREEN] => -> { "red or green" },
28
+ # else: -> { "blue or yellow" },
29
+ # })
30
+ module Case
31
+ def self.included(klass)
32
+ klass.extend(ClassMethods)
33
+ end
34
+
35
+ ##
36
+ # @see Ruby::Enum::Case
37
+ module ClassMethods
38
+ class ValuesNotDefinedError < StandardError
39
+ end
40
+
41
+ class NotAllCasesHandledError < StandardError
42
+ end
43
+
44
+ def case(value, cases)
45
+ validate_cases(cases)
46
+
47
+ filtered_cases = cases.select do |values, _proc|
48
+ values = [values] unless values.is_a?(Array)
49
+ values.include?(value)
50
+ end
51
+
52
+ return call_proc(cases[:else], value) if filtered_cases.none?
53
+
54
+ results = filtered_cases.map { |_values, proc| call_proc(proc, value) }
55
+
56
+ # Return the first result if there is only one result
57
+ results.size == 1 ? results.first : results
58
+ end
59
+
60
+ private
61
+
62
+ def call_proc(proc, value)
63
+ return if proc.nil?
64
+
65
+ if proc.arity == 1
66
+ proc.call(value)
67
+ else
68
+ proc.call
69
+ end
70
+ end
71
+
72
+ def validate_cases(cases)
73
+ all_values = cases.keys.flatten - [:else]
74
+ else_defined = cases.key?(:else)
75
+ superfluous_values = all_values - values
76
+ missing_values = values - all_values
77
+
78
+ raise ValuesNotDefinedError, "Value(s) not defined: #{superfluous_values.join(', ')}" if superfluous_values.any?
79
+ raise NotAllCasesHandledError, "Not all cases handled: #{missing_values.join(', ')}" if missing_values.any? && !else_defined
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ module Ruby
5
+ module Enum
6
+ ##
7
+ # Mock I18n module in case the i18n gem is not available.
8
+ module I18nMock
9
+ def self.load_path
10
+ []
11
+ end
12
+
13
+ def self.translate(key, _options = {})
14
+ key
15
+ end
16
+ end
17
+ end
18
+ end
19
+ # :nocov:
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Ruby
4
4
  module Enum
5
+ class << self
6
+ # Needed for I18n mock
7
+ attr_accessor :i18n
8
+ end
9
+
5
10
  attr_reader :key, :value
6
11
 
7
12
  def initialize(key, value)
@@ -144,9 +149,7 @@ module Ruby
144
149
  end
145
150
 
146
151
  def to_h
147
- Hash[@_enum_hash.map do |key, enum|
148
- [key, enum.value]
149
- end]
152
+ @_enum_hash.transform_values(&:value)
150
153
  end
151
154
 
152
155
  private
@@ -22,13 +22,13 @@ module Ruby
22
22
  @summary = create_summary(key, attributes)
23
23
  @resolution = create_resolution(key, attributes)
24
24
 
25
- "\nProblem:\n #{@problem}"\
25
+ "\nProblem:\n #{@problem}" \
26
26
  "\nSummary:\n #{@summary}" + "\nResolution:\n #{@resolution}"
27
27
  end
28
28
 
29
29
  private
30
30
 
31
- BASE_KEY = 'ruby.enum.errors.messages' #:nodoc:
31
+ BASE_KEY = 'ruby.enum.errors.messages' # :nodoc:
32
32
 
33
33
  # Given the key of the specific error and the options hash, translate the
34
34
  # message.
@@ -39,7 +39,7 @@ module Ruby
39
39
  #
40
40
  # Returns a localized error message string.
41
41
  def translate(key, options)
42
- ::I18n.translate("#{BASE_KEY}.#{key}", **{ locale: :en }.merge(options)).strip
42
+ Ruby::Enum.i18n.translate("#{BASE_KEY}.#{key}", locale: :en, **options).strip
43
43
  end
44
44
 
45
45
  # Create the problem.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ruby
4
4
  module Enum
5
- VERSION = '0.9.0'
5
+ VERSION = '1.0.0'
6
6
  end
7
7
  end
data/lib/ruby-enum.rb CHANGED
@@ -1,11 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n'
4
-
5
3
  require 'ruby-enum/version'
6
4
  require 'ruby-enum/enum'
5
+ require 'ruby-enum/enum/case'
6
+ require 'ruby-enum/enum/i18n_mock'
7
+
8
+ # Try to load the I18n gem and provide a mock if it is not available.
9
+ begin
10
+ require 'i18n'
11
+ Ruby::Enum.i18n = I18n
12
+ rescue LoadError
13
+ # I18n is not available
14
+ # :nocov:
15
+ # Tests for this loading are in the spec_i18n folder
16
+ Ruby::Enum.i18n = Ruby::Enum::I18nMock
17
+ # :nocov:
18
+ end
7
19
 
8
- I18n.load_path << File.join(File.dirname(__FILE__), 'config', 'locales', 'en.yml')
20
+ Ruby::Enum.i18n.load_path << File.join(File.dirname(__FILE__), 'config', 'locales', 'en.yml')
9
21
 
10
22
  require 'ruby-enum/errors/base'
11
23
  require 'ruby-enum/errors/uninitialized_constant_error'
data/ruby-enum.gemspec CHANGED
@@ -10,10 +10,11 @@ Gem::Specification.new do |s|
10
10
  s.email = 'dblock@dblock.org'
11
11
  s.platform = Gem::Platform::RUBY
12
12
  s.required_rubygems_version = '>= 1.3.6'
13
+ s.required_ruby_version = '>= 2.7'
13
14
  s.files = Dir['**/*']
14
15
  s.require_paths = ['lib']
15
16
  s.homepage = 'http://github.com/dblock/ruby-enum'
16
17
  s.licenses = ['MIT']
17
18
  s.summary = 'Enum-like behavior for Ruby.'
18
- s.add_dependency 'i18n'
19
+ s.metadata['rubygems_mfa_required'] = 'true'
19
20
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Ruby::Enum::Case do
6
+ test_enum =
7
+ Class.new do
8
+ include Ruby::Enum
9
+ include Ruby::Enum::Case
10
+
11
+ define :RED, :red
12
+ define :GREEN, :green
13
+ define :BLUE, :blue
14
+ end
15
+
16
+ describe '.case' do
17
+ context 'when all cases are defined' do
18
+ subject { test_enum.case(test_enum::RED, cases) }
19
+
20
+ let(:cases) do
21
+ {
22
+ [test_enum::RED, test_enum::GREEN] => -> { 'red or green' },
23
+ test_enum::BLUE => -> { 'blue' }
24
+ }
25
+ end
26
+
27
+ it { is_expected.to eq('red or green') }
28
+
29
+ context 'when the value is nil' do
30
+ subject { test_enum.case(nil, cases) }
31
+
32
+ it { is_expected.to be_nil }
33
+ end
34
+
35
+ context 'when the value is empty' do
36
+ subject { test_enum.case('', cases) }
37
+
38
+ it { is_expected.to be_nil }
39
+ end
40
+
41
+ context 'when the value is the value of the enum' do
42
+ subject { test_enum.case(:red, cases) }
43
+
44
+ it { is_expected.to eq('red or green') }
45
+ end
46
+
47
+ context 'when the value is used inside the lambda' do
48
+ subject { test_enum.case(test_enum::RED, cases) }
49
+
50
+ let(:cases) do
51
+ {
52
+ [test_enum::RED, test_enum::GREEN] => ->(color) { "is #{color}" },
53
+ test_enum::BLUE => -> { 'blue' }
54
+ }
55
+ end
56
+
57
+ it { is_expected.to eq('is red') }
58
+ end
59
+ end
60
+
61
+ context 'when there are mutliple matches' do
62
+ subject do
63
+ test_enum.case(
64
+ test_enum::RED,
65
+ {
66
+ [test_enum::RED, test_enum::GREEN] => -> { 'red or green' },
67
+ test_enum::RED => -> { 'red' },
68
+ test_enum::BLUE => -> { 'blue' }
69
+ }
70
+ )
71
+ end
72
+
73
+ it { is_expected.to eq(['red or green', 'red']) }
74
+ end
75
+
76
+ context 'when not all cases are defined' do
77
+ it 'raises an error' do
78
+ expect do
79
+ test_enum.case(
80
+ test_enum::RED,
81
+ { [test_enum::RED, test_enum::GREEN] => -> { 'red or green' } }
82
+ )
83
+ end.to raise_error(Ruby::Enum::Case::ClassMethods::NotAllCasesHandledError)
84
+ end
85
+ end
86
+
87
+ context 'when not all cases are defined but :else is specified (default case)' do
88
+ it 'does not raise an error' do
89
+ expect do
90
+ result = test_enum.case(
91
+ test_enum::BLUE,
92
+ {
93
+ [test_enum::RED, test_enum::GREEN] => -> { 'red or green' },
94
+ else: -> { 'blue' }
95
+ }
96
+ )
97
+
98
+ expect(result).to eq('blue')
99
+ end.not_to raise_error
100
+ end
101
+ end
102
+
103
+ context 'when a superfluous case is defined' do
104
+ it 'raises an error' do
105
+ expect do
106
+ test_enum.case(
107
+ test_enum::RED,
108
+ {
109
+ [test_enum::RED, test_enum::GREEN] => -> { 'red or green' },
110
+ test_enum::BLUE => -> { 'blue' },
111
+ :something => -> { 'green' }
112
+ }
113
+ )
114
+ end.to raise_error(Ruby::Enum::Case::ClassMethods::ValuesNotDefinedError)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -22,10 +22,28 @@ describe Ruby::Enum do
22
22
  expect(Colors::RED).to eq 'red'
23
23
  expect(Colors::GREEN).to eq 'green'
24
24
  end
25
- it 'raises UninitializedConstantError on an invalid constant' do
26
- expect { Colors::ANYTHING }.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /The constant Colors::ANYTHING has not been defined./
25
+
26
+ context 'when the i18n gem is loaded' do
27
+ it 'raises UninitializedConstantError on an invalid constant' do
28
+ expect do
29
+ Colors::ANYTHING
30
+ end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /The constant Colors::ANYTHING has not been defined./
31
+ end
27
32
  end
28
- context '#each' do
33
+
34
+ context 'when the i18n gem is not loaded' do
35
+ before do
36
+ allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock)
37
+ end
38
+
39
+ it 'raises UninitializedConstantError on an invalid constant' do
40
+ expect do
41
+ Colors::ANYTHING
42
+ end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /ruby.enum.errors.messages.uninitialized_constant.summary/
43
+ end
44
+ end
45
+
46
+ describe '#each' do
29
47
  it 'iterates over constants' do
30
48
  keys = []
31
49
  enum_keys = []
@@ -40,7 +58,8 @@ describe Ruby::Enum do
40
58
  expect(enum_values).to eq %w[red green]
41
59
  end
42
60
  end
43
- context '#map' do
61
+
62
+ describe '#map' do
44
63
  it 'maps constants' do
45
64
  key_key_values = Colors.map do |key, enum|
46
65
  [key, enum.key, enum.value]
@@ -50,103 +69,152 @@ describe Ruby::Enum do
50
69
  expect(key_key_values[1]).to eq [:GREEN, :GREEN, 'green']
51
70
  end
52
71
  end
53
- context '#parse' do
72
+
73
+ describe '#parse' do
54
74
  it 'parses exact value' do
55
75
  expect(Colors.parse('red')).to eq(Colors::RED)
56
76
  end
77
+
57
78
  it 'is case-insensitive' do
58
79
  expect(Colors.parse('ReD')).to eq(Colors::RED)
59
80
  end
81
+
60
82
  it 'returns nil for a null value' do
61
83
  expect(Colors.parse(nil)).to be_nil
62
84
  end
85
+
63
86
  it 'returns nil for an invalid value' do
64
87
  expect(Colors.parse('invalid')).to be_nil
65
88
  end
66
89
  end
67
- context '#key?' do
90
+
91
+ describe '#key?' do
68
92
  it 'returns true for valid keys accessed directly' do
69
93
  Colors.keys.each do |key| # rubocop:disable Style/HashEachMethods
70
- expect(Colors.key?(key)).to eq(true)
94
+ expect(Colors.key?(key)).to be(true)
71
95
  end
72
96
  end
97
+
73
98
  it 'returns true for valid keys accessed via each_keys' do
74
99
  Colors.each_key do |key|
75
- expect(Colors.key?(key)).to eq(true)
100
+ expect(Colors.key?(key)).to be(true)
76
101
  end
77
102
  end
103
+
78
104
  it 'returns false for invalid keys' do
79
- expect(Colors.key?(:NOT_A_KEY)).to eq(false)
105
+ expect(Colors.key?(:NOT_A_KEY)).to be(false)
80
106
  end
81
107
  end
82
- context '#value' do
108
+
109
+ describe '#value' do
83
110
  it 'returns string values for keys' do
84
111
  Colors.each do |key, enum|
85
112
  expect(Colors.value(key)).to eq(enum.value)
86
113
  end
87
114
  end
115
+
88
116
  it 'returns nil for an invalid key' do
89
117
  expect(Colors.value(:NOT_A_KEY)).to be_nil
90
118
  end
91
119
  end
92
- context '#value?' do
120
+
121
+ describe '#value?' do
93
122
  it 'returns true for valid values accessed directly' do
94
123
  Colors.values.each do |value| # rubocop:disable Style/HashEachMethods
95
- expect(Colors.value?(value)).to eq(true)
124
+ expect(Colors.value?(value)).to be(true)
96
125
  end
97
126
  end
127
+
98
128
  it 'returns true for valid values accessed via each_value' do
99
129
  Colors.each_value do |value|
100
- expect(Colors.value?(value)).to eq(true)
130
+ expect(Colors.value?(value)).to be(true)
101
131
  end
102
132
  end
133
+
103
134
  it 'returns false for invalid values' do
104
- expect(Colors.value?('I am not a value')).to eq(false)
135
+ expect(Colors.value?('I am not a value')).to be(false)
105
136
  end
106
137
  end
107
- context '#key' do
138
+
139
+ describe '#key' do
108
140
  it 'returns enum instances for values' do
109
- Colors.each do |_, enum|
141
+ Colors.each do |_, enum| # rubocop:disable Style/HashEachMethods
110
142
  expect(Colors.key(enum.value)).to eq(enum.key)
111
143
  end
112
144
  end
145
+
113
146
  it 'returns nil for an invalid value' do
114
147
  expect(Colors.key('invalid')).to be_nil
115
148
  end
116
149
  end
117
- context '#keys' do
150
+
151
+ describe '#keys' do
118
152
  it 'returns keys' do
119
153
  expect(Colors.keys).to eq(%i[RED GREEN])
120
154
  end
121
155
  end
122
- context '#values' do
156
+
157
+ describe '#values' do
123
158
  it 'returns values' do
124
159
  expect(Colors.values).to eq(%w[red green])
125
160
  end
126
161
  end
127
- context '#to_h' do
162
+
163
+ describe '#to_h' do
128
164
  it 'returns a hash of key:values' do
129
165
  expect(Colors.to_h).to eq(RED: 'red', GREEN: 'green')
130
166
  end
131
167
  end
132
168
 
133
- context 'on duplicate keys' do
134
- it 'raises DuplicateKeyError' do
135
- expect do
136
- Colors.class_eval do
137
- define :RED, 'some'
138
- end
139
- end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /The constant Colors::RED has already been defined./
169
+ context 'when a duplicate key is used' do
170
+ context 'when the i18n gem is loaded' do
171
+ it 'raises DuplicateKeyError' do
172
+ expect do
173
+ Colors.class_eval do
174
+ define :RED, 'some'
175
+ end
176
+ end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /The constant Colors::RED has already been defined./
177
+ end
178
+ end
179
+
180
+ context 'when the i18n gem is not loaded' do
181
+ before do
182
+ allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock)
183
+ end
184
+
185
+ it 'raises DuplicateKeyError' do
186
+ expect do
187
+ Colors.class_eval do
188
+ define :RED, 'some'
189
+ end
190
+ end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /ruby.enum.errors.messages.duplicate_key.message/
191
+ end
140
192
  end
141
193
  end
142
194
 
143
- context 'on duplicate values' do
144
- it 'raises a DuplicateValueError' do
145
- expect do
146
- Colors.class_eval do
147
- define :Other, 'red'
148
- end
149
- end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /The value red has already been defined./
195
+ context 'when a duplicate value is used' do
196
+ context 'when the i18n gem is loaded' do
197
+ it 'raises a DuplicateValueError' do
198
+ expect do
199
+ Colors.class_eval do
200
+ define :Other, 'red'
201
+ end
202
+ end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /The value red has already been defined./
203
+ end
204
+ end
205
+
206
+ context 'when the i18n gem is not loaded' do
207
+ before do
208
+ allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock)
209
+ end
210
+
211
+ it 'raises a DuplicateValueError' do
212
+ expect do
213
+ Colors.class_eval do
214
+ define :Other, 'red'
215
+ end
216
+ end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /ruby.enum.errors.messages.duplicate_value.summary/
217
+ end
150
218
  end
151
219
  end
152
220
 
@@ -176,11 +244,14 @@ describe Ruby::Enum do
176
244
  it 'contains its own enums' do
177
245
  expect(FirstSubclass::ORANGE).to eq 'orange'
178
246
  end
247
+
179
248
  it 'parent class should not have enums defined in child classes' do
180
249
  expect { Colors::ORANGE }.to raise_error Ruby::Enum::Errors::UninitializedConstantError
181
250
  end
182
- context 'Given a 2 level depth subclass' do
251
+
252
+ context 'when defining a 2 level depth subclass' do
183
253
  subject { SecondSubclass }
254
+
184
255
  it 'contains its own enums and all the enums defined in the parent classes' do
185
256
  expect(subject::RED).to eq 'red'
186
257
  expect(subject::GREEN).to eq 'green'
@@ -227,6 +298,7 @@ describe Ruby::Enum do
227
298
  define :undefined
228
299
  end
229
300
  subject { States }
301
+
230
302
  it 'behaves like an enum' do
231
303
  expect(subject.created).to eq 'Created'
232
304
  expect(subject.published).to eq 'Published'
data/spec_i18n/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'http://rubygems.org'
4
+
5
+ gemspec path: '../', name: 'ruby-enum'
6
+
7
+ # This Gemfile should not include any gem that has i18n as a dependency.
8
+ gem 'rake'
9
+ gem 'rspec', '~> 3.0'
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ ruby-enum (1.0.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.5.0)
10
+ rake (13.1.0)
11
+ rspec (3.12.0)
12
+ rspec-core (~> 3.12.0)
13
+ rspec-expectations (~> 3.12.0)
14
+ rspec-mocks (~> 3.12.0)
15
+ rspec-core (3.12.2)
16
+ rspec-support (~> 3.12.0)
17
+ rspec-expectations (3.12.3)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.12.0)
20
+ rspec-mocks (3.12.6)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.12.0)
23
+ rspec-support (3.12.1)
24
+
25
+ PLATFORMS
26
+ arm64-darwin-22
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ rake
31
+ rspec (~> 3.0)
32
+ ruby-enum!
33
+
34
+ BUNDLED WITH
35
+ 2.5.1
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+
5
+ require 'rspec/core'
6
+ require 'rspec/core/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.pattern = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ task default: %i[spec]
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ test_class = Class.new do
6
+ include Ruby::Enum
7
+
8
+ define :RED, 'red'
9
+ define :GREEN, 'green'
10
+ end
11
+
12
+ describe Ruby::Enum do
13
+ context 'when the i18n gem is not loaded' do
14
+ it 'raises UninitializedConstantError on an invalid constant' do
15
+ expect do
16
+ test_class::ANYTHING
17
+ end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /ruby.enum.errors.messages.uninitialized_constant.summary/
18
+ end
19
+
20
+ context 'when a duplicate key is used' do
21
+ before do
22
+ allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock)
23
+ end
24
+
25
+ it 'raises DuplicateKeyError' do
26
+ expect do
27
+ test_class.class_eval do
28
+ define :RED, 'some'
29
+ end
30
+ end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /ruby.enum.errors.messages.duplicate_key.message/
31
+ end
32
+ end
33
+
34
+ context 'when a duplicate value is used' do
35
+ before do
36
+ allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock)
37
+ end
38
+
39
+ it 'raises a DuplicateValueError' do
40
+ expect do
41
+ test_class.class_eval do
42
+ define :Other, 'red'
43
+ end
44
+ end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /ruby.enum.errors.messages.duplicate_value.summary/
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
4
+
5
+ require 'rubygems'
6
+
7
+ require 'rspec'
8
+ require 'ruby-enum'