3
\$\begingroup\$

This is my initial class. (I am not trying to reinvent state-flows again, just practicing) As you can see straight forward, it dynamically generates methods like .recommend,.recommend!, .recommendation_sent?

class Project < ActiveRecord::Base

  STATUS_TRANSITIONS =
  {
    initial: :not_started,
    not_started: { value: "On the board" },
    recommendation_sent: { value: "Recs: Recommendations Sent", event: :recommend},
    restarted: { event: :restart, to: :not_started }
  }

  after_initialize :default_status

  def default_status
    self.status ||= STATUS_TRANSITIONS[STATUS_TRANSITIONS[:initial]][:value]
  end

  STATUS_TRANSITIONS.except(:initial).each do |k,v|
    value = v[:to] ?  STATUS_TRANSITIONS[v[:to]][:value] : v[:value]
    if value.present?
      define_method "#{k}?" do
        self.status == value
      end
    end
    if v[:event].present?
      define_method v[:event] do
        self.status = value
      end
      define_method "#{v[:event]}!" do
        self.send(v[:event])
        self.save
      end
    end
  end

  def status_class
    if self.not_started?
      "label-info"
    elsif self.recommendation_sent?
      "label-warning"
    else
      "label-primary"
    end
  end

end

Now, I would like to clean up my model and extract behavior to a separate module and would like to present a pleasant dsl like this:

class Project < ActiveRecord::Base

  include StatusMachine

  status_machine initial: :not_started do

    transition :not_started, value: "On the Desk", css: "label-info"

    transition :recommendation_sent, value: "Recs: Recommendations Sent", event: "recommend", color: "label-success"

    transition :restarted, to: :not_started, event: "restart"

  end

 end

My solution looks like this:

module StatusMachine

  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods

    @@states = {}
    @@initial = nil

    def transition t, opts
      if opts[:to]
        to = @@states[opts[:to]]
        value = to[:value]
        css = to[:css]
        state = { value: value, event: opts[:event], to: opts[:to], css: css }
      else
        state = { value: opts[:value], event: opts[:event], to: opts[:to], css: opts[:css] }
      end
      @@states[t] = state
      value = state[:value]

      self.class_eval do
        if value.present?
          define_method "#{t}?" do
            self.status == value
          end
        end
        if opts[:event].present?
          define_method opts[:event] do
            self.status = value
          end
          define_method "#{opts[:event]}!" do
            self.send(opts[:event])
            self.save
          end
        end
      end
    end

    def status_machine opts
      yield
      @@initial = @@states[opts[:initial]][:value]
      self.class_eval do
        after_initialize :default_status
        def default_status
          self.status ||= @@initial
        end

        def status_css
          @@states.values.find{|e| e[:value] == self.status }[:css]
        end
      end
    end
  end
end

Being quite new to meta world, I am not sure if this is the way to do such stuffs so I would like to see comments on my implementation. And also the "transition" method is ugly. I tried to refactor that, extracting to a separate method, but I don't want to add another class method to expose. And if I do private class method, things will be way more complicated. How can I approach this?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.