├── lib ├── str_enum │ ├── version.rb │ └── model.rb └── str_enum.rb ├── .gitignore ├── gemfiles ├── activerecord72.gemfile ├── activerecord80.gemfile └── activerecord71.gemfile ├── Rakefile ├── Gemfile ├── str_enum.gemspec ├── test ├── test_helper.rb └── str_enum_test.rb ├── .github └── workflows │ └── build.yml ├── CHANGELOG.md ├── LICENSE.txt └── README.md /lib/str_enum/version.rb: -------------------------------------------------------------------------------- 1 | module StrEnum 2 | VERSION = "0.5.0" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.lock 11 | -------------------------------------------------------------------------------- /gemfiles/activerecord72.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "activerecord", "~> 7.2.0" 8 | gem "sqlite3" 9 | -------------------------------------------------------------------------------- /gemfiles/activerecord80.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "activerecord", "~> 8.0.0" 8 | gem "sqlite3" 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.test_files = FileList["test/**/*_test.rb"] 6 | end 7 | 8 | task default: :test 9 | -------------------------------------------------------------------------------- /gemfiles/activerecord71.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "activerecord", "~> 7.1.0" 8 | gem "sqlite3", "< 2" 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "activerecord", "~> 8.1.0" 8 | gem "sqlite3", platform: :ruby 9 | gem "sqlite3-ffi", platform: :jruby 10 | -------------------------------------------------------------------------------- /lib/str_enum.rb: -------------------------------------------------------------------------------- 1 | # dependencies 2 | require "active_support" 3 | 4 | # modules 5 | require_relative "str_enum/model" 6 | require_relative "str_enum/version" 7 | 8 | ActiveSupport.on_load(:active_record) do 9 | include StrEnum::Model 10 | end 11 | -------------------------------------------------------------------------------- /str_enum.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/str_enum/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "str_enum" 5 | spec.version = StrEnum::VERSION 6 | spec.summary = "String enums for Rails" 7 | spec.homepage = "https://github.com/ankane/str_enum" 8 | spec.license = "MIT" 9 | 10 | spec.author = "Andrew Kane" 11 | spec.email = "andrew@ankane.org" 12 | 13 | spec.files = Dir["*.{md,txt}", "{lib}/**/*"] 14 | spec.require_path = "lib" 15 | 16 | spec.required_ruby_version = ">= 3.2" 17 | 18 | spec.add_dependency "activesupport", ">= 7.1" 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | Bundler.require(:default) 3 | require "minitest/autorun" 4 | require "active_record" 5 | 6 | ActiveRecord::Base.logger = Logger.new(ENV["VERBOSE"] ? STDOUT : nil) 7 | ActiveRecord::Migration.verbose = ENV["VERBOSE"] 8 | 9 | # migrations 10 | ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:" 11 | 12 | ActiveRecord::Schema.define do 13 | create_table :users do |t| 14 | t.string :status 15 | t.string :address_status 16 | t.string :kind 17 | end 18 | end 19 | 20 | class User < ActiveRecord::Base 21 | str_enum :status, [:active, :archived] 22 | str_enum :address_status, [:active, :archived], prefix: :address 23 | str_enum :kind, [:guest, :vip], suffix: true 24 | end 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | include: 10 | - ruby: 3.4 11 | gemfile: Gemfile 12 | - ruby: 3.4 13 | gemfile: gemfiles/activerecord80.gemfile 14 | - ruby: 3.3 15 | gemfile: gemfiles/activerecord72.gemfile 16 | - ruby: 3.2 17 | gemfile: gemfiles/activerecord71.gemfile 18 | env: 19 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 20 | steps: 21 | - uses: actions/checkout@v5 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - run: bundle exec rake test 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.0 (2025-04-03) 2 | 3 | - Dropped support for Ruby < 3.2 and Active Record < 7.1 4 | 5 | ## 0.4.1 (2023-11-28) 6 | 7 | - Fixed issue with `select` 8 | 9 | ## 0.4.0 (2023-07-02) 10 | 11 | - Dropped support for Ruby < 3 and Active Record < 6.1 12 | 13 | ## 0.3.0 (2022-01-10) 14 | 15 | - Dropped support for Ruby < 2.6 and Active Record < 5.2 16 | 17 | ## 0.2.0 (2019-10-28) 18 | 19 | - Added negative scopes 20 | - Moved presence validation message before inclusion message 21 | 22 | ## 0.1.6 (2019-07-15) 23 | 24 | - Added update methods 25 | 26 | ## 0.1.5 (2016-12-12) 27 | 28 | - Added `default` option 29 | - Added ability to include manually 30 | 31 | ## 0.1.4 (2016-10-28) 32 | 33 | - Fixed error with `select` 34 | 35 | ## 0.1.3 (2016-10-24) 36 | 37 | - Added `suffix` option 38 | 39 | ## 0.1.2 (2016-10-24) 40 | 41 | - Added method to list values 42 | 43 | ## 0.1.1 (2016-10-23) 44 | 45 | - Added `prefix` option 46 | 47 | ## 0.1.0 (2016-10-23) 48 | 49 | - First release 50 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2025 Andrew Kane 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/str_enum/model.rb: -------------------------------------------------------------------------------- 1 | require "active_support/concern" 2 | 3 | module StrEnum 4 | module Model 5 | extend ActiveSupport::Concern 6 | 7 | class_methods do 8 | def str_enum(column, values, validate: true, scopes: true, accessor_methods: true, update_methods: true, prefix: false, suffix: false, default: true, allow_nil: false) 9 | values = values.map(&:to_s) 10 | if validate 11 | validate_options = {} 12 | if allow_nil 13 | validate_options[:allow_nil] = true 14 | else 15 | validate_options[:presence] = true 16 | end 17 | validate_options[:inclusion] = {in: values} 18 | validates column, validate_options 19 | end 20 | values.each do |value| 21 | prefix = column if prefix == true 22 | suffix = column if suffix == true 23 | method_name = [prefix, value, suffix].select { |v| v }.join("_") 24 | if scopes 25 | scope method_name, -> { where(column => value) } unless respond_to?(method_name) 26 | scope "not_#{method_name}", -> { where.not(column => value) } unless respond_to?("not_#{method_name}") 27 | end 28 | if accessor_methods && !method_defined?("#{method_name}?") 29 | define_method "#{method_name}?" do 30 | read_attribute(column) == value 31 | end 32 | end 33 | if update_methods && !method_defined?("#{method_name}!") 34 | define_method "#{method_name}!" do 35 | update!(column => value) 36 | end 37 | end 38 | end 39 | default_value = default == true ? values.first : default 40 | after_initialize do 41 | send("#{column}=", default_value) if has_attribute?(column) && !try(column) 42 | end 43 | define_singleton_method column.to_s.pluralize do 44 | values 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/str_enum_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class StrEnumTest < Minitest::Test 4 | def setup 5 | User.delete_all 6 | end 7 | 8 | def test_default 9 | user = User.new 10 | assert_equal "active", user.status 11 | end 12 | 13 | def test_scopes 14 | User.create! 15 | assert_equal 1, User.active.count 16 | assert_equal 0, User.archived.count 17 | end 18 | 19 | def test_accessor_methods 20 | user = User.create! 21 | assert user.active? 22 | assert !user.archived? 23 | end 24 | 25 | def test_state_change_methods 26 | user = User.create! 27 | user.archived! 28 | assert user.archived? 29 | user.reload 30 | assert user.archived? 31 | user.active! 32 | assert user.active? 33 | user.reload 34 | assert user.active? 35 | end 36 | 37 | def test_validation 38 | user = User.new(status: "unknown") 39 | assert !user.save 40 | assert_equal ["Status is not included in the list"], user.errors.full_messages 41 | end 42 | 43 | def test_validation_blank_first 44 | user = User.new(status: "") 45 | assert !user.save 46 | assert_equal ["Status can't be blank", "Status is not included in the list"], user.errors.full_messages 47 | end 48 | 49 | def test_list_values 50 | assert_equal %w(active archived), User.statuses 51 | end 52 | 53 | def test_prefix_scopes 54 | User.create! 55 | assert_equal 1, User.address_active.count 56 | assert_equal 0, User.address_archived.count 57 | end 58 | 59 | def test_prefix_accessors 60 | user = User.create! 61 | assert user.address_active? 62 | assert !user.address_archived? 63 | end 64 | 65 | def test_suffix_scopes 66 | User.create! 67 | assert_equal 1, User.guest_kind.count 68 | assert_equal 0, User.vip_kind.count 69 | end 70 | 71 | def test_suffix_accessors 72 | user = User.create! 73 | assert user.guest_kind? 74 | assert !user.vip_kind? 75 | end 76 | 77 | def test_select 78 | User.create! 79 | user = User.active.select(:id).last 80 | assert user.has_attribute?(:id) 81 | refute user.has_attribute?(:status) 82 | end 83 | 84 | def test_negative_scopes 85 | User.create! 86 | assert_equal 0, User.not_active.count 87 | assert_equal 1, User.not_archived.count 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # str_enum 2 | 3 | Don’t like storing enums as integers in your database? Introducing... 4 | 5 | String enums for Rails!! :tada: 6 | 7 | - scopes 8 | - validations 9 | - accessor methods 10 | - update methods 11 | 12 | [![Build Status](https://github.com/ankane/str_enum/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/str_enum/actions) 13 | 14 | ## Getting Started 15 | 16 | Add this line to your application’s Gemfile: 17 | 18 | ```ruby 19 | gem "str_enum" 20 | ``` 21 | 22 | Add a string column to your model. 23 | 24 | ```ruby 25 | add_column :users, :status, :string 26 | ``` 27 | 28 | And use: 29 | 30 | ```ruby 31 | class User < ActiveRecord::Base 32 | str_enum :status, [:active, :archived] 33 | end 34 | ``` 35 | 36 | The first value will be the initial value. This gives you: 37 | 38 | #### Scopes 39 | 40 | ```ruby 41 | User.active 42 | User.archived 43 | ``` 44 | 45 | And negative scopes 46 | 47 | ```ruby 48 | User.not_active 49 | User.not_archived 50 | ``` 51 | 52 | #### Validations 53 | 54 | ```ruby 55 | user = User.new(status: "unknown") 56 | user.valid? # false 57 | ``` 58 | 59 | #### Accessor Methods 60 | 61 | ```ruby 62 | user.active? 63 | user.archived? 64 | ``` 65 | 66 | #### Update Methods 67 | 68 | ```ruby 69 | user.active! 70 | user.archived! 71 | ``` 72 | 73 | #### Forms 74 | 75 | ```erb 76 | <%= f.select :status, User.statuses.map { |s| [s.titleize, s] } %> 77 | ``` 78 | 79 | ## Options 80 | 81 | Choose which features you want with (default values shown): 82 | 83 | ```ruby 84 | class User < ActiveRecord::Base 85 | str_enum :status, [:active, :archived], 86 | accessor_methods: true, 87 | allow_nil: false, 88 | default: true, 89 | prefix: false, 90 | scopes: true, 91 | suffix: false, 92 | update_methods: true, 93 | validate: true 94 | end 95 | ``` 96 | 97 | Prevent method name collisions with the `prefix` and `suffix` options. 98 | 99 | ```ruby 100 | class User < ActiveRecord::Base 101 | str_enum :address_status, [:active, :archived], suffix: :address 102 | end 103 | 104 | # scopes 105 | User.active_address 106 | User.archived_address 107 | 108 | # accessor methods 109 | user.active_address? 110 | user.archived_address? 111 | 112 | # update methods 113 | user.active_address! 114 | user.archived_address! 115 | ``` 116 | 117 | ## History 118 | 119 | View the [changelog](https://github.com/ankane/str_enum/blob/master/CHANGELOG.md) 120 | 121 | ## Contributing 122 | 123 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 124 | 125 | - [Report bugs](https://github.com/ankane/str_enum/issues) 126 | - Fix bugs and [submit pull requests](https://github.com/ankane/str_enum/pulls) 127 | - Write, clarify, or fix documentation 128 | - Suggest or add new features 129 | 130 | To get started with development and testing: 131 | 132 | ```sh 133 | git clone https://github.com/ankane/str_enum.git 134 | cd str_enum 135 | bundle install 136 | bundle exec rake test 137 | ``` 138 | --------------------------------------------------------------------------------