├── .github └── workflows │ ├── coverage.yml │ ├── danger.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dangerfile ├── Gemfile ├── LICENSE.md ├── README.md ├── RELEASING.md ├── Rakefile ├── UPGRADING.md ├── benchmarks └── case.rb ├── lib ├── config │ └── locales │ │ └── en.yml ├── ruby-enum.rb ├── ruby-enum │ ├── enum.rb │ ├── enum │ │ ├── case.rb │ │ └── i18n_mock.rb │ ├── errors │ │ ├── base.rb │ │ ├── duplicate_key_error.rb │ │ ├── duplicate_value_error.rb │ │ └── uninitialized_constant_error.rb │ └── version.rb └── ruby_enum.rb ├── ruby-enum.gemspec ├── spec ├── ruby-enum │ ├── enum │ │ └── case_spec.rb │ ├── enum_spec.rb │ └── version_spec.rb └── spec_helper.rb └── spec_i18n ├── Gemfile ├── Rakefile └── spec ├── i18n_spec.rb └── spec_helper.rb /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: coverage 3 | on: [push] 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - name: Install Ruby (3.2) 10 | uses: ruby/setup-ruby@v1 11 | with: 12 | ruby-version: 3.2 13 | - uses: amancevice/setup-code-climate@v0 14 | with: 15 | cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} 16 | - run: cc-test-reporter before-build 17 | - name: Build and test with RSpec 18 | run: | 19 | bundle install --jobs 4 --retry 3 20 | bundle exec rspec 21 | - run: cc-test-reporter after-build 22 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: danger 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited, synchronize] 6 | jobs: 7 | danger: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v3 11 | with: 12 | fetch-depth: 0 13 | - name: Set up Ruby 14 | uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: 2.7 17 | bundler-cache: true 18 | - name: Run Danger 19 | run: | 20 | # the personal token is public, this is ok, base64 encode to avoid tripping Github 21 | TOKEN=$(echo -n Z2hwX0xNQ3VmanBFeTBvYkZVTWh6NVNqVFFBOEUxU25abzBqRUVuaAo= | base64 --decode) 22 | DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | jobs: 4 | rubocop: 5 | name: RuboCop 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v3 10 | - name: Set up Ruby 11 | uses: ruby/setup-ruby@v1 12 | with: 13 | ruby-version: 2.7 14 | bundler-cache: true 15 | - run: bundle exec rubocop 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | on: [push, pull_request] 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | entry: 11 | - { ruby: '2.7', allowed-failure: false } 12 | - { ruby: '3.0', allowed-failure: false } 13 | - { ruby: '3.1', allowed-failure: false } 14 | - { ruby: '3.2', allowed-failure: false } 15 | - { ruby: 'ruby-head', allowed-failure: true } 16 | - { ruby: 'truffleruby-head', allowed-failure: true } 17 | - { ruby: 'jruby-head', allowed-failure: true } 18 | name: test (${{ matrix.entry.ruby }}) 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.entry.ruby }} 24 | - run: bundle install --jobs=3 --retry=3 --path=vendor/bundle 25 | - run: bundle exec rake spec 26 | continue-on-error: ${{ matrix.entry.allowed-failure }} 27 | - name: Specs for when the i18n gem is not available 28 | run: | 29 | cd spec_i18n 30 | bundle install --jobs=3 --retry=3 31 | pwd 32 | bundle exec rake spec 33 | continue-on-error: ${{ matrix.entry.allowed-failure }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg 3 | coverage 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rake 3 | - rubocop-rspec 4 | 5 | AllCops: 6 | TargetRubyVersion: 2.7 7 | NewCops: enable 8 | Exclude: 9 | - vendor/**/* 10 | 11 | Metrics/BlockLength: 12 | Exclude: 13 | - 'spec/**/*_spec.rb' 14 | 15 | RSpec/SpecFilePathFormat: 16 | Enabled: false 17 | 18 | RSpec/FilePath: 19 | Enabled: false 20 | 21 | Style/HashEachMethods: 22 | Enabled: true 23 | 24 | Style/HashTransformKeys: 25 | Enabled: true 26 | 27 | Style/HashTransformValues: 28 | Enabled: true 29 | 30 | inherit_from: .rubocop_todo.yml 31 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2023-02-14 01:34:25 UTC using RuboCop version 1.45.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 4 10 | # Configuration parameters: AllowedMethods. 11 | # AllowedMethods: enums 12 | Lint/ConstantDefinitionInBlock: 13 | Exclude: 14 | - 'spec/ruby-enum/enum_spec.rb' 15 | 16 | # Offense count: 1 17 | # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. 18 | # CheckDefinitionPathHierarchyRoots: lib, spec, test, src 19 | # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS 20 | Naming/FileName: 21 | Exclude: 22 | - 'lib/ruby-enum.rb' 23 | 24 | # Offense count: 6 25 | # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. 26 | # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to 27 | Naming/MethodParameterName: 28 | Exclude: 29 | - 'lib/ruby-enum/enum.rb' 30 | 31 | # Offense count: 3 32 | # Configuration parameters: CountAsOne. 33 | RSpec/ExampleLength: 34 | Max: 11 35 | 36 | # Offense count: 4 37 | RSpec/LeakyConstantDeclaration: 38 | Exclude: 39 | - 'spec/ruby-enum/enum_spec.rb' 40 | 41 | # Offense count: 6 42 | RSpec/MultipleExpectations: 43 | Max: 11 44 | 45 | # Offense count: 18 46 | # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. 47 | # SupportedStyles: always, named_only 48 | RSpec/NamedSubject: 49 | Exclude: 50 | - 'spec/ruby-enum/enum_spec.rb' 51 | 52 | # Offense count: 1 53 | # Configuration parameters: AllowedGroups. 54 | RSpec/NestedGroups: 55 | Max: 4 56 | 57 | # Offense count: 4 58 | # Configuration parameters: AllowedConstants. 59 | Style/Documentation: 60 | Exclude: 61 | - 'spec/**/*' 62 | - 'test/**/*' 63 | - 'lib/ruby-enum/enum.rb' 64 | - 'lib/ruby-enum/errors/base.rb' 65 | - 'lib/ruby-enum/errors/uninitialized_constant_error.rb' 66 | 67 | # Offense count: 1 68 | # This cop supports safe autocorrection (--autocorrect). 69 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. 70 | # URISchemes: http, https 71 | Layout/LineLength: 72 | Max: 148 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.0.1 (Next) 2 | 3 | * Your contribution here. 4 | 5 | ### 1.0.0 (2023/01/10) 6 | 7 | * [#41](https://github.com/dblock/ruby-enum/pull/41): Make i18n dependency optional - [@peterfication](https://github.com/peterfication). 8 | * [#43](https://github.com/dblock/ruby-enum/pull/43): Add exhaustive case matcher - [@peterfication](https://github.com/peterfication). 9 | * [#40](https://github.com/dblock/ruby-enum/pull/39): Enable new Rubocop cops and address/allowlist lints - [@petergoldstein](https://github.com/petergoldstein). 10 | * [#39](https://github.com/dblock/ruby-enum/pull/39): Require Ruby >= 2.7 - [@petergoldstein](https://github.com/petergoldstein). 11 | * [#38](https://github.com/dblock/ruby-enum/pull/38): Ensure Ruby >= 2.3 - [@ojab](https://github.com/ojab). 12 | 13 | ### 0.9.0 (2021/01/21) 14 | 15 | * [#34](https://github.com/dblock/ruby-enum/pull/34): Added support for Ruby 3.0 - [@dblock](https://github.com/dblock). 16 | * [#29](https://github.com/dblock/ruby-enum/pull/29): Added superclass values when enumerating with `Ruby::Enum#values` - [@gi](https://github.com/gi). 17 | * [#30](https://github.com/dblock/ruby-enum/pull/30): Default value to key - [@gi](https://github.com/gi). 18 | * [#34](https://github.com/dblock/ruby-enum/pull/34): Replaced Travis-CI with Github Actions, added Danger PR linter - [@dblock](https://github.com/dblock). 19 | * [#37](https://github.com/dblock/ruby-enum/pull/37): Added code coverage - [@dblock](https://github.com/dblock). 20 | 21 | ### 0.8.0 (2020/03/27) 22 | 23 | * [#22](https://github.com/dblock/ruby-enum/pull/22): Added `Ruby::Enum#each_key` and `Ruby::Enum#each_value` - [@dblock](https://github.com/dblock). 24 | * [#22](https://github.com/dblock/ruby-enum/pull/22): Dropped support for Ruby 2.2 - [@dblock](https://github.com/dblock). 25 | * [#22](https://github.com/dblock/ruby-enum/pull/22): Upgraded RuboCop to 0.80.1 - [@dblock](https://github.com/dblock). 26 | 27 | ### 0.7.2 (2017/03/15) 28 | 29 | * [#18](https://github.com/dblock/ruby-enum/pull/18): Added support for non constant definition - [@laertispappas](https://github.com/laertispappas). 30 | 31 | ### 0.7.1 (2017/02/23) 32 | 33 | * [#16](https://github.com/dblock/ruby-enum/pull/16): Replaced `const_missing` with `const_set` - [@laertispappas](https://github.com/laertispappas). 34 | 35 | ### 0.7.0 (2017/02/21) 36 | 37 | * [#3](https://github.com/dblock/ruby-enum/pull/13): Added support for subclassing an Enum - [@laertispappas](https://github.com/laertispappas). 38 | 39 | ### 0.6.0 (2016/05/12) 40 | 41 | * [#12](https://github.com/dblock/ruby-enum/pull/12): A `Ruby::Enum::Errors::DuplicateKeyError` or a `Ruby::Enum::Errors::DuplciateKeyValyeError` will now be raised when duplicate keys / values are defined - [@laertispappas](https://github.com/laertispappas). 42 | 43 | ### 0.5.0 (2015/11/20) 44 | 45 | * [#8](https://github.com/dblock/ruby-enum/pull/8): Added `Ruby::Enum#key`, `Ruby::Enum#value`, `Ruby::Enum#key?`, and `Ruby::Enum#value?` - [@dmolesUC3](https://github.com/dmolesUC3). 46 | 47 | ### 0.4.0 (2014/06/29) 48 | 49 | * [#5](https://github.com/dblock/ruby-enum/pull/5): Mixed in `Enumerable` - [@kgann](https://github.com/kgann). 50 | 51 | ### 0.3.0 (2014/05/19) 52 | 53 | * [#4](https://github.com/dblock/ruby-enum/pull/4): Added `Ruby::Enum#map` - [@ArnaudRinquin](https://github.com/ArnaudRinquin). 54 | 55 | ### 0.2.1 (2013/05/15) 56 | 57 | * Added `Ruby::Enum#values`, `Ruby::Enum#keys` and `Ruby::Enum#to_h` - [@dblock](https://github.com/dblock). 58 | * A `Ruby::Enum::Errors::UninitializedConstantError` will now be raised when referencing an undefined enum - [@dblock](https://github.com/dblock). 59 | 60 | ### 0.1.0 (2013/05/14) 61 | 62 | * Initial public release, live-coded during [May 2013 NYC.rb](http://code.dblock.org/your-first-ruby-gem) - [@dblock](https://github.com/dblock). 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Ruby-Enum 2 | 3 | This project is work of [many contributors](https://github.com/dblock/ruby-enum/graphs/contributors). 4 | 5 | You're encouraged to submit [pull requests](https://github.com/dblock/ruby-enum/pulls), [propose features and discuss issues](https://github.com/dblock/ruby-enum/issues). 6 | 7 | In the examples below, substitute your Github username for `contributor` in URLs. 8 | 9 | ### Fork the Project 10 | 11 | Fork the [project on Github](https://github.com/dblock/ruby-enum) and check out your copy. 12 | 13 | ``` 14 | git clone https://github.com/contributor/ruby-enum.git 15 | cd ruby-enum 16 | git remote add upstream https://github.com/dblock/ruby-enum.git 17 | ``` 18 | 19 | ### Bundle Install and Test 20 | 21 | Ensure that you can build the project and run tests. 22 | 23 | ``` 24 | bundle install 25 | bundle exec rake 26 | ``` 27 | 28 | ## Contribute Code 29 | 30 | ### Create a Topic Branch 31 | 32 | Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. 33 | 34 | ``` 35 | git checkout master 36 | git pull upstream master 37 | git checkout -b my-feature-branch 38 | ``` 39 | 40 | ### Write Tests 41 | 42 | Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add tests to [spec](spec). 43 | 44 | We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. 45 | 46 | ### Write Code 47 | 48 | Implement your feature or bug fix. 49 | 50 | Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop). Run `bundle exec rubocop` and fix any style issues highlighted, auto-correct issues when possible with `bundle exec rubocop -a`. To silence generally ingored issues, including line lengths or code complexity metrics, run `bundle exec rubocop --auto-gen-config`. 51 | 52 | Make sure that `bundle exec rake` completes without errors. 53 | 54 | ### Write Documentation 55 | 56 | Document any external behavior in the [README](README.md). 57 | 58 | ### Update Changelog 59 | 60 | Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Don't remove *Your contribution here*. 61 | 62 | Make it look like every other line, including a link to the issue being fixed, your name and link to your Github account. 63 | 64 | ### Commit Changes 65 | 66 | Make sure git knows your name and email address: 67 | 68 | ``` 69 | git config --global user.name "Your Name" 70 | git config --global user.email "contributor@example.com" 71 | ``` 72 | 73 | Writing good commit logs is important. A commit log should describe what changed and why. 74 | 75 | ``` 76 | git add ... 77 | git commit 78 | ``` 79 | 80 | ### Push 81 | 82 | ``` 83 | git push origin my-feature-branch 84 | ``` 85 | 86 | ### Make a Pull Request 87 | 88 | Go to https://github.com/contributor/ruby-enum and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. 89 | 90 | ### Update CHANGELOG Again 91 | 92 | Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. 93 | 94 | ``` 95 | * [#123](https://github.com/dblock/ruby-enum/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). 96 | ``` 97 | 98 | Amend your previous commit and force push the changes. 99 | 100 | ``` 101 | git commit --amend 102 | git push origin my-feature-branch -f 103 | ``` 104 | 105 | ### Rebase 106 | 107 | If you've been working on a change for a while, rebase with upstream/master. 108 | 109 | ``` 110 | git fetch upstream 111 | git rebase upstream/master 112 | git push origin my-feature-branch -f 113 | ``` 114 | 115 | ### Check on Your Pull Request 116 | 117 | Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. 118 | 119 | ### Be Patient 120 | 121 | It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! 122 | 123 | ## Thank You 124 | 125 | Please do know that we really appreciate and value your time and work. We love you, really. 126 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | changelog.check! 4 | toc.check! 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'http://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'rake' 8 | 9 | group :development, :test do 10 | gem 'danger' 11 | gem 'danger-changelog', '0.6.1' 12 | gem 'danger-toc', '0.2.0' 13 | gem 'rspec', '~> 3.0' 14 | gem 'rubocop', '~> 1.0' 15 | gem 'rubocop-rake' 16 | gem 'rubocop-rspec' 17 | end 18 | 19 | group :test do 20 | gem 'simplecov', require: false 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2021 Daniel Doubrovkine. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ruby::Enum 2 | ========== 3 | 4 | [![Gem Version](http://img.shields.io/gem/v/ruby-enum.svg)](http://badge.fury.io/rb/ruby-enum) 5 | [![Build Status](https://github.com/dblock/ruby-enum/workflows/test/badge.svg?branch=master)](https://github.com/dblock/ruby-enum/actions) 6 | [![Code Climate](https://codeclimate.com/github/dblock/ruby-enum.svg)](https://codeclimate.com/github/dblock/ruby-enum) 7 | 8 | Enum-like behavior for Ruby, heavily inspired by [this](http://www.rubyfleebie.com/enumerations-and-ruby), and improved upon [another blog post](http://code.dblock.org/how-to-define-enums-in-ruby). 9 | 10 | ## Table of Contents 11 | 12 | - [Usage](#usage) 13 | - [Constants](#constants) 14 | - [Class Methods](#class-methods) 15 | - [Default Value](#default-value) 16 | - [Enumerating](#enumerating) 17 | - [Iterating](#iterating) 18 | - [Mapping](#mapping) 19 | - [Reducing](#reducing) 20 | - [Sorting](#sorting) 21 | - [Hashing](#hashing) 22 | - [Retrieving keys and values](#retrieving-keys-and-values) 23 | - [Mapping keys to values](#mapping-keys-to-values) 24 | - [Mapping values to keys](#mapping-values-to-keys) 25 | - [Duplicate enumerator keys or duplicate values](#duplicate-enumerator-keys-or-duplicate-values) 26 | - [Inheritance](#inheritance) 27 | - [Exhaustive case matcher](#exhaustive-case-matcher) 28 | - [I18n support](#i18n-support) 29 | - [Benchmarks](#benchmarks) 30 | - [Contributing](#contributing) 31 | - [Copyright and License](#copyright-and-license) 32 | - [Related Projects](#related-projects) 33 | 34 | ## Usage 35 | 36 | Enums can be defined and accessed either as constants, or class methods, which is a matter of preference. 37 | 38 | ### Constants 39 | 40 | Define enums, and reference them as constants. 41 | 42 | ``` ruby 43 | class OrderState 44 | include Ruby::Enum 45 | 46 | define :CREATED, 'created' 47 | define :PAID, 'paid' 48 | end 49 | ``` 50 | 51 | ``` ruby 52 | OrderState::CREATED # 'created' 53 | OrderState::PAID # 'paid' 54 | OrderState::UNKNOWN # raises Ruby::Enum::Errors::UninitializedConstantError 55 | OrderState.keys # [ :CREATED, :PAID ] 56 | OrderState.values # [ 'created', 'paid' ] 57 | OrderState.to_h # { :CREATED => 'created', :PAID => 'paid' } 58 | ``` 59 | 60 | ### Class Methods 61 | 62 | Define enums, and reference them as class methods. 63 | 64 | ``` ruby 65 | class OrderState 66 | include Ruby::Enum 67 | 68 | define :created, 'created' 69 | define :paid, 'paid' 70 | end 71 | ``` 72 | 73 | ```ruby 74 | OrderState.created # 'created' 75 | OrderState.paid # 'paid' 76 | OrderState.undefined # NoMethodError is raised 77 | OrderState.keys # [ :created, :paid ] 78 | OrderState.values # ['created', 'paid'] 79 | OrderState.to_h # { :created => 'created', :paid => 'paid' } 80 | ``` 81 | 82 | ### Default Value 83 | 84 | The value is optional. If unspecified, the value will default to the key. 85 | 86 | ``` ruby 87 | class OrderState 88 | include Ruby::Enum 89 | 90 | define :UNSPECIFIED 91 | define :unspecified 92 | end 93 | ``` 94 | 95 | ``` ruby 96 | OrderState::UNSPECIFIED # :UNSPECIFIED 97 | OrderState.unspecified # :unspecified 98 | ``` 99 | 100 | ### Enumerating 101 | 102 | Enums support all `Enumerable` methods. 103 | 104 | #### Iterating 105 | 106 | ``` ruby 107 | OrderState.each do |key, enum| 108 | # key and enum.key are :CREATED, :PAID 109 | # enum.value is 'created', 'paid' 110 | end 111 | ``` 112 | 113 | ``` ruby 114 | OrderState.each_key do |key| 115 | # :CREATED, :PAID 116 | end 117 | ``` 118 | 119 | ``` ruby 120 | OrderState.each_value do |value| 121 | # 'created', 'paid' 122 | end 123 | ``` 124 | 125 | #### Mapping 126 | 127 | ``` ruby 128 | OrderState.map do |key, enum| 129 | # key and enum.key are :CREATED, :PAID 130 | # enum.value is 'created', 'paid' 131 | [enum.value, key] 132 | end 133 | 134 | # => [ ['created', :CREATED], ['paid', :PAID] ] 135 | ``` 136 | 137 | #### Reducing 138 | 139 | ``` ruby 140 | OrderState.reduce([]) do |arr, (key, enum)| 141 | # key and enum.key are :CREATED, :PAID 142 | # enum.value is 'created', 'paid' 143 | arr << [enum.value, key] 144 | end 145 | 146 | # => [ ['created', :CREATED], ['paid', :PAID] ] 147 | ``` 148 | 149 | #### Sorting 150 | 151 | ``` ruby 152 | OrderState.sort_by do |key, enum| 153 | # key and enum.key are :CREATED, :PAID 154 | # enum.value is 'created', 'paid' 155 | enum.value.length 156 | end 157 | 158 | # => [[:PAID, #], [:CREATED, #]] 159 | ``` 160 | 161 | ### Hashing 162 | 163 | Several hash-like methods are supported. 164 | 165 | #### Retrieving keys and values 166 | 167 | ``` ruby 168 | OrderState.keys 169 | # => [:CREATED, :PAID] 170 | 171 | OrderState.values 172 | # => ['created', 'paid'] 173 | ``` 174 | 175 | #### Mapping keys to values 176 | 177 | ``` ruby 178 | OrderState.key?(:CREATED) 179 | # => true 180 | 181 | OrderState.value(:CREATED) 182 | # => 'created' 183 | 184 | OrderState.key?(:FAILED) 185 | # => false 186 | 187 | OrderState.value(:FAILED) 188 | # => nil 189 | ``` 190 | 191 | #### Mapping values to keys 192 | 193 | ``` ruby 194 | OrderState.value?('paid') 195 | # => true 196 | 197 | OrderState.key('paid') 198 | # => :PAID 199 | 200 | OrderState.value?('failed') 201 | # => false 202 | 203 | OrderState.key('failed') 204 | # => nil 205 | ``` 206 | 207 | ### Duplicate enumerator keys or duplicate values 208 | 209 | Defining duplicate enums raises `Ruby::Enum::Errors::DuplicateKeyError`. 210 | 211 | ```ruby 212 | class OrderState 213 | include Ruby::Enum 214 | 215 | define :CREATED, 'created' 216 | define :CREATED, 'recreated' # raises DuplicateKeyError 217 | end 218 | ``` 219 | 220 | Defining a duplicate value raises `Ruby::Enum::Errors::DuplicateValueError`. 221 | 222 | ```ruby 223 | class OrderState 224 | include Ruby::Enum 225 | 226 | define :CREATED, 'created' 227 | define :RECREATED, 'created' # raises DuplicateValueError 228 | end 229 | ``` 230 | 231 | The `DuplicateValueError` exception is raised to be consistent with the unique key constraint. Since keys are unique, there needs to be a way to map values to keys using `OrderState.value('created')`. 232 | 233 | ### Inheritance 234 | 235 | 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. 236 | 237 | ``` ruby 238 | class OrderState 239 | include Ruby::Enum 240 | 241 | define :CREATED, 'CREATED' 242 | define :PAID, 'PAID' 243 | end 244 | 245 | class ShippedOrderState < OrderState 246 | define :PREPARED, 'PREPARED' 247 | define :SHIPPED, 'SHIPPED' 248 | end 249 | ``` 250 | 251 | ``` ruby 252 | ShippedOrderState::CREATED # 'CREATED' 253 | ShippedOrderState::PAID # 'PAID' 254 | ShippedOrderState::PREPARED # 'PREPARED' 255 | ShippedOrderState::SHIPPED # 'SHIPPED' 256 | ``` 257 | 258 | The `values` class method will enumerate the values from all base classes. 259 | 260 | ``` ruby 261 | OrderState.values # ['CREATED', 'PAID'] 262 | ShippedOrderState.values # ['CREATED', 'PAID', 'PREPARED', SHIPPED'] 263 | ``` 264 | 265 | ### Exhaustive case matcher 266 | 267 | 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. 268 | 269 | > NOTE: This will add checks at runtime which might lead to worse performance. See [benchmarks](#benchmarks). 270 | 271 | > NOTE: `:else` is a reserved keyword if you want to use `Ruby::Enum::Case`. 272 | 273 | ```ruby 274 | class Color < OrderState 275 | include Ruby::Enum 276 | include Ruby::Enum::Case 277 | 278 | define :RED, :red 279 | define :GREEN, :green 280 | define :BLUE, :blue 281 | define :YELLOW, :yellow 282 | end 283 | ``` 284 | 285 | ```ruby 286 | color = Color::RED 287 | Color.Case(color, { 288 | [Color::GREEN, Color::BLUE] => -> { "order is green or blue" }, 289 | Color::YELLOW => -> { "order is yellow" }, 290 | Color::RED => -> { "order is red" }, 291 | }) 292 | ``` 293 | 294 | It also supports default/else: 295 | 296 | ```ruby 297 | color = Color::RED 298 | Color.Case(color, { 299 | [Color::GREEN, Color::BLUE] => -> { "order is green or blue" }, 300 | else: -> { "order is yellow or red" }, 301 | }) 302 | ``` 303 | 304 | ### I18n support 305 | 306 | This gem has an optional dependency to `i18n`. If it's available, the error messages will have a nice description and can be translated. If it's not available, the errors will only contain the message keys. 307 | 308 | ```ruby 309 | # Add this to your Gemfile if you want to have a nice error description instead of just a message key. 310 | gem "i18n" 311 | ``` 312 | 313 | ## Benchmarks 314 | 315 | Benchmark scripts are defined in the [`benchmarks`](benchmarks) folder and can be run with Rake: 316 | 317 | ```console 318 | rake benchmarks:case 319 | ``` 320 | 321 | ## Contributing 322 | 323 | You're encouraged to contribute to ruby-enum. See [CONTRIBUTING](CONTRIBUTING.md) for details. 324 | 325 | ## Copyright and License 326 | 327 | Copyright (c) 2013-2021, Daniel Doubrovkine and [Contributors](CHANGELOG.md). 328 | 329 | This project is licensed under the [MIT License](LICENSE.md). 330 | 331 | ## Related Projects 332 | 333 | * [typesafe_enum](https://github.com/dmolesUC3/typesafe_enum): Typesafe enums, inspired by Java. 334 | * [renum](https://github.com/duelinmarkers/renum): A readable, but terse enum. 335 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Ruby-Enum 2 | 3 | There are no hard rules about when to release ruby-enum. Release bug fixes frequently, features not so frequently and breaking API changes rarely. 4 | 5 | ### Release 6 | 7 | Run tests, check that all tests succeed locally. 8 | 9 | ``` 10 | bundle install 11 | rake 12 | ``` 13 | 14 | Check that the last build succeeded in [Travis CI](https://travis-ci.org/dblock/ruby-enum) for all supported platforms. 15 | 16 | Add a date to this release in [CHANGELOG.md](CHANGELOG.md). 17 | 18 | ``` 19 | ### 0.2.2 (2015/7/10) 20 | ``` 21 | 22 | Remove the line with "Your contribution here.", since there will be no more contributions to this release. 23 | 24 | Commit your changes. 25 | 26 | ``` 27 | git add README.md CHANGELOG.md lib/ruby-enum/version.rb 28 | git commit -m "Preparing for release, 0.2.2." 29 | ``` 30 | 31 | Release. 32 | 33 | ``` 34 | $ rake release 35 | 36 | ruby-enum 0.2.2 built to pkg/ruby-enum-0.2.2.gem. 37 | Tagged v0.2.2. 38 | Pushed git commits and tags. 39 | Pushed ruby-enum 0.2.2 to rubygems.org. 40 | ``` 41 | 42 | ### Prepare for the Next Version 43 | 44 | Add the next release to [CHANGELOG.md](CHANGELOG.md). 45 | 46 | ``` 47 | ### 0.2.3 (Next) 48 | 49 | * Your contribution here. 50 | ``` 51 | 52 | Increment the third version number in [lib/ruby-enum/version.rb](lib/ruby-enum/version.rb). 53 | 54 | Commit your changes. 55 | 56 | ``` 57 | git add CHANGELOG.md lib/ruby-enum/version.rb 58 | git commit -m "Preparing for next development iteration, 0.2.3." 59 | git push origin master 60 | ``` 61 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'bundler/gem_tasks' 5 | 6 | Bundler.setup :default, :development 7 | 8 | require 'rspec/core' 9 | require 'rspec/core/rake_task' 10 | 11 | RSpec::Core::RakeTask.new(:spec) do |spec| 12 | spec.pattern = FileList['spec/**/*_spec.rb'] 13 | end 14 | 15 | require 'rubocop/rake_task' 16 | RuboCop::RakeTask.new(:rubocop) 17 | 18 | task default: %i[rubocop spec] 19 | 20 | namespace :benchmark do 21 | desc 'Run benchmark for the Ruby::Enum::Case' 22 | task :case do 23 | require_relative 'benchmarks/case' 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading Ruby::Enum 2 | 3 | ## Upgrading to >= 0.9.0 4 | 5 | ### Inheritance & `Ruby::Enum.values` 6 | 7 | This only applies to classes that inherit from another which is a `Ruby::Enum`. 8 | 9 | Prior to version `0.9.0`, the `values` class method would enumerate only the 10 | values defined in the class. 11 | 12 | As of version `0.9.0`, the `values` class method enumerates values defined in 13 | the entire class heirarchy, ancestors first. 14 | 15 | ``` ruby 16 | class PrimaryColors 17 | include Ruby::Enum 18 | 19 | define :RED, 'RED' 20 | define :GREEN, 'GREEN' 21 | define :BLUE, 'BLUE' 22 | end 23 | 24 | class RainbowColors < PrimaryColors 25 | define :ORANGE, 'ORANGE' 26 | define :YELLOW, 'YELLOW' 27 | define :INIDGO, 'INIDGO' 28 | define :VIOLET, 'VIOLET' 29 | end 30 | ``` 31 | 32 | `gem 'ruby-enum', '< 0.9.0'` 33 | 34 | ``` ruby 35 | RainbowColors.values # ['ORANGE', 'YELLOW', 'INIDGO', 'VIOLET'] 36 | ``` 37 | 38 | `gem 'ruby-enum', '>= 0.9.0'` 39 | 40 | ``` ruby 41 | RainbowColors.values # ['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'INIDGO', 'VIOLET'] 42 | ``` 43 | 44 | See [#29](https://github.com/dblock/ruby-enum/pull/29) for more information. 45 | -------------------------------------------------------------------------------- /benchmarks/case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | 5 | require 'benchmark' 6 | require 'ruby-enum' 7 | 8 | ## 9 | # Test enum 10 | class Color 11 | include Ruby::Enum 12 | include Ruby::Enum::Case 13 | 14 | define :RED, :red 15 | define :GREEN, :green 16 | define :BLUE, :blue 17 | end 18 | 19 | puts 'Running 1.000.000 normal case statements' 20 | case_statement_time = Benchmark.realtime do 21 | 1_000_000.times do 22 | case Color::RED 23 | when Color::RED, Color::GREEN 24 | 'red or green' 25 | when Color::BLUE 26 | 'blue' 27 | end 28 | end 29 | end 30 | 31 | puts 'Running 1.000.000 ruby-enum case statements' 32 | ruby_enum_time = Benchmark.realtime do 33 | 1_000_000.times do 34 | Color.case(Color::RED, 35 | { 36 | [Color::RED, Color::GREEN] => -> { 'red or green' }, 37 | Color::BLUE => -> { 'blue' } 38 | }) 39 | end 40 | end 41 | 42 | puts "ruby-enum case: #{ruby_enum_time.round(4)}" 43 | puts "case statement: #{case_statement_time.round(4)}" 44 | 45 | puts "ruby-enum case is #{(ruby_enum_time / case_statement_time).round(2)} times slower" 46 | -------------------------------------------------------------------------------- /lib/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | ruby: 3 | enum: 4 | errors: 5 | messages: 6 | uninitialized_constant: 7 | message: "Uninitialized constant." 8 | summary: "The constant %{name}::%{key} has not been defined." 9 | resolution: "The enumerated value could not be found in class %{name}. Use 'define' to declare it.\n 10 | \_Example:\n 11 | \_\_module %{name}\n 12 | \_\_\_include Ruby::Enum\n 13 | \_\_\_define %{key}, 'value'\n 14 | \_\_end" 15 | duplicate_key: 16 | message: 'Duplicate key.' 17 | summary: "The constant %{name}::%{key} has already been defined." 18 | resolution: "The enumerated key is already defined in class %{name}%. Please use a different name to declare it." 19 | duplicate_value: 20 | message: 'Duplicate value found.' 21 | summary: "The value %{value} has already been defined in %{name}%." 22 | resolution: "The enumerated value %{value} is already defined in class %{name}%. Please use a different value." 23 | -------------------------------------------------------------------------------- /lib/ruby-enum.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ruby-enum/version' 4 | require 'ruby-enum/enum' 5 | require 'ruby-enum/enum/case' 6 | require 'ruby-enum/enum/i18n_mock' 7 | 8 | # Try to load the I18n gem and provide a mock if it is not available. 9 | begin 10 | require 'i18n' 11 | Ruby::Enum.i18n = I18n 12 | rescue LoadError 13 | # I18n is not available 14 | # :nocov: 15 | # Tests for this loading are in the spec_i18n folder 16 | Ruby::Enum.i18n = Ruby::Enum::I18nMock 17 | # :nocov: 18 | end 19 | 20 | Ruby::Enum.i18n.load_path << File.join(File.dirname(__FILE__), 'config', 'locales', 'en.yml') 21 | 22 | require 'ruby-enum/errors/base' 23 | require 'ruby-enum/errors/uninitialized_constant_error' 24 | require 'ruby-enum/errors/duplicate_key_error' 25 | require 'ruby-enum/errors/duplicate_value_error' 26 | -------------------------------------------------------------------------------- /lib/ruby-enum/enum.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ruby 4 | module Enum 5 | class << self 6 | # Needed for I18n mock 7 | attr_accessor :i18n 8 | end 9 | 10 | attr_reader :key, :value 11 | 12 | def initialize(key, value) 13 | @key = key 14 | @value = value 15 | end 16 | 17 | def self.included(base) 18 | base.extend Enumerable 19 | base.extend ClassMethods 20 | 21 | base.private_class_method(:new) 22 | end 23 | 24 | module ClassMethods 25 | # Define an enumerated value. 26 | # 27 | # === Parameters 28 | # [key] Enumerator key. 29 | # [value] Enumerator value. 30 | def define(key, value = key) 31 | @_enum_hash ||= {} 32 | @_enums_by_value ||= {} 33 | 34 | validate_key!(key) 35 | validate_value!(value) 36 | 37 | store_new_instance(key, value) 38 | 39 | if upper?(key.to_s) 40 | const_set key, value 41 | else 42 | define_singleton_method(key) { value } 43 | end 44 | end 45 | 46 | def store_new_instance(key, value) 47 | new_instance = new(key, value) 48 | @_enum_hash[key] = new_instance 49 | @_enums_by_value[value] = new_instance 50 | end 51 | 52 | def const_missing(key) 53 | raise Ruby::Enum::Errors::UninitializedConstantError, name: name, key: key 54 | end 55 | 56 | # Iterate over all enumerated values. 57 | # Required for Enumerable mixin 58 | def each(&block) 59 | @_enum_hash.each(&block) 60 | end 61 | 62 | # Attempt to parse an enum key and return the 63 | # corresponding value. 64 | # 65 | # === Parameters 66 | # [k] The key string to parse. 67 | # 68 | # Returns the corresponding value or nil. 69 | def parse(k) 70 | k = k.to_s.upcase 71 | each do |key, enum| 72 | return enum.value if key.to_s.upcase == k 73 | end 74 | nil 75 | end 76 | 77 | # Whether the specified key exists in this enum. 78 | # 79 | # === Parameters 80 | # [k] The string key to check. 81 | # 82 | # Returns true if the key exists, false otherwise. 83 | def key?(k) 84 | @_enum_hash.key?(k) 85 | end 86 | 87 | # Gets the string value for the specified key. 88 | # 89 | # === Parameters 90 | # [k] The key symbol to get the value for. 91 | # 92 | # Returns the corresponding enum instance or nil. 93 | def value(k) 94 | enum = @_enum_hash[k] 95 | enum&.value 96 | end 97 | 98 | # Whether the specified value exists in this enum. 99 | # 100 | # === Parameters 101 | # [k] The string value to check. 102 | # 103 | # Returns true if the value exists, false otherwise. 104 | def value?(v) 105 | @_enums_by_value.key?(v) 106 | end 107 | 108 | # Gets the key symbol for the specified value. 109 | # 110 | # === Parameters 111 | # [v] The string value to parse. 112 | # 113 | # Returns the corresponding key symbol or nil. 114 | def key(v) 115 | enum = @_enums_by_value[v] 116 | enum&.key 117 | end 118 | 119 | # Returns all enum keys. 120 | def keys 121 | @_enum_hash.values.map(&:key) 122 | end 123 | 124 | # Returns all enum values. 125 | def values 126 | result = @_enum_hash.values.map(&:value) 127 | 128 | if superclass < Ruby::Enum 129 | superclass.values + result 130 | else 131 | result 132 | end 133 | end 134 | 135 | # Iterate over all enumerated values. 136 | # Required for Enumerable mixin 137 | def each_value(&_block) 138 | @_enum_hash.each_value do |v| 139 | yield v.value 140 | end 141 | end 142 | 143 | # Iterate over all enumerated keys. 144 | # Required for Enumerable mixin 145 | def each_key(&_block) 146 | @_enum_hash.each_value do |v| 147 | yield v.key 148 | end 149 | end 150 | 151 | def to_h 152 | @_enum_hash.transform_values(&:value) 153 | end 154 | 155 | private 156 | 157 | def upper?(s) 158 | !/[[:upper:]]/.match(s).nil? 159 | end 160 | 161 | def validate_key!(key) 162 | return unless @_enum_hash.key?(key) 163 | 164 | raise Ruby::Enum::Errors::DuplicateKeyError, name: name, key: key 165 | end 166 | 167 | def validate_value!(value) 168 | return unless @_enums_by_value.key?(value) 169 | 170 | raise Ruby::Enum::Errors::DuplicateValueError, name: name, value: value 171 | end 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/ruby-enum/enum/case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ruby 4 | module Enum 5 | ## 6 | # Adds a method to an enum class that allows for exhaustive matching on a value. 7 | # 8 | # @example 9 | # class Color 10 | # include Ruby::Enum 11 | # include Ruby::Enum::Case 12 | # 13 | # define :RED, :red 14 | # define :GREEN, :green 15 | # define :BLUE, :blue 16 | # define :YELLOW, :yellow 17 | # end 18 | # 19 | # Color.case(Color::RED, { 20 | # [Color::RED, Color::GREEN] => -> { "red or green" }, 21 | # Color::BLUE => -> { "blue" }, 22 | # Color::YELLOW => -> { "yellow" }, 23 | # }) 24 | # 25 | # Reserves the :else key for a default case: 26 | # Color.case(Color::RED, { 27 | # [Color::RED, Color::GREEN] => -> { "red or green" }, 28 | # else: -> { "blue or yellow" }, 29 | # }) 30 | module Case 31 | def self.included(klass) 32 | klass.extend(ClassMethods) 33 | end 34 | 35 | ## 36 | # @see Ruby::Enum::Case 37 | module ClassMethods 38 | class ValuesNotDefinedError < StandardError 39 | end 40 | 41 | class NotAllCasesHandledError < StandardError 42 | end 43 | 44 | def case(value, cases) 45 | validate_cases(cases) 46 | 47 | filtered_cases = cases.select do |values, _proc| 48 | values = [values] unless values.is_a?(Array) 49 | values.include?(value) 50 | end 51 | 52 | return call_proc(cases[:else], value) if filtered_cases.none? 53 | 54 | results = filtered_cases.map { |_values, proc| call_proc(proc, value) } 55 | 56 | # Return the first result if there is only one result 57 | results.size == 1 ? results.first : results 58 | end 59 | 60 | private 61 | 62 | def call_proc(proc, value) 63 | return if proc.nil? 64 | 65 | if proc.arity == 1 66 | proc.call(value) 67 | else 68 | proc.call 69 | end 70 | end 71 | 72 | def validate_cases(cases) 73 | all_values = cases.keys.flatten - [:else] 74 | else_defined = cases.key?(:else) 75 | superfluous_values = all_values - values 76 | missing_values = values - all_values 77 | 78 | raise ValuesNotDefinedError, "Value(s) not defined: #{superfluous_values.join(', ')}" if superfluous_values.any? 79 | raise NotAllCasesHandledError, "Not all cases handled: #{missing_values.join(', ')}" if missing_values.any? && !else_defined 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/ruby-enum/enum/i18n_mock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # :nocov: 4 | module Ruby 5 | module Enum 6 | ## 7 | # Mock I18n module in case the i18n gem is not available. 8 | module I18nMock 9 | def self.load_path 10 | [] 11 | end 12 | 13 | def self.translate(key, _options = {}) 14 | key 15 | end 16 | end 17 | end 18 | end 19 | # :nocov: 20 | -------------------------------------------------------------------------------- /lib/ruby-enum/errors/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ruby 4 | module Enum 5 | module Errors 6 | class Base < StandardError 7 | # Problem occurred. 8 | attr_reader :problem 9 | 10 | # Summary of the problem. 11 | attr_reader :summary 12 | 13 | # Suggested problem resolution. 14 | attr_reader :resolution 15 | 16 | # Compose the message. 17 | # === Parameters 18 | # [key] Lookup key in the translation table. 19 | # [attributes] The objects to pass to create the message. 20 | def compose_message(key, attributes = {}) 21 | @problem = create_problem(key, attributes) 22 | @summary = create_summary(key, attributes) 23 | @resolution = create_resolution(key, attributes) 24 | 25 | "\nProblem:\n #{@problem}" \ 26 | "\nSummary:\n #{@summary}" + "\nResolution:\n #{@resolution}" 27 | end 28 | 29 | private 30 | 31 | BASE_KEY = 'ruby.enum.errors.messages' # :nodoc: 32 | 33 | # Given the key of the specific error and the options hash, translate the 34 | # message. 35 | # 36 | # === Parameters 37 | # [key] The key of the error in the locales. 38 | # [options] The objects to pass to create the message. 39 | # 40 | # Returns a localized error message string. 41 | def translate(key, options) 42 | Ruby::Enum.i18n.translate("#{BASE_KEY}.#{key}", locale: :en, **options).strip 43 | end 44 | 45 | # Create the problem. 46 | # 47 | # === Parameters 48 | # [key] The error key. 49 | # [attributes] The attributes to interpolate. 50 | # 51 | # Returns the problem. 52 | def create_problem(key, attributes) 53 | translate("#{key}.message", attributes) 54 | end 55 | 56 | # Create the summary. 57 | # 58 | # === Parameters 59 | # [key] The error key. 60 | # [attributes] The attributes to interpolate. 61 | # 62 | # Returns the summary. 63 | def create_summary(key, attributes) 64 | translate("#{key}.summary", attributes) 65 | end 66 | 67 | # Create the resolution. 68 | # 69 | # === Parameters 70 | # [key] The error key. 71 | # [attributes] The attributes to interpolate. 72 | # 73 | # Returns the resolution. 74 | def create_resolution(key, attributes) 75 | translate("#{key}.resolution", attributes) 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/ruby-enum/errors/duplicate_key_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ruby 4 | module Enum 5 | module Errors 6 | # Error raised when a duplicate enum key is found 7 | class DuplicateKeyError < Base 8 | def initialize(attrs) 9 | super(compose_message('duplicate_key', attrs)) 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/ruby-enum/errors/duplicate_value_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ruby 4 | module Enum 5 | module Errors 6 | # Error raised when a duplicate enum value is found 7 | class DuplicateValueError < Base 8 | def initialize(attrs) 9 | super(compose_message('duplicate_value', attrs)) 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/ruby-enum/errors/uninitialized_constant_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ruby 4 | module Enum 5 | module Errors 6 | class UninitializedConstantError < Base 7 | def initialize(attrs) 8 | super(compose_message('uninitialized_constant', attrs)) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ruby-enum/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ruby 4 | module Enum 5 | VERSION = '1.0.1' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/ruby_enum.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ruby-enum' 4 | -------------------------------------------------------------------------------- /ruby-enum.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path('lib', __dir__) 4 | require 'ruby-enum/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'ruby-enum' 8 | s.version = Ruby::Enum::VERSION 9 | s.authors = ['Daniel Doubrovkine'] 10 | s.email = 'dblock@dblock.org' 11 | s.platform = Gem::Platform::RUBY 12 | s.required_rubygems_version = '>= 1.3.6' 13 | s.required_ruby_version = '>= 2.7' 14 | s.files = Dir['**/*'] 15 | s.require_paths = ['lib'] 16 | s.homepage = 'http://github.com/dblock/ruby-enum' 17 | s.licenses = ['MIT'] 18 | s.summary = 'Enum-like behavior for Ruby.' 19 | s.metadata['rubygems_mfa_required'] = 'true' 20 | end 21 | -------------------------------------------------------------------------------- /spec/ruby-enum/enum/case_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Ruby::Enum::Case do 6 | test_enum = 7 | Class.new do 8 | include Ruby::Enum 9 | include Ruby::Enum::Case 10 | 11 | define :RED, :red 12 | define :GREEN, :green 13 | define :BLUE, :blue 14 | end 15 | 16 | describe '.case' do 17 | context 'when all cases are defined' do 18 | subject { test_enum.case(test_enum::RED, cases) } 19 | 20 | let(:cases) do 21 | { 22 | [test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, 23 | test_enum::BLUE => -> { 'blue' } 24 | } 25 | end 26 | 27 | it { is_expected.to eq('red or green') } 28 | 29 | context 'when the value is nil' do 30 | subject { test_enum.case(nil, cases) } 31 | 32 | it { is_expected.to be_nil } 33 | end 34 | 35 | context 'when the value is empty' do 36 | subject { test_enum.case('', cases) } 37 | 38 | it { is_expected.to be_nil } 39 | end 40 | 41 | context 'when the value is the value of the enum' do 42 | subject { test_enum.case(:red, cases) } 43 | 44 | it { is_expected.to eq('red or green') } 45 | end 46 | 47 | context 'when the value is used inside the lambda' do 48 | subject { test_enum.case(test_enum::RED, cases) } 49 | 50 | let(:cases) do 51 | { 52 | [test_enum::RED, test_enum::GREEN] => ->(color) { "is #{color}" }, 53 | test_enum::BLUE => -> { 'blue' } 54 | } 55 | end 56 | 57 | it { is_expected.to eq('is red') } 58 | end 59 | end 60 | 61 | context 'when there are mutliple matches' do 62 | subject do 63 | test_enum.case( 64 | test_enum::RED, 65 | { 66 | [test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, 67 | test_enum::RED => -> { 'red' }, 68 | test_enum::BLUE => -> { 'blue' } 69 | } 70 | ) 71 | end 72 | 73 | it { is_expected.to eq(['red or green', 'red']) } 74 | end 75 | 76 | context 'when not all cases are defined' do 77 | it 'raises an error' do 78 | expect do 79 | test_enum.case( 80 | test_enum::RED, 81 | { [test_enum::RED, test_enum::GREEN] => -> { 'red or green' } } 82 | ) 83 | end.to raise_error(Ruby::Enum::Case::ClassMethods::NotAllCasesHandledError) 84 | end 85 | end 86 | 87 | context 'when not all cases are defined but :else is specified (default case)' do 88 | it 'does not raise an error' do 89 | expect do 90 | result = test_enum.case( 91 | test_enum::BLUE, 92 | { 93 | [test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, 94 | else: -> { 'blue' } 95 | } 96 | ) 97 | 98 | expect(result).to eq('blue') 99 | end.not_to raise_error 100 | end 101 | end 102 | 103 | context 'when a superfluous case is defined' do 104 | it 'raises an error' do 105 | expect do 106 | test_enum.case( 107 | test_enum::RED, 108 | { 109 | [test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, 110 | test_enum::BLUE => -> { 'blue' }, 111 | :something => -> { 'green' } 112 | } 113 | ) 114 | end.to raise_error(Ruby::Enum::Case::ClassMethods::ValuesNotDefinedError) 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/ruby-enum/enum_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | class Colors 6 | include Ruby::Enum 7 | 8 | define :RED, 'red' 9 | define :GREEN, 'green' 10 | end 11 | 12 | class FirstSubclass < Colors 13 | define :ORANGE, 'orange' 14 | end 15 | 16 | class SecondSubclass < FirstSubclass 17 | define :PINK, 'pink' 18 | end 19 | 20 | describe Ruby::Enum do 21 | it 'returns an enum value' do 22 | expect(Colors::RED).to eq 'red' 23 | expect(Colors::GREEN).to eq 'green' 24 | end 25 | 26 | context 'when the i18n gem is loaded' do 27 | it 'raises UninitializedConstantError on an invalid constant' do 28 | expect do 29 | Colors::ANYTHING 30 | end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /The constant Colors::ANYTHING has not been defined./ 31 | end 32 | end 33 | 34 | context 'when the i18n gem is not loaded' do 35 | before do 36 | allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) 37 | end 38 | 39 | it 'raises UninitializedConstantError on an invalid constant' do 40 | expect do 41 | Colors::ANYTHING 42 | end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /ruby.enum.errors.messages.uninitialized_constant.summary/ 43 | end 44 | end 45 | 46 | describe '#each' do 47 | it 'iterates over constants' do 48 | keys = [] 49 | enum_keys = [] 50 | enum_values = [] 51 | Colors.each do |key, enum| 52 | keys << key 53 | enum_keys << enum.key 54 | enum_values << enum.value 55 | end 56 | expect(keys).to eq %i[RED GREEN] 57 | expect(enum_keys).to eq %i[RED GREEN] 58 | expect(enum_values).to eq %w[red green] 59 | end 60 | end 61 | 62 | describe '#map' do 63 | it 'maps constants' do 64 | key_key_values = Colors.map do |key, enum| 65 | [key, enum.key, enum.value] 66 | end 67 | expect(key_key_values.count).to eq 2 68 | expect(key_key_values[0]).to eq [:RED, :RED, 'red'] 69 | expect(key_key_values[1]).to eq [:GREEN, :GREEN, 'green'] 70 | end 71 | end 72 | 73 | describe '#parse' do 74 | it 'parses exact value' do 75 | expect(Colors.parse('red')).to eq(Colors::RED) 76 | end 77 | 78 | it 'is case-insensitive' do 79 | expect(Colors.parse('ReD')).to eq(Colors::RED) 80 | end 81 | 82 | it 'returns nil for a null value' do 83 | expect(Colors.parse(nil)).to be_nil 84 | end 85 | 86 | it 'returns nil for an invalid value' do 87 | expect(Colors.parse('invalid')).to be_nil 88 | end 89 | end 90 | 91 | describe '#key?' do 92 | it 'returns true for valid keys accessed directly' do 93 | Colors.keys.each do |key| # rubocop:disable Style/HashEachMethods 94 | expect(Colors.key?(key)).to be(true) 95 | end 96 | end 97 | 98 | it 'returns true for valid keys accessed via each_keys' do 99 | Colors.each_key do |key| 100 | expect(Colors.key?(key)).to be(true) 101 | end 102 | end 103 | 104 | it 'returns false for invalid keys' do 105 | expect(Colors.key?(:NOT_A_KEY)).to be(false) 106 | end 107 | end 108 | 109 | describe '#value' do 110 | it 'returns string values for keys' do 111 | Colors.each do |key, enum| 112 | expect(Colors.value(key)).to eq(enum.value) 113 | end 114 | end 115 | 116 | it 'returns nil for an invalid key' do 117 | expect(Colors.value(:NOT_A_KEY)).to be_nil 118 | end 119 | end 120 | 121 | describe '#value?' do 122 | it 'returns true for valid values accessed directly' do 123 | Colors.values.each do |value| # rubocop:disable Style/HashEachMethods 124 | expect(Colors.value?(value)).to be(true) 125 | end 126 | end 127 | 128 | it 'returns true for valid values accessed via each_value' do 129 | Colors.each_value do |value| 130 | expect(Colors.value?(value)).to be(true) 131 | end 132 | end 133 | 134 | it 'returns false for invalid values' do 135 | expect(Colors.value?('I am not a value')).to be(false) 136 | end 137 | end 138 | 139 | describe '#key' do 140 | it 'returns enum instances for values' do 141 | Colors.each do |_, enum| # rubocop:disable Style/HashEachMethods 142 | expect(Colors.key(enum.value)).to eq(enum.key) 143 | end 144 | end 145 | 146 | it 'returns nil for an invalid value' do 147 | expect(Colors.key('invalid')).to be_nil 148 | end 149 | end 150 | 151 | describe '#keys' do 152 | it 'returns keys' do 153 | expect(Colors.keys).to eq(%i[RED GREEN]) 154 | end 155 | end 156 | 157 | describe '#values' do 158 | it 'returns values' do 159 | expect(Colors.values).to eq(%w[red green]) 160 | end 161 | end 162 | 163 | describe '#to_h' do 164 | it 'returns a hash of key:values' do 165 | expect(Colors.to_h).to eq(RED: 'red', GREEN: 'green') 166 | end 167 | end 168 | 169 | context 'when a duplicate key is used' do 170 | context 'when the i18n gem is loaded' do 171 | it 'raises DuplicateKeyError' do 172 | expect do 173 | Colors.class_eval do 174 | define :RED, 'some' 175 | end 176 | end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /The constant Colors::RED has already been defined./ 177 | end 178 | end 179 | 180 | context 'when the i18n gem is not loaded' do 181 | before do 182 | allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) 183 | end 184 | 185 | it 'raises DuplicateKeyError' do 186 | expect do 187 | Colors.class_eval do 188 | define :RED, 'some' 189 | end 190 | end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /ruby.enum.errors.messages.duplicate_key.message/ 191 | end 192 | end 193 | end 194 | 195 | context 'when a duplicate value is used' do 196 | context 'when the i18n gem is loaded' do 197 | it 'raises a DuplicateValueError' do 198 | expect do 199 | Colors.class_eval do 200 | define :Other, 'red' 201 | end 202 | end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /The value red has already been defined./ 203 | end 204 | end 205 | 206 | context 'when the i18n gem is not loaded' do 207 | before do 208 | allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) 209 | end 210 | 211 | it 'raises a DuplicateValueError' do 212 | expect do 213 | Colors.class_eval do 214 | define :Other, 'red' 215 | end 216 | end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /ruby.enum.errors.messages.duplicate_value.summary/ 217 | end 218 | end 219 | end 220 | 221 | describe 'Given a class that has not defined any enums' do 222 | class EmptyEnums 223 | include Ruby::Enum 224 | end 225 | it do 226 | expect { EmptyEnums::ORANGE }.to raise_error Ruby::Enum::Errors::UninitializedConstantError 227 | end 228 | end 229 | 230 | context 'when a constant is redefined in a global namespace' do 231 | before do 232 | RED = 'black' 233 | end 234 | 235 | it { expect(Colors::RED).to eq 'red' } 236 | end 237 | 238 | describe 'Subclass behavior' do 239 | it 'contains the enums defined in the parent class' do 240 | expect(FirstSubclass::GREEN).to eq 'green' 241 | expect(FirstSubclass::RED).to eq 'red' 242 | end 243 | 244 | it 'contains its own enums' do 245 | expect(FirstSubclass::ORANGE).to eq 'orange' 246 | end 247 | 248 | it 'parent class should not have enums defined in child classes' do 249 | expect { Colors::ORANGE }.to raise_error Ruby::Enum::Errors::UninitializedConstantError 250 | end 251 | 252 | context 'when defining a 2 level depth subclass' do 253 | subject { SecondSubclass } 254 | 255 | it 'contains its own enums and all the enums defined in the parent classes' do 256 | expect(subject::RED).to eq 'red' 257 | expect(subject::GREEN).to eq 'green' 258 | expect(subject::ORANGE).to eq 'orange' 259 | expect(subject::PINK).to eq 'pink' 260 | end 261 | 262 | describe '#values' do 263 | subject { SecondSubclass.values } 264 | 265 | it 'contains the values from all of the parent classes' do 266 | expect(subject).to eq(%w[red green orange pink]) 267 | end 268 | end 269 | end 270 | 271 | describe '#values' do 272 | subject { FirstSubclass.values } 273 | 274 | it 'contains the values from the parent class' do 275 | expect(subject).to eq(%w[red green orange]) 276 | end 277 | end 278 | end 279 | 280 | describe 'default value' do 281 | class Default 282 | include Ruby::Enum 283 | define :KEY 284 | end 285 | 286 | subject { Default::KEY } 287 | 288 | it 'equals the key' do 289 | expect(subject).to eq(:KEY) 290 | end 291 | end 292 | 293 | describe 'non constant definitions' do 294 | class States 295 | include Ruby::Enum 296 | define :created, 'Created' 297 | define :published, 'Published' 298 | define :undefined 299 | end 300 | subject { States } 301 | 302 | it 'behaves like an enum' do 303 | expect(subject.created).to eq 'Created' 304 | expect(subject.published).to eq 'Published' 305 | expect(subject.undefined).to eq :undefined 306 | 307 | expect(subject.key?(:created)).to be true 308 | expect(subject.key('Created')).to eq :created 309 | 310 | expect(subject.value?('Created')).to be true 311 | expect(subject.value(:created)).to eq 'Created' 312 | 313 | expect(subject.key?(:undefined)).to be true 314 | expect(subject.key(:undefined)).to eq :undefined 315 | 316 | expect(subject.value?(:undefined)).to be true 317 | expect(subject.value(:undefined)).to eq :undefined 318 | end 319 | end 320 | end 321 | -------------------------------------------------------------------------------- /spec/ruby-enum/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Ruby::Enum do 6 | it 'has a version' do 7 | expect(Ruby::Enum::VERSION).not_to be_nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | 5 | require 'rubygems' 6 | 7 | require 'simplecov' 8 | SimpleCov.start 9 | 10 | require 'rspec' 11 | require 'ruby-enum' 12 | 13 | RSpec.configure(&:raise_errors_for_deprecations!) 14 | -------------------------------------------------------------------------------- /spec_i18n/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'http://rubygems.org' 4 | 5 | gemspec path: '../', name: 'ruby-enum' 6 | 7 | # This Gemfile should not include any gem that has i18n as a dependency. 8 | gem 'rake' 9 | gem 'rspec', '~> 3.0' 10 | -------------------------------------------------------------------------------- /spec_i18n/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | 5 | require 'rspec/core' 6 | require 'rspec/core/rake_task' 7 | 8 | RSpec::Core::RakeTask.new(:spec) do |spec| 9 | spec.pattern = FileList['spec/**/*_spec.rb'] 10 | end 11 | 12 | task default: %i[spec] 13 | -------------------------------------------------------------------------------- /spec_i18n/spec/i18n_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | test_class = Class.new do 6 | include Ruby::Enum 7 | 8 | define :RED, 'red' 9 | define :GREEN, 'green' 10 | end 11 | 12 | describe Ruby::Enum do 13 | context 'when the i18n gem is not loaded' do 14 | it 'raises UninitializedConstantError on an invalid constant' do 15 | expect do 16 | test_class::ANYTHING 17 | end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /ruby.enum.errors.messages.uninitialized_constant.summary/ 18 | end 19 | 20 | context 'when a duplicate key is used' do 21 | before do 22 | allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) 23 | end 24 | 25 | it 'raises DuplicateKeyError' do 26 | expect do 27 | test_class.class_eval do 28 | define :RED, 'some' 29 | end 30 | end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /ruby.enum.errors.messages.duplicate_key.message/ 31 | end 32 | end 33 | 34 | context 'when a duplicate value is used' do 35 | before do 36 | allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) 37 | end 38 | 39 | it 'raises a DuplicateValueError' do 40 | expect do 41 | test_class.class_eval do 42 | define :Other, 'red' 43 | end 44 | end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /ruby.enum.errors.messages.duplicate_value.summary/ 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec_i18n/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib')) 4 | 5 | require 'rubygems' 6 | 7 | require 'rspec' 8 | require 'ruby-enum' 9 | --------------------------------------------------------------------------------