生涯未熟

生涯未熟

プログラミングをちょこちょこと。

テーブルと紐付かないモデルでenumを実装してみた Part.2

前回の記事からの続きです。

syossan.hateblo.jp

前回は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

klassself を入れていますが、何故に 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についてはこちらを参照)、 namestatus をシンボルにキャストしています。

そして、 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_indexvalues をレシーバにして pairs 変数に入れています。

※正直ここで values.each_pairpairs に入れている意味が良く分からないのですが誰か教えて頂ければ・・・

その後、 pairs をeachでループさせます。

ハッシュの場合、 value にはシンボルが i には値が入ります。

そして、それぞれで ?! の処理を追加しています。この処理は難しいものでもないので説明は割愛で。

という感じの全体の処理になりました。

もっとこうした方がスマートだよ!とかツッコミ頂けるとありがたいです!