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,6 +1,7 @@
### 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).
* [#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).
* Your contribution here.

### 1.1.0 (2026/6/20)
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ The `DuplicateValueError` exception is raised to be consistent with the unique k

### Inheritance

When inheriting from a `Ruby::Enum` class, all defined enums in the parent class will be accessible in sub classes as well. Sub classes can also provide extra enums, as usual.
When inheriting from a `Ruby::Enum` class, all defined enums in the parent class will be accessible in subclasses as well. Subclasses can also provide extra enums, as usual.

``` ruby
class OrderState
Expand Down Expand Up @@ -262,6 +262,26 @@ OrderState.values # ['CREATED', 'PAID']
ShippedOrderState.values # ['CREATED', 'PAID', 'PREPARED', SHIPPED']
```

All other enumerating and hashing methods (`keys`, `key?`, `value?`, `key`, `value`, `to_h`, `parse` and `each`) also consider enums defined anywhere in the class hierarchy.

``` ruby
ShippedOrderState.keys # [:CREATED, :PAID, :PREPARED, :SHIPPED]
ShippedOrderState.key?(:CREATED) # true
ShippedOrderState.value(:CREATED) # 'CREATED'
```

A subclass may redefine a key or value already used by a parent class without raising `DuplicateKeyError` or `DuplicateValueError`; its own definition takes precedence.

``` ruby
class ShippedOrderState < OrderState
define :CREATED, 'RECREATED' # does not raise, overrides the parent class' definition
end

ShippedOrderState::CREATED # 'RECREATED'
ShippedOrderState.value(:CREATED) # 'RECREATED'
OrderState.value(:CREATED) # 'CREATED', unaffected
```

### Exhaustive case matcher

If you want to make sure that you cover all cases in a case stament, you can use the exhaustive case matcher: `Ruby::Enum::Case`. It will raise an error if a case/enum value is not handled, or if a value is specified that's not part of the enum. This is inspired by the [Rust Pattern Syntax](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html). If multiple cases match, all matches are being executed. The return value is the value from the matched case, or an array of return values if multiple cases matched.
Expand Down
39 changes: 39 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
# Upgrading Ruby::Enum

## Upgrading to >= 1.2.0

### Inheritance & `keys`, `key?`, `value?`, `key`, `value`, `to_h`, `parse` and `each`

This only applies to classes that inherit from another which is a `Ruby::Enum`.

Prior to version `1.2.0`, only `values` enumerated enums defined in the entire class hierarchy; `keys`, `key?`, `value?`, `key`, `value`, `to_h`, `parse` and `each` only considered enums defined directly on the class, silently ignoring anything defined in a superclass.

As of version `1.2.0`, these methods behave consistently with `values` and also enumerate/consider enums defined anywhere in the class hierarchy, ancestors first. A subclass may still redefine a key or value already used by a superclass; its own definition takes precedence.

``` ruby
class PrimaryColors
include Ruby::Enum

define :RED, 'RED'
define :GREEN, 'GREEN'
end

class RainbowColors < PrimaryColors
define :ORANGE, 'ORANGE'
end
```

`gem 'ruby-enum', '< 1.2.0'`

``` ruby
RainbowColors.keys # => [:ORANGE]
RainbowColors.key?(:RED) # => false
```

`gem 'ruby-enum', '>= 1.2.0'`

``` ruby
RainbowColors.keys # => [:RED, :GREEN, :ORANGE]
RainbowColors.key?(:RED) # => true
```

See [#49](https://github.com/dblock/ruby-enum/issues/49) for more information.

## Upgrading to >= 0.9.0

### Inheritance & `Ruby::Enum.values`
Expand Down
61 changes: 42 additions & 19 deletions lib/ruby-enum/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def self.included(base)

base.private_class_method(:new)

base.instance_variable_set(:@_enum_hash, {})
base.instance_variable_set(:@_enums_by_value, {})
base.instance_variable_set(:@_own_enum_hash, {})
base.instance_variable_set(:@_own_enums_by_value, {})
end

module ClassMethods
Expand All @@ -45,15 +45,15 @@ 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
_own_enum_hash[key] = new_instance
_own_enums_by_value[value] = new_instance
end

def const_missing(key)
raise Ruby::Enum::Errors::UninitializedConstantError, name: name, key: key
end

# Iterate over all enumerated values.
# Iterate over all enumerated values, including those defined in a superclass.
# Required for Enumerable mixin
def each(&block)
_enum_hash.each(&block)
Expand All @@ -74,7 +74,7 @@ def parse(k)
nil
end

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

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

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

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

# Returns all enum keys.
# Returns all enum keys, including those defined in a superclass.
def keys
_enum_hash.values.map(&:key)
end

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

if superclass < Ruby::Enum
superclass.values + result
Expand All @@ -132,22 +132,23 @@ def values
end
end

# Iterate over all enumerated values.
# Iterate over all enumerated values, including those defined in a superclass.
# Required for Enumerable mixin
def each_value(&_block)
_enum_hash.each_value do |v|
yield v.value
end
end

# Iterate over all enumerated keys.
# Iterate over all enumerated keys, including those defined in a superclass.
# Required for Enumerable mixin
def each_key(&_block)
_enum_hash.each_value do |v|
yield v.key
end
end

# Returns a hash of key:values, including those defined in a superclass.
def to_h
_enum_hash.transform_values(&:value)
end
Expand All @@ -157,29 +158,51 @@ def to_h
# 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
# its `@_own_enum_hash` instance variable set, since it's only initialized
# in `define` and in the `included` hook.
def _enum_hash
@_enum_hash ||= {}
def _own_enum_hash
@_own_enum_hash ||= {}
end

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

# Returns the enum hash for this class merged with all of its superclasses,
# with keys defined in this class taking precedence over those inherited
# from a superclass.
def _enum_hash
if superclass < Ruby::Enum
superclass.send(:_enum_hash).merge(_own_enum_hash)
else
_own_enum_hash
end
end

# Returns the enums-by-value hash for this class merged with all of its
# superclasses, with values defined in this class taking precedence over
# those inherited from a superclass.
def _enums_by_value
@_enums_by_value ||= {}
if superclass < Ruby::Enum
superclass.send(:_enums_by_value).merge(_own_enums_by_value)
else
_own_enums_by_value
end
end

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

def validate_key!(key)
return unless _enum_hash.key?(key)
return unless _own_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 _own_enums_by_value.key?(value)

raise Ruby::Enum::Errors::DuplicateValueError, name: name, value: value
end
Expand Down
Loading
Loading