From 0618d2d84a501aea93c898aec504ff9a0e09d6f2 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Thu, 4 Feb 2021 14:13:16 +0900 Subject: [PATCH] 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 ``` --- activerecord/CHANGELOG.md | 22 +++++++++ activerecord/lib/active_record/enum.rb | 38 ++++++++-------- activerecord/test/cases/enum_test.rb | 59 ++++++++++++++++++++++++- guides/source/active_record_querying.md | 6 +-- 4 files changed, 101 insertions(+), 24 deletions(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 76c221c11a..5fbb20c7cb 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -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. diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index d1eb17ab99..8ff743ee66 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -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 diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 90a2c25fe9..dca75080c5 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -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" diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index b9ab0afd54..ad1eeffca3 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -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 ```