Skip to content

Commit 9d858e3

Browse files
dblockCopilot
andcommitted
Inherit keys, key?, value?, key, value, to_h, parse and each from superclass
Follow-up to #49/#58. values already merged enums from a superclass chain, but keys, key?, value?, key, value, to_h, parse and each only looked at a class' own enums, silently ignoring any enums defined in a superclass. Introduces _enum_hash/_enums_by_value that merge a class' own enums with those of its superclass chain (subclass definitions take precedence over the same key/value defined in a superclass), and uses them for all read methods. define/duplicate validation continues to use the class' own enums only (_own_enum_hash/_own_enums_by_value), so a subclass may still redefine a key or value already used by a superclass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 19b3249 commit 9d858e3

3 files changed

Lines changed: 101 additions & 28 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### 1.2.0 (Next)
22

33
* [#49](https://github.com/dblock/ruby-enum/issues/49): Fixed `NoMethodError` on `keys`, `values`, `key?`, `value?`, `key`, `value`, `to_h`, `parse` and `each` when a subclass defines no enums of its own - [@dblock](https://github.com/dblock).
4+
* [#49](https://github.com/dblock/ruby-enum/issues/49): `keys`, `key?`, `value?`, `key`, `value`, `to_h`, `parse` and `each` now include enums defined in a superclass, matching the existing behavior of `values` - [@dblock](https://github.com/dblock).
45
* Your contribution here.
56

67
### 1.1.0 (2026/6/20)

‎lib/ruby-enum/enum.rb‎

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ def self.included(base)
2020

2121
base.private_class_method(:new)
2222

23-
base.instance_variable_set(:@_enum_hash, {})
24-
base.instance_variable_set(:@_enums_by_value, {})
23+
base.instance_variable_set(:@_own_enum_hash, {})
24+
base.instance_variable_set(:@_own_enums_by_value, {})
2525
end
2626

2727
module ClassMethods
@@ -45,15 +45,15 @@ def define(key, value = key)
4545

4646
def store_new_instance(key, value)
4747
new_instance = new(key, value)
48-
_enum_hash[key] = new_instance
49-
_enums_by_value[value] = new_instance
48+
_own_enum_hash[key] = new_instance
49+
_own_enums_by_value[value] = new_instance
5050
end
5151

5252
def const_missing(key)
5353
raise Ruby::Enum::Errors::UninitializedConstantError, name: name, key: key
5454
end
5555

56-
# Iterate over all enumerated values.
56+
# Iterate over all enumerated values, including those defined in a superclass.
5757
# Required for Enumerable mixin
5858
def each(&block)
5959
_enum_hash.each(&block)
@@ -74,7 +74,7 @@ def parse(k)
7474
nil
7575
end
7676

77-
# Whether the specified key exists in this enum.
77+
# Whether the specified key exists in this enum, including those defined in a superclass.
7878
#
7979
# === Parameters
8080
# [k] The string key to check.
@@ -84,7 +84,7 @@ def key?(k)
8484
_enum_hash.key?(k)
8585
end
8686

87-
# Gets the string value for the specified key.
87+
# Gets the string value for the specified key, including those defined in a superclass.
8888
#
8989
# === Parameters
9090
# [k] The key symbol to get the value for.
@@ -95,7 +95,7 @@ def value(k)
9595
enum&.value
9696
end
9797

98-
# Whether the specified value exists in this enum.
98+
# Whether the specified value exists in this enum, including those defined in a superclass.
9999
#
100100
# === Parameters
101101
# [k] The string value to check.
@@ -105,7 +105,7 @@ def value?(v)
105105
_enums_by_value.key?(v)
106106
end
107107

108-
# Gets the key symbol for the specified value.
108+
# Gets the key symbol for the specified value, including those defined in a superclass.
109109
#
110110
# === Parameters
111111
# [v] The string value to parse.
@@ -116,14 +116,14 @@ def key(v)
116116
enum&.key
117117
end
118118

119-
# Returns all enum keys.
119+
# Returns all enum keys, including those defined in a superclass.
120120
def keys
121121
_enum_hash.values.map(&:key)
122122
end
123123

124124
# Returns all enum values.
125125
def values
126-
result = _enum_hash.values.map(&:value)
126+
result = _own_enum_hash.values.map(&:value)
127127

128128
if superclass < Ruby::Enum
129129
superclass.values + result
@@ -132,22 +132,23 @@ def values
132132
end
133133
end
134134

135-
# Iterate over all enumerated values.
135+
# Iterate over all enumerated values, including those defined in a superclass.
136136
# Required for Enumerable mixin
137137
def each_value(&_block)
138138
_enum_hash.each_value do |v|
139139
yield v.value
140140
end
141141
end
142142

143-
# Iterate over all enumerated keys.
143+
# Iterate over all enumerated keys, including those defined in a superclass.
144144
# Required for Enumerable mixin
145145
def each_key(&_block)
146146
_enum_hash.each_value do |v|
147147
yield v.key
148148
end
149149
end
150150

151+
# Returns a hash of key:values, including those defined in a superclass.
151152
def to_h
152153
_enum_hash.transform_values(&:value)
153154
end
@@ -157,29 +158,51 @@ def to_h
157158
# Returns this class' own enum hash, defaulting to an empty hash.
158159
#
159160
# A subclass that does not `define` any of its own enums does not have
160-
# its `@_enum_hash` instance variable set, since it's only initialized
161+
# its `@_own_enum_hash` instance variable set, since it's only initialized
161162
# in `define` and in the `included` hook.
162-
def _enum_hash
163-
@_enum_hash ||= {}
163+
def _own_enum_hash
164+
@_own_enum_hash ||= {}
164165
end
165166

166167
# Returns this class' own enums-by-value hash, defaulting to an empty hash.
168+
def _own_enums_by_value
169+
@_own_enums_by_value ||= {}
170+
end
171+
172+
# Returns the enum hash for this class merged with all of its superclasses,
173+
# with keys defined in this class taking precedence over those inherited
174+
# from a superclass.
175+
def _enum_hash
176+
if superclass < Ruby::Enum
177+
superclass.send(:_enum_hash).merge(_own_enum_hash)
178+
else
179+
_own_enum_hash
180+
end
181+
end
182+
183+
# Returns the enums-by-value hash for this class merged with all of its
184+
# superclasses, with values defined in this class taking precedence over
185+
# those inherited from a superclass.
167186
def _enums_by_value
168-
@_enums_by_value ||= {}
187+
if superclass < Ruby::Enum
188+
superclass.send(:_enums_by_value).merge(_own_enums_by_value)
189+
else
190+
_own_enums_by_value
191+
end
169192
end
170193

171194
def upper?(s)
172195
!/[[:upper:]]/.match(s).nil?
173196
end
174197

175198
def validate_key!(key)
176-
return unless _enum_hash.key?(key)
199+
return unless _own_enum_hash.key?(key)
177200

178201
raise Ruby::Enum::Errors::DuplicateKeyError, name: name, key: key
179202
end
180203

181204
def validate_value!(value)
182-
return unless _enums_by_value.key?(value)
205+
return unless _own_enums_by_value.key?(value)
183206

184207
raise Ruby::Enum::Errors::DuplicateValueError, name: name, value: value
185208
end

‎spec/ruby-enum/enum_spec.rb‎

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ class SubclassWithNoOwnDefines < Colors
6666
expect(enum_keys).to eq %i[RED GREEN]
6767
expect(enum_values).to eq %w[red green]
6868
end
69+
70+
context 'when a subclass defines its own enums' do
71+
it 'iterates over the enums defined in the parent class too' do
72+
keys = FirstSubclass.map { |key, _enum| key }
73+
expect(keys).to eq %i[RED GREEN ORANGE]
74+
end
75+
end
6976
end
7077

7178
describe '#map' do
@@ -95,6 +102,12 @@ class SubclassWithNoOwnDefines < Colors
95102
it 'returns nil for an invalid value' do
96103
expect(Colors.parse('invalid')).to be_nil
97104
end
105+
106+
context 'when a subclass defines its own enums' do
107+
it 'parses a value defined in the parent class' do
108+
expect(FirstSubclass.parse('red')).to eq('red')
109+
end
110+
end
98111
end
99112

100113
describe '#key?' do
@@ -113,6 +126,12 @@ class SubclassWithNoOwnDefines < Colors
113126
it 'returns false for invalid keys' do
114127
expect(Colors.key?(:NOT_A_KEY)).to be(false)
115128
end
129+
130+
context 'when a subclass defines its own enums' do
131+
it 'returns true for keys inherited from the parent class' do
132+
expect(FirstSubclass.key?(:RED)).to be(true)
133+
end
134+
end
116135
end
117136

118137
describe '#value' do
@@ -125,6 +144,12 @@ class SubclassWithNoOwnDefines < Colors
125144
it 'returns nil for an invalid key' do
126145
expect(Colors.value(:NOT_A_KEY)).to be_nil
127146
end
147+
148+
context 'when a subclass defines its own enums' do
149+
it 'returns the value for a key inherited from the parent class' do
150+
expect(FirstSubclass.value(:RED)).to eq('red')
151+
end
152+
end
128153
end
129154

130155
describe '#value?' do
@@ -143,6 +168,12 @@ class SubclassWithNoOwnDefines < Colors
143168
it 'returns false for invalid values' do
144169
expect(Colors.value?('I am not a value')).to be(false)
145170
end
171+
172+
context 'when a subclass defines its own enums' do
173+
it 'returns true for values inherited from the parent class' do
174+
expect(FirstSubclass.value?('red')).to be(true)
175+
end
176+
end
146177
end
147178

148179
describe '#key' do
@@ -155,12 +186,24 @@ class SubclassWithNoOwnDefines < Colors
155186
it 'returns nil for an invalid value' do
156187
expect(Colors.key('invalid')).to be_nil
157188
end
189+
190+
context 'when a subclass defines its own enums' do
191+
it 'returns the key for a value inherited from the parent class' do
192+
expect(FirstSubclass.key('red')).to eq(:RED)
193+
end
194+
end
158195
end
159196

160197
describe '#keys' do
161198
it 'returns keys' do
162199
expect(Colors.keys).to eq(%i[RED GREEN])
163200
end
201+
202+
context 'when a subclass defines its own enums' do
203+
it 'includes keys from the parent class' do
204+
expect(FirstSubclass.keys).to eq(%i[RED GREEN ORANGE])
205+
end
206+
end
164207
end
165208

166209
describe '#values' do
@@ -197,6 +240,12 @@ class SubclassWithNoOwnDefines < Colors
197240
it 'returns a hash of key:values' do
198241
expect(Colors.to_h).to eq(RED: 'red', GREEN: 'green')
199242
end
243+
244+
context 'when a subclass defines its own enums' do
245+
it 'includes key:values from the parent class' do
246+
expect(FirstSubclass.to_h).to eq(RED: 'red', GREEN: 'green', ORANGE: 'orange')
247+
end
248+
end
200249
end
201250

202251
context 'when a duplicate key is used' do
@@ -354,15 +403,15 @@ class EmptyEnums
354403
expect(SubclassWithNoOwnDefines.values).to eq(%w[red green])
355404
end
356405

357-
it 'does not raise when calling keys, key?, value?, key, value, to_h, parse or each' do
358-
expect(SubclassWithNoOwnDefines.keys).to eq([])
359-
expect(SubclassWithNoOwnDefines.key?(:RED)).to be false
360-
expect(SubclassWithNoOwnDefines.value?('red')).to be false
361-
expect(SubclassWithNoOwnDefines.key('red')).to be_nil
362-
expect(SubclassWithNoOwnDefines.value(:RED)).to be_nil
363-
expect(SubclassWithNoOwnDefines.to_h).to eq({})
364-
expect(SubclassWithNoOwnDefines.parse('red')).to be_nil
365-
expect(SubclassWithNoOwnDefines.each.to_a).to eq([])
406+
it 'inherits the parent class enums for keys, key?, value?, key, value, to_h, parse and each' do
407+
expect(SubclassWithNoOwnDefines.keys).to eq(%i[RED GREEN])
408+
expect(SubclassWithNoOwnDefines.key?(:RED)).to be true
409+
expect(SubclassWithNoOwnDefines.value?('red')).to be true
410+
expect(SubclassWithNoOwnDefines.key('red')).to eq :RED
411+
expect(SubclassWithNoOwnDefines.value(:RED)).to eq 'red'
412+
expect(SubclassWithNoOwnDefines.to_h).to eq(RED: 'red', GREEN: 'green')
413+
expect(SubclassWithNoOwnDefines.parse('red')).to eq 'red'
414+
expect(SubclassWithNoOwnDefines.each.map { |key, _enum| key }).to eq(%i[RED GREEN])
366415
end
367416
end
368417
end

0 commit comments

Comments
 (0)