Railsのenumを読む

Railsのソースコードを読みます。

enumに関して

```ruby

class User < ApplicationRecord

binding.pry
  enum status: { active: 0, inactive: 1 }
end
```

bindingを使ってコードの中に入っていく。

```
    153:         attr_reader :name, :mapping, :subtype
    154:     end
    155: 
    156:     def enum(definitions)
    157:       klass = self
 => 158:       enum_prefix = definitions.delete(:_prefix)
    159:       enum_suffix = definitions.delete(:_suffix)
    160:       enum_scopes = definitions.delete(:_scopes)
    161:       definitions.each do |name, values|
    162:         assert_valid_enum_definition_values(values)
    163:         # statuses = { }
```

_prefix・_suffix・_scopesが存在すれば、削除して代入。ちなみに、_prefixは接頭辞、_suffixは接尾辞、_scopesはスコープの設定である。

```
[3] pry(User)> klass
=> User (call 'User.connection' to establish a connection)
[4] pry(User)> definitions
=> {:status=>{:active=>0, :inactive=>1}}
```

```
    156:     def enum(definitions)
    157:       klass = self
    158:       enum_prefix = definitions.delete(:_prefix)
    159:       enum_suffix = definitions.delete(:_suffix)
    160:       enum_scopes = definitions.delete(:_scopes)
 => 161:       definitions.each do |name, values|
    162:         assert_valid_enum_definition_values(values)
    163:         # statuses = { }
    164:         enum_values = ActiveSupport::HashWithIndifferentAccess.new
    165:         name = name.to_s
    166: 
```

```
[5] pry(User)> name
=> :status
[6] pry(User)> values
=> {:active=>0, :inactive=>1}
```

assert_valid_enum_definition_valuesメソッドの中に入っていく。

```
    232: def assert_valid_enum_definition_values(values)
 => 233:   unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) }
    234:     error_message = <<~MSG
    235:       Enum values #{values} must be either a hash, an array of symbols, or an array of strings.
    236:     MSG
    237:     raise ArgumentError, error_message
    238:   end
    239: 
    240:   if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?)
    241:     raise ArgumentError, "Enum label name must not be blank."
    242:   end
    243: end
```

valuesがハッシュかvaluesの要素全てがシンボルかvaluesの要素全てが文字列かを判定して、それがfalseだったらエラーを返す。今回はtrueなので実行されない。

```
    232: def assert_valid_enum_definition_values(values)
    233:   unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) }
    234:     error_message = <<~MSG
    235:       Enum values #{values} must be either a hash, an array of symbols, or an array of strings.
    236:     MSG
    237:     raise ArgumentError, error_message
    238:   end
    239: 
 => 240:   if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?)
    241:     raise ArgumentError, "Enum label name must not be blank."
    242:   end
    243: end
```

valuesがハッシュかつvaluesのキーがblankかvaluesが配列かつvaluesの要素がblankかを判定して、trueならraiseする。今回はfalseなので次へ。

```
    159:       enum_suffix = definitions.delete(:_suffix)
    160:       enum_scopes = definitions.delete(:_scopes)
    161:       definitions.each do |name, values|
    162:         assert_valid_enum_definition_values(values)
    163:         # statuses = { }
 => 164:         enum_values = ActiveSupport::HashWithIndifferentAccess.new
    165:         name = name.to_s
    166: 
    167:         # def self.statuses() statuses end
    168:         detect_enum_conflict!(name, name.pluralize, true)
    169:         singleton_class.define_method(name.pluralize) { enum_values }
```

ActiveSupport::HashWithIndifferentAccess.newで空のインスタンスをenum_valuesに代入。またnameを文字列に変換。次。

```
    163:         # statuses = { }
    164:         enum_values = ActiveSupport::HashWithIndifferentAccess.new
    165:         name = name.to_s
    166: 
    167:         # def self.statuses() statuses end
 => 168:         detect_enum_conflict!(name, name.pluralize, true)
    169:         singleton_class.define_method(name.pluralize) { enum_values }
    170:         defined_enums[name] = enum_values
    171: 
    172:         detect_enum_conflict!(name, name)
    173:         detect_enum_conflict!(name, "#{name}=")
```

detect_enum_conflict!の中を見ていく。

