Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,53 @@ user.active_address!
user.archived_address!
```

## Migrating columns from Rails built-in `enum` to `str_enum`

If you are migrating legacy columns away from Rails built-in `enum`, but wish
to retain the same enum name, you may want to take a multi-step migration
process, especially if you have large tables.

One possible migration strategy involves specifying a column that differs from
your desired `str_enum` name.

For example, if you have an enum called `rank`

1. Create a migration to add a new column for your `str_enum`, e.g.
`rank_str`.
2. Set up a double writing scheme to make sure all writes to the legacy
enum are also written to the new `str_enum` column, for example via
a callback:

```ruby
before_validation :populate_str_enum_for_migration
private def populate_str_enum_for_migration
if rank_changed?
self.rank_str = rank
end
end
```

3. Create a data migration to copy all existing values from `rank` to
`rank_str` as their string equivalents.
4. Remove the legacy `enum` declaration and replace it with a `str_enum`
declaration with an explicit `:column` property. Existing aliases and
scopes should now refer to the those defined by `str_enum`. (**NOTE**: SQL
statements that specify the column explicitly may need to be changed!)

```ruby
class User < ActiveRecord::Base
# DEPRECATED rank field that previously used the "rank
# enum rank: [:lowly, :middling, :high_falutin]

str_enum :rank, [:lowly, :middling, :high_falutin], column: :rank_str
end
```

5. Once you have validated that legacy column is no longer being written to,
you may create a migration that deletes it, renames the new `str_enum`
column to its name, then remove the `:column` specification from the
`str_enum` above.

## History

View the [changelog](https://github.com/ankane/str_enum/blob/master/CHANGELOG.md)
Expand Down
18 changes: 14 additions & 4 deletions lib/str_enum/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Model
extend ActiveSupport::Concern

class_methods do
def str_enum(column, values, validate: true, scopes: true, accessor_methods: true, update_methods: true, prefix: false, suffix: false, default: true, allow_nil: false)
def str_enum(enum_name, values, validate: true, scopes: true, accessor_methods: true, update_methods: true, prefix: false, suffix: false, default: true, allow_nil: false, column: enum_name)
values = values.map(&:to_s)
if validate
validate_options = {}
Expand All @@ -18,8 +18,8 @@ def str_enum(column, values, validate: true, scopes: true, accessor_methods: tru
validates column, validate_options
end
values.each do |value|
prefix = column if prefix == true
suffix = column if suffix == true
prefix = enum_name if prefix == true
suffix = enum_name if suffix == true
method_name = [prefix, value, suffix].select { |v| v }.join("_")
if scopes
scope method_name, -> { where(column => value) } unless respond_to?(method_name)
Expand All @@ -40,9 +40,19 @@ def str_enum(column, values, validate: true, scopes: true, accessor_methods: tru
after_initialize do
send("#{column}=", default_value) unless try(column)
end
define_singleton_method column.to_s.pluralize do
define_singleton_method enum_name.to_s.pluralize do
values
end
if enum_name.to_s != column.to_s
# the enum_name is then an alias to the column
define_method(enum_name) do
read_attribute(column)
end

define_method("#{enum_name}=") do |value|
send("#{column}=", value)
end
end
end
end
end
Expand Down
44 changes: 44 additions & 0 deletions test/str_enum_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,48 @@ def test_negative_scopes
assert_equal 0, User.not_active.count
assert_equal 1, User.not_archived.count
end

def test_explicit_column_defaults
user = User.new
assert_equal "lowly", user.rank
assert_equal "lowly", user.rank_str
end

def test_explicit_column_scopes
User.create!
assert_equal 1, User.lowly.count
assert_equal 0, User.high_falutin.count
end

def test_explicit_column_accessors
user = User.new
assert user.lowly?
assert !user.high_falutin?
end

def test_explicit_column_state_change_methods
user = User.create!
user.middling!
assert user.middling?
user.reload
assert user.middling?
user.high_falutin!
assert user.high_falutin?
user.reload
assert user.high_falutin?
end

def test_explicit_column_validation
user = User.new(rank: "unknown")
assert !user.save
assert_equal ["Rank str is not included in the list"], user.errors.full_messages

user = User.new(rank_str: "unknown")
assert !user.save
assert_equal ["Rank str is not included in the list"], user.errors.full_messages
end

def test_explicit_column_list_values
assert_equal %w(lowly middling high_falutin), User.ranks
end
end
2 changes: 2 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
t.string :status
t.string :address_status
t.string :kind
t.string :rank_str
end

class User < ActiveRecord::Base
str_enum :status, [:active, :archived]
str_enum :address_status, [:active, :archived], prefix: :address
str_enum :kind, [:guest, :vip], suffix: true
str_enum :rank, [:lowly, :middling, :high_falutin], column: :rank_str
end