Tasty enumeration utilities for Ruby
Nummy provides utilities that that build on Ruby's Enumerable module to provide functionality like enumerated types ("enums"), enumerating over constants in a module, and iterating over the members of data classes.
Note
This module does NOT add additional methods to the Enumerable module, or change its behavior in any way.
Install the gem and add to the application's Gemfile by executing:
bundle add nummy
If bundler is not being used to manage dependencies, install the gem by executing:
gem install nummy
To load nummy
, use:
require "nummy"
This will lazily require (using autoload
) the individual utilities as needed.
You can also explicitly load individual modules:
require "nummy/enum"
require "nummy/member_enumerable"
# ...
The main feature of Nummy is the Nummy::Enum
class, which is an opinionated class that can be used to define enumerated types ("enums") that use Ruby constants for the key-value pairs.
The recommended way to use Nummy::Enum
is to define constants explicitly:
require "nummy"
class CardinalDirection < Nummy::Enum
NORTH = 0
EAST = 90
SOUTH = 180
WEST = 270
end
Tip
Statically defining the constants like this allows the enums to work really nicely out of the box with language servers like Ruby LSP because the fields are just statically defined constants. The sugary ways of defining enums (see below) use const_set
behind the scenes, which does not work as well with language servers and tooling.
If you don't particularly care about the values, you can use the auto
helper to define them automatically:
class Status < Nummy::Enum
DRAFT = auto
PUBLISHED = auto
ARCHIVED = auto
end
Tip
auto
comes from the Nummy::AutoSequenceable
module, and has some extra features not shown in this example. For more information, see the section below on Nummy::AutoSequenceable
.
There are also two sugary ways to define enums:
require "nummy"
Nummy.enum(NORTH: 0, EAST: 90, SOUTH: 180, WEST: 270)
# which is an alias for:
Nummy::Enum.define(NORTH: 0, EAST: 90, SOUTH: 180, WEST: 270)
Or if you want the values to be automatically generated:
require "nummy"
Status = Nummy.enum(:DRAFT, :PUBLISHED, :ARCHIVED)
Status.pairs
# => [[:DRAFT, 0], [:PUBLISHED, 1], [:ARCHIVED, 2]]
You can customize the generated enum by providing a block:
require "nummy"
Status = Nummy.enum(:DRAFT, :PUBLISHED, :ARCHIVED) do |enum|
def self.custom_method
puts "Hello from #{self}!"
end
end
Status.custom_method
# => Hello from Status!
The Nummy::Enum
class provides a number of methods for working with enums. For example, using the CardinalDirection
example from above:
CardinalDirection.keys
# => [:NORTH, :EAST, :SOUTH, :WEST]
CardinalDirection.values
# => [0, 90, 180, 270]
CardinalDirection.pairs
# => [[:NORTH, 0], [:EAST, 90], [:SOUTH, 180], [:WEST, 270]]
CardinalDirection.include?(90)
# => true
They can even be used in case
expressions to see if the case value is ==
to any of the values in the enum:
case some_angle
when CardinalDirection
puts "The angle is a cardinal direction!"
else
puts "Not a cardinal direction!"
end
Tip
Nummy::Enum
extends Enumerable
and iterates over the values of the enum, so you have access to things like .include?(value)
, .any?
, and .find
.
To use nummy enums as ActiveRecord enums, you can use the .to_attribute
method, which converts the keys to snake_case
by default:
class Conversation < ActiveRecord::Base
class Status < Nummy::Enum
ACTIVE = auto
ARCHIVED = auto
end
enum :status, Status.to_attribute
end
Note
to_attribute
will transform keys using String#underscore
if it is defined, otherwise it will use Symbol#downcase
.
Tip
You can also do custom transformations by passing a block to to_attribute
. See the documentation for more details.
Using to_attribute
allows you to use all of the Rails magic for enums, like scopes and boolean helpers, while also being able to refer to values in a safer way than hash lookups.
That is, these two are the same:
Conversation.statuses[:active] # => 0
Conversation::Status::ACTIVE # => 0
But these are not:
Conversation.statuses[:acitve]
# => nil
Conversation::Status::ACITVE # => nil
# =>
# uninitialized constant Conversation::Status::ACITVE (NameError)
# Did you mean? Conversation::Status::ACTIVE
You can get similar behavior using #fetch
:
Conversation.statuses.fetch(:acitve)
# => key not found: :acitve (KeyError)
# Did you mean? :active
But that still misses out on some of the DX benefits of using constants, like improved support for things like autocompletion, documentation, and navigation ("Go To Definition") in editors.
Nummy::MemberEnumerable
is a module that includes Enumerable
and makes it possible to iterate over any class or module that responds to members
.
The motivation for this module is being able to iterate over Data
classes that represent a finite collection of similar values.
require "nummy"
SomeCollection = Data.define(:foo, :bar, :baz) do
include Nummy::MemberEnumerable
end
collection = SomeCollection[123, 456, 789]
collection.values
# => [123, 456, 789]
Note
The Nummy::MemberEnumerable
module provides fewer enumeration features than you might expect, because it's modeled after Struct
rather than Hash
.
Nummy::ConstEnumerable
is a module that only provides methods to iterate over the names, values, and pairs of a module's own constants. It is meant to provide low-level functionality for other classes, such as Nummy::Enum
.
require "nummy"
module SomeModule
extend Nummy::ConstEnumerable
FOO = 123
BAR = 456
BAZ = 789
end
SomeModule.each_const_name { |name| puts name }
# => :BAR
# => :BAZ
# => :FOO
SomeModule.each_const_pair.to_a
# => [[:BAR, 456], [:BAZ, 789], [:FOO, 123]]
Warning
The enumeration order for Nummy::ConstEnumerable
is not guaranteed because it uses Module#constants
behind the scenes.
If you require stable ordering, see Nummy::OrderedConstEnumerable
.
Nummy::OrderedConstEnumerable
provides the same API as Nummy::ConstEnumerable
, but guarantees three things:
- any constants defined before the module is extended will be sorted alphabetically before any other constants
- any constants defined after the module is extended will be sorted in insertion order
- the order of constants is stable across calls
For example:
require "nummy"
class CustomEnum
BEFORE_C = :c
BEFORE_B = :b
BEFORE_A = :a
extend Nummy::OrderedConstEnumerable
AFTER_Z = :z
AFTER_Y = :y
AFTER_X = :x
end
CustomEnum.each_const_name.to_a
# => [:BEFORE_A, :BEFORE_B, :BEFORE_C, :AFTER_Z, :AFTER_Y, :AFTER_X]
Nummy::AutoSequenceable
is a module that provides a single method, auto
, which returns a unique value for each call. It is meant to provide low-level functionality for other classes, such as Nummy::Enum
.
require "nummy"
class Weekday
extend Nummy::AutoSequenceable
SUNDAY = auto
MONDAY = auto
TUESDAY = auto
WEDNESDAY = auto
THURSDAY = auto
FRIDAY = auto
SATURDAY = auto
end
Weekday::SUNDAY
# => 0
Weekday::SATURDAY
# => 6
Like with iota
in Go, or Enum.auto
in Python, you can also customize the sequence behavior:
require "nummy"
require "bigdecimal"
module MetricPrefix
extend Nummy::AutoSequenceable
DECI = auto(1) { |n| BigDecimal(10) ** -n }
CENTI = auto
MILLI = auto
MICRO = auto(6)
NANO = auto(9)
DECA = auto(1) { |n| BigDecimal(10) ** n }
HECA = auto
KILO = auto
MEGA = auto(6)
GIGA = auto(9)
end
MetricPrefix::DECI # => 0.1e0
MetricPrefix::CENTI # => 0.1e-1
MetricPrefix::MILLI # => 0.1e-2
MetricPrefix::MICRO # => 0.1e-5
MetricPrefix::NANO # => 0.1e-8
MetricPrefix::DECA # => 0.1e2
MetricPrefix::HECA # => 0.1e3
MetricPrefix::KILO # => 0.1e4
MetricPrefix::MEGA # => 0.1e7
MetricPrefix::GIGA # => 0.1e10
See the implementation, tests, and documentation for more details.
After checking out the repo, run bin/setup
to install dependencies.
To install this gem onto your local machine, run bundle exec rake install
.
You can run bundle exec rake console
for an interactive prompt that will allow you to experiment with the gem.
The console has a pre-configured Nummy::Enum
that you can use for testing:
irb(main):001> CardinalDirection
=> #<CardinalDirection NORTH=0 EAST=90 SOUTH=180 WEST=270>
irb(main):002> CardinalDirection.values
=> [0, 90, 180, 270]
You can start a documentation server by running rake docs
. This will start a server at http://localhost:8808 that will pick up changes to the documentation whenever you refresh the page.
Note
If you make major changes to the gem, sometimes the server can get in a weird state and display incorrect documentation, especially on the sidebar. If this happens, you can try removing the yard cache with rm -rf .yardoc
, then restart the server and refresh the docs page.
To run tests, you can use:
bundle exec rake test
To run all of the tests in a specific file, you can use the TEST
argument:
bundle exec rake test TEST="test/nummy/enum_test.rb"
To run a specific test by name, you can use the N
argument and pass it a name or regex pattern:
bundle exec rake test N="/version/"
Tip
The N
argument comes from Minitest::TestTask
. See the Minitest README or the Minitest documentation for more information.
Tip
You can also combine multiple options together:
bundle exec rake test N="/positional args/" TEST=test/nummy/enum_test.rb
Note
Coverage is not collected by default, because our configuration reports incorrect metrics when not run against the entire test suite. We could configure coverage to only report against files that were actually required, but because we lazily load some files, it would be easy to miss coverage for an entire file.
To collect coverage, you can set COVERAGE
to true
or 1
:
bundle exec rake test COVERAGE=true
bundle exec rake test COVERAGE=1
To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/bdchauvette/nummy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
The gem is available as open source under the terms of the MIT No Attribution License.
Everyone interacting in the Nummy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.