diff --git a/README.md b/README.md index 58cf3f5..63144ed 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/lib/str_enum/model.rb b/lib/str_enum/model.rb index da57e79..a4cb6d9 100644 --- a/lib/str_enum/model.rb +++ b/lib/str_enum/model.rb @@ -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 = {} @@ -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) @@ -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 diff --git a/test/str_enum_test.rb b/test/str_enum_test.rb index fe22e08..4a44e7e 100644 --- a/test/str_enum_test.rb +++ b/test/str_enum_test.rb @@ -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 diff --git a/test/test_helper.rb b/test/test_helper.rb index a49226d..02d8eb8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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