Nummy
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.
Installation
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
Usage
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"
Nummy::Enum
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:
Creating (Recommended)
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
.
Creating (Sugar)
There are also two sugary ways to define enums:
require "nummy"
Nummy.enum(NORTH: 0, EAST: 90, SOUTH: 180, WEST: 270)
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
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
Working with enums
The Nummy::Enum
class provides a number of methods for working with enums. For example, using the CardinalDirection
example from above:
CardinalDirection.keys
CardinalDirection.values
CardinalDirection.pairs
CardinalDirection.include?(90)
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
.
Rails / ActiveRecord integration
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]
Conversation::Status::ACTIVE
But these are not:
Conversation.statuses[:acitve]
Conversation::Status::ACITVE
You can get similar behavior using #fetch
:
Conversation.statuses.fetch(:acitve)
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
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
[!NOTE]
The Nummy::MemberEnumerable
module provides fewer enumeration features than you might expect, because it's modeled after Struct
rather than Hash
.
Nummy::ConstEnumerable
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 }
SomeModule.each_const_pair.to_a
[!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
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
Nummy::AutoSequenceable
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
Weekday::SATURDAY
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
MetricPrefix::CENTI
MetricPrefix::MILLI
MetricPrefix::MICRO
MetricPrefix::NANO
MetricPrefix::DECA
MetricPrefix::HECA
MetricPrefix::KILO
MetricPrefix::MEGA
MetricPrefix::GIGA
See the implementation, tests, and documentation for more details.
Development
Setup
After checking out the repo, run bin/setup
to install dependencies.
To install this gem onto your local machine, run bundle exec rake install
.
Console
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]
Documentation
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.
Testing
To run tests, you can use:
bundle exec rake test
Running subsets of tests
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
Coverage
[!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
Releasing
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.
Contributing
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.
License
The gem is available as open source under the terms of the MIT No Attribution License.
Code of Conduct
Everyone interacting in the Nummy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.