diff options
37 files changed, 791 insertions, 200 deletions
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..f8fab3c --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,21 @@ +--- +name: coverage +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Ruby (3.2) + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + - uses: amancevice/setup-code-climate@v0 + with: + cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} + - run: cc-test-reporter before-build + - name: Build and test with RSpec + run: | + bundle install --jobs 4 --retry 3 + bundle exec rspec + - run: cc-test-reporter after-build diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 0000000..c643c4b --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,22 @@ +--- +name: danger +on: + pull_request: + types: [opened, reopened, edited, synchronize] +jobs: + danger: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + - name: Run Danger + run: | + # the personal token is public, this is ok, base64 encode to avoid tripping Github + TOKEN=$(echo -n Z2hwX0xNQ3VmanBFeTBvYkZVTWh6NVNqVFFBOEUxU25abzBqRUVuaAo= | base64 --decode) + DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..9ffe62f --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,16 @@ +name: Lint +on: [push, pull_request] +jobs: + rubocop: + name: RuboCop + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + - run: bundle exec rubocop + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..cdbde68 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +--- +name: test +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + entry: + - { ruby: '2.7', allowed-failure: false } + - { ruby: '3.0', allowed-failure: false } + - { ruby: '3.1', allowed-failure: false } + - { ruby: '3.2', allowed-failure: false } + - { ruby: 'ruby-head', allowed-failure: true } + - { ruby: 'truffleruby-head', allowed-failure: true } + - { ruby: 'jruby-head', allowed-failure: true } + name: test (${{ matrix.entry.ruby }}) + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.entry.ruby }} + - run: bundle install --jobs=3 --retry=3 --path=vendor/bundle + - run: bundle exec rake spec + continue-on-error: ${{ matrix.entry.allowed-failure }} + - name: Specs for when the i18n gem is not available + run: | + cd spec_i18n + bundle install --jobs=3 --retry=3 + pwd + bundle exec rake spec + continue-on-error: ${{ matrix.entry.allowed-failure }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70c558c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +Gemfile.lock +pkg +coverage @@ -0,0 +1,2 @@ +--format documentation +--color diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..1f7eac4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,30 @@ +require: + - rubocop-rake + - rubocop-rspec + +AllCops: + TargetRubyVersion: 2.7 + NewCops: enable + Exclude: + - vendor/**/* + +Metrics/BlockLength: + Exclude: + - 'spec/**/*_spec.rb' + +RSpec/SpecFilePathFormat: + Enabled: false + +RSpec/FilePath: + Enabled: false + +Style/HashEachMethods: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true + +inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..9039cdc --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,72 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2023-02-14 01:34:25 UTC using RuboCop version 1.45.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 4 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'spec/ruby-enum/enum_spec.rb' + +# Offense count: 1 +# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. +# CheckDefinitionPathHierarchyRoots: lib, spec, test, src +# 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 +Naming/FileName: + Exclude: + - 'lib/ruby-enum.rb' + +# Offense count: 6 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to +Naming/MethodParameterName: + Exclude: + - 'lib/ruby-enum/enum.rb' + +# Offense count: 3 +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 11 + +# Offense count: 4 +RSpec/LeakyConstantDeclaration: + Exclude: + - 'spec/ruby-enum/enum_spec.rb' + +# Offense count: 6 +RSpec/MultipleExpectations: + Max: 11 + +# Offense count: 18 +# Configuration parameters: EnforcedStyle, IgnoreSharedExamples. +# SupportedStyles: always, named_only +RSpec/NamedSubject: + Exclude: + - 'spec/ruby-enum/enum_spec.rb' + +# Offense count: 1 +# Configuration parameters: AllowedGroups. +RSpec/NestedGroups: + Max: 4 + +# Offense count: 4 +# Configuration parameters: AllowedConstants. +Style/Documentation: + Exclude: + - 'spec/**/*' + - 'test/**/*' + - 'lib/ruby-enum/enum.rb' + - 'lib/ruby-enum/errors/base.rb' + - 'lib/ruby-enum/errors/uninitialized_constant_error.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 148 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6566fe7..4145b7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### 1.0.0 (2023/01/10) + +* [#41](https://github.com/dblock/ruby-enum/pull/41): Make i18n dependency optional - [@peterfication](https://github.com/peterfication). +* [#43](https://github.com/dblock/ruby-enum/pull/43): Add exhaustive case matcher - [@peterfication](https://github.com/peterfication). +* [#40](https://github.com/dblock/ruby-enum/pull/39): Enable new Rubocop cops and address/allowlist lints - [@petergoldstein](https://github.com/petergoldstein). +* [#39](https://github.com/dblock/ruby-enum/pull/39): Require Ruby >= 2.7 - [@petergoldstein](https://github.com/petergoldstein). +* [#38](https://github.com/dblock/ruby-enum/pull/38): Ensure Ruby >= 2.3 - [@ojab](https://github.com/ojab). + ### 0.9.0 (2021/01/21) * [#34](https://github.com/dblock/ruby-enum/pull/34): Added support for Ruby 3.0 - [@dblock](https://github.com/dblock). @@ -11,7 +11,9 @@ group :development, :test do gem 'danger-changelog', '0.6.1' gem 'danger-toc', '0.2.0' gem 'rspec', '~> 3.0' - gem 'rubocop', '0.80.1' + gem 'rubocop', '~> 1.0' + gem 'rubocop-rake' + gem 'rubocop-rspec' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index f2954cd..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,136 +0,0 @@ -PATH - remote: . - specs: - ruby-enum (0.9.0) - i18n - -GEM - remote: http://rubygems.org/ - specs: - activesupport (6.1.0) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) - ast (2.4.2) - claide (1.0.3) - claide-plugins (0.9.2) - cork - nap - open4 (~> 1.3) - colored2 (3.1.2) - concurrent-ruby (1.1.7) - cork (0.3.0) - colored2 (~> 3.1) - danger (8.2.1) - claide (~> 1.0) - claide-plugins (>= 0.9.2) - colored2 (~> 3.1) - cork (~> 0.1) - faraday (>= 0.9.0, < 2.0) - faraday-http-cache (~> 2.0) - git (~> 1.7) - kramdown (~> 2.3) - kramdown-parser-gfm (~> 1.0) - no_proxy_fix - octokit (~> 4.7) - terminal-table (~> 1) - danger-changelog (0.6.1) - danger-plugin-api (~> 1.0) - danger-plugin-api (1.0.0) - danger (> 2.0) - danger-toc (0.2.0) - activesupport - danger-plugin-api (~> 1.0) - kramdown - diff-lcs (1.3) - docile (1.3.5) - faraday (1.3.0) - faraday-net_http (~> 1.0) - multipart-post (>= 1.2, < 3) - ruby2_keywords - faraday-http-cache (2.2.0) - faraday (>= 0.8) - faraday-net_http (1.0.0) - git (1.8.1) - rchardet (~> 1.8) - i18n (1.8.7) - concurrent-ruby (~> 1.0) - jaro_winkler (1.5.4) - kramdown (2.3.0) - rexml - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - minitest (5.14.3) - multipart-post (2.1.1) - nap (1.1.0) - no_proxy_fix (0.1.2) - octokit (4.20.0) - faraday (>= 0.9) - sawyer (~> 0.8.0, >= 0.5.3) - open4 (1.3.4) - parallel (1.20.1) - parser (3.0.0.0) - ast (~> 2.4.1) - public_suffix (4.0.6) - rainbow (3.0.0) - rake (13.0.1) - rchardet (1.8.0) - rexml (3.2.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.1) - rspec-support (~> 3.9.1) - rspec-expectations (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-support (3.9.2) - rubocop (0.80.1) - jaro_winkler (~> 1.5.1) - parallel (~> 1.10) - parser (>= 2.7.0.1) - rainbow (>= 2.2.2, < 4.0) - rexml - ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 1.7) - ruby-progressbar (1.11.0) - ruby2_keywords (0.0.2) - sawyer (0.8.2) - addressable (>= 2.3.5) - faraday (> 0.8, < 2.0) - simplecov (0.21.2) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) - simplecov_json_formatter (0.1.2) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - unicode-display_width (1.6.1) - zeitwerk (2.4.2) - -PLATFORMS - ruby - -DEPENDENCIES - danger - danger-changelog (= 0.6.1) - danger-toc (= 0.2.0) - rake - rspec (~> 3.0) - rubocop (= 0.80.1) - ruby-enum! - simplecov - -BUNDLED WITH - 2.1.4 @@ -24,6 +24,9 @@ Enum-like behavior for Ruby, heavily inspired by [this](http://www.rubyfleebie.c - [Mapping values to keys](#mapping-values-to-keys) - [Duplicate enumerator keys or duplicate values](#duplicate-enumerator-keys-or-duplicate-values) - [Inheritance](#inheritance) + - [Exhaustive case matcher](#exhaustive-case-matcher) + - [I18n support](#i18n-support) +- [Benchmarks](#benchmarks) - [Contributing](#contributing) - [Copyright and License](#copyright-and-license) - [Related Projects](#related-projects) @@ -259,6 +262,62 @@ OrderState.values # ['CREATED', 'PAID'] ShippedOrderState.values # ['CREATED', 'PAID', 'PREPARED', SHIPPED'] ``` +### 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. + +> NOTE: This will add checks at runtime which might lead to worse performance. See [benchmarks](#benchmarks). + +> NOTE: `:else` is a reserved keyword if you want to use `Ruby::Enum::Case`. + +```ruby +class Color < OrderState + include Ruby::Enum + include Ruby::Enum::Case + + define :RED, :red + define :GREEN, :green + define :BLUE, :blue + define :YELLOW, :yellow +end +``` + +```ruby +color = Color::RED +Color.Case(color, { + [Color::GREEN, Color::BLUE] => -> { "order is green or blue" }, + Color::YELLOW => -> { "order is yellow" }, + Color::RED => -> { "order is red" }, +}) +``` + +It also supports default/else: + +```ruby +color = Color::RED +Color.Case(color, { + [Color::GREEN, Color::BLUE] => -> { "order is green or blue" }, + else: -> { "order is yellow or red" }, +}) +``` + +### I18n support + +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. + +```ruby +# Add this to your Gemfile if you want to have a nice error description instead of just a message key. +gem "i18n" +``` + +## Benchmarks + +Benchmark scripts are defined in the [`benchmarks`](benchmarks) folder and can be run with Rake: + +```console +rake benchmarks:case +``` + ## Contributing You're encouraged to contribute to ruby-enum. See [CONTRIBUTING](CONTRIBUTING.md) for details. @@ -16,3 +16,10 @@ require 'rubocop/rake_task' RuboCop::RakeTask.new(:rubocop) task default: %i[rubocop spec] + +namespace :benchmark do + desc 'Run benchmark for the Ruby::Enum::Case' + task :case do + require_relative 'benchmarks/case' + end +end diff --git a/benchmarks/case.rb b/benchmarks/case.rb new file mode 100644 index 0000000..2c51d0f --- /dev/null +++ b/benchmarks/case.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + +require 'benchmark' +require 'ruby-enum' + +## +# Test enum +class Color + include Ruby::Enum + include Ruby::Enum::Case + + define :RED, :red + define :GREEN, :green + define :BLUE, :blue +end + +puts 'Running 1.000.000 normal case statements' +case_statement_time = Benchmark.realtime do + 1_000_000.times do + case Color::RED + when Color::RED, Color::GREEN + 'red or green' + when Color::BLUE + 'blue' + end + end +end + +puts 'Running 1.000.000 ruby-enum case statements' +ruby_enum_time = Benchmark.realtime do + 1_000_000.times do + Color.case(Color::RED, + { + [Color::RED, Color::GREEN] => -> { 'red or green' }, + Color::BLUE => -> { 'blue' } + }) + end +end + +puts "ruby-enum case: #{ruby_enum_time.round(4)}" +puts "case statement: #{case_statement_time.round(4)}" + +puts "ruby-enum case is #{(ruby_enum_time / case_statement_time).round(2)} times slower" diff --git a/debian/changelog b/debian/changelog index 5aba022..fd1f256 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,25 @@ +ruby-enum (1.0.0-1) unstable; urgency=medium + + * Team upload. + + [ Lucas Nussbaum ] + * debian/gbp.conf: Add for DEP-14 + * debian/.gitattributes: remove + * debian/salsa-ci.yml: use team-specific include + + [ Simon Quigley ] + * Upgrade the watch file to version 5. + * New upstream release. + * Use --gem-install layout in rules. + * Refresh the upstream metadata. + * Update Standards-Version to 4.7.4. + * Drop {XS,XB}-Ruby-Versions from control. + * Bump debhelper-compat to 14, dropping ${misc:Depends}, + ${shlibs:Depends}, and ${ruby:Depends} from runtime dependencies. + * Drop Files-Excluded, it's no longer applicable. + + -- Simon Quigley <tsimonq2@debian.org> Sat, 20 Jun 2026 15:03:32 -0500 + ruby-enum (0.9.0+ds-1) unstable; urgency=medium * Team upload. diff --git a/debian/control b/debian/control index f6df20f..87a4c7b 100644 --- a/debian/control +++ b/debian/control @@ -1,27 +1,21 @@ Source: ruby-enum Section: ruby -Priority: optional Maintainer: Debian Ruby Team <pkg-ruby-extras-maintainers@lists.alioth.debian.org> Uploaders: Pirate Praveen <praveen@debian.org> -Build-Depends: debhelper-compat (= 13), +Build-Depends: debhelper-compat (= 14), gem2deb, rake, ruby-i18n, ruby-rspec -Standards-Version: 4.6.0 +Standards-Version: 4.7.4 Vcs-Git: https://salsa.debian.org/ruby-team/ruby-enum.git Vcs-Browser: https://salsa.debian.org/ruby-team/ruby-enum Homepage: https://github.com/dblock/ruby-enum Testsuite: autopkgtest-pkg-ruby -XS-Ruby-Versions: all -Rules-Requires-Root: no Package: ruby-enum Architecture: all -XB-Ruby-Versions: ${ruby:Versions} -Depends: ruby-i18n, - ${misc:Depends}, - ${shlibs:Depends} +Depends: ruby-i18n Description: Enum-like behavior for Ruby Enums can be defined and accessed either as constants or class methods. . diff --git a/debian/copyright b/debian/copyright index d606b34..446a6e3 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,7 +1,6 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: ruby-enum Source: http://github.com/dblock/ruby-enum -Files-Excluded: coverage/* Files: * Copyright: 2013-2016, Daniel Doubrovkine. diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..6b65fe0 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,4 @@ +[DEFAULT] +debian-branch = debian/latest +upstream-branch = upstream/latest +pristine-tar = True diff --git a/debian/patches/0001-Do-not-require-simplecov-to-run-tests.patch b/debian/patches/0001-Do-not-require-simplecov-to-run-tests.patch index ab3334e..70b6d32 100644 --- a/debian/patches/0001-Do-not-require-simplecov-to-run-tests.patch +++ b/debian/patches/0001-Do-not-require-simplecov-to-run-tests.patch @@ -7,11 +7,9 @@ Forwarded: not-needed spec/spec_helper.rb | 3 --- 1 file changed, 3 deletions(-) -diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb -index 38a174f..ba7dfec 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb -@@ -4,9 +4,6 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +@@ -4,9 +4,6 @@ $LOAD_PATH.unshift(File.join(File.dirnam require 'rubygems' diff --git a/debian/rules b/debian/rules index 3454d59..4e91465 100755 --- a/debian/rules +++ b/debian/rules @@ -1,6 +1,7 @@ #!/usr/bin/make -f export GEM2DEB_TEST_RUNNER = --check-dependencies +export DH_RUBY = --gem-install %: dh $@ --buildsystem=ruby --with ruby diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml index 33c3a64..7b5d2f7 100644 --- a/debian/salsa-ci.yml +++ b/debian/salsa-ci.yml @@ -1,4 +1,3 @@ --- include: - - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml - - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml + - https://salsa.debian.org/ruby-team/meta/raw/master/salsa-ci.yml diff --git a/debian/upstream/metadata b/debian/upstream/metadata index 964b078..943a2fc 100644 --- a/debian/upstream/metadata +++ b/debian/upstream/metadata @@ -1,4 +1,7 @@ +--- +Archive: GitHub Bug-Database: https://github.com/dblock/ruby-enum/issues Bug-Submit: https://github.com/dblock/ruby-enum/issues/new +Changelog: https://github.com/dblock/ruby-enum/blob/master/CHANGELOG.md Repository: https://github.com/dblock/ruby-enum.git Repository-Browse: https://github.com/dblock/ruby-enum diff --git a/debian/watch b/debian/watch index 48f0adf..7aadb01 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,4 @@ -version=4 -opts=dversionmangle=s/\.ds// \ -https://gemwatch.debian.net/ruby-enum .*/ruby-enum-(.*).tar.gz +Version: 5 +Template: GitHub +Owner: dblock +Project: ruby-enum diff --git a/lib/ruby-enum.rb b/lib/ruby-enum.rb index 855c8ee..4d9b921 100644 --- a/lib/ruby-enum.rb +++ b/lib/ruby-enum.rb @@ -1,11 +1,23 @@ # frozen_string_literal: true -require 'i18n' - require 'ruby-enum/version' require 'ruby-enum/enum' +require 'ruby-enum/enum/case' +require 'ruby-enum/enum/i18n_mock' + +# Try to load the I18n gem and provide a mock if it is not available. +begin + require 'i18n' + Ruby::Enum.i18n = I18n +rescue LoadError + # I18n is not available + # :nocov: + # Tests for this loading are in the spec_i18n folder + Ruby::Enum.i18n = Ruby::Enum::I18nMock + # :nocov: +end -I18n.load_path << File.join(File.dirname(__FILE__), 'config', 'locales', 'en.yml') +Ruby::Enum.i18n.load_path << File.join(File.dirname(__FILE__), 'config', 'locales', 'en.yml') require 'ruby-enum/errors/base' require 'ruby-enum/errors/uninitialized_constant_error' diff --git a/lib/ruby-enum/enum.rb b/lib/ruby-enum/enum.rb index f7abb4c..0930e86 100644 --- a/lib/ruby-enum/enum.rb +++ b/lib/ruby-enum/enum.rb @@ -2,6 +2,11 @@ module Ruby module Enum + class << self + # Needed for I18n mock + attr_accessor :i18n + end + attr_reader :key, :value def initialize(key, value) @@ -144,9 +149,7 @@ module Ruby end def to_h - Hash[@_enum_hash.map do |key, enum| - [key, enum.value] - end] + @_enum_hash.transform_values(&:value) end private diff --git a/lib/ruby-enum/enum/case.rb b/lib/ruby-enum/enum/case.rb new file mode 100644 index 0000000..79e1cab --- /dev/null +++ b/lib/ruby-enum/enum/case.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module Ruby + module Enum + ## + # Adds a method to an enum class that allows for exhaustive matching on a value. + # + # @example + # class Color + # include Ruby::Enum + # include Ruby::Enum::Case + # + # define :RED, :red + # define :GREEN, :green + # define :BLUE, :blue + # define :YELLOW, :yellow + # end + # + # Color.case(Color::RED, { + # [Color::RED, Color::GREEN] => -> { "red or green" }, + # Color::BLUE => -> { "blue" }, + # Color::YELLOW => -> { "yellow" }, + # }) + # + # Reserves the :else key for a default case: + # Color.case(Color::RED, { + # [Color::RED, Color::GREEN] => -> { "red or green" }, + # else: -> { "blue or yellow" }, + # }) + module Case + def self.included(klass) + klass.extend(ClassMethods) + end + + ## + # @see Ruby::Enum::Case + module ClassMethods + class ValuesNotDefinedError < StandardError + end + + class NotAllCasesHandledError < StandardError + end + + def case(value, cases) + validate_cases(cases) + + filtered_cases = cases.select do |values, _proc| + values = [values] unless values.is_a?(Array) + values.include?(value) + end + + return call_proc(cases[:else], value) if filtered_cases.none? + + results = filtered_cases.map { |_values, proc| call_proc(proc, value) } + + # Return the first result if there is only one result + results.size == 1 ? results.first : results + end + + private + + def call_proc(proc, value) + return if proc.nil? + + if proc.arity == 1 + proc.call(value) + else + proc.call + end + end + + def validate_cases(cases) + all_values = cases.keys.flatten - [:else] + else_defined = cases.key?(:else) + superfluous_values = all_values - values + missing_values = values - all_values + + raise ValuesNotDefinedError, "Value(s) not defined: #{superfluous_values.join(', ')}" if superfluous_values.any? + raise NotAllCasesHandledError, "Not all cases handled: #{missing_values.join(', ')}" if missing_values.any? && !else_defined + end + end + end + end +end diff --git a/lib/ruby-enum/enum/i18n_mock.rb b/lib/ruby-enum/enum/i18n_mock.rb new file mode 100644 index 0000000..b9adbb9 --- /dev/null +++ b/lib/ruby-enum/enum/i18n_mock.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# :nocov: +module Ruby + module Enum + ## + # Mock I18n module in case the i18n gem is not available. + module I18nMock + def self.load_path + [] + end + + def self.translate(key, _options = {}) + key + end + end + end +end +# :nocov: diff --git a/lib/ruby-enum/errors/base.rb b/lib/ruby-enum/errors/base.rb index c4a726c..5ab2ee1 100644 --- a/lib/ruby-enum/errors/base.rb +++ b/lib/ruby-enum/errors/base.rb @@ -22,13 +22,13 @@ module Ruby @summary = create_summary(key, attributes) @resolution = create_resolution(key, attributes) - "\nProblem:\n #{@problem}"\ + "\nProblem:\n #{@problem}" \ "\nSummary:\n #{@summary}" + "\nResolution:\n #{@resolution}" end private - BASE_KEY = 'ruby.enum.errors.messages' #:nodoc: + BASE_KEY = 'ruby.enum.errors.messages' # :nodoc: # Given the key of the specific error and the options hash, translate the # message. @@ -39,7 +39,7 @@ module Ruby # # Returns a localized error message string. def translate(key, options) - ::I18n.translate("#{BASE_KEY}.#{key}", **{ locale: :en }.merge(options)).strip + Ruby::Enum.i18n.translate("#{BASE_KEY}.#{key}", locale: :en, **options).strip end # Create the problem. diff --git a/lib/ruby-enum/version.rb b/lib/ruby-enum/version.rb index 1a3c94b..a329355 100644 --- a/lib/ruby-enum/version.rb +++ b/lib/ruby-enum/version.rb @@ -2,6 +2,6 @@ module Ruby module Enum - VERSION = '0.9.0' + VERSION = '1.0.0' end end diff --git a/pkg/ruby-enum-0.8.0.gem b/pkg/ruby-enum-0.8.0.gem Binary files differdeleted file mode 100644 index 1949d89..0000000 --- a/pkg/ruby-enum-0.8.0.gem +++ /dev/null diff --git a/ruby-enum.gemspec b/ruby-enum.gemspec index 9eee0bd..50da6a9 100644 --- a/ruby-enum.gemspec +++ b/ruby-enum.gemspec @@ -10,10 +10,11 @@ Gem::Specification.new do |s| s.email = 'dblock@dblock.org' s.platform = Gem::Platform::RUBY s.required_rubygems_version = '>= 1.3.6' + s.required_ruby_version = '>= 2.7' s.files = Dir['**/*'] s.require_paths = ['lib'] s.homepage = 'http://github.com/dblock/ruby-enum' s.licenses = ['MIT'] s.summary = 'Enum-like behavior for Ruby.' - s.add_dependency 'i18n' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/spec/ruby-enum/enum/case_spec.rb b/spec/ruby-enum/enum/case_spec.rb new file mode 100644 index 0000000..8e7c588 --- /dev/null +++ b/spec/ruby-enum/enum/case_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ruby::Enum::Case do + test_enum = + Class.new do + include Ruby::Enum + include Ruby::Enum::Case + + define :RED, :red + define :GREEN, :green + define :BLUE, :blue + end + + describe '.case' do + context 'when all cases are defined' do + subject { test_enum.case(test_enum::RED, cases) } + + let(:cases) do + { + [test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, + test_enum::BLUE => -> { 'blue' } + } + end + + it { is_expected.to eq('red or green') } + + context 'when the value is nil' do + subject { test_enum.case(nil, cases) } + + it { is_expected.to be_nil } + end + + context 'when the value is empty' do + subject { test_enum.case('', cases) } + + it { is_expected.to be_nil } + end + + context 'when the value is the value of the enum' do + subject { test_enum.case(:red, cases) } + + it { is_expected.to eq('red or green') } + end + + context 'when the value is used inside the lambda' do + subject { test_enum.case(test_enum::RED, cases) } + + let(:cases) do + { + [test_enum::RED, test_enum::GREEN] => ->(color) { "is #{color}" }, + test_enum::BLUE => -> { 'blue' } + } + end + + it { is_expected.to eq('is red') } + end + end + + context 'when there are mutliple matches' do + subject do + test_enum.case( + test_enum::RED, + { + [test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, + test_enum::RED => -> { 'red' }, + test_enum::BLUE => -> { 'blue' } + } + ) + end + + it { is_expected.to eq(['red or green', 'red']) } + end + + context 'when not all cases are defined' do + it 'raises an error' do + expect do + test_enum.case( + test_enum::RED, + { [test_enum::RED, test_enum::GREEN] => -> { 'red or green' } } + ) + end.to raise_error(Ruby::Enum::Case::ClassMethods::NotAllCasesHandledError) + end + end + + context 'when not all cases are defined but :else is specified (default case)' do + it 'does not raise an error' do + expect do + result = test_enum.case( + test_enum::BLUE, + { + [test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, + else: -> { 'blue' } + } + ) + + expect(result).to eq('blue') + end.not_to raise_error + end + end + + context 'when a superfluous case is defined' do + it 'raises an error' do + expect do + test_enum.case( + test_enum::RED, + { + [test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, + test_enum::BLUE => -> { 'blue' }, + :something => -> { 'green' } + } + ) + end.to raise_error(Ruby::Enum::Case::ClassMethods::ValuesNotDefinedError) + end + end + end +end diff --git a/spec/ruby-enum/enum_spec.rb b/spec/ruby-enum/enum_spec.rb index 6a7e560..bd0f1c9 100644 --- a/spec/ruby-enum/enum_spec.rb +++ b/spec/ruby-enum/enum_spec.rb @@ -22,10 +22,28 @@ describe Ruby::Enum do expect(Colors::RED).to eq 'red' expect(Colors::GREEN).to eq 'green' end - it 'raises UninitializedConstantError on an invalid constant' do - expect { Colors::ANYTHING }.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /The constant Colors::ANYTHING has not been defined./ + + context 'when the i18n gem is loaded' do + it 'raises UninitializedConstantError on an invalid constant' do + expect do + Colors::ANYTHING + end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /The constant Colors::ANYTHING has not been defined./ + end end - context '#each' do + + context 'when the i18n gem is not loaded' do + before do + allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) + end + + it 'raises UninitializedConstantError on an invalid constant' do + expect do + Colors::ANYTHING + end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /ruby.enum.errors.messages.uninitialized_constant.summary/ + end + end + + describe '#each' do it 'iterates over constants' do keys = [] enum_keys = [] @@ -40,7 +58,8 @@ describe Ruby::Enum do expect(enum_values).to eq %w[red green] end end - context '#map' do + + describe '#map' do it 'maps constants' do key_key_values = Colors.map do |key, enum| [key, enum.key, enum.value] @@ -50,103 +69,152 @@ describe Ruby::Enum do expect(key_key_values[1]).to eq [:GREEN, :GREEN, 'green'] end end - context '#parse' do + + describe '#parse' do it 'parses exact value' do expect(Colors.parse('red')).to eq(Colors::RED) end + it 'is case-insensitive' do expect(Colors.parse('ReD')).to eq(Colors::RED) end + it 'returns nil for a null value' do expect(Colors.parse(nil)).to be_nil end + it 'returns nil for an invalid value' do expect(Colors.parse('invalid')).to be_nil end end - context '#key?' do + + describe '#key?' do it 'returns true for valid keys accessed directly' do Colors.keys.each do |key| # rubocop:disable Style/HashEachMethods - expect(Colors.key?(key)).to eq(true) + expect(Colors.key?(key)).to be(true) end end + it 'returns true for valid keys accessed via each_keys' do Colors.each_key do |key| - expect(Colors.key?(key)).to eq(true) + expect(Colors.key?(key)).to be(true) end end + it 'returns false for invalid keys' do - expect(Colors.key?(:NOT_A_KEY)).to eq(false) + expect(Colors.key?(:NOT_A_KEY)).to be(false) end end - context '#value' do + + describe '#value' do it 'returns string values for keys' do Colors.each do |key, enum| expect(Colors.value(key)).to eq(enum.value) end end + it 'returns nil for an invalid key' do expect(Colors.value(:NOT_A_KEY)).to be_nil end end - context '#value?' do + + describe '#value?' do it 'returns true for valid values accessed directly' do Colors.values.each do |value| # rubocop:disable Style/HashEachMethods - expect(Colors.value?(value)).to eq(true) + expect(Colors.value?(value)).to be(true) end end + it 'returns true for valid values accessed via each_value' do Colors.each_value do |value| - expect(Colors.value?(value)).to eq(true) + expect(Colors.value?(value)).to be(true) end end + it 'returns false for invalid values' do - expect(Colors.value?('I am not a value')).to eq(false) + expect(Colors.value?('I am not a value')).to be(false) end end - context '#key' do + + describe '#key' do it 'returns enum instances for values' do - Colors.each do |_, enum| + Colors.each do |_, enum| # rubocop:disable Style/HashEachMethods expect(Colors.key(enum.value)).to eq(enum.key) end end + it 'returns nil for an invalid value' do expect(Colors.key('invalid')).to be_nil end end - context '#keys' do + + describe '#keys' do it 'returns keys' do expect(Colors.keys).to eq(%i[RED GREEN]) end end - context '#values' do + + describe '#values' do it 'returns values' do expect(Colors.values).to eq(%w[red green]) end end - context '#to_h' do + + describe '#to_h' do it 'returns a hash of key:values' do expect(Colors.to_h).to eq(RED: 'red', GREEN: 'green') end end - context 'on duplicate keys' do - it 'raises DuplicateKeyError' do - expect do - Colors.class_eval do - define :RED, 'some' - end - end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /The constant Colors::RED has already been defined./ + context 'when a duplicate key is used' do + context 'when the i18n gem is loaded' do + it 'raises DuplicateKeyError' do + expect do + Colors.class_eval do + define :RED, 'some' + end + end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /The constant Colors::RED has already been defined./ + end + end + + context 'when the i18n gem is not loaded' do + before do + allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) + end + + it 'raises DuplicateKeyError' do + expect do + Colors.class_eval do + define :RED, 'some' + end + end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /ruby.enum.errors.messages.duplicate_key.message/ + end end end - context 'on duplicate values' do - it 'raises a DuplicateValueError' do - expect do - Colors.class_eval do - define :Other, 'red' - end - end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /The value red has already been defined./ + context 'when a duplicate value is used' do + context 'when the i18n gem is loaded' do + it 'raises a DuplicateValueError' do + expect do + Colors.class_eval do + define :Other, 'red' + end + end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /The value red has already been defined./ + end + end + + context 'when the i18n gem is not loaded' do + before do + allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) + end + + it 'raises a DuplicateValueError' do + expect do + Colors.class_eval do + define :Other, 'red' + end + end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /ruby.enum.errors.messages.duplicate_value.summary/ + end end end @@ -176,11 +244,14 @@ describe Ruby::Enum do it 'contains its own enums' do expect(FirstSubclass::ORANGE).to eq 'orange' end + it 'parent class should not have enums defined in child classes' do expect { Colors::ORANGE }.to raise_error Ruby::Enum::Errors::UninitializedConstantError end - context 'Given a 2 level depth subclass' do + + context 'when defining a 2 level depth subclass' do subject { SecondSubclass } + it 'contains its own enums and all the enums defined in the parent classes' do expect(subject::RED).to eq 'red' expect(subject::GREEN).to eq 'green' @@ -227,6 +298,7 @@ describe Ruby::Enum do define :undefined end subject { States } + it 'behaves like an enum' do expect(subject.created).to eq 'Created' expect(subject.published).to eq 'Published' diff --git a/spec_i18n/Gemfile b/spec_i18n/Gemfile new file mode 100644 index 0000000..84d9f8e --- /dev/null +++ b/spec_i18n/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source 'http://rubygems.org' + +gemspec path: '../', name: 'ruby-enum' + +# This Gemfile should not include any gem that has i18n as a dependency. +gem 'rake' +gem 'rspec', '~> 3.0' diff --git a/spec_i18n/Rakefile b/spec_i18n/Rakefile new file mode 100644 index 0000000..9827cdf --- /dev/null +++ b/spec_i18n/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'rubygems' + +require 'rspec/core' +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList['spec/**/*_spec.rb'] +end + +task default: %i[spec] diff --git a/spec_i18n/spec/i18n_spec.rb b/spec_i18n/spec/i18n_spec.rb new file mode 100644 index 0000000..653b9ed --- /dev/null +++ b/spec_i18n/spec/i18n_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +test_class = Class.new do + include Ruby::Enum + + define :RED, 'red' + define :GREEN, 'green' +end + +describe Ruby::Enum do + context 'when the i18n gem is not loaded' do + it 'raises UninitializedConstantError on an invalid constant' do + expect do + test_class::ANYTHING + end.to raise_error Ruby::Enum::Errors::UninitializedConstantError, /ruby.enum.errors.messages.uninitialized_constant.summary/ + end + + context 'when a duplicate key is used' do + before do + allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) + end + + it 'raises DuplicateKeyError' do + expect do + test_class.class_eval do + define :RED, 'some' + end + end.to raise_error Ruby::Enum::Errors::DuplicateKeyError, /ruby.enum.errors.messages.duplicate_key.message/ + end + end + + context 'when a duplicate value is used' do + before do + allow(described_class).to receive(:i18n).and_return(Ruby::Enum::I18nMock) + end + + it 'raises a DuplicateValueError' do + expect do + test_class.class_eval do + define :Other, 'red' + end + end.to raise_error Ruby::Enum::Errors::DuplicateValueError, /ruby.enum.errors.messages.duplicate_value.summary/ + end + end + end +end diff --git a/spec_i18n/spec/spec_helper.rb b/spec_i18n/spec/spec_helper.rb new file mode 100644 index 0000000..cf56090 --- /dev/null +++ b/spec_i18n/spec/spec_helper.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib')) + +require 'rubygems' + +require 'rspec' +require 'ruby-enum' |
