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