├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── enumerate_it.gemspec ├── gemfiles ├── rails_6.0.gemfile ├── rails_6.1.gemfile ├── rails_7.0.gemfile ├── rails_7.1.gemfile ├── rails_7.2.gemfile └── rails_8.0.gemfile ├── lib ├── enumerate_it.rb ├── enumerate_it │ ├── base.rb │ ├── class_methods.rb │ └── version.rb └── generators │ └── enumerate_it │ └── enum │ ├── USAGE │ ├── enum_generator.rb │ └── templates │ ├── enumerate_it.rb │ └── locale.yml └── spec ├── enumerate_it └── base_spec.rb ├── enumerate_it_spec.rb ├── i18n ├── en.yml └── pt.yml ├── spec_helper.rb └── support └── test_classes.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | 10 | matrix: 11 | ruby: 12 | - 3.0 13 | - 3.1 14 | - 3.2 15 | - 3.3 16 | - 3.4 17 | gemfile: 18 | - gemfiles/rails_6.0.gemfile 19 | - gemfiles/rails_6.1.gemfile 20 | - gemfiles/rails_7.0.gemfile 21 | - gemfiles/rails_7.1.gemfile 22 | - gemfiles/rails_7.2.gemfile 23 | - gemfiles/rails_8.0.gemfile 24 | exclude: 25 | # Rails 8 requires Ruby 3.2 or newer 26 | - ruby: 3.0 27 | gemfile: gemfiles/rails_8.0.gemfile 28 | - ruby: 3.1 29 | gemfile: gemfiles/rails_8.0.gemfile 30 | 31 | env: 32 | BUNDLE_GEMFILE: "${{ matrix.gemfile }}" 33 | 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | - name: Set up Ruby 40 | uses: ruby/setup-ruby@v1 41 | with: 42 | ruby-version: ${{ matrix.ruby }} 43 | bundler-cache: true 44 | 45 | - name: Rubocop 46 | if: ${{ matrix.ruby == '3.4' }} 47 | run: "bundle exec rubocop" 48 | 49 | - name: Tests 50 | run: bundle exec rake spec 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /gemfiles/*.lock 4 | /gemfiles/.bundle/ 5 | .ruby-gemset 6 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-rspec 3 | - rubocop-rake 4 | 5 | inherit_mode: 6 | merge: 7 | - Exclude 8 | 9 | AllCops: 10 | NewCops: enable 11 | TargetRubyVersion: 3.4 12 | 13 | Exclude: 14 | - 'lib/generators/enumerate_it/enum/templates/**/*' 15 | - 'gemfiles/vendor/**/*' 16 | 17 | Gemspec/DevelopmentDependencies: 18 | Enabled: false 19 | 20 | Gemspec/RequiredRubyVersion: 21 | Enabled: false 22 | 23 | Layout/EndAlignment: 24 | Enabled: false 25 | 26 | Layout/LineLength: 27 | Max: 100 28 | 29 | Lint/RaiseException: 30 | Enabled: true 31 | 32 | Lint/StructNewOverride: 33 | Enabled: true 34 | 35 | Metrics/BlockLength: 36 | Exclude: 37 | - 'spec/**/*' 38 | 39 | Layout/HashAlignment: 40 | EnforcedColonStyle: table 41 | EnforcedLastArgumentHashStyle: ignore_implicit 42 | 43 | Layout/ElseAlignment: 44 | Enabled: false 45 | 46 | Layout/IndentationWidth: 47 | Enabled: false 48 | 49 | Layout/MultilineMethodCallIndentation: 50 | EnforcedStyle: indented 51 | 52 | Style/FrozenStringLiteralComment: 53 | Enabled: false 54 | 55 | Style/Documentation: 56 | Enabled: false 57 | 58 | Style/GuardClause: 59 | MinBodyLength: 3 60 | 61 | Style/HashEachMethods: 62 | Enabled: true 63 | 64 | Style/HashTransformKeys: 65 | Enabled: true 66 | 67 | Style/HashTransformValues: 68 | Enabled: true 69 | 70 | Naming/PredicateName: 71 | Exclude: 72 | - 'lib/enumerate_it/class_methods.rb' 73 | 74 | Naming/VariableNumber: 75 | EnforcedStyle: snake_case 76 | 77 | RSpec/MultipleExpectations: 78 | Enabled: false 79 | 80 | RSpec/NestedGroups: 81 | Enabled: false 82 | 83 | RSpec/MessageExpectation: 84 | Enabled: false 85 | 86 | RSpec/MessageSpies: 87 | Enabled: false 88 | 89 | RSpec/ContextWording: 90 | Enabled: false 91 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'json' 3 | 4 | rails_versions = JSON.parse(Net::HTTP.get(URI('https://rubygems.org/api/v1/versions/rails.json'))) 5 | .group_by { |version| version['number'] }.keys.grep_v(/rc|racecar|alpha|beta|pre/) 6 | 7 | %w[6.0 6.1 7.0 7.1 7.2 8.0].each do |rails_version| 8 | appraise "rails_#{rails_version}" do 9 | current_version = rails_versions 10 | .select { |key| key.match(/\A#{rails_version}/) } 11 | .max { |a, b| Gem::Version.new(a) <=> Gem::Version.new(b) } 12 | 13 | gem 'activesupport', "~> #{current_version}" 14 | gem 'activerecord', "~> #{current_version}" 15 | 16 | if Gem::Version.new(rails_version) > Gem::Version.new(7.0) 17 | gem 'sqlite3' 18 | else 19 | gem 'sqlite3', '< 2' # Rails 6.x and 7.0 require sqlite3 v1.x 20 | end 21 | 22 | # The following is likely necessary due to this issue (or something related): 23 | # https://stackoverflow.com/a/79385484/1445184 24 | if Gem::Version.new(rails_version) < Gem::Version.new(7.1) 25 | gem 'base64' 26 | gem 'bigdecimal' 27 | gem 'mutex_m' 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | Changes follows the [Semantic Versioning](https://semver.org/) specification and 4 | you can see them on the [releases page](../../releases). 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in enumerate_it.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | enumerate_it (4.1.0) 5 | activesupport (>= 6.0.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (8.0.2) 11 | activesupport (= 8.0.2) 12 | activerecord (8.0.2) 13 | activemodel (= 8.0.2) 14 | activesupport (= 8.0.2) 15 | timeout (>= 0.4.0) 16 | activesupport (8.0.2) 17 | base64 18 | benchmark (>= 0.3) 19 | bigdecimal 20 | concurrent-ruby (~> 1.0, >= 1.3.1) 21 | connection_pool (>= 2.2.5) 22 | drb 23 | i18n (>= 1.6, < 2) 24 | logger (>= 1.4.2) 25 | minitest (>= 5.1) 26 | securerandom (>= 0.3) 27 | tzinfo (~> 2.0, >= 2.0.5) 28 | uri (>= 0.13.1) 29 | appraisal (2.5.0) 30 | bundler 31 | rake 32 | thor (>= 0.14.0) 33 | ast (2.4.3) 34 | base64 (0.2.0) 35 | benchmark (0.4.0) 36 | bigdecimal (3.1.9) 37 | coderay (1.1.3) 38 | concurrent-ruby (1.3.5) 39 | connection_pool (2.5.3) 40 | diff-lcs (1.6.1) 41 | drb (2.2.1) 42 | i18n (1.14.7) 43 | concurrent-ruby (~> 1.0) 44 | json (2.11.3) 45 | language_server-protocol (3.17.0.4) 46 | lint_roller (1.1.0) 47 | logger (1.7.0) 48 | method_source (1.1.0) 49 | mini_portile2 (2.8.8) 50 | minitest (5.25.5) 51 | parallel (1.27.0) 52 | parser (3.3.8.0) 53 | ast (~> 2.4.1) 54 | racc 55 | prism (1.4.0) 56 | pry (0.15.2) 57 | coderay (~> 1.1) 58 | method_source (~> 1.0) 59 | racc (1.8.1) 60 | rainbow (3.1.1) 61 | rake (13.2.1) 62 | regexp_parser (2.10.0) 63 | rspec (3.13.0) 64 | rspec-core (~> 3.13.0) 65 | rspec-expectations (~> 3.13.0) 66 | rspec-mocks (~> 3.13.0) 67 | rspec-core (3.13.3) 68 | rspec-support (~> 3.13.0) 69 | rspec-expectations (3.13.3) 70 | diff-lcs (>= 1.2.0, < 2.0) 71 | rspec-support (~> 3.13.0) 72 | rspec-mocks (3.13.2) 73 | diff-lcs (>= 1.2.0, < 2.0) 74 | rspec-support (~> 3.13.0) 75 | rspec-support (3.13.2) 76 | rubocop (1.75.4) 77 | json (~> 2.3) 78 | language_server-protocol (~> 3.17.0.2) 79 | lint_roller (~> 1.1.0) 80 | parallel (~> 1.10) 81 | parser (>= 3.3.0.2) 82 | rainbow (>= 2.2.2, < 4.0) 83 | regexp_parser (>= 2.9.3, < 3.0) 84 | rubocop-ast (>= 1.44.0, < 2.0) 85 | ruby-progressbar (~> 1.7) 86 | unicode-display_width (>= 2.4.0, < 4.0) 87 | rubocop-ast (1.44.1) 88 | parser (>= 3.3.7.2) 89 | prism (~> 1.4) 90 | rubocop-rake (0.7.1) 91 | lint_roller (~> 1.1) 92 | rubocop (>= 1.72.1) 93 | rubocop-rspec (3.6.0) 94 | lint_roller (~> 1.1) 95 | rubocop (~> 1.72, >= 1.72.1) 96 | ruby-progressbar (1.13.0) 97 | securerandom (0.4.1) 98 | sqlite3 (1.7.3) 99 | mini_portile2 (~> 2.8.0) 100 | thor (1.3.2) 101 | timeout (0.4.3) 102 | tzinfo (2.0.6) 103 | concurrent-ruby (~> 1.0) 104 | unicode-display_width (3.1.4) 105 | unicode-emoji (~> 4.0, >= 4.0.4) 106 | unicode-emoji (4.0.4) 107 | uri (1.0.3) 108 | 109 | PLATFORMS 110 | ruby 111 | 112 | DEPENDENCIES 113 | activerecord 114 | appraisal 115 | bundler 116 | enumerate_it! 117 | pry 118 | rake 119 | rspec 120 | rubocop 121 | rubocop-rake 122 | rubocop-rspec 123 | sqlite3 (< 2) 124 | 125 | BUNDLED WITH 126 | 2.6.6 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2010-2025 Cássio Marques and Lucas Caton 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EnumerateIt 2 | 3 | Enumerations for Ruby with some magic powers! 🎩 4 | 5 | [![CI Status](https://github.com/lucascaton/enumerate_it/workflows/CI/badge.svg)](https://github.com/lucascaton/enumerate_it/actions?query=workflow%3ACI) 6 | [![Gem Version](https://badge.fury.io/rb/enumerate_it.svg)](https://rubygems.org/gems/enumerate_it) 7 | [![Downloads](https://img.shields.io/gem/dt/enumerate_it.svg)](https://rubygems.org/gems/enumerate_it) 8 | [![Changelog](https://img.shields.io/badge/changelog--brightgreen.svg?style=flat)](https://github.com/lucascaton/enumerate_it/releases) 9 | 10 | **EnumerateIt** helps you declare and use enumerations in a very simple and 11 | flexible way. 12 | 13 | ### Why would I want a gem if Rails already has native enumeration support? 14 | 15 | Firstly, although **EnumerateIt** works well with **Rails**, it isn't required! 16 | This means you can add it to any **Ruby** project! Secondly, you can 17 | [define your enumerations in classes](#creating-enumerations), allowing you to 18 | **add behavior** and **reuse** them! 😀 19 | 20 | --- 21 | 22 | 23 | 24 | ## Table of Contents 25 | 26 | - [Installation](#installation) 27 | - [Using with Rails](#using-with-rails) 28 | - [Creating enumerations](#creating-enumerations) 29 | - [Sorting enumerations](#sorting-enumerations) 30 | - [Using enumerations](#using-enumerations) 31 | - [FAQ](#faq) 32 | - [Why define enumerations outside the class that uses them?](#why-define-enumerations-outside-the-class-that-uses-them) 33 | - [Can I use `enumerate_it` gem without Rails?](#can-i-use-enumerate_it-gem-without-rails) 34 | - [What versions of Ruby and Rails are supported?](#what-versions-of-ruby-and-rails-are-supported) 35 | - [Can I set a value to always be at the end of a sorted list?](#can-i-set-a-value-to-always-be-at-the-end-of-a-sorted-list) 36 | - [I18n](#i18n) 37 | - [Translate a namespaced enumeration](#translate-a-namespaced-enumeration) 38 | - [Handling a legacy database](#handling-a-legacy-database) 39 | - [Changelog](#changelog) 40 | - [Note on Patches/Pull Requests](#note-on-patchespull-requests) 41 | - [Copyright](#copyright) 42 | 43 | 44 | 45 | ## Installation 46 | 47 | ```bash 48 | gem install enumerate_it 49 | ``` 50 | 51 | ## Using with Rails 52 | 53 | Add the gem to your `Gemfile`: 54 | 55 | ```ruby 56 | gem 'enumerate_it' 57 | ``` 58 | 59 | You can use a Rails generator to create both an enumeration and its locale file: 60 | 61 | ```bash 62 | rails generate enumerate_it:enum --help 63 | ``` 64 | 65 | ## Creating enumerations 66 | 67 | Enumerations are created as classes and should be placed inside the 68 | `app/enumerations` folder. 69 | 70 | You can pass an array of symbols, where each symbol's value will be its 71 | stringified version: 72 | 73 | ```ruby 74 | class RelationshipStatus < EnumerateIt::Base 75 | associate_values( 76 | :single, 77 | :married, 78 | :divorced 79 | ) 80 | end 81 | ``` 82 | 83 | This will generate some nice stuff: 84 | 85 | - Constants for each enumeration value: 86 | 87 | ```ruby 88 | RelationshipStatus::SINGLE 89 | #=> 'single' 90 | 91 | RelationshipStatus::MARRIED 92 | #=> 'married' 93 | ``` 94 | 95 | - A list of all enumeration codes: 96 | 97 | ```ruby 98 | RelationshipStatus.list 99 | #=> ['divorced', 'married', 'single'] 100 | ``` 101 | 102 | - A JSON representation: 103 | 104 | ```ruby 105 | RelationshipStatus.to_json 106 | #=> "[{\"value\":\"divorced\",\"label\":\"Divorced\"},{\"value\":\"married\", ... 107 | ``` 108 | 109 | - An array of options for Rails helpers, such as `select`, `select_tag`, etc.: 110 | 111 | ```ruby 112 | RelationshipStatus.to_a 113 | #=> [['Divorced', 'divorced'], ['Married', 'married'], ['Single', 'single']] 114 | ``` 115 | 116 | - You can retrieve a list with values for a group of enumeration constants. 117 | 118 | ```ruby 119 | RelationshipStatus.values_for %w(MARRIED SINGLE) 120 | #=> ['married', 'single'] 121 | ``` 122 | 123 | - You can retrieve the value for a specific enumeration constant: 124 | 125 | ```ruby 126 | RelationshipStatus.value_for('MARRIED') 127 | #=> 'married' 128 | ``` 129 | 130 | - You can retrieve the symbol used to declare a specific enumeration value: 131 | 132 | ```ruby 133 | RelationshipStatus.key_for(RelationshipStatus::MARRIED) 134 | #=> :married 135 | ``` 136 | 137 | - You can iterate over the list of the enumeration's values: 138 | 139 | ```ruby 140 | RelationshipStatus.each_value { |value| ... } 141 | ``` 142 | 143 | - You can iterate over the list of the enumeration's translations: 144 | 145 | ```ruby 146 | RelationshipStatus.each_translation { |translation| ... } 147 | ``` 148 | 149 | - You can also retrieve all the translations of the enumeration: 150 | 151 | ```ruby 152 | RelationshipStatus.translations 153 | ``` 154 | 155 | - You can ask for the enumeration's length: 156 | 157 | ```ruby 158 | RelationshipStatus.length 159 | #=> 3 160 | ``` 161 | 162 | ### Sorting enumerations 163 | 164 | When calling methods like `to_a`, `to_json` and `list`, values are sorted in the 165 | order they were passed to `associate_values`, by default. 166 | 167 | You can override this with the `sort_by` class method: 168 | 169 | ```ruby 170 | class RelationshipStatus < EnumerateIt::Base 171 | associate_values :single, :married 172 | 173 | sort_by :translation 174 | end 175 | ``` 176 | 177 | Accepted values for `sort_by`: 178 | 179 | | Value | Behavior | 180 | | :----------------------- | :------------------------------------------------------------------------------------ | 181 | | `:none` | Uses the original order from `associate_values` | 182 | | `:name` | Sorts by the name of each enumeration option | 183 | | `:translation` | Sorts by their translations | 184 | | `:normalize_translation` | Sorts by their translations normalized with NFKD unicode method (without accents) | 185 | | `:value` | Sorts by assigned values (useful for [legacy databases](#handling-a-legacy-database)) | 186 | 187 | ## Using enumerations 188 | 189 | The cool part is that you can use these enumerations in any class, whether 190 | ActiveRecord-based or not: 191 | 192 | ```ruby 193 | # ActiveRecord instance 194 | class Person < ApplicationRecord 195 | has_enumeration_for :relationship_status 196 | end 197 | ``` 198 | 199 | ```ruby 200 | # Non-ActiveRecord instance 201 | class Person 202 | extend EnumerateIt 203 | attr_accessor :relationship_status 204 | 205 | has_enumeration_for :relationship_status 206 | end 207 | ``` 208 | 209 | > **Note:** If the enumeration class name differs from the attribute name, use 210 | > the `with` option: 211 | > 212 | > `has_enumeration_for :relationship_status, with: RelationshipStatus` 213 | 214 | This will create: 215 | 216 | - A "humanized" version of the hash's key to humanize the attribute's value: 217 | 218 | ```ruby 219 | p = Person.new 220 | p.relationship_status = RelationshipStatus::DIVORCED 221 | p.relationship_status_humanize 222 | #=> 'Divorced' 223 | ``` 224 | 225 | - A translation for your options, if you include a locale to represent it (see 226 | more in the [I18n section](#i18n)). 227 | 228 | ```ruby 229 | p = Person.new 230 | p.relationship_status = RelationshipStatus::DIVORCED 231 | p.relationship_status_humanize 232 | #=> 'Divorciado' 233 | ``` 234 | 235 | - The associated enumerations, which can be retrieved with the `enumerations` 236 | class method: 237 | 238 | ```ruby 239 | Person.enumerations 240 | #=> { relationship_status: RelationshipStatus } 241 | ``` 242 | 243 | - A helper method for each enumeration option, if you pass the `create_helpers` 244 | option as `true`: 245 | 246 | ```ruby 247 | class Person < ApplicationRecord 248 | has_enumeration_for :relationship_status, with: RelationshipStatus, create_helpers: true 249 | end 250 | 251 | p = Person.new 252 | p.relationship_status = RelationshipStatus::MARRIED 253 | 254 | p.married? 255 | #=> true 256 | 257 | p.divorced? 258 | #=> false 259 | ``` 260 | 261 | It's also possible to "namespace" the created helper methods, passing a hash 262 | to the `create_helpers` option. This can be useful when two or more of the 263 | enumerations used share the same constants: 264 | 265 | ```ruby 266 | class Person < ApplicationRecord 267 | has_enumeration_for :relationship_status, 268 | with: RelationshipStatus, create_helpers: { prefix: true } 269 | end 270 | 271 | p = Person.new 272 | p.relationship_status = RelationshipStatus::MARRIED 273 | 274 | p.relationship_status_married? 275 | #=> true 276 | 277 | p.relationship_status_divorced? 278 | #=> false 279 | ``` 280 | 281 | You can define polymorphic behavior for the enumeration values, so you can 282 | define a class for each of them: 283 | 284 | ```ruby 285 | class RelationshipStatus < EnumerateIt::Base 286 | associate_values :married, :single 287 | 288 | class Married 289 | def saturday_night 290 | 'At home with the kids' 291 | end 292 | end 293 | 294 | class Single 295 | def saturday_night 296 | 'Party hard!' 297 | end 298 | end 299 | end 300 | 301 | class Person < ApplicationRecord 302 | has_enumeration_for :relationship_status, 303 | with: RelationshipStatus, create_helpers: { polymorphic: true } 304 | end 305 | 306 | p = Person.new 307 | p.relationship_status = RelationshipStatus::MARRIED 308 | p.relationship_status_object.saturday_night 309 | #=> 'At home with the kids' 310 | 311 | p.relationship_status = RelationshipStatus::SINGLE 312 | p.relationship_status_object.saturday_night 313 | #=> 'Party hard!' 314 | ``` 315 | 316 | You can also change the suffix `_object`, using the `suffix` option: 317 | 318 | ```ruby 319 | class Person < ApplicationRecord 320 | has_enumeration_for :relationship_status, 321 | with: RelationshipStatus, create_helpers: { polymorphic: { suffix: '_mode' } } 322 | end 323 | 324 | p.relationship_status_mode.saturday_night 325 | ``` 326 | 327 | The `create_helpers` also creates some mutator helper methods, that can be 328 | used to change the attribute's value. 329 | 330 | ```ruby 331 | p = Person.new 332 | p.married! 333 | 334 | p.married? 335 | #=> true 336 | ``` 337 | 338 | - A scope method for each enumeration option if you pass the `create_scopes` 339 | option as `true`: 340 | 341 | ```ruby 342 | class Person < ApplicationRecord 343 | has_enumeration_for :relationship_status, with: RelationshipStatus, create_scopes: true 344 | end 345 | 346 | Person.married.to_sql 347 | #=> SELECT "users".* FROM "users" WHERE "users"."relationship_status" = "married" 348 | ``` 349 | 350 | The `:create_scopes` also accepts `prefix` option. 351 | 352 | ```ruby 353 | class Person < ApplicationRecord 354 | has_enumeration_for :relationship_status, 355 | with: RelationshipStatus, create_scopes: { prefix: true } 356 | end 357 | 358 | Person.relationship_status_married.to_sql 359 | ``` 360 | 361 | - An inclusion validation (if your class can manage validations and responds to 362 | `validates_inclusion_of`): 363 | 364 | ```ruby 365 | class Person < ApplicationRecord 366 | has_enumeration_for :relationship_status, with: RelationshipStatus 367 | end 368 | 369 | p = Person.new(relationship_status: 'invalid') 370 | p.valid? 371 | #=> false 372 | p.errors[:relationship_status] 373 | #=> 'is not included in the list' 374 | ``` 375 | 376 | - A presence validation (if your class can manage validations and responds to 377 | `validates_presence_of` and you pass the `required` options as `true`): 378 | 379 | ```ruby 380 | class Person < ApplicationRecord 381 | has_enumeration_for :relationship_status, required: true 382 | end 383 | 384 | p = Person.new relationship_status: nil 385 | p.valid? 386 | #=> false 387 | p.errors[:relationship_status] 388 | #=> "can't be blank" 389 | ``` 390 | 391 | If you pass the `skip_validation` option as `true`, it will not create any 392 | validations: 393 | 394 | ```ruby 395 | class Person < ApplicationRecord 396 | has_enumeration_for :relationship_status, with: RelationshipStatus, skip_validation: true 397 | end 398 | 399 | p = Person.new(relationship_status: 'invalid') 400 | p.valid? 401 | #=> true 402 | ``` 403 | 404 | Remember that you can add validations to any kind of class and not only 405 | `ActiveRecord` ones. 406 | 407 | ## FAQ 408 | 409 | #### Why define enumerations outside the class that uses them? 410 | 411 | - It's clearer. 412 | - You can add behavior to the enumeration class. 413 | - You can reuse the enumeration inside other classes. 414 | 415 | #### Can I use `enumerate_it` gem without Rails? 416 | 417 | You sure can! 😄 418 | 419 | #### What versions of Ruby and Rails are supported? 420 | 421 | - **Ruby**: `3.0+` 422 | - **Rails**: `6.0+` 423 | 424 | All versions are tested via 425 | [GitHub Actions](https://github.com/lucascaton/enumerate_it/blob/HEAD/.github/workflows/ci.yml). 426 | 427 | #### Can I set a value to always be at the end of a sorted list? 428 | 429 | Yes, 430 | [see more details here](https://github.com/lucascaton/enumerate_it/issues/60). 431 | 432 | ## I18n 433 | 434 | I18n lookup is provided for both `_humanized` and `Enumeration#to_a` methods, 435 | given the hash key is a Symbol. The I18n strings are located on 436 | `enumerations..`: 437 | 438 | ```yaml 439 | # Your locale file 440 | pt-BR: 441 | enumerations: 442 | relationship_status: 443 | married: Casado 444 | ``` 445 | 446 | ```ruby 447 | class RelationshipStatus < EnumerateIt::Base 448 | associate_values( 449 | :married, 450 | :single 451 | ) 452 | end 453 | 454 | p = Person.new 455 | p.relationship_status = RelationshipStatus::MARRIED 456 | p.relationship_status_humanize # Existent key 457 | #=> 'Casado' 458 | 459 | p.relationship_status = RelationshipStatus::SINGLE 460 | p.relationship_status_humanize # Non-existent key 461 | #=> 'Single' 462 | ``` 463 | 464 | You can also translate specific values: 465 | 466 | ```ruby 467 | status = RelationshipStatus::MARRIED 468 | RelationshipStatus.t(status) 469 | #=> 'Casado' 470 | ``` 471 | 472 | ### Translate a namespaced enumeration 473 | 474 | In order to translate an enumeration in a specific namespace (say 475 | `Design::Color`), use the following structure: 476 | 477 | ```yaml 478 | pt-BR: 479 | enumerations: 480 | "design/color": 481 | blue: Azul 482 | red: Vermelho 483 | ``` 484 | 485 | ## Handling a legacy database 486 | 487 | **EnumerateIt** can help you build a Rails application around a legacy database 488 | which was filled with those small and unchangeable tables used to create foreign 489 | key constraints everywhere, like the following example: 490 | 491 | ```sql 492 | Table "public.relationship_status" 493 | 494 | Column | Type | Modifiers 495 | -------------+---------------+----------- 496 | code | character(1) | not null 497 | description | character(11) | 498 | 499 | Indexes: 500 | "relationship_status_pkey" PRIMARY KEY, btree (code) 501 | 502 | SELECT * FROM relationship_status; 503 | 504 | code | description 505 | ---- +-------------- 506 | 1 | Single 507 | 2 | Married 508 | 3 | Divorced 509 | ``` 510 | 511 | You might also have something like a `users` table with a `relationship_status` 512 | column and a foreign key pointing to the `relationship_status` table. 513 | 514 | While this is a good thing from the database normalization perspective, managing 515 | these values in tests is very hard. Doing database joins just to get the 516 | description of some value is absurd. And, more than this, referencing them in 517 | the code using 518 | [magic numbers]() was 519 | terrible and meaningless: what does it mean when we say that someone or 520 | something is `2`? 521 | 522 | To solve this, you can pass a **hash** to your enumeration values: 523 | 524 | ```ruby 525 | class RelationshipStatus < EnumerateIt::Base 526 | associate_values( 527 | single: 1, 528 | married: 2, 529 | divorced: 3 530 | ) 531 | end 532 | ``` 533 | 534 | ```ruby 535 | RelationshipStatus::MARRIED 536 | #=> 2 537 | ``` 538 | 539 | You can also sort it by its **value** using `sort_by :value`. 540 | 541 | ## Changelog 542 | 543 | Changes follows the [Semantic Versioning](https://semver.org/) specification and 544 | you can see them on the [releases page](../../releases). 545 | 546 | ## Note on Patches/Pull Requests 547 | 548 | - Fork the project. 549 | - Make your feature addition or bug fix. 550 | - Add tests for it. This is important so we don't break it in a future version 551 | unintentionally. 552 | - [Optional] Run the tests against a specific Gemfile: 553 | `bundle exec appraisal rails_8.0 rake spec`. 554 | - Run the tests against all supported versions: `bundle exec rake` 555 | - Commit, but please do not mess with `Rakefile`, version, or history. 556 | - Send a Pull Request. Bonus points for topic branches. 557 | 558 | ## Copyright 559 | 560 | Copyright (c) 2010-2025 Cássio Marques and Lucas Caton. See `LICENSE` file for 561 | details. 562 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | Bundler::GemHelper.install_tasks 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task default: :spec 8 | -------------------------------------------------------------------------------- /enumerate_it.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path('lib', __dir__) 2 | require 'enumerate_it/version' 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ['Cássio Marques', 'Lucas Caton'] 6 | gem.summary = 'Ruby Enumerations' 7 | gem.description = 'Enumerations for Ruby with some magic powers!' 8 | gem.homepage = 'https://github.com/lucascaton/enumerate_it' 9 | gem.license = 'MIT' 10 | 11 | gem.files = `git ls-files`.split("\n") 12 | gem.name = 'enumerate_it' 13 | gem.require_paths = ['lib'] 14 | gem.version = EnumerateIt::VERSION 15 | gem.required_ruby_version = '>= 3.0.0' 16 | 17 | gem.metadata = { 18 | 'source_code_uri' => 'https://github.com/lucascaton/enumerate_it', 19 | 'changelog_uri' => 'https://github.com/lucascaton/enumerate_it/releases', 20 | 'rubygems_mfa_required' => 'true' 21 | } 22 | 23 | gem.add_dependency 'activesupport', '>= 6.0.0' 24 | 25 | gem.add_development_dependency 'activerecord' 26 | gem.add_development_dependency 'appraisal' 27 | gem.add_development_dependency 'bundler' 28 | gem.add_development_dependency 'pry' 29 | gem.add_development_dependency 'rake' 30 | gem.add_development_dependency 'rspec' 31 | gem.add_development_dependency 'rubocop' 32 | gem.add_development_dependency 'rubocop-rake' 33 | gem.add_development_dependency 'rubocop-rspec' 34 | gem.add_development_dependency 'sqlite3', '< 2' 35 | end 36 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'activerecord', '~> 6.0.6.1' 6 | gem 'activesupport', '~> 6.0.6.1' 7 | gem 'base64' 8 | gem 'bigdecimal' 9 | gem 'mutex_m' 10 | gem 'sqlite3', '< 2' 11 | 12 | gemspec path: '../' 13 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'activerecord', '~> 6.1.7.10' 6 | gem 'activesupport', '~> 6.1.7.10' 7 | gem 'base64' 8 | gem 'bigdecimal' 9 | gem 'mutex_m' 10 | gem 'sqlite3', '< 2' 11 | 12 | gemspec path: '../' 13 | -------------------------------------------------------------------------------- /gemfiles/rails_7.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'activerecord', '~> 7.0.8.7' 6 | gem 'activesupport', '~> 7.0.8.7' 7 | gem 'base64' 8 | gem 'bigdecimal' 9 | gem 'mutex_m' 10 | gem 'sqlite3', '< 2' 11 | 12 | gemspec path: '../' 13 | -------------------------------------------------------------------------------- /gemfiles/rails_7.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'activerecord', '~> 7.1.5.1' 6 | gem 'activesupport', '~> 7.1.5.1' 7 | gem 'sqlite3' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /gemfiles/rails_7.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'activerecord', '~> 7.2.2.1' 6 | gem 'activesupport', '~> 7.2.2.1' 7 | gem 'sqlite3' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /gemfiles/rails_8.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'activerecord', '~> 8.0.2' 6 | gem 'activesupport', '~> 8.0.2' 7 | gem 'sqlite3' 8 | 9 | gemspec path: '../' 10 | -------------------------------------------------------------------------------- /lib/enumerate_it.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/class/attribute' 2 | require 'active_support/inflector' 3 | require 'enumerate_it/base' 4 | require 'enumerate_it/class_methods' 5 | 6 | module EnumerateIt 7 | def self.extended(receiver) 8 | receiver.class_attribute :enumerations, instance_writer: false, instance_reader: false 9 | receiver.enumerations = {} 10 | 11 | receiver.extend ClassMethods 12 | end 13 | end 14 | 15 | ActiveSupport.on_load(:active_record) { ActiveRecord::Base.extend EnumerateIt } 16 | -------------------------------------------------------------------------------- /lib/enumerate_it/base.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module EnumerateIt 4 | class Base # rubocop:disable Metrics/ClassLength 5 | class << self 6 | extend Forwardable 7 | 8 | attr_reader :sort_mode 9 | 10 | def_delegators :enumeration, :keys, :each_key 11 | 12 | def associate_values(*args) 13 | values = values_hash(args) 14 | 15 | register_enumeration(normalize_enumeration(values)) 16 | 17 | values.each_pair do |value_name, attributes| 18 | define_enumeration_constant value_name, attributes[0] 19 | end 20 | end 21 | 22 | def sort_by(sort_mode) 23 | @sort_mode = sort_mode 24 | end 25 | 26 | def list 27 | sorted_map.map { |_k, v| v.first } 28 | end 29 | 30 | def to_h 31 | sorted_map.transform_values(&:first) 32 | end 33 | 34 | def enumeration 35 | @registered_enumerations[self] 36 | end 37 | 38 | def to_a 39 | sorted_map.map { |_k, v| [translate(v[1]), v[0]] } 40 | end 41 | 42 | def length 43 | list.length 44 | end 45 | 46 | def each_translation(&block) 47 | each_value { |value| block.call t(value) } 48 | end 49 | 50 | def translations 51 | list.map { |value| t(value) } 52 | end 53 | 54 | def each_value(&) 55 | list.each(&) 56 | end 57 | 58 | def to_json(options = nil) 59 | sorted_map.map { |_k, v| { value: v[0], label: translate(v[1]) } }.to_json(options) 60 | end 61 | 62 | def t(value) 63 | target = to_a.detect { |item| item[1] == value } 64 | target ? target[0] : value 65 | end 66 | 67 | def values_for(values) 68 | values.map { |v| value_for v.to_sym } 69 | end 70 | 71 | def value_for(value) 72 | const_get(value.to_sym, false) 73 | rescue NameError 74 | nil 75 | end 76 | 77 | def value_from_key(key) 78 | return if key.nil? 79 | 80 | (enumeration[key.to_sym] || []).first 81 | end 82 | 83 | def key_for(value) 84 | enumeration.map { |e| e[0] if e[1][0] == value }.compact.first 85 | end 86 | 87 | def to_range 88 | (list.min..list.max) 89 | end 90 | 91 | def translate(value) 92 | return value unless value.is_a? Symbol 93 | 94 | default = value.to_s.tr('_', ' ').split.map(&:capitalize).join(' ') 95 | I18n.t("enumerations.#{name.underscore}.#{value.to_s.underscore}", default: default) 96 | end 97 | 98 | private 99 | 100 | def sorted_map 101 | return enumeration if sort_mode.nil? || sort_mode == :none 102 | 103 | enumeration.sort_by { |k, v| sort_lambda.call(k, v) } 104 | end 105 | 106 | def sort_lambda 107 | { 108 | value: ->(_k, v) { v[0] }, 109 | name: ->(k, _v) { k }, 110 | translation: ->(_k, v) { translate(v[1]) }, 111 | normalize_translation: ->(_k, v) { normalize_translation(translate(v[1])) } 112 | }[sort_mode] 113 | end 114 | 115 | def normalize_translation(text) 116 | text.unicode_normalize(:nfkd).gsub(/[^\x00-\x7F]/, '') 117 | end 118 | 119 | def normalize_enumeration(values_hash) 120 | values_hash.each_pair do |key, value| 121 | values_hash[key] = [value, key] unless value.is_a? Array 122 | end 123 | end 124 | 125 | def register_enumeration(values_hash) 126 | @registered_enumerations ||= {} 127 | @registered_enumerations[self] = values_hash 128 | end 129 | 130 | def define_enumeration_constant(name, value) 131 | const_set name.to_s.tr('-', '_').gsub(/\p{blank}/, '_').upcase, value 132 | end 133 | 134 | def values_hash(args) 135 | return args.first if args.first.is_a?(Hash) 136 | 137 | args.each_with_object({}) do |value, hash| 138 | hash[value] = value.to_s 139 | end 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/enumerate_it/class_methods.rb: -------------------------------------------------------------------------------- 1 | module EnumerateIt 2 | module ClassMethods 3 | def has_enumeration_for(attribute, options = {}) 4 | self.enumerations = enumerations.dup 5 | 6 | define_enumeration_class(attribute, options) 7 | create_enumeration_humanize_method(options[:with], attribute) 8 | create_enumeration_key_method(options[:with], attribute) 9 | store_enumeration(options[:with], attribute) 10 | 11 | handle_options(attribute, options) 12 | end 13 | 14 | private 15 | 16 | def handle_options(attribute, options) 17 | set_validations(attribute, options) unless options[:skip_validation] 18 | 19 | if options[:create_helpers] 20 | %w[create_helper_methods create_mutator_methods create_polymorphic_methods].each do |method| 21 | send(method, options[:with], attribute, options[:create_helpers]) 22 | end 23 | end 24 | 25 | create_scopes options[:with], attribute, options[:create_scopes] if options[:create_scopes] 26 | end 27 | 28 | def store_enumeration(klass, attribute) 29 | enumerations[attribute] = klass 30 | end 31 | 32 | def create_enumeration_humanize_method(klass, attribute_name) 33 | class_eval do 34 | define_method "#{attribute_name}_humanize" do 35 | values = klass.enumeration.values.detect { |v| v[0] == send(attribute_name) } 36 | 37 | values ? klass.translate(values[1]) : nil 38 | end 39 | end 40 | end 41 | 42 | def create_enumeration_key_method(klass, attribute_name) 43 | class_eval do 44 | define_method "#{attribute_name}_key" do 45 | value = public_send(attribute_name) 46 | 47 | value ? klass.key_for(value) : nil 48 | end 49 | end 50 | end 51 | 52 | def create_helper_methods(klass, attribute_name, helpers) 53 | prefix_name = "#{attribute_name}_" if helpers.is_a?(Hash) && helpers[:prefix] 54 | 55 | class_eval do 56 | klass.enumeration.each_key do |option| 57 | define_method "#{prefix_name}#{option}?" do 58 | send(attribute_name) == klass.enumeration[option].first 59 | end 60 | end 61 | end 62 | end 63 | 64 | def create_scopes(klass, attribute_name, helpers) 65 | return unless respond_to? :scope 66 | 67 | prefix_name = "#{attribute_name}_" if helpers.is_a?(Hash) && helpers[:prefix] 68 | 69 | klass.enumeration.each_key do |key| 70 | scope("#{prefix_name}#{key}", -> { where(attribute_name => klass.enumeration[key].first) }) 71 | end 72 | end 73 | 74 | def create_mutator_methods(klass, attribute_name, helpers) 75 | prefix_name = "#{attribute_name}_" if helpers.is_a?(Hash) && helpers[:prefix] 76 | 77 | class_eval do 78 | klass.enumeration.each_pair do |key, values| 79 | define_method "#{prefix_name}#{key}!" do 80 | send "#{attribute_name}=", values.first 81 | save! if respond_to?(:save!) 82 | end 83 | end 84 | end 85 | end 86 | 87 | def create_polymorphic_methods(klass, attribute_name, helpers) 88 | return unless helpers.is_a?(Hash) && helpers[:polymorphic] 89 | 90 | options = helpers[:polymorphic] 91 | suffix = options.is_a?(Hash) && options[:suffix] 92 | suffix ||= '_object' 93 | 94 | class_eval do 95 | define_method "#{attribute_name}#{suffix}" do 96 | value = public_send(attribute_name) 97 | 98 | klass.const_get(klass.key_for(value).to_s.camelize).new if value 99 | end 100 | end 101 | end 102 | 103 | def define_enumeration_class(attribute, options) 104 | return if options[:with] 105 | 106 | inner_enum_class_name = attribute.to_s.camelize.to_sym 107 | 108 | options[:with] = if constants.include?(inner_enum_class_name) 109 | const_get(inner_enum_class_name) 110 | else 111 | attribute.to_s.camelize.constantize 112 | end 113 | end 114 | 115 | def set_validations(attribute, options) 116 | if respond_to?(:validates_inclusion_of) 117 | validates_inclusion_of(attribute, in: options[:with].list, allow_blank: true) 118 | end 119 | 120 | if options[:required] && respond_to?(:validates_presence_of) 121 | opts = options[:required].is_a?(Hash) ? options[:required] : {} 122 | validates_presence_of(attribute, opts) 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/enumerate_it/version.rb: -------------------------------------------------------------------------------- 1 | module EnumerateIt 2 | VERSION = '4.1.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/enumerate_it/enum/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates an EnumerateIt class and its locale file 3 | 4 | Example: 5 | rails g enumerate_it:enum CivilStatus single married divorced widower concubinage separated stable 6 | 7 | This will create: 8 | app/enumerations/civil_status.rb with `associate_values :single, :married, :divorced, :widower, :concubinage, :separated, :stable` 9 | config/locales/civil_status.yml 10 | 11 | rails g enumerate_it:enum CivilStatus single:1 married:2 divorced:3 widower:4 12 | 13 | This will create: 14 | app/enumerations/civil_status.rb with `associate_values single: 1, married: 2, divorced: 3, widower: 4` 15 | config/locales/civil_status.yml 16 | -------------------------------------------------------------------------------- /lib/generators/enumerate_it/enum/enum_generator.rb: -------------------------------------------------------------------------------- 1 | module EnumerateIt 2 | module Generators 3 | class EnumGenerator < Rails::Generators::NamedBase 4 | source_root File.expand_path('templates', __dir__) 5 | 6 | argument :attributes, type: :hash 7 | 8 | class_option :singular, type: 'string', desc: 'Singular name for i18n' 9 | 10 | class_option :lang, type: 'string', desc: 'Language to use in i18n', default: 'en' 11 | 12 | desc 'Creates a locale file on config/locales' 13 | def create_locale 14 | template 'locale.yml', File.join('config/locales', "#{singular_name}.yml") 15 | end 16 | 17 | desc 'Creates the enumeration' 18 | def create_enumerate_it 19 | template 'enumerate_it.rb', File.join('app/enumerations', "#{singular_name}.rb") 20 | end 21 | 22 | private 23 | 24 | def default_lang 25 | options[:lang] 26 | end 27 | 28 | def singular 29 | singular_name 30 | end 31 | 32 | def fields 33 | if attributes.empty? 34 | args 35 | elsif attributes.first.type == :string 36 | attributes.map(&:name) 37 | else 38 | attributes.map { |attribute| [attribute.name, attribute.type] } 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/generators/enumerate_it/enum/templates/enumerate_it.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %> < EnumerateIt::Base 2 | associate_values( 3 | <%= fields.map { |field, value| value ? "#{field}: #{value}" : ":#{field}"}.join(",\n ") %> 4 | ) 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/enumerate_it/enum/templates/locale.yml: -------------------------------------------------------------------------------- 1 | <%= default_lang %>: 2 | enumerations: 3 | <%= singular_name %>: 4 | <%- fields.each do |name, _| -%> 5 | <%= name %>: '<%= name.humanize %>' 6 | <%- end -%> 7 | -------------------------------------------------------------------------------- /spec/enumerate_it/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EnumerateIt::Base do 4 | it 'creates constants for each enumeration value' do 5 | constants = [TestEnumeration::VALUE_1, TestEnumeration::VALUE_2, TestEnumeration::VALUE_3, 6 | TestEnumeration::VALUE_4] 7 | 8 | constants.each.with_index(1) do |constant, index| 9 | expect(constant).to eq(index.to_s) 10 | end 11 | end 12 | 13 | it 'creates constants for camel case values' do 14 | expect(TestEnumerationWithCamelCase::IPHONE).to eq('iPhone') 15 | end 16 | 17 | it 'creates constants replacing its dashes with underscores' do 18 | expect(TestEnumerationWithDash::PT_BR).to eq('pt-BR') 19 | end 20 | 21 | it 'creates constants for values with spaces' do 22 | expect(TestEnumerationWithSpaces::SPA_CES).to eq('spa ces') 23 | end 24 | 25 | describe '.list' do 26 | it "creates a method that returns the allowed values in the enumeration's class" do 27 | expect(TestEnumeration.list).to eq(%w[1 2 3 4]) 28 | end 29 | 30 | context 'specifying a default sort mode' do 31 | subject { create_enumeration_class_with_sort_mode(sort_mode).list } 32 | 33 | context 'by value' do 34 | let(:sort_mode) { :value } 35 | 36 | it { is_expected.to eq(%w[0 1 2 3 4]) } 37 | end 38 | 39 | context 'by name' do 40 | let(:sort_mode) { :name } 41 | 42 | it { is_expected.to eq(%w[2 4 1 3 0]) } 43 | end 44 | 45 | context 'by translation' do 46 | let(:sort_mode) { :translation } 47 | 48 | it { is_expected.to eq(%w[3 2 0 1 4]) } 49 | end 50 | 51 | context 'by normalize translation' do 52 | let(:sort_mode) { :normalize_translation } 53 | 54 | it { is_expected.to eq(%w[3 4 2 0 1]) } 55 | end 56 | 57 | context 'by nothing' do 58 | let(:sort_mode) { :none } 59 | 60 | it { is_expected.to eq(%w[1 2 3 4 0]) } 61 | end 62 | end 63 | end 64 | 65 | it 'creates a method that returns the enumeration specification' do 66 | expect(TestEnumeration.enumeration).to eq( 67 | value_1: ['1', 'Hey, I am 1!'], value_2: ['2', 'Hey, I am 2!'], 68 | value_3: ['3', 'Hey, I am 3!'], value_4: ['4', 'Héy, I ãm 2!'] 69 | ) 70 | end 71 | 72 | describe '.length' do 73 | it 'returns the length of the enumeration' do 74 | expect(TestEnumeration.length).to eq(4) 75 | end 76 | end 77 | 78 | describe '.each_translation' do 79 | it "yields each enumeration's value translation" do 80 | translations = [] 81 | TestEnumeration.each_translation do |translation| 82 | translations << translation 83 | end 84 | expect(translations).to eq(['Hey, I am 1!', 'Hey, I am 2!', 'Hey, I am 3!', 'Héy, I ãm 2!']) 85 | end 86 | end 87 | 88 | describe '.translations' do 89 | it 'returns all translations' do 90 | expect(TestEnumeration.translations).to eq(['Hey, I am 1!', 'Hey, I am 2!', 'Hey, I am 3!', 91 | 'Héy, I ãm 2!']) 92 | end 93 | end 94 | 95 | describe '.each_key' do 96 | it "yields each enumeration's key" do 97 | keys = [] 98 | TestEnumeration.each_key do |key| 99 | keys << key 100 | end 101 | expect(keys).to eq(%i[value_1 value_2 value_3 value_4]) 102 | end 103 | end 104 | 105 | describe '.each_value' do 106 | it "yields each enumeration's value" do 107 | values = [] 108 | TestEnumeration.each_value do |value| 109 | values << value 110 | end 111 | expect(values).to eq(TestEnumeration.list) 112 | end 113 | end 114 | 115 | describe '.to_a' do 116 | it 'returns an array with the values and human representations' do 117 | expect(TestEnumeration.to_a) 118 | .to eq([['Hey, I am 1!', '1'], ['Hey, I am 2!', '2'], ['Hey, I am 3!', '3'], 119 | ['Héy, I ãm 2!', '4']]) 120 | end 121 | 122 | it 'translates the available values' do 123 | I18n.locale = :en 124 | expect(TestEnumerationWithoutArray.to_a).to eq([['First Value', '1'], ['Value Two', '2']]) 125 | I18n.locale = :pt 126 | expect(TestEnumerationWithoutArray.to_a).to eq([['Primeiro Valor', '1'], ['Value Two', '2']]) 127 | end 128 | 129 | it 'can be extended from the enumeration class' do 130 | expect(TestEnumerationWithExtendedBehaviour.to_a).to eq([%w[Second 2], %w[First 1]]) 131 | end 132 | end 133 | 134 | describe '.to_h' do 135 | it 'returns a hash' do 136 | expect(TestEnumerationWithoutArray.to_h).to eq(value_one: '1', value_two: '2') 137 | end 138 | end 139 | 140 | describe '.to_json' do 141 | it 'gives a valid json back' do 142 | I18n.locale = :inexsistent 143 | expect(TestEnumerationWithoutArray.to_json) 144 | .to eq('[{"value":"1","label":"Value One"},{"value":"2","label":"Value Two"}]') 145 | end 146 | 147 | it 'give translated values when available' do 148 | I18n.locale = :pt 149 | expect(TestEnumerationWithoutArray.to_json) 150 | .to eq('[{"value":"1","label":"Primeiro Valor"},{"value":"2","label":"Value Two"}]') 151 | end 152 | end 153 | 154 | describe '.t' do 155 | it 'translates a given value' do 156 | I18n.locale = :pt 157 | expect(TestEnumerationWithoutArray.t('1')).to eq('Primeiro Valor') 158 | end 159 | end 160 | 161 | describe '.to_range' do 162 | it "returns a Range object containing the enumeration's value interval" do 163 | expect(TestEnumeration.to_range).to eq('1'..'4') 164 | end 165 | end 166 | 167 | describe '.values_for' do 168 | it "returns an array representing some of the enumeration's values" do 169 | expect(TestEnumeration.values_for(%w[VALUE_1 VALUE_2])) 170 | .to eq([TestEnumeration::VALUE_1, TestEnumeration::VALUE_2]) 171 | end 172 | 173 | it 'returns nil if the a constant named after one of the given strings cannot be found' do 174 | expect(TestEnumeration.values_for(%w[VALUE_1 THIS_IS_WRONG])) 175 | .to eq([TestEnumeration::VALUE_1, nil]) 176 | end 177 | end 178 | 179 | describe '.value_for' do 180 | it "returns the enumeration's value" do 181 | expect(TestEnumeration.value_for('VALUE_1')).to eq(TestEnumeration::VALUE_1) 182 | end 183 | 184 | context 'when a constant named after the received value cannot be found' do 185 | it 'returns nil' do 186 | expect(TestEnumeration.value_for('THIS_IS_WRONG')).to be_nil 187 | end 188 | end 189 | 190 | context 'when a constant named after the received value exists as an ancestor' do 191 | it 'returns nil' do 192 | expect(TestEnumeration.value_for('Module')).to be_nil 193 | end 194 | end 195 | end 196 | 197 | describe '.value_from_key' do 198 | it 'returns the correct value when the key is a string' do 199 | expect(TestEnumeration.value_from_key('value_1')).to eq(TestEnumeration::VALUE_1) 200 | end 201 | 202 | it 'returns the correct value when the key is a symbol' do 203 | expect(TestEnumeration.value_from_key(:value_1)).to eq(TestEnumeration::VALUE_1) 204 | end 205 | 206 | it 'returns nil when the key does not exist in the enumeration' do 207 | expect(TestEnumeration.value_from_key('wrong')).to be_nil 208 | end 209 | 210 | it 'returns nil when the given value is nil' do 211 | expect(TestEnumeration.value_from_key(nil)).to be_nil 212 | end 213 | end 214 | 215 | describe '.keys' do 216 | it 'returns a list with the keys used to define the enumeration' do 217 | expect(TestEnumeration.keys).to eq(%i[value_1 value_2 value_3 value_4]) 218 | end 219 | end 220 | 221 | describe '.key_for' do 222 | it 'returns the key for the given value inside the enumeration' do 223 | expect(TestEnumeration.key_for(TestEnumeration::VALUE_1)).to eq(:value_1) 224 | end 225 | 226 | it 'returns nil if the enumeration does not have the given value' do 227 | expect(TestEnumeration.key_for('foo')).to be_nil 228 | end 229 | end 230 | 231 | context 'associate values with a list' do 232 | it 'creates constants for each enumeration value' do 233 | expect(TestEnumerationWithList::FIRST).to eq('first') 234 | expect(TestEnumerationWithList::SECOND).to eq('second') 235 | end 236 | 237 | it 'returns an array with the values and human representations' do 238 | expect(TestEnumerationWithList.to_a).to eq([%w[First first], %w[Second second]]) 239 | end 240 | end 241 | 242 | context 'not specifying a sort mode' do 243 | subject(:enumeration) { create_enumeration_class_with_sort_mode(nil) } 244 | 245 | it 'does not sort' do 246 | expect(enumeration.to_a).to eq([%w[xyz 1], %w[fgh 2], %w[abc 3], %w[ábc 4], %w[jkl 0]]) 247 | end 248 | end 249 | 250 | context 'specifying a sort mode' do 251 | subject(:enumeration) { create_enumeration_class_with_sort_mode(sort_mode) } 252 | 253 | context 'by value' do 254 | let(:sort_mode) { :value } 255 | 256 | it { expect(enumeration.to_a).to eq([%w[jkl 0], %w[xyz 1], %w[fgh 2], %w[abc 3], %w[ábc 4]]) } 257 | end 258 | 259 | context 'by name' do 260 | let(:sort_mode) { :name } 261 | 262 | it { expect(enumeration.to_a).to eq([%w[fgh 2], %w[ábc 4], %w[xyz 1], %w[abc 3], %w[jkl 0]]) } 263 | end 264 | 265 | context 'by translation' do 266 | let(:sort_mode) { :translation } 267 | 268 | it { expect(enumeration.to_a).to eq([%w[abc 3], %w[fgh 2], %w[jkl 0], %w[xyz 1], %w[ábc 4]]) } 269 | end 270 | 271 | context 'by normalize translation' do 272 | let(:sort_mode) { :normalize_translation } 273 | 274 | it { expect(enumeration.to_a).to eq([%w[abc 3], %w[ábc 4], %w[fgh 2], %w[jkl 0], %w[xyz 1]]) } 275 | end 276 | 277 | context 'by nothing' do 278 | let(:sort_mode) { :none } 279 | 280 | it { expect(enumeration.to_a).to eq([%w[xyz 1], %w[fgh 2], %w[abc 3], %w[ábc 4], %w[jkl 0]]) } 281 | end 282 | end 283 | 284 | context 'when included in ActiveRecord::Base' do 285 | let(:active_record_stub_class) do 286 | Class.new do 287 | extend EnumerateIt 288 | 289 | attr_accessor :bla 290 | 291 | class << self 292 | def validates_inclusion_of(_attribute, _options) 293 | true 294 | end 295 | 296 | def validates_presence_of 297 | true 298 | end 299 | end 300 | end 301 | end 302 | 303 | it 'creates a validation for inclusion' do 304 | expect(active_record_stub_class) 305 | .to receive(:validates_inclusion_of).with(:bla, in: TestEnumeration.list, allow_blank: true) 306 | 307 | active_record_stub_class.class_eval do 308 | has_enumeration_for :bla, with: TestEnumeration 309 | end 310 | end 311 | 312 | context 'using the :required option' do 313 | before do 314 | allow(active_record_stub_class).to receive(:validates_presence_of).and_return(true) 315 | end 316 | 317 | it 'creates a validation for presence' do 318 | expect(active_record_stub_class).to receive(:validates_presence_of) 319 | active_record_stub_class.class_eval do 320 | has_enumeration_for :bla, with: TestEnumeration, required: true 321 | end 322 | end 323 | 324 | it 'passes the given options to the validation method' do 325 | active_record_stub_class.class_eval do 326 | has_enumeration_for :bla, with: TestEnumeration, required: { if: :some_method } 327 | end 328 | 329 | expect(active_record_stub_class) 330 | .to have_received(:validates_presence_of).with(:bla, if: :some_method) 331 | end 332 | 333 | it 'does not require the attribute by default' do 334 | expect(active_record_stub_class).not_to receive(:validates_presence_of) 335 | active_record_stub_class.class_eval do 336 | has_enumeration_for :bla, with: TestEnumeration 337 | end 338 | end 339 | end 340 | 341 | context 'using :skip_validation option' do 342 | it "doesn't create a validation for inclusion" do 343 | expect(active_record_stub_class).not_to receive(:validates_inclusion_of) 344 | active_record_stub_class.class_eval do 345 | has_enumeration_for :bla, with: TestEnumeration, skip_validation: true 346 | end 347 | end 348 | 349 | it "doesn't create a validation for presence" do 350 | expect(active_record_stub_class).not_to receive(:validates_presence_of) 351 | active_record_stub_class.class_eval do 352 | has_enumeration_for :bla, with: TestEnumeration, require: true, skip_validation: true 353 | end 354 | end 355 | end 356 | end 357 | end 358 | -------------------------------------------------------------------------------- /spec/enumerate_it_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EnumerateIt do 4 | let :test_class do 5 | Class.new do 6 | extend EnumerateIt 7 | attr_accessor :foobar 8 | 9 | has_enumeration_for :foobar, with: TestEnumeration 10 | 11 | def initialize(foobar) 12 | @foobar = foobar 13 | end 14 | 15 | I18n.locale = :en 16 | end 17 | end 18 | 19 | let(:target) { test_class.new(TestEnumeration::VALUE_2) } 20 | 21 | context 'associating an enumeration with a class attribute' do 22 | it "creates an humanized description for the attribute's value" do 23 | expect(target.foobar_humanize).to eq('Hey, I am 2!') 24 | end 25 | 26 | it 'if the attribute is blank, the humanize description is nil' do 27 | target.foobar = nil 28 | expect(target.foobar_humanize).to be_nil 29 | end 30 | 31 | it 'creates the key method' do 32 | expect(target.foobar_key).to eq(:value_2) 33 | end 34 | 35 | it 'if the attribute is blank, the key method returns nil' do 36 | target.foobar = nil 37 | expect(target.foobar_key).to be_nil 38 | end 39 | 40 | it 'defaults to not creating helper methods' do 41 | expect(target).not_to respond_to(:value_1?) 42 | end 43 | 44 | it 'stores the enumeration class in a class-level hash' do 45 | expect(test_class.enumerations[:foobar]).to eq(TestEnumeration) 46 | end 47 | 48 | context 'use the same enumeration from an inherited class' do 49 | let(:some_class_without_enum) { Class.new(BaseClass) } 50 | let(:target) { some_class_without_enum.new } 51 | let(:base) { BaseClass.new } 52 | 53 | it 'has use the correct class' do 54 | expect(base.class.enumerations[:foobar]).to eq(TestEnumeration) 55 | expect(target.class.enumerations[:foobar]).to eq(TestEnumeration) 56 | end 57 | end 58 | 59 | context 'declaring a simple enum on an inherited class' do 60 | let(:some_class) { Class.new(BaseClass) { has_enumeration_for :foobar } } 61 | let(:target) { some_class.new } 62 | let(:base) { BaseClass.new } 63 | 64 | it 'has use the corret class' do 65 | expect(base.class.enumerations[:foobar]).to eq(TestEnumeration) 66 | expect(target.class.enumerations[:foobar]).to eq(Foobar) 67 | end 68 | end 69 | 70 | context 'passing options values without the human string (just the value, without an array)' do 71 | let :test_class_for_enumeration_without_array do 72 | Class.new do 73 | extend EnumerateIt 74 | attr_accessor :foobar 75 | 76 | has_enumeration_for :foobar, with: TestEnumerationWithoutArray 77 | 78 | def initialize(foobar) 79 | @foobar = foobar 80 | end 81 | end 82 | end 83 | 84 | let :target do 85 | test_class_for_enumeration_without_array.new(TestEnumerationWithoutArray::VALUE_TWO) 86 | end 87 | 88 | it 'humanizes the respective hash key' do 89 | expect(target.foobar_humanize).to eq('Value Two') 90 | end 91 | 92 | it 'translates the respective hash key when a translation is found' do 93 | target.foobar = TestEnumerationWithoutArray::VALUE_ONE 94 | expect(target.foobar_humanize).to eq('First Value') 95 | I18n.locale = :pt 96 | 97 | expect(target.foobar_humanize).to eq('Primeiro Valor') 98 | end 99 | end 100 | 101 | context 'without passing the enumeration class' do 102 | let :foo_bar_class do 103 | Class.new do 104 | extend EnumerateIt 105 | attr_accessor :test_enumeration 106 | 107 | has_enumeration_for :test_enumeration 108 | 109 | def initialize(test_enumeration_value) 110 | @test_enumeration = test_enumeration_value 111 | end 112 | end 113 | end 114 | 115 | let(:target) { foo_bar_class.new(TestEnumeration::VALUE_1) } 116 | 117 | it 'finds out which enumeration class to use' do 118 | expect(target.test_enumeration_humanize).to eq('Hey, I am 1!') 119 | end 120 | 121 | context 'when using a nested class as the enumeration' do 122 | let :class_with_nested_enum do 123 | Class.new do 124 | # rubocop:disable Lint/ConstantDefinitionInBlock 125 | # rubocop:disable RSpec/LeakyConstantDeclaration 126 | class NestedEnum < EnumerateIt::Base 127 | associate_values foo: %w[1 Blerrgh], bar: ['2' => 'Blarghhh'] 128 | end 129 | # rubocop:enable RSpec/LeakyConstantDeclaration 130 | # rubocop:enable Lint/ConstantDefinitionInBlock 131 | 132 | extend EnumerateIt 133 | attr_accessor :nested_enum 134 | 135 | has_enumeration_for :nested_enum 136 | 137 | def initialize(nested_enum_value) 138 | @nested_enum = nested_enum_value 139 | end 140 | end 141 | end 142 | 143 | it 'uses the inner class as the enumeration class' do 144 | expect(class_with_nested_enum.new('1').nested_enum_humanize).to eq('Blerrgh') 145 | end 146 | end 147 | end 148 | end 149 | 150 | context 'using the :create_helpers option' do 151 | let :test_class_with_helper do 152 | Class.new do 153 | extend EnumerateIt 154 | attr_accessor :foobar 155 | 156 | has_enumeration_for :foobar, with: TestEnumeration, create_helpers: true 157 | 158 | def initialize(foobar) 159 | @foobar = foobar 160 | end 161 | end 162 | end 163 | 164 | it 'creates helpers methods with question marks for each enumeration option' do 165 | target = test_class_with_helper.new(TestEnumeration::VALUE_2) 166 | expect(target).to be_value_2 167 | expect(target).not_to be_value_1 168 | end 169 | 170 | it 'creates a mutator method for each enumeration value' do 171 | %i[value_1 value_2 value_3].each do |value| 172 | expect(test_class_with_helper.new(TestEnumeration::VALUE_1)).to respond_to(:"#{value}!") 173 | end 174 | end 175 | 176 | it "changes the attribute's value through mutator methods" do 177 | target = test_class_with_helper.new(TestEnumeration::VALUE_2) 178 | target.value_3! 179 | expect(target.foobar).to eq(TestEnumeration::VALUE_3) 180 | end 181 | 182 | context 'when class responds to save! method' do 183 | it 'calls save!' do 184 | target = test_class_with_helper.new(TestEnumeration::VALUE_2) 185 | allow(target).to receive(:save!) 186 | target.value_3! 187 | expect(target).to have_received(:save!) 188 | end 189 | end 190 | 191 | context 'with :prefix option' do 192 | let :test_class_with_prefixed_helper do 193 | Class.new do 194 | extend EnumerateIt 195 | attr_accessor :foobar 196 | 197 | has_enumeration_for :foobar, with: TestEnumeration, create_helpers: { prefix: true } 198 | 199 | def initialize(foobar) 200 | @foobar = foobar 201 | end 202 | end 203 | end 204 | 205 | it 'creates helpers methods with question marks and prefixes for each enumeration option' do 206 | target = test_class_with_prefixed_helper.new(TestEnumeration::VALUE_2) 207 | expect(target).to be_foobar_value_2 208 | end 209 | 210 | it 'creates a mutator method for each enumeration value' do 211 | %i[value_1 value_2 value_3].each do |value| 212 | expect(test_class_with_prefixed_helper.new(TestEnumeration::VALUE_1)) 213 | .to respond_to(:"foobar_#{value}!") 214 | end 215 | end 216 | 217 | it "changes the attribute's value through mutator methods" do 218 | target = test_class_with_prefixed_helper.new(TestEnumeration::VALUE_2) 219 | target.foobar_value_3! 220 | expect(target.foobar).to eq(TestEnumeration::VALUE_3) 221 | end 222 | 223 | context 'when class responds to save! method' do 224 | it 'calls save!' do 225 | target = test_class_with_prefixed_helper.new(TestEnumeration::VALUE_2) 226 | allow(target).to receive(:save!) 227 | target.foobar_value_3! 228 | expect(target).to have_received(:save!) 229 | end 230 | end 231 | end 232 | 233 | context 'with :polymorphic option' do 234 | let :polymorphic_class do 235 | Class.new do 236 | extend EnumerateIt 237 | attr_accessor :foo 238 | 239 | has_enumeration_for :foo, with: PolymorphicEnum, create_helpers: { polymorphic: true } 240 | end 241 | end 242 | 243 | let(:target) { polymorphic_class.new } 244 | 245 | it "calls methods on the enum constants' objects" do 246 | target.foo = PolymorphicEnum::NORMAL 247 | 248 | expect(target.foo_object.print('Gol')).to eq("I'm Normal: Gol") 249 | 250 | target.foo = PolymorphicEnum::CRAZY 251 | 252 | expect(target.foo_object.print('Gol')).to eq('Whoa!: Gol') 253 | end 254 | 255 | it 'returns nil if foo is not set' do 256 | expect(target.foo_object).to be_nil 257 | end 258 | 259 | context 'and :suffix' do 260 | let :polymorphic_class_with_suffix do 261 | Class.new do 262 | extend EnumerateIt 263 | attr_accessor :foo 264 | 265 | has_enumeration_for :foo, with: PolymorphicEnum, 266 | create_helpers: { polymorphic: { suffix: '_strategy' } } 267 | end 268 | end 269 | 270 | let(:target) { polymorphic_class_with_suffix.new } 271 | 272 | it "calls methods on the enum constants' objects" do 273 | target.foo = PolymorphicEnum::NORMAL 274 | 275 | expect(target.foo_strategy.print('Gol')).to eq("I'm Normal: Gol") 276 | 277 | target.foo = PolymorphicEnum::CRAZY 278 | 279 | expect(target.foo_strategy.print('Gol')).to eq('Whoa!: Gol') 280 | end 281 | end 282 | end 283 | end 284 | 285 | describe 'using the :create_scopes option' do 286 | context 'if the hosting class responds to :scope' do 287 | let :test_class_with_scope do 288 | Class.new(ActiveRecord::Base) do 289 | self.table_name = 'test_class_with_scopes' 290 | has_enumeration_for :foobar, with: TestEnumeration, create_scopes: true 291 | end 292 | end 293 | 294 | it 'creates a scope for each enumeration value' do 295 | TestEnumeration.enumeration.each_key do |symbol| 296 | expect(test_class_with_scope).to respond_to(symbol) 297 | end 298 | end 299 | 300 | it 'when called, the scopes create the correct query', :sqlite do 301 | ActiveRecord::Schema.define { create_table :test_class_with_scopes } 302 | 303 | TestEnumeration.enumeration.each do |symbol, pair| 304 | expect(test_class_with_scope.public_send(symbol).to_sql) 305 | .to match(/WHERE "test_class_with_scopes"."foobar" = '#{pair.first}'/) 306 | end 307 | end 308 | end 309 | 310 | context 'when the hosting class does not respond to :scope' do 311 | let(:generic_class) { Class.new { extend EnumerateIt } } 312 | 313 | it 'raises no errors' do 314 | expect do 315 | generic_class.has_enumeration_for(:foobar, with: TestEnumeration, create_scopes: true) 316 | end.not_to raise_error 317 | end 318 | end 319 | 320 | context 'with :prefix option' do 321 | let :other_test_class do 322 | Class.new(ActiveRecord::Base) do 323 | has_enumeration_for :foobar, with: TestEnumerationWithReservedWords, 324 | create_scopes: { prefix: true } 325 | end 326 | end 327 | 328 | it 'creates a scope with prefix for each enumeration value' do 329 | TestEnumerationWithReservedWords.enumeration.each_key do |symbol| 330 | expect(other_test_class).to respond_to(:"foobar_#{symbol}") 331 | end 332 | end 333 | end 334 | end 335 | end 336 | -------------------------------------------------------------------------------- /spec/i18n/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | enumerations: 3 | test_enumeration_without_array: 4 | value_one: First Value 5 | -------------------------------------------------------------------------------- /spec/i18n/pt.yml: -------------------------------------------------------------------------------- 1 | pt: 2 | enumerations: 3 | test_enumeration_without_array: 4 | value_one: Primeiro Valor 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 2 | 3 | require 'enumerate_it' 4 | 5 | require 'logger' # Required by Rails 7.0 or older - https://stackoverflow.com/a/79385484/1445184 6 | require 'active_support/all' 7 | require 'active_record' 8 | 9 | Dir['./spec/support/**/*.rb'].each { |f| require f } 10 | 11 | I18n.config.enforce_available_locales = false 12 | I18n.load_path = Dir['spec/i18n/*.yml'] 13 | 14 | RSpec.configure do |config| 15 | config.filter_run_when_matching :focus 16 | 17 | config.before(:each, :sqlite) do 18 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/test_classes.rb: -------------------------------------------------------------------------------- 1 | class TestEnumeration < EnumerateIt::Base 2 | associate_values( 3 | value_1: ['1', 'Hey, I am 1!'], 4 | value_2: ['2', 'Hey, I am 2!'], 5 | value_3: ['3', 'Hey, I am 3!'], 6 | value_4: ['4', 'Héy, I ãm 2!'] 7 | ) 8 | end 9 | 10 | class TestEnumerationWithoutArray < EnumerateIt::Base 11 | associate_values value_one: '1', value_two: '2' 12 | end 13 | 14 | class TestEnumerationWithExtendedBehaviour < EnumerateIt::Base 15 | associate_values first: '1', second: '2' 16 | 17 | def self.to_a 18 | super.reverse 19 | end 20 | end 21 | 22 | class TestEnumerationWithList < EnumerateIt::Base 23 | associate_values :first, :second 24 | end 25 | 26 | class TestEnumerationWithReservedWords < EnumerateIt::Base 27 | associate_values new: 1, no_schedule: 2, with_schedule: 3, suspended: 4 28 | end 29 | 30 | class TestEnumerationWithDash < EnumerateIt::Base 31 | associate_values 'pt-BR' 32 | end 33 | 34 | class TestEnumerationWithCamelCase < EnumerateIt::Base 35 | associate_values 'iPhone' 36 | end 37 | 38 | class TestEnumerationWithSpaces < EnumerateIt::Base 39 | associate_values 'spa ces' 40 | end 41 | 42 | class Foobar < EnumerateIt::Base 43 | associate_values bar: 'foo' 44 | end 45 | 46 | class PolymorphicEnum < EnumerateIt::Base 47 | associate_values :normal, :crazy 48 | 49 | class Normal 50 | def print(msg) 51 | "I'm Normal: #{msg}" 52 | end 53 | end 54 | 55 | class Crazy 56 | def print(msg) 57 | "Whoa!: #{msg}" 58 | end 59 | end 60 | end 61 | 62 | class BaseClass 63 | extend EnumerateIt 64 | 65 | has_enumeration_for :foobar, with: TestEnumeration 66 | end 67 | 68 | def create_enumeration_class_with_sort_mode(sort_mode) 69 | Class.new(EnumerateIt::Base) do 70 | sort_by(sort_mode) 71 | 72 | associate_values( 73 | foo: %w[1 xyz], 74 | bar: %w[2 fgh], 75 | omg: %w[3 abc], 76 | bra: %w[4 ábc], 77 | zomg: %w[0 jkl] 78 | ) 79 | end 80 | end 81 | --------------------------------------------------------------------------------