前回の記事からの続きです。
前回は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
には値が入ります。
そして、それぞれで ?
と !
の処理を追加しています。この処理は難しいものでもないので説明は割愛で。
という感じの全体の処理になりました。
もっとこうした方がスマートだよ!とかツッコミ頂けるとありがたいです!