Allow new syntax for `enum` to avoid leading `_` from reserved options

Unlike other features built on Attribute API, reserved options for
`enum` has leading `_`.

* `_prefix`/`_suffix`: #19813, #20999
* `_scopes`: #34605
* `_default`: #39820

That is due to `enum` takes one hash argument only, which contains both
enum definitions and reserved options.

I propose new syntax for `enum` to avoid leading `_` from reserved
options, by allowing `enum(attr_name, ..., **options)` more Attribute
API like syntax.

Before:

```ruby
class Book < ActiveRecord::Base
  enum status: [ :proposed, :written ], _prefix: true, _scopes: false
  enum cover: [ :hard, :soft ], _suffix: true, _default: :hard
end
```

After:

```ruby
class Book < ActiveRecord::Base
  enum :status, [ :proposed, :written ], prefix: true, scopes: false
  enum :cover, [ :hard, :soft ], suffix: true, default: :hard
end
```
This commit is contained in:
Ryuta Kamizono 2021-02-04 14:13:16 +09:00
parent bc9a4c51a7
commit 0618d2d84a
4 changed files with 101 additions and 24 deletions

View File

@ -1,3 +1,25 @@
* Allow new syntax for `enum` to avoid leading `_` from reserved options.
Before:
```ruby
class Book < ActiveRecord::Base
enum status: [ :proposed, :written ], _prefix: true, _scopes: false
enum cover: [ :hard, :soft ], _suffix: true, _default: :hard
end
```
After:
```ruby
class Book < ActiveRecord::Base
enum :status, [ :proposed, :written ], prefix: true, scopes: false
enum :cover, [ :hard, :soft ], suffix: true, default: :hard
end
```
*Ryuta Kamizono*
* Add `ActiveRecord::Relation#load_async`.
This method schedules the query to be performed asynchronously from a thread pool.

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/hash/slice"
require "active_support/core_ext/object/deep_dup"
module ActiveRecord
@ -7,7 +8,7 @@ module ActiveRecord
# but can be queried by name. Example:
#
# class Conversation < ActiveRecord::Base
# enum status: [ :active, :archived ]
# enum :status, [ :active, :archived ]
# end
#
# # conversation.update! status: 0
@ -41,16 +42,16 @@ module ActiveRecord
# Conversation.where(status: [:active, :archived])
# Conversation.where.not(status: :active)
#
# Defining scopes can be disabled by setting +:_scopes+ to +false+.
# Defining scopes can be disabled by setting +:scopes+ to +false+.
#
# class Conversation < ActiveRecord::Base
# enum status: [ :active, :archived ], _scopes: false
# enum :status, [ :active, :archived ], scopes: false
# end
#
# You can set the default enum value by setting +:_default+, like:
# You can set the default enum value by setting +:default+, like:
#
# class Conversation < ActiveRecord::Base
# enum status: [ :active, :archived ], _default: "active"
# enum :status, [ :active, :archived ], default: :active
# end
#
# conversation = Conversation.new
@ -60,7 +61,7 @@ module ActiveRecord
# database integer with a hash:
#
# class Conversation < ActiveRecord::Base
# enum status: { active: 0, archived: 1 }
# enum :status, active: 0, archived: 1
# end
#
# Note that when an array is used, the implicit mapping from the values to database
@ -85,14 +86,14 @@ module ActiveRecord
#
# Conversation.where("status <> ?", Conversation.statuses[:archived])
#
# You can use the +:_prefix+ or +:_suffix+ options when you need to define
# You can use the +:prefix+ or +:suffix+ options when you need to define
# multiple enums with same values. If the passed value is +true+, the methods
# are prefixed/suffixed with the name of the enum. It is also possible to
# supply a custom value:
#
# class Conversation < ActiveRecord::Base
# enum status: [:active, :archived], _suffix: true
# enum comments_status: [:active, :inactive], _prefix: :comments
# enum :status, [ :active, :archived ], suffix: true
# enum :comments_status, [ :active, :inactive ], prefix: :comments
# end
#
# With the above example, the bang and predicate methods along with the
@ -158,17 +159,16 @@ module ActiveRecord
attr_reader :name, :mapping
end
def enum(definitions)
prefix = definitions.delete(:_prefix)
suffix = definitions.delete(:_suffix)
scopes = definitions.delete(:_scopes) != false
default = {}
default[:default] = definitions.delete(:_default) if definitions.key?(:_default)
definitions.each do |name, values|
_enum(name, values, prefix: prefix, suffix: suffix, scopes: scopes, **default)
def enum(name = nil, values = nil, **options)
if name
values, options = options, {} unless values
return _enum(name, values, **options)
end
definitions = options.slice!(:_prefix, :_suffix, :_scopes, :_default)
options.transform_keys! { |key| :"#{key[1..-1]}" }
definitions.each { |name, values| _enum(name, values, **options) }
end
private

View File

@ -639,7 +639,7 @@ class EnumTest < ActiveRecord::TestCase
assert_equal "published", klass.new.status
end
test "overloaded default" do
test "overloaded default by :_default" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [:proposed, :written, :published], _default: :published
@ -648,7 +648,7 @@ class EnumTest < ActiveRecord::TestCase
assert_equal "published", klass.new.status
end
test "scopes can be disabled" do
test "scopes can be disabled by :_scopes" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum status: [:proposed, :written], _scopes: false
@ -657,6 +657,61 @@ class EnumTest < ActiveRecord::TestCase
assert_raises(NoMethodError) { klass.proposed }
end
test "overloaded default by :default" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum :status, [:proposed, :written, :published], default: :published
end
assert_equal "published", klass.new.status
end
test "scopes can be disabled by :scopes" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum :status, [:proposed, :written], scopes: false
end
assert_raises(NoMethodError) { klass.proposed }
end
test "query state by predicate with :prefix" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum :status, { proposed: 0, written: 1 }, prefix: true
enum :last_read, { unread: 0, reading: 1, read: 2 }, prefix: :being
end
book = klass.new
assert_respond_to book, :status_proposed?
assert_respond_to book, :being_unread?
end
test "query state by predicate with :suffix" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum :cover, { hard: 0, soft: 1 }, suffix: true
enum :difficulty, { easy: 0, medium: 1, hard: 2 }, suffix: :to_read
end
book = klass.new
assert_respond_to book, :hard_cover?
assert_respond_to book, :easy_to_read?
end
test "option names can be used as label" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
enum :status, default: 0, scopes: 1, prefix: 2, suffix: 3
end
book = klass.new
assert_predicate book, :default?
assert_not_predicate book, :scopes?
assert_not_predicate book, :prefix?
assert_not_predicate book, :suffix?
end
test "scopes are named like methods" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "cats"

View File

@ -62,7 +62,7 @@ class Order < ApplicationRecord
belongs_to :customer
has_and_belongs_to_many :books, join_table: 'books_orders'
enum status: [:shipped, :being_packed, :complete, :cancelled]
enum :status, [:shipped, :being_packed, :complete, :cancelled]
scope :created_before, ->(time) { where('created_at < ?', time) }
end
@ -73,7 +73,7 @@ class Review < ApplicationRecord
belongs_to :customer
belongs_to :book
enum state: [:not_reviewed, :published, :hidden]
enum :state, [:not_reviewed, :published, :hidden]
end
```
@ -1769,7 +1769,7 @@ For example, given this [`enum`][] declaration:
```ruby
class Order < ApplicationRecord
enum status: [:shipped, :being_packaged, :complete, :cancelled]
enum :status, [:shipped, :being_packaged, :complete, :cancelled]
end
```