```
    251: def detect_enum_conflict!(enum_name, method_name, klass_method = false)
 => 252:   if klass_method && dangerous_class_method?(method_name)
    253:     raise_conflict_error(enum_name, method_name, type: "class")
    254:   elsif klass_method && method_defined_within?(method_name, Relation)
    255:     raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
    256:   elsif !klass_method && dangerous_attribute_method?(method_name)
    257:     raise_conflict_error(enum_name, method_name)
    258:   elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
    259:     raise_conflict_error(enum_name, method_name, source: "another enum")
    260:   end
    261: end
```

detect_enum_conflist!は第2引数で与えられたものがどこかに定義されているかどうかを判断して例外を発生させるメソッド。今回は例外は発生しない。

```
    164:         enum_values = ActiveSupport::HashWithIndifferentAccess.new
    165:         name = name.to_s
    166: 
    167:         # def self.statuses() statuses end
    168:         detect_enum_conflict!(name, name.pluralize, true)
 => 169:         singleton_class.define_method(name.pluralize) { enum_values }
    170:         defined_enums[name] = enum_values
    171: 
    172:         detect_enum_conflict!(name, name)
    173:         detect_enum_conflict!(name, "#{name}=")

```

singleton_class.define_method(name.pluralize) { enum_values }でname.pluralizeという特異メソッドを定義する。

```
    165:         name = name.to_s
    166: 
    167:         # def self.statuses() statuses end
    168:         detect_enum_conflict!(name, name.pluralize, true)
    169:         singleton_class.define_method(name.pluralize) { enum_values }
 => 170:         defined_enums[name] = enum_values
    171: 
    172:         detect_enum_conflict!(name, name)
    173:         detect_enum_conflict!(name, "#{name}=")
    174: 
    175:         attr = attribute_alias?(name) ? attribute_alias(name) : name
```

defined_enums[name]に空のハッシュを代入

```
    168:         detect_enum_conflict!(name, name.pluralize, true)
    169:         singleton_class.define_method(name.pluralize) { enum_values }
    170:         defined_enums[name] = enum_values
    171: 
    172:         detect_enum_conflict!(name, name)
 => 173:         detect_enum_conflict!(name, "#{name}=")
    174: 
    175:         attr = attribute_alias?(name) ? attribute_alias(name) : name
    176:         decorate_attribute_type(attr, :enum) do |subtype|
    177:           EnumType.new(attr, enum_values, subtype)
    178:         end
```

detect_enum_conflict!でnameがどこかで定義されていないか判定。

```
    170:         defined_enums[name] = enum_values
    171: 
    172:         detect_enum_conflict!(name, name)
    173:         detect_enum_conflict!(name, "#{name}=")
    174: 
 => 175:         attr = attribute_alias?(name) ? attribute_alias(name) : name
    176:         decorate_attribute_type(attr, :enum) do |subtype|
    177:           EnumType.new(attr, enum_values, subtype)
    178:         end
    179: 
    180:         _enum_methods_module.module_eval do

```

エイリアスが存在したら、それを返す。

```
    171: 
    172:         detect_enum_conflict!(name, name)
    173:         detect_enum_conflict!(name, "#{name}=")
    174: 
    175:         attr = attribute_alias?(name) ? attribute_alias(name) : name
 => 176:         decorate_attribute_type(attr, :enum) do |subtype|
    177:           EnumType.new(attr, enum_values, subtype)
    178:         end
    179: 
    180:         _enum_methods_module.module_eval do
    181:           pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
```

ブロックを引数としてdecorate_attribute_typeを実行。decorate_attribute_typeの中身を見ていく。

```
    23: def decorate_attribute_type(column_name, decorator_name, &block)
    24:   matcher = ->(name, _) { name == column_name.to_s }
 => 25:   key = "_#{column_name}_#{decorator_name}"
    26:   decorate_matching_attribute_types(matcher, key, &block)
    27: end
```

matcherにname==column_name.to_sを判定するようなブロックを代入。keyに文字列を代入。それぞれの値とブロックを引数としてdecorate_matching_attribute_typesを呼び出し。

```
    40: def decorate_matching_attribute_types(matcher, decorator_name, &block)
 => 41:   reload_schema_from_cache
    42:   decorator_name = decorator_name.to_s
    43: 
    44:   # Create new hashes so we don't modify parent classes
    45:   self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
    46: end
```

cacheからschemaをリロード。selfのattribute_type_decorationsにdecorator_nameがキーでmatcherとblockが値となるようなハッシュをmerge。ここで与えられているblockがEnumType.new(attr, enum_values, subtype)なので、ここでRailsのenumとしての機能が使えるような処理をしている。

```
    175:         attr = attribute_alias?(name) ? attribute_alias(name) : name
    176:         decorate_attribute_type(attr, :enum) do |subtype|
    177:           EnumType.new(attr, enum_values, subtype)
    178:         end
    179: 
 => 180:         _enum_methods_module.module_eval do
    181:           pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
    182:           pairs.each do |label, value|
    183:             if enum_prefix == true
    184:               prefix = "#{name}_"
    185:             elsif enum_prefix
```

_enum_methods_moduleに対してmodule_evalを呼ぶ。

```
    176:         decorate_attribute_type(attr, :enum) do |subtype|
    177:           EnumType.new(attr, enum_values, subtype)
    178:         end
    179: 
    180:         _enum_methods_module.module_eval do
 => 181:           pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
    182:           pairs.each do |label, value|
    183:             if enum_prefix == true
    184:               prefix = "#{name}_"
    185:             elsif enum_prefix
    186:               prefix = "#{enum_prefix}_"
```

valuesがeachに応答できるかどうかを判定して、trueならvalues.each_pair、falseならvalues.each_with_indexを返す。

```
    177:           EnumType.new(attr, enum_values, subtype)
    178:         end
    179: 
    180:         _enum_methods_module.module_eval do
    181:           pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
 => 182:           pairs.each do |label, value|
    183:             if enum_prefix == true
    184:               prefix = "#{name}_"
    185:             elsif enum_prefix
    186:               prefix = "#{enum_prefix}_"
    187:             end

```

ここから複数行は、prefixとsuffixに関しての内容。enum_prefixとenum_suffixを判定して、その判定によって、prefixとsuffixの内容を変更する。

```
    189:               suffix = "_#{name}"
    190:             elsif enum_suffix
    191:               suffix = "_#{enum_suffix}"
    192:             end
    193: 
 => 194:             value_method_name = "#{prefix}#{label}#{suffix}"
    195:             enum_values[label] = value
    196:             label = label.to_s
    197: 
    198:             # def active?() status == "active" end
    199:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
```

先程判定したprefixとsuffixを使ってlabelに接頭辞と接尾辞をつけたものをvalue_method_nameに代入。

```
    190:             elsif enum_suffix
    191:               suffix = "_#{enum_suffix}"
    192:             end
    193: 
    194:             value_method_name = "#{prefix}#{label}#{suffix}"
 => 195:             enum_values[label] = value
    196:             label = label.to_s
    197: 
    198:             # def active?() status == "active" end
    199:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
    200:             define_method("#{value_method_name}?") { self[attr] == label }
```

enum_valuesハッシュにキーであるlabelと値であるvalueを追加。

```
    191:               suffix = "_#{enum_suffix}"
    192:             end
    193: 
    194:             value_method_name = "#{prefix}#{label}#{suffix}"
    195:             enum_values[label] = value
 => 196:             label = label.to_s
    197: 
    198:             # def active?() status == "active" end
    199:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
    200:             define_method("#{value_method_name}?") { self[attr] == label }
    201: 

```

labelを文字列に変更。

```
    194:             value_method_name = "#{prefix}#{label}#{suffix}"
    195:             enum_values[label] = value
    196:             label = label.to_s
    197: 
    198:             # def active?() status == "active" end
 => 199:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
    200:             define_method("#{value_method_name}?") { self[attr] == label }
    201: 
    202:             # def active!() update!(status: 0) end
    203:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
    204:             define_method("#{value_method_name}!") { update!(attr => value) }
```

すでに#{value_method_name}?がメソッドとして定義されていないか判定。

```
    195:             enum_values[label] = value
    196:             label = label.to_s
    197: 
    198:             # def active?() status == "active" end
    199:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
 => 200:             define_method("#{value_method_name}?") { self[attr] == label }
    201: 
    202:             # def active!() update!(status: 0) end
    203:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
    204:             define_method("#{value_method_name}!") { update!(attr => value) }
    205: 

```

#{value_method_name}?というメソッドを定義して、self[attr] == labelという処理を行う。

```
    198:             # def active?() status == "active" end
    199:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
    200:             define_method("#{value_method_name}?") { self[attr] == label }
    201: 
    202:             # def active!() update!(status: 0) end
 => 203:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
    204:             define_method("#{value_method_name}!") { update!(attr => value) }
    205: 
    206:             # scope :active, -> { where(status: 0) }
    207:             # scope :not_active, -> { where.not(status: 0) }
    208:             if enum_scopes != false

```

すでに#{value_method_name}!がメソッドとして定義されていないか判定。

```
    199:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
    200:             define_method("#{value_method_name}?") { self[attr] == label }
    201: 
    202:             # def active!() update!(status: 0) end
    203:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
 => 204:             define_method("#{value_method_name}!") { update!(attr => value) }
    205: 
    206:             # scope :active, -> { where(status: 0) }
    207:             # scope :not_active, -> { where.not(status: 0) }
    208:             if enum_scopes != false
    209:               klass.send(:detect_negative_condition!, value_method_name)
```

#{value_method_name}!というメソッドを定義して、update!(attr => value)という処理を行う。

```
    203:             klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
    204:             define_method("#{value_method_name}!") { update!(attr => value) }
    205: 
    206:             # scope :active, -> { where(status: 0) }
    207:             # scope :not_active, -> { where.not(status: 0) }
 => 208:             if enum_scopes != false
    209:               klass.send(:detect_negative_condition!, value_method_name)
    210: 
    211:               klass.send(:detect_enum_conflict!, name, value_method_name, true)
    212:               klass.scope value_method_name, -> { where(attr => value) }
    213: 
```

ここで、enumで設定した値によって、scopeできるようにしている。enum_scopesがfalseかどうかを判定。

```
    204:             define_method("#{value_method_name}!") { update!(attr => value) }
    205: 
    206:             # scope :active, -> { where(status: 0) }
    207:             # scope :not_active, -> { where.not(status: 0) }
    208:             if enum_scopes != false
 => 209:               klass.send(:detect_negative_condition!, value_method_name)
    210: 
    211:               klass.send(:detect_enum_conflict!, name, value_method_name, true)
    212:               klass.scope value_method_name, -> { where(attr => value) }
    213: 
    214:               klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true)

```

notがついたvalue_method_nameメソッドが定義されていないか判定。

```
    206:             # scope :active, -> { where(status: 0) }
    207:             # scope :not_active, -> { where.not(status: 0) }
    208:             if enum_scopes != false
    209:               klass.send(:detect_negative_condition!, value_method_name)
    210: 
 => 211:               klass.send(:detect_enum_conflict!, name, value_method_name, true)
    212:               klass.scope value_method_name, -> { where(attr => value) }
    213: 
    214:               klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true)
    215:               klass.scope "not_#{value_method_name}", -> { where.not(attr => value) }
    216:             end

```

nameやvalue_method_nameがメソッドとして定義されていないか判定。

```
    207:             # scope :not_active, -> { where.not(status: 0) }
    208:             if enum_scopes != false
    209:               klass.send(:detect_negative_condition!, value_method_name)
    210: 
    211:               klass.send(:detect_enum_conflict!, name, value_method_name, true)
 => 212:               klass.scope value_method_name, -> { where(attr => value) }
    213: 
    214:               klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true)
    215:               klass.scope "not_#{value_method_name}", -> { where.not(attr => value) }
    216:             end
    217:           end

```

scopeとしてvalue_method_nameメソッドでwhere(attr => value)を定義する。

```
    209:               klass.send(:detect_negative_condition!, value_method_name)
    210: 
    211:               klass.send(:detect_enum_conflict!, name, value_method_name, true)
    212:               klass.scope value_method_name, -> { where(attr => value) }
    213: 
 => 214:               klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true)
    215:               klass.scope "not_#{value_method_name}", -> { where.not(attr => value) }
    216:             end
    217:           end
    218:         end
    219:         enum_values.freeze

```

not_#{value_method_name}が定義されているか判定

```
    210: 
    211:               klass.send(:detect_enum_conflict!, name, value_method_name, true)
    212:               klass.scope value_method_name, -> { where(attr => value) }
    213: 
    214:               klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true)
 => 215:               klass.scope "not_#{value_method_name}", -> { where.not(attr => value) }
    216:             end
    217:           end
    218:         end
    219:         enum_values.freeze
    220:       end
```

scopeとしてnot_#{value_method_name}メソッドを定義してwhere.not(attr => value)を行う。

これでenumのソースコードリーディング終了。