ruby-enum 0.9.0 → 1.0.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,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'