From 74cb05698684f237a7eb91afadec0020d8910c70 Mon Sep 17 00:00:00 2001 From: rick Date: Sat, 28 Jun 2008 09:19:44 -0700 Subject: add basic events and transitions. still more tests to convert --- activemodel/lib/active_model/state_machine.rb | 11 +++- .../lib/active_model/state_machine/event.rb | 67 ++++++++++++++++++++++ .../lib/active_model/state_machine/machine.rb | 44 ++++++++++---- .../lib/active_model/state_machine/state.rb | 18 ++++-- .../active_model/state_machine/state_transition.rb | 40 +++++++++++++ 5 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 activemodel/lib/active_model/state_machine/event.rb create mode 100644 activemodel/lib/active_model/state_machine/state_transition.rb (limited to 'activemodel/lib') diff --git a/activemodel/lib/active_model/state_machine.rb b/activemodel/lib/active_model/state_machine.rb index bb038f6b7a..2a5ac95a3e 100644 --- a/activemodel/lib/active_model/state_machine.rb +++ b/activemodel/lib/active_model/state_machine.rb @@ -5,6 +5,9 @@ end module ActiveModel module StateMachine + class InvalidTransition < Exception + end + def self.included(base) base.extend ClassMethods end @@ -34,10 +37,14 @@ module ActiveModel end end - def current_state(name = nil) + def current_state(name = nil, new_state = nil) sm = self.class.state_machine(name) ivar = "@#{sm.name}_current_state" - instance_variable_get(ivar) || instance_variable_set(ivar, sm.initial_state) + if name && new_state + instance_variable_set(ivar, new_state) + else + instance_variable_get(ivar) || instance_variable_set(ivar, sm.initial_state) + end end end end \ No newline at end of file diff --git a/activemodel/lib/active_model/state_machine/event.rb b/activemodel/lib/active_model/state_machine/event.rb new file mode 100644 index 0000000000..cc7d563214 --- /dev/null +++ b/activemodel/lib/active_model/state_machine/event.rb @@ -0,0 +1,67 @@ +module ActiveModel + module StateMachine + class Event + attr_reader :name, :success + + def initialize(name, options = {}, &block) + @name, @transitions = name, [] + machine = options.delete(:machine) + if machine + machine.klass.send(:define_method, "#{name.to_s}!") do |*args| + machine.fire_event(name, self, true, *args) + end + + machine.klass.send(:define_method, "#{name.to_s}") do |*args| + machine.fire_event(name, self, false, *args) + end + end + update(options, &block) + end + + def fire(obj, to_state = nil, *args) + transitions = @transitions.select { |t| t.from == obj.current_state } + raise InvalidTransition if transitions.size == 0 + + next_state = nil + transitions.each do |transition| + next if to_state && !Array(transition.to).include?(to_state) + if transition.perform(obj) + next_state = to_state || Array(transition.to).first + transition.execute(obj, *args) + break + end + end + next_state + end + + def transitions_from_state?(state) + @transitions.any? { |t| t.from? state } + end + + def success? + !!@success + end + + def ==(event) + if event.is_a? Symbol + name == event + else + name == event.name + end + end + + def update(options = {}, &block) + if options.key?(:success) then @success = options[:success] end + if block then instance_eval(&block) end + self + end + + 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 diff --git a/activemodel/lib/active_model/state_machine/machine.rb b/activemodel/lib/active_model/state_machine/machine.rb index 75ed8f8b65..1da48290c5 100644 --- a/activemodel/lib/active_model/state_machine/machine.rb +++ b/activemodel/lib/active_model/state_machine/machine.rb @@ -1,29 +1,49 @@ module ActiveModel module StateMachine class Machine - attr_accessor :initial_state, :states, :event + attr_accessor :initial_state, :states, :events, :state_index attr_reader :klass, :name - def initialize(klass, name) - @klass, @name, @states, @events = klass, name, [], {} + def initialize(klass, name, options = {}, &block) + @klass, @name, @states, @state_index, @events = klass, name, [], {}, {} + update(options, &block) + end + + def initial_state + @initial_state ||= (states.first ? states.first.name : nil) + end + + def update(options = {}, &block) + if options.key?(:initial) then @initial_state = options[:initial] end + if block then instance_eval(&block) end + self + end + + def fire_event(name, record, persist, *args) + state_index[record.current_state].call_action(:exit, record) + if new_state = @events[name].fire(record, *args) + state_index[new_state].call_action(:enter, record) + record.current_state(@name, new_state) + else + false + end end def states_for_select states.map { |st| [st.display_name, st.name.to_s] } end - def state(name, options = {}) - @states << State.new(self, name, options) + def events_for(state) + events = @events.values.select { |event| event.transitions_from_state?(state) } + events.map! { |event| event.name } end - - def initial_state - @initial_state ||= (states.first ? states.first.name : nil) + private + def state(name, options = {}) + @states << (state_index[name] ||= State.new(name, :machine => self)).update(options) end - def update(options = {}, &block) - @initial_state = options[:initial] - instance_eval(&block) - self + def event(name, options = {}, &block) + (@events[name] ||= Event.new(name, :machine => self)).update(options, &block) end end end diff --git a/activemodel/lib/active_model/state_machine/state.rb b/activemodel/lib/active_model/state_machine/state.rb index 5851a6fb79..68eb2aa34a 100644 --- a/activemodel/lib/active_model/state_machine/state.rb +++ b/activemodel/lib/active_model/state_machine/state.rb @@ -3,11 +3,15 @@ module ActiveModel class State attr_reader :name, :options - def initialize(machine, name, options={}) - @machine, @name, @options, @display_name = machine, name, options, options.delete(:display) - machine.klass.send(:define_method, "#{name}?") do - current_state.to_s == name.to_s + def initialize(name, options = {}) + @name = name + machine = options.delete(:machine) + if machine + machine.klass.send(:define_method, "#{name}?") do + current_state.to_s == name.to_s + end end + update(options) end def ==(state) @@ -35,6 +39,12 @@ module ActiveModel def for_select [display_name, name.to_s] end + + def update(options = {}) + if options.key?(:display) then @display_name = options.delete(:display) end + @options = options + self + 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 new file mode 100644 index 0000000000..f9df998ea4 --- /dev/null +++ b/activemodel/lib/active_model/state_machine/state_transition.rb @@ -0,0 +1,40 @@ +module ActiveModel + module StateMachine + class StateTransition + attr_reader :from, :to, :options + + def initialize(opts) + @from, @to, @guard, @on_transition = opts[:from], opts[:to], opts[:guard], opts[:on_transition] + @options = opts + end + + def perform(obj) + case @guard + when Symbol, String + obj.send(@guard) + when Proc + @guard.call(obj) + else + true + end + end + + def execute(obj, *args) + case @on_transition + when Symbol, String + obj.send(@on_transition, *args) + when Proc + @on_transition.call(obj, *args) + end + end + + def ==(obj) + @from == obj.from && @to == obj.to + end + + def from?(value) + @from == value + end + end + end +end -- cgit v1.2.3