Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 1.2.0 (Next)

* [#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).
* Your contribution here.

### 1.1.0 (2026/6/20)
Expand Down
45 changes: 28 additions & 17 deletions lib/ruby-enum/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ module ClassMethods
# [key] Enumerator key.
# [value] Enumerator value.
def define(key, value = key)
@_enum_hash ||= {}
@_enums_by_value ||= {}

validate_key!(key)
validate_value!(value)

Expand All @@ -48,8 +45,8 @@ def define(key, value = key)

def store_new_instance(key, value)
new_instance = new(key, value)
@_enum_hash[key] = new_instance
@_enums_by_value[value] = new_instance
_enum_hash[key] = new_instance
_enums_by_value[value] = new_instance
end

def const_missing(key)
Expand All @@ -59,7 +56,7 @@ def const_missing(key)
# Iterate over all enumerated values.
# Required for Enumerable mixin
def each(&block)
@_enum_hash.each(&block)
_enum_hash.each(&block)
end

# Attempt to parse an enum key and return the
Expand All @@ -84,7 +81,7 @@ def parse(k)
#
# Returns true if the key exists, false otherwise.
def key?(k)
@_enum_hash.key?(k)
_enum_hash.key?(k)
end

# Gets the string value for the specified key.
Expand All @@ -94,7 +91,7 @@ def key?(k)
#
# Returns the corresponding enum instance or nil.
def value(k)
enum = @_enum_hash[k]
enum = _enum_hash[k]
enum&.value
end

Expand All @@ -105,7 +102,7 @@ def value(k)
#
# Returns true if the value exists, false otherwise.
def value?(v)
@_enums_by_value.key?(v)
_enums_by_value.key?(v)
end

# Gets the key symbol for the specified value.
Expand All @@ -115,18 +112,18 @@ def value?(v)
#
# Returns the corresponding key symbol or nil.
def key(v)
enum = @_enums_by_value[v]
enum = _enums_by_value[v]
enum&.key
end

# Returns all enum keys.
def keys
@_enum_hash.values.map(&:key)
_enum_hash.values.map(&:key)
end

# Returns all enum values.
def values
result = @_enum_hash.values.map(&:value)
result = _enum_hash.values.map(&:value)

if superclass < Ruby::Enum
superclass.values + result
Expand All @@ -138,37 +135,51 @@ def values
# Iterate over all enumerated values.
# Required for Enumerable mixin
def each_value(&_block)
@_enum_hash.each_value do |v|
_enum_hash.each_value do |v|
yield v.value
end
end

# Iterate over all enumerated keys.
# Required for Enumerable mixin
def each_key(&_block)
@_enum_hash.each_value do |v|
_enum_hash.each_value do |v|
yield v.key
end
end

def to_h
@_enum_hash.transform_values(&:value)
_enum_hash.transform_values(&:value)
end

private

# Returns this class' own enum hash, defaulting to an empty hash.
#
# A subclass that does not `define` any of its own enums does not have
# its `@_enum_hash` instance variable set, since it's only initialized
# in `define` and in the `included` hook.
def _enum_hash
@_enum_hash ||= {}
end

# Returns this class' own enums-by-value hash, defaulting to an empty hash.
def _enums_by_value
@_enums_by_value ||= {}
end

def upper?(s)
!/[[:upper:]]/.match(s).nil?
end

def validate_key!(key)
return unless @_enum_hash.key?(key)
return unless _enum_hash.key?(key)

raise Ruby::Enum::Errors::DuplicateKeyError, name: name, key: key
end

def validate_value!(value)
return unless @_enums_by_value.key?(value)
return unless _enums_by_value.key?(value)

raise Ruby::Enum::Errors::DuplicateValueError, name: name, value: value
end
Expand Down
31 changes: 31 additions & 0 deletions spec/ruby-enum/enum_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class OtherSecondSubclass < FirstSubclass
define :MAGENTA, 'magenta'
end

class SubclassWithNoOwnDefines < Colors
end

it 'returns an enum value' do
expect(Colors::RED).to eq 'red'
expect(Colors::GREEN).to eq 'green'
Expand Down Expand Up @@ -182,6 +185,12 @@ class OtherSecondSubclass < FirstSubclass
expect(OtherSecondSubclass.values).to eq(%w[red green orange magenta])
end
end

context 'when a subclass defines no enums of its own' do
it 'returns the values from the parent class' do
expect(SubclassWithNoOwnDefines.values).to eq(%w[red green])
end
end
end

describe '#to_h' do
Expand Down Expand Up @@ -334,6 +343,28 @@ class EmptyEnums
expect(FirstSubclass.values).to eq(%w[red green orange])
end
end

context 'when a subclass defines no enums of its own (issue #49)' do
it 'inherits the parent class enums via constants' do
expect(SubclassWithNoOwnDefines::RED).to eq 'red'
expect(SubclassWithNoOwnDefines::GREEN).to eq 'green'
end

it 'inherits the parent class values' do
expect(SubclassWithNoOwnDefines.values).to eq(%w[red green])
end

it 'does not raise when calling keys, key?, value?, key, value, to_h, parse or each' do
expect(SubclassWithNoOwnDefines.keys).to eq([])
expect(SubclassWithNoOwnDefines.key?(:RED)).to be false
expect(SubclassWithNoOwnDefines.value?('red')).to be false
expect(SubclassWithNoOwnDefines.key('red')).to be_nil
expect(SubclassWithNoOwnDefines.value(:RED)).to be_nil
expect(SubclassWithNoOwnDefines.to_h).to eq({})
expect(SubclassWithNoOwnDefines.parse('red')).to be_nil
expect(SubclassWithNoOwnDefines.each.to_a).to eq([])
end
end
end

describe 'default value' do
Expand Down
Loading