LoginSignup
0
0

More than 3 years have passed since last update.

Railsのenumを読む

Posted at

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

enumに関して


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のソースコードリーディング終了。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0