前回の記事からの続きです。
前回はActiveModel内にenumの処理を書いていましたが、これだとenumを追加する毎に処理を書き足さなければいけないため非常に取り回しにくいです。
なので、今回はこれをModuleに切り出してみました。
やったこと
やったこととしては、enumの処理を参考に新たにModuleを作成してみた次第です。
想定としては以下のコードで動くようにします。
class Hoge
include ActiveModel::Model
extend TablelessEnum
enum status: { ok: true, ng: false }
end
Moduleへの切り出し
enumの処理をパクった結果以下のようになりました。
module TablelessEnum
extend ActiveSupport::Concern
def enum(definitions)
klass = self
definitions.each do |name, values|
# statuses = { }
enum_values = ActiveSupport::HashWithIndifferentAccess.new
name = name.to_sym
# def self.statuses statuses end
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
# def status=(value) self[:status] = statuses[value] end
define_method("#{name}=") { |value|
if enum_values.has_key?(value)
eval("@#{name} = #{enum_values[value]}")
elsif enum_values.has_value?(value)
# self[name] = value
eval("@#{name} = #{value}")
else
raise ArgumentError, "'#{value}' is not a valid #{name}"
end
}
# def status() statuses.key self[:status] end
define_method(name) { enum_values.key eval("@#{name}") }
# def status_before_type_cast() statuses.key self[:status] end
define_method("#{name}_before_type_cast") { enum_values.key eval("@#{name}") }
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
pairs.each do |value, i|
enum_values[value] = i
# def active?() status == 0 end
define_method("#{value}?") { eval("@#{name}") == i }
# def active!() update! status: :active end
define_method("#{value}!") { eval("@#{name} = #{i}") }
end
end
end
end
解説
def enum の中身を解説していきます。
klass = self
klass に self を入れていますが、何故に klass という名前を用いているのだろうと疑問に思いました。
調べてみると class は予約語のため、代わりに klass を用いるのが慣例のようです。
クラスオブジェクトのインスタンスを入れる時などに用いるようですね。(パーフェクトRubyの6-1-4でも klass が用いられています)
definitions.each do |name, values|
enumの引数をeachでループさせます。
今回の場合、name には status が、 values には { ok: true, ng: false } が入ります。
# statuses = { }
enum_values = ActiveSupport::HashWithIndifferentAccess.new
name = name.to_sym
# def self.statuses statuses end
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
これは Hoge.statuses についての処理になります。
enum_values はハッシュを生成しており(HashWithIndifferentAccessについてはこちらを参照)、 name は status をシンボルにキャストしています。
そして、 klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } でklassに statuses という特異メソッドを追加します。
現在空の enum_values ですが、後のコードで中身を追加していきます。
さて今度はインスタンスメソッドを定義していきます。
# def status=(value) self[:status] = statuses[value] end
define_method("#{name}=") { |value|
if enum_values.has_key?(value)
eval("@#{name} = #{enum_values[value]}")
elsif enum_values.has_value?(value)
# self[name] = value
eval("@#{name} = #{value}")
else
raise ArgumentError, "'#{value}' is not a valid #{name}"
end
}
これは hoge.status = true などの代入処理を書いています。
内容としてはenum内のキーに代入する値があればそのキーに対応する値を代入し、代入する値が直接値としてenumにあればそのまま代入する。
どれにも合致しない場合はraiseする、といった感じです。
次は hoge.status とした時の処理です。
# def status() statuses.key self[:status] end
define_method(name) { enum_values.key eval("@#{name}") }
単純に enum_values の値に対応するキーを返しています。
今度は同じような処理内容で、型変換前の値を取得する時に使う _before_type_cast() の処理を書いてます。
# def status_before_type_cast() statuses.key self[:status] end
define_method("#{name}_before_type_cast") { enum_values.key eval("@#{name}") }
最後に、 hoge.ok? や hoge.ng! といった処理を追加していきます。
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
pairs.each do |value, i|
enum_values[value] = i
# def active?() status == 0 end
define_method("#{value}?") { eval("@#{name}") == i }
# def active!() update! status: :active end
define_method("#{value}!") { eval("@#{name} = #{i}") }
end
まずは values がハッシュかどうか判定をして、ハッシュの場合は each_pair 、配列の場合は each_with_index を values をレシーバにして pairs 変数に入れています。
※正直ここで values.each_pair を pairs に入れている意味が良く分からないのですが誰か教えて頂ければ・・・
その後、 pairs をeachでループさせます。
ハッシュの場合、 value にはシンボルが i には値が入ります。
そして、それぞれで ? と ! の処理を追加しています。この処理は難しいものでもないので説明は割愛で。
という感じの全体の処理になりました。
もっとこうした方がスマートだよ!とかツッコミ頂けるとありがたいです!