Deep clone state machine during inheritance (with tests) (#429)

* Deep clone state machine during inheritance.
Allows child classes to extend the parent state machine without modifying the parents state machine.

* Add tests

* Also need to clone global callbacks to support inheritance

* Added tests to ensure parent is not changed in inheritance
This commit is contained in:
Tyler Hogan 2017-03-07 22:58:27 -08:00 committed by Anil Kumar Maurya
parent e1adfcd126
commit 77cd065eeb
7 changed files with 118 additions and 6 deletions

View File

@ -27,6 +27,16 @@ module AASM::Core
], &block) if block
end
# called internally by Ruby 1.9 after clone()
def initialize_copy(orig)
super
@transitions = @transitions.collect { |transition| transition.clone }
@guards = @guards.dup
@unless = @unless.dup
@options = {}
orig.options.each_pair { |name, setting| @options[name] = setting.is_a?(Hash) || setting.is_a?(Array) ? setting.dup : setting }
end
# a neutered version of fire - it doesn't actually fire the event, it just
# executes the transition guards to determine if a transition is even
# an option given current conditions.

View File

@ -9,6 +9,13 @@ module AASM::Core
update(options)
end
# called internally by Ruby 1.9 after clone()
def initialize_copy(orig)
super
@options = {}
orig.options.each_pair { |name, setting| @options[name] = setting.is_a?(Hash) || setting.is_a?(Array) ? setting.dup : setting }
end
def ==(state)
if state.is_a? Symbol
name == state

View File

@ -28,6 +28,15 @@ module AASM::Core
@opts = opts
end
# called internally by Ruby 1.9 after clone()
def initialize_copy(orig)
super
@guards = @guards.dup
@unless = @unless.dup
@opts = {}
orig.opts.each_pair { |name, setting| @opts[name] = setting.is_a?(Hash) || setting.is_a?(Array) ? setting.dup : setting }
end
def allowed?(obj, *args)
invoke_callbacks_compatible_with_guard(@guards, obj, args, :guard => true) &&
invoke_callbacks_compatible_with_guard(@unless, obj, args, :unless => true)

View File

@ -16,8 +16,10 @@ module AASM
# called internally by Ruby 1.9 after clone()
def initialize_copy(orig)
super
@states = @states.dup
@events = @events.dup
@states = orig.states.collect { |state| state.clone }
@events = {}
orig.events.each_pair { |name, event| @events[name] = event.clone }
@global_callbacks = @global_callbacks.dup
end
def add_state(state_name, klass, options)

View File

@ -1,7 +1,41 @@
require_relative 'super_class'
class SubClass < SuperClass
# Add an after callback that is not defined in the parent
aasm.state_machine.events[:foo].options[:after] = [:after_foo_event]
# Add global callback that is not defined in the parent
aasm.state_machine.global_callbacks[:after_all_transitions] = :after_all_event
attr_accessor :called_after
def after_foo_event
self.called_after = true
end
def after_all_event; end
end
class SubClassMultiple < SuperClassMultiple
# Add after callbacks that are not defined in the parent
aasm(:left).state_machine.events[:foo].options[:after] = [:left_after_foo_event]
aasm(:right).state_machine.events[:close].options[:after] = [:right_after_close_event]
# Add global callback that is not defined in the parent
aasm(:left).state_machine.global_callbacks[:after_all_transitions] = :left_after_all_event
aasm(:right).state_machine.global_callbacks[:after_all_transitions] = :right_after_all_event
attr_accessor :left_called_after, :right_called_after
def left_after_foo_event
self.left_called_after = true
end
def right_after_close_event
self.right_called_after = true
end
def left_after_all_event; end
def right_after_all_event; end
end

View File

@ -19,7 +19,7 @@ describe 'subclassing with multiple state machines' do
expect(SuperClassMultiple.aasm(:right).states).not_to include(:archived)
end
it "should have the same events as its parent" do
it 'should have the same events as its parent' do
expect(SubClassMultiple.aasm(:left).events).to eq(SuperClassMultiple.aasm(:left).events)
expect(SubClassMultiple.aasm(:right).events).to eq(SuperClassMultiple.aasm(:right).events)
end
@ -35,5 +35,40 @@ describe 'subclassing with multiple state machines' do
expect(son.aasm(:left).current_state).to eq(:ended)
end
end
it 'should allow the child to modify its left state machine' do
son = SubClassMultiple.new
expect(son.left_called_after).to eq(nil)
expect(son.right_called_after).to eq(nil)
son.foo
expect(son.left_called_after).to eq(true)
expect(son.right_called_after).to eq(nil)
global_callbacks = SubClassMultiple.aasm(:left).state_machine.global_callbacks
expect(global_callbacks).to_not be_empty
expect(global_callbacks[:after_all_transitions]).to eq :left_after_all_event
end
it 'should allow the child to modify its right state machine' do
son = SubClassMultiple.new
expect(son.right_called_after).to eq(nil)
expect(son.left_called_after).to eq(nil)
son.close
expect(son.right_called_after).to eq(true)
expect(son.left_called_after).to eq(nil)
global_callbacks = SubClassMultiple.aasm(:right).state_machine.global_callbacks
expect(global_callbacks).to_not be_empty
expect(global_callbacks[:after_all_transitions]).to eq :right_after_all_event
end
it 'should not modify the parent left state machine' do
super_class_event = SuperClassMultiple.aasm(:left).events.select { |event| event.name == :foo }.first
expect(super_class_event.options).to be_empty
expect(SuperClassMultiple.aasm(:left).state_machine.global_callbacks).to be_empty
end
it 'should not modify the parent right state machine' do
super_class_event = SuperClassMultiple.aasm(:right).events.select { |event| event.name == :close }.first
expect(super_class_event.options).to be_empty
expect(SuperClassMultiple.aasm(:right).state_machine.global_callbacks).to be_empty
end
end

View File

@ -13,7 +13,7 @@ describe 'subclassing' do
expect(SuperClass.aasm.states).not_to include(:foo)
end
it "should have the same events as its parent" do
it 'should have the same events as its parent' do
expect(SubClass.aasm.events).to eq(SuperClass.aasm.events)
end
@ -27,5 +27,20 @@ describe 'subclassing' do
expect(son.aasm.current_state).to eq(:ended)
end
end
it 'should allow the child to modify its state machine' do
son = SubClass.new
expect(son.called_after).to eq(nil)
son.foo
expect(son.called_after).to eq(true)
global_callbacks = SubClass.aasm.state_machine.global_callbacks
expect(global_callbacks).to_not be_empty
expect(global_callbacks[:after_all_transitions]).to eq :after_all_event
end
it 'should not modify the parent state machine' do
super_class_event = SuperClass.aasm.events.select { |event| event.name == :foo }.first
expect(super_class_event.options).to be_empty
expect(SuperClass.aasm.state_machine.global_callbacks).to be_empty
end
end