From aad5a30bf25d8a3167afd685fc91c99f4f09cc57 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 4 Aug 2009 11:03:57 -0500 Subject: [PATCH] Add simple support for ActiveModel's StateMachine for ActiveRecord --- activemodel/lib/active_model/state_machine.rb | 7 +--- .../lib/active_model/state_machine/event.rb | 12 +++--- .../lib/active_model/state_machine/machine.rb | 29 ++++++------- .../state_machine/state_transition.rb | 2 +- activerecord/lib/active_record.rb | 1 + .../lib/active_record/state_machine.rb | 24 +++++++++++ activerecord/test/cases/state_machine_test.rb | 42 +++++++++++++++++++ activerecord/test/models/traffic_light.rb | 27 ++++++++++++ activerecord/test/schema/schema.rb | 7 ++++ 9 files changed, 122 insertions(+), 29 deletions(-) create mode 100644 activerecord/lib/active_record/state_machine.rb create mode 100644 activerecord/test/cases/state_machine_test.rb create mode 100644 activerecord/test/models/traffic_light.rb diff --git a/activemodel/lib/active_model/state_machine.rb b/activemodel/lib/active_model/state_machine.rb index 1172d31ea35e7..527794b34da10 100644 --- a/activemodel/lib/active_model/state_machine.rb +++ b/activemodel/lib/active_model/state_machine.rb @@ -5,12 +5,9 @@ module StateMachine autoload :State, 'active_model/state_machine/state' autoload :StateTransition, 'active_model/state_machine/state_transition' - class InvalidTransition < Exception - end + extend ActiveSupport::Concern - def self.included(base) - require 'active_model/state_machine/machine' - base.extend ClassMethods + class InvalidTransition < Exception end module ClassMethods diff --git a/activemodel/lib/active_model/state_machine/event.rb b/activemodel/lib/active_model/state_machine/event.rb index 3eb656b6d6c59..30e9601dc2125 100644 --- a/activemodel/lib/active_model/state_machine/event.rb +++ b/activemodel/lib/active_model/state_machine/event.rb @@ -1,5 +1,3 @@ -require 'active_model/state_machine/state_transition' - module ActiveModel module StateMachine class Event @@ -53,12 +51,12 @@ def update(options = {}, &block) self end - private - def transitions(trans_opts) - Array(trans_opts[:from]).each do |s| - @transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym})) + private + def transitions(trans_opts) + Array(trans_opts[:from]).each do |s| + @transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym})) + end end - end end end end diff --git a/activemodel/lib/active_model/state_machine/machine.rb b/activemodel/lib/active_model/state_machine/machine.rb index a5ede021b1f16..777531213e8ab 100644 --- a/activemodel/lib/active_model/state_machine/machine.rb +++ b/activemodel/lib/active_model/state_machine/machine.rb @@ -1,6 +1,3 @@ -require 'active_model/state_machine/state' -require 'active_model/state_machine/event' - module ActiveModel module StateMachine class Machine @@ -57,22 +54,22 @@ def current_state_variable "@#{@name}_current_state" end - private - def state(name, options = {}) - @states << (state_index[name] ||= State.new(name, :machine => self)).update(options) - end + private + def state(name, options = {}) + @states << (state_index[name] ||= State.new(name, :machine => self)).update(options) + end - def event(name, options = {}, &block) - (@events[name] ||= Event.new(self, name)).update(options, &block) - end + def event(name, options = {}, &block) + (@events[name] ||= Event.new(self, name)).update(options, &block) + end - def event_fired_callback - @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired' - end + def event_fired_callback + @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired' + end - def event_failed_callback - @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed' - end + def event_failed_callback + @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed' + end end end end diff --git a/activemodel/lib/active_model/state_machine/state_transition.rb b/activemodel/lib/active_model/state_machine/state_transition.rb index f9df998ea4ce0..b0c5504de786d 100644 --- a/activemodel/lib/active_model/state_machine/state_transition.rb +++ b/activemodel/lib/active_model/state_machine/state_transition.rb @@ -18,7 +18,7 @@ def perform(obj) true end end - + def execute(obj, *args) case @on_transition when Symbol, String diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 5e9ce0600a099..68b025198234d 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -65,6 +65,7 @@ def self.load_all! autoload :SchemaDumper, 'active_record/schema_dumper' autoload :Serialization, 'active_record/serialization' autoload :SessionStore, 'active_record/session_store' + autoload :StateMachine, 'active_record/state_machine' autoload :TestCase, 'active_record/test_case' autoload :Timestamp, 'active_record/timestamp' autoload :Transactions, 'active_record/transactions' diff --git a/activerecord/lib/active_record/state_machine.rb b/activerecord/lib/active_record/state_machine.rb new file mode 100644 index 0000000000000..aebd03344aa1b --- /dev/null +++ b/activerecord/lib/active_record/state_machine.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module StateMachine #:nodoc: + extend ActiveSupport::Concern + include ActiveModel::StateMachine + + included do + before_validation :set_initial_state + validates_presence_of :state + end + + protected + def write_state(state_machine, state) + update_attributes! :state => state.to_s + end + + def read_state(state_machine) + self.state.to_sym + end + + def set_initial_state + self.state ||= self.class.state_machine.initial_state.to_s + end + end +end diff --git a/activerecord/test/cases/state_machine_test.rb b/activerecord/test/cases/state_machine_test.rb new file mode 100644 index 0000000000000..5d13668babd81 --- /dev/null +++ b/activerecord/test/cases/state_machine_test.rb @@ -0,0 +1,42 @@ +require 'cases/helper' +require 'models/traffic_light' + +class StateMachineTest < ActiveRecord::TestCase + def setup + @light = TrafficLight.create! + end + + test "states initial state" do + assert @light.off? + assert_equal :off, @light.current_state + end + + test "transition to a valid state" do + @light.reset + assert @light.red? + assert_equal :red, @light.current_state + + @light.green_on + assert @light.green? + assert_equal :green, @light.current_state + end + + test "transition does not persist state" do + @light.reset + assert_equal :red, @light.current_state + @light.reload + assert_equal "off", @light.state + end + + test "transition does persists state" do + @light.reset! + assert_equal :red, @light.current_state + @light.reload + assert_equal "red", @light.state + end + + test "transition to an invalid state" do + assert_raise(ActiveModel::StateMachine::InvalidTransition) { @light.yellow_on } + assert_equal :off, @light.current_state + end +end diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb new file mode 100644 index 0000000000000..f8cfddbef9209 --- /dev/null +++ b/activerecord/test/models/traffic_light.rb @@ -0,0 +1,27 @@ +class TrafficLight < ActiveRecord::Base + include ActiveRecord::StateMachine + + state_machine do + state :off + + state :red + state :green + state :yellow + + event :red_on do + transitions :to => :red, :from => [:yellow] + end + + event :green_on do + transitions :to => :green, :from => [:red] + end + + event :yellow_on do + transitions :to => :yellow, :from => [:green] + end + + event :reset do + transitions :to => :red, :from => [:off] + end + end +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 2b7d3856b7b1e..1e47cdbaf6493 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -448,6 +448,13 @@ def create_table(*args, &block) t.integer :pet_id, :integer end + create_table :traffic_lights, :force => true do |t| + t.string :location + t.string :state + t.datetime :created_at + t.datetime :updated_at + end + create_table :treasures, :force => true do |t| t.column :name, :string t.column :looter_id, :integer