ActiveRecord::Enumを使って状態遷移するやつを自作した話

RailsにはAASMというモリモリした状態遷移ライブラリがあるみたいなんだけど、
ActiveRecord::Enum(4.1から)と一部被ってるし多機能すぎる印象があったので作ってみた。

自分の欲しかった機能

  1. ある状態の時に遷移する時は、特定の状態からでしか遷移できないという条件をつけたい
  2. かつ、状態名を渡すと遷移できるようにしたい

環境

rails4.1.4
ActiveRecord::Enumをすごく使ってる

使い方

要は、#state_update("#{state_name}")と#can_#{state_name}?が使えるようになる。

モデルにActiveRecord::Enum の宣言をする。
この例だと請求書ステータスモデルのstatusカラムをEnumに使うと書いてる。

class InvoiceStatus < ActiveRecord::Base
  enum status: [
    :status_published,
    :status_ng,
    :status_checked,
    :status_wating_send
  ]

  # 〜後述するクラスマクロのコードがここにある〜
  # 〜後述するクラスマクロのコードがここにある〜
  # 〜後述するクラスマクロのコードがここにある〜

  # 今回自作したクラスマクロの宣言例
  # InvoiceStatus#update_status(:status_published)を実行したらこのブロックが実行される
  ai_status :status_published, from: [:status_ng] do |record| # can_status_published? が生える. :status_ngの時にいるならブロックが実行される。
    // 何かする
    record.status_published! # AR#Enumのメソッド
    // 何かする
  end

  ai_status :status_ng, from: [:status_published] do |record|
    // 何かする
    record.status_ng!
    // 何かする
  end

  ai_status :status_checked, from: [:status_published] do |record|
    // 何かする
    record.status_checked!
    // 何かする
  end
end
ステータスを変更する時
@invoice_status = InvoiceStatus.take

# 定義通り状態遷移できるケース
@invoice_status.status # => :status_ng
@invoice_status.update_status(:status_published) # => true
@invoice_status.status  # => :status_published

@invoice_status.status # => :status_published
@invoice_status.update_status(:status_ng) # => true
@invoice_status.status #  => :status_ng

# 定義に書いていない遷移をしようとしたケース
@invoice_status.status # => :status_checked
@invoice_status.update_status(:status_ng) # => 例外を投げるので どっかでキャッチすればいいと思う
@invoice_status.status # => :status_checked
viewでボタンの表示非表示をする時とかに使いそう
@invoice_status = InvoiceStatus.take

@invoice_status.status # => :status_ng
@invoice_status.can_status_published? # => true
@invoice_status.can_status_checked?   # => false

link_to_if(@invoice_status.can_status_published?, "ボタンは表示されます", "/update_state?state=status_published")

ソース

  def self.ai_status(status_name, from: , &block)
    define_method "can_#{status_name}?" do # can_hoge?が定義される
      from.map(&:to_s).include?(self.status) ? true : false
    end
    class_variable_set("@@_block_#{status_name}", block)
  end

  def update_status(status_name)
    raise('想定していな遷移だ') unless send("can_#{status_name}?")
    self.class.class_variable_get("@@_block_#{status_name}").call(self)
  end

所感

状態遷移の条件に一覧性ないけどDRYだしいいんじゃないの。
コールバック用のブロックとかないしシンプル。