Skip to content

Commit db41eb8

Browse files
author
David Heinemeier Hansson
committed
Added ActiveRecord::Base#enum for declaring enum attributes where the values map to integers in the database, but can be queried by name
1 parent deaf285 commit db41eb8

File tree

7 files changed

+128
-2
lines changed

7 files changed

+128
-2
lines changed

activerecord/CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
* Added ActiveRecord::Base#enum for declaring enum attributes where the values map to integers in the database, but can be queried by name.
2+
3+
Example:
4+
class Conversation < ActiveRecord::Base
5+
enum status: %i( active archived )
6+
end
7+
8+
Conversation::STATUS # => { active: 0, archived: 1 }
9+
10+
# conversation.update! status: 0
11+
conversation.active!
12+
conversation.active? # => true
13+
conversation.status # => :active
14+
15+
# conversation.update! status: 1
16+
conversation.archived!
17+
conversation.archived? # => true
18+
conversation.status # => :archived
19+
20+
# conversation.update! status: 1
21+
conversation.status = :archived
22+
23+
*DHH*
24+
25+
126
* ActiveRecord::Base#attribute_for_inspect now truncates long arrays (more than 10 elements)
227

328
*Jan Bernacki*

activerecord/lib/active_record.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module ActiveRecord
3737
autoload :ConnectionHandling
3838
autoload :CounterCache
3939
autoload :DynamicMatchers
40+
autoload :Enum
4041
autoload :Explain
4142
autoload :Inheritance
4243
autoload :Integration

activerecord/lib/active_record/base.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ class Base
291291
extend Translation
292292
extend DynamicMatchers
293293
extend Explain
294+
extend Enum
294295
extend Delegation::DelegateCache
295296

296297
include Persistence
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
module ActiveRecord
2+
# Declare an enum attribute where the values map to integers in the database, but can be queried by name. Example:
3+
#
4+
# class Conversation < ActiveRecord::Base
5+
# enum status: %i( active archived )
6+
# end
7+
#
8+
# Conversation::STATUS # => { active: 0, archived: 1 }
9+
#
10+
# # conversation.update! status: 0
11+
# conversation.active!
12+
# conversation.active? # => true
13+
# conversation.status # => :active
14+
#
15+
# # conversation.update! status: 1
16+
# conversation.archived!
17+
# conversation.archived? # => true
18+
# conversation.status # => :archived
19+
#
20+
# # conversation.update! status: 1
21+
# conversation.status = :archived
22+
#
23+
# You can set the default value from the database declaration, like:
24+
#
25+
# create_table :conversation do
26+
# t.column :status, :integer, default: 0
27+
# end
28+
#
29+
# Good practice is to let the first declared status be the default.
30+
module Enum
31+
def enum(definitions)
32+
definitions.each do |name, values|
33+
const_name = name.to_s.upcase
34+
35+
# DIRECTION = { }
36+
const_set const_name, {}
37+
38+
# def direction=(value) self[:direction] = DIRECTION[value] end
39+
class_eval "def #{name}=(value) self[:#{name}] = #{const_name}[value] end"
40+
41+
# def direction() DIRECTION.key self[:direction] end
42+
class_eval "def #{name}() #{const_name}.key self[:#{name}] end"
43+
44+
values.each_with_index do |value, i|
45+
# DIRECTION[:incoming] = 0
46+
const_get(const_name)[value] = i
47+
48+
# scope :incoming, -> { where direction: 0 }
49+
scope value, -> { where name => i }
50+
51+
# def incoming?() direction == 0 end
52+
class_eval "def #{value}?() self[:#{name}] == #{i} end"
53+
54+
# def incoming! update! direction: :incoming end
55+
class_eval "def #{value}!() update! #{name}: :#{value} end"
56+
end
57+
end
58+
end
59+
end
60+
end

activerecord/test/cases/enum_test.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require 'cases/helper'
2+
require 'models/book'
3+
4+
class StoreTest < ActiveRecord::TestCase
5+
fixtures :books
6+
7+
setup do
8+
@book = Book.create! name: 'REMOTE'
9+
end
10+
11+
test "query state by predicate" do
12+
assert @book.proposed?
13+
assert_not @book.written?
14+
assert_not @book.published?
15+
end
16+
17+
test "query state with symbol" do
18+
assert_equal :proposed, @book.status
19+
end
20+
21+
test "update by declaration" do
22+
@book.written!
23+
assert @book.written?
24+
end
25+
26+
test "update by setter" do
27+
@book.update! status: :written
28+
assert @book.written?
29+
end
30+
31+
test "constant" do
32+
assert_equal 0, Book::STATUS[:proposed]
33+
assert_equal 1, Book::STATUS[:written]
34+
assert_equal 2, Book::STATUS[:published]
35+
end
36+
end

activerecord/test/models/book.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ class Book < ActiveRecord::Base
22
has_many :authors
33

44
has_many :citations, :foreign_key => 'book1_id'
5-
has_many :references, -> { distinct }, :through => :citations, :source => :reference_of
5+
has_many :references, -> { distinct }, through: :citations, source: :reference_of
66

77
has_many :subscriptions
8-
has_many :subscribers, :through => :subscriptions
8+
has_many :subscribers, through: :subscriptions
9+
10+
enum status: %i( proposed written published )
911
end

activerecord/test/schema/schema.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def create_table(*args, &block)
9494
create_table :books, :force => true do |t|
9595
t.integer :author_id
9696
t.column :name, :string
97+
t.column :status, :integer, default: 0
9798
end
9899

99100
create_table :booleans, :force => true do |t|

0 commit comments

Comments
 (0)