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?