From eca394bb9bff36426d0a5e238674d878ef2c4351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 5 May 2015 22:16:25 +1200 Subject: [PATCH 01/45] using default name (all tests green) --- lib/aasm/aasm.rb | 81 +++++++++++-------- lib/aasm/base.rb | 44 ++++++---- lib/aasm/instance_base.rb | 25 +++--- .../persistence/active_record_persistence.rb | 66 ++++++++------- lib/aasm/persistence/base.rb | 14 ++-- .../persistence/mongo_mapper_persistence.rb | 30 ++++--- lib/aasm/persistence/mongoid_persistence.rb | 28 ++++--- lib/aasm/persistence/plain_persistence.rb | 13 +-- lib/aasm/persistence/sequel_persistence.rb | 22 ++--- lib/aasm/state_machine.rb | 2 +- .../provided_and_persisted_state.rb | 6 +- spec/models/parametrised_event.rb | 2 +- spec/models/provided_state.rb | 6 +- .../active_record_persistence_spec.rb | 8 +- 14 files changed, 193 insertions(+), 154 deletions(-) diff --git a/lib/aasm/aasm.rb b/lib/aasm/aasm.rb index 790af8d..d2d2af4 100644 --- a/lib/aasm/aasm.rb +++ b/lib/aasm/aasm.rb @@ -8,36 +8,47 @@ module AASM # do not overwrite existing state machines, which could have been created by # inheritance, see class method inherited - AASM::StateMachine[base] ||= AASM::StateMachine.new + AASM::StateMachine[base] ||= {} AASM::Persistence.load_persistence(base) super end module ClassMethods - # make sure inheritance (aka subclassing) works with AASM def inherited(base) - AASM::StateMachine[base] = AASM::StateMachine[self].clone + AASM::StateMachine[base] = {} + AASM::StateMachine[self].keys.each do |state_machine_name| + AASM::StateMachine[base][state_machine_name] = AASM::StateMachine[self][state_machine_name].clone + end super end # this is the entry point for all state and event definitions - def aasm(options={}, &block) - @aasm ||= AASM::Base.new(self, options) - @aasm.instance_eval(&block) if block # new DSL - @aasm - end + def aasm(*args, &block) + if args[0].is_a?(Symbol) || args[0].is_a?(String) + # using custom name + name = args[0].to_sym + options = args[1] || {} + else + # using the default name + name = :default + options = args[0] || {} + end - # deprecated, remove in version 4.1 - def aasm_human_event_name(event) # event_name? - warn '[DEPRECATION] AASM: aasm_human_event_name is deprecated, use aasm.human_event_name instead' - aasm.human_event_name(event) + AASM::StateMachine[self][name] ||= AASM::StateMachine.new + + @aasm ||= {} + @aasm[name] ||= AASM::Base.new(self, name, AASM::StateMachine[self][name], options) + @aasm[name].instance_eval(&block) if block # new DSL + @aasm[name] end end # ClassMethods - def aasm - @aasm ||= AASM::InstanceBase.new(self) + # this is the entry point for all instance-level access to AASM + def aasm(name=:default) + @aasm ||= {} + @aasm[name.to_sym] ||= AASM::InstanceBase.new(self, name.to_sym) end private @@ -56,10 +67,10 @@ private return args end - def aasm_fire_event(event_name, options, *args, &block) - event = self.class.aasm.state_machine.events[event_name] + def aasm_fire_event(state_machine_name, event_name, options, *args, &block) + event = self.class.aasm(state_machine_name).state_machine.events[event_name] begin - old_state = aasm.state_object_for_name(aasm.current_state) + old_state = aasm(state_machine_name).state_object_for_name(aasm(state_machine_name).current_state) # new event before callback event.fire_callbacks( @@ -70,58 +81,58 @@ private if may_fire_to = event.may_fire?(self, *args) old_state.fire_callbacks(:before_exit, self, - *process_args(event, aasm.current_state, *args)) + *process_args(event, aasm(state_machine_name).current_state, *args)) old_state.fire_callbacks(:exit, self, - *process_args(event, aasm.current_state, *args)) # TODO: remove for AASM 4? + *process_args(event, aasm(state_machine_name).current_state, *args)) if new_state_name = event.fire(self, {:may_fire => may_fire_to}, *args) - aasm_fired(event, old_state, new_state_name, options, *args, &block) + aasm_fired(state_machine_name, event, old_state, new_state_name, options, *args, &block) else - aasm_failed(event_name, old_state) + aasm_failed(state_machine_name, event_name, old_state) end else - aasm_failed(event_name, old_state) + aasm_failed(state_machine_name, event_name, old_state) end rescue StandardError => e - event.fire_callbacks(:error, self, e, *process_args(event, aasm.current_state, *args)) || raise(e) + event.fire_callbacks(:error, self, e, *process_args(event, aasm(state_machine_name).current_state, *args)) || raise(e) end end - def aasm_fired(event, old_state, new_state_name, options, *args) + def aasm_fired(state_machine_name, event, old_state, new_state_name, options, *args) persist = options[:persist] - new_state = aasm.state_object_for_name(new_state_name) + new_state = aasm(state_machine_name).state_object_for_name(new_state_name) new_state.fire_callbacks(:before_enter, self, - *process_args(event, aasm.current_state, *args)) + *process_args(event, aasm(state_machine_name).current_state, *args)) new_state.fire_callbacks(:enter, self, - *process_args(event, aasm.current_state, *args)) # TODO: remove for AASM 4? + *process_args(event, aasm(state_machine_name).current_state, *args)) # TODO: remove for AASM 4? persist_successful = true if persist - persist_successful = aasm.set_current_state_with_persistence(new_state_name) + persist_successful = aasm(state_machine_name).set_current_state_with_persistence(new_state_name) if persist_successful yield if block_given? event.fire_callbacks(:success, self) end else - aasm.current_state = new_state_name + aasm(state_machine_name).current_state = new_state_name yield if block_given? end if persist_successful old_state.fire_callbacks(:after_exit, self, - *process_args(event, aasm.current_state, *args)) + *process_args(event, aasm(state_machine_name).current_state, *args)) new_state.fire_callbacks(:after_enter, self, - *process_args(event, aasm.current_state, *args)) + *process_args(event, aasm(state_machine_name).current_state, *args)) event.fire_callbacks( :after, self, *process_args(event, old_state.name, *args) ) - self.aasm_event_fired(event.name, old_state.name, aasm.current_state) if self.respond_to?(:aasm_event_fired) + self.aasm_event_fired(event.name, old_state.name, aasm(state_machine_name).current_state) if self.respond_to?(:aasm_event_fired) else self.aasm_event_failed(event.name, old_state.name) if self.respond_to?(:aasm_event_failed) end @@ -129,13 +140,13 @@ private persist_successful end - def aasm_failed(event_name, old_state) + def aasm_failed(state_machine_name, event_name, old_state) if self.respond_to?(:aasm_event_failed) self.aasm_event_failed(event_name, old_state.name) end - if AASM::StateMachine[self.class].config.whiny_transitions - raise AASM::InvalidTransition.new(self, event_name) + if AASM::StateMachine[self.class][state_machine_name].config.whiny_transitions + raise AASM::InvalidTransition.new(self, event_name) else false end diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index 3e16a82..071be04 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -3,9 +3,11 @@ module AASM attr_reader :state_machine - def initialize(klass, options={}, &block) + def initialize(klass, name, state_machine, options={}, &block) @klass = klass - @state_machine = AASM::StateMachine[@klass] + @name = name + # @state_machine = @klass.aasm(@name).state_machine + @state_machine = state_machine @state_machine.config.column ||= (options[:column] || :aasm_state).to_sym # aasm4 # @state_machine.config.column = options[:column].to_sym if options[:column] # master @options = options @@ -56,10 +58,16 @@ module AASM def state(name, options={}) @state_machine.add_state(name, @klass, options) - @klass.send(:define_method, "#{name}?") do - aasm.current_state == name + if @klass.instance_methods.include?("#{name}?") + warn "The state name #{name} is already used!" end + @klass.class_eval <<-EORUBY, __FILE__, __LINE__ + 1 + def #{name}? + aasm(:#{@name}).current_state == :#{name} + end + EORUBY + unless @klass.const_defined?("STATE_#{name.upcase}") @klass.const_set("STATE_#{name.upcase}", name) end @@ -69,22 +77,28 @@ module AASM def event(name, options={}, &block) @state_machine.events[name] = AASM::Core::Event.new(name, options, &block) + if @klass.instance_methods.include?("may_#{name}?") + warn "The event name #{name} is already used!" + end + # an addition over standard aasm so that, before firing an event, you can ask # may_event? and get back a boolean that tells you whether the guard method # on the transition will let this happen. - @klass.send(:define_method, "may_#{name}?") do |*args| - aasm.may_fire_event?(name, *args) - end + @klass.class_eval <<-EORUBY, __FILE__, __LINE__ + 1 + def may_#{name}?(*args) + aasm(:#{@name}).may_fire_event?(:#{name}, *args) + end - @klass.send(:define_method, "#{name}!") do |*args, &block| - aasm.current_event = "#{name}!".to_sym - aasm_fire_event(name, {:persist => true}, *args, &block) - end + def #{name}!(*args, &block) + aasm(:#{@name}).current_event = :#{name}! + aasm_fire_event(:#{@name}, :#{name}, {:persist => true}, *args, &block) + end - @klass.send(:define_method, "#{name}") do |*args, &block| - aasm.current_event = name.to_sym - aasm_fire_event(name, {:persist => false}, *args, &block) - end + def #{name}(*args, &block) + aasm(:#{@name}).current_event = :#{name} + aasm_fire_event(:#{@name}, :#{name}, {:persist => false}, *args, &block) + end + EORUBY end def states diff --git a/lib/aasm/instance_base.rb b/lib/aasm/instance_base.rb index f3a811c..a5c0b8e 100644 --- a/lib/aasm/instance_base.rb +++ b/lib/aasm/instance_base.rb @@ -3,21 +3,22 @@ module AASM attr_accessor :from_state, :to_state, :current_event - def initialize(instance) + def initialize(instance, name=:default) # instance of the class including AASM, name of the state machine @instance = instance + @name = name end def current_state - @instance.aasm_read_state + @instance.aasm_read_state(@name) end def current_state=(state) - @instance.aasm_write_state_without_persistence(state) - @current_state = state + @instance.aasm_write_state_without_persistence(state, @name) + # @current_state = state end def enter_initial_state - state_name = determine_state_name(@instance.class.aasm.initial_state) + state_name = determine_state_name(@instance.class.aasm(@name).initial_state) state_object = state_object_for_name(state_name) state_object.fire_callbacks(:before_enter, @instance) @@ -36,17 +37,17 @@ module AASM if options[:permitted] # ugliness level 1000 permitted_event_names = events(:permitted => true).map(&:name) - transitions = @instance.class.aasm.state_machine.events.values_at(*permitted_event_names).compact.map {|e| e.transitions_from_state(current_state) } + transitions = @instance.class.aasm(@name).state_machine.events.values_at(*permitted_event_names).compact.map {|e| e.transitions_from_state(current_state) } tos = transitions.map {|t| t[0] ? t[0].to : nil}.flatten.compact.map(&:to_sym).uniq - @instance.class.aasm.states.select {|s| tos.include?(s.name.to_sym)} + @instance.class.aasm(@name).states.select {|s| tos.include?(s.name.to_sym)} else - @instance.class.aasm.states + @instance.class.aasm(@name).states end end def events(options={}) state = options[:state] || current_state - events = @instance.class.aasm.events.select {|e| e.transitions_from_state?(state) } + events = @instance.class.aasm(@name).events.select {|e| e.transitions_from_state?(state) } if options[:permitted] # filters the results of events_for_current_state so that only those that @@ -58,7 +59,7 @@ module AASM end def state_object_for_name(name) - obj = @instance.class.aasm.states.find {|s| s == name} + obj = @instance.class.aasm(@name).states.find {|s| s == name} raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil? obj end @@ -75,7 +76,7 @@ module AASM end def may_fire_event?(name, *args) - if event = @instance.class.aasm.state_machine.events[name] + if event = @instance.class.aasm(@name).state_machine.events[name] !!event.may_fire?(@instance, *args) else false # unknown event @@ -83,7 +84,7 @@ module AASM end def set_current_state_with_persistence(state) - save_success = @instance.aasm_write_state(state) + save_success = @instance.aasm_write_state(state, @name) self.current_state = state if save_success save_success end diff --git a/lib/aasm/persistence/active_record_persistence.rb b/lib/aasm/persistence/active_record_persistence.rb index db84345..1d404ea 100644 --- a/lib/aasm/persistence/active_record_persistence.rb +++ b/lib/aasm/persistence/active_record_persistence.rb @@ -80,18 +80,18 @@ module AASM # Foo.find(1).aasm.current_state # => :closed # # NOTE: intended to be called from an event - def aasm_write_state(state) - old_value = read_attribute(self.class.aasm.attribute_name) - aasm_write_attribute state + def aasm_write_state(state, name=:default) + old_value = read_attribute(self.class.aasm(name).attribute_name) + aasm_write_attribute state, name - success = if aasm_skipping_validations - value = aasm_raw_attribute_value state - self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm.attribute_name => value) == 1 + success = if aasm_skipping_validations(name) + value = aasm_raw_attribute_value(state, name) + self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm(name).attribute_name => value) == 1 else self.save end unless success - write_attribute(self.class.aasm.attribute_name, old_value) + write_attribute(self.class.aasm(name).attribute_name, old_value) return false end @@ -110,17 +110,17 @@ module AASM # Foo.find(1).aasm.current_state # => :closed # # NOTE: intended to be called from an event - def aasm_write_state_without_persistence(state) - aasm_write_attribute state + def aasm_write_state_without_persistence(state, name=:default) + aasm_write_attribute(state, name) end private - def aasm_enum - case AASM::StateMachine[self.class].config.enum + def aasm_enum(name=:default) + case AASM::StateMachine[self.class][name].config.enum when false then nil when true then aasm_guess_enum_method when nil then aasm_guess_enum_method if aasm_column_looks_like_enum - else AASM::StateMachine[self.class].config.enum + else AASM::StateMachine[self.class][name].config.enum end end @@ -132,17 +132,17 @@ module AASM self.class.aasm.attribute_name.to_s.pluralize.to_sym end - def aasm_skipping_validations - AASM::StateMachine[self.class].config.skip_validation_on_save + def aasm_skipping_validations(state_machine_name) + AASM::StateMachine[self.class][state_machine_name].config.skip_validation_on_save end - def aasm_write_attribute(state) - write_attribute self.class.aasm.attribute_name, aasm_raw_attribute_value(state) + def aasm_write_attribute(state, name=:default) + write_attribute(self.class.aasm(name).attribute_name, aasm_raw_attribute_value(state, name)) end - def aasm_raw_attribute_value(state) - if aasm_enum - self.class.send(aasm_enum)[state] + def aasm_raw_attribute_value(state, name=:default) + if aasm_enum(name) + self.class.send(aasm_enum(name))[state] else state.to_s end @@ -164,32 +164,36 @@ module AASM # foo.aasm_state # => nil # def aasm_ensure_initial_state - # checking via respond_to? does not work in Rails <= 3 - # if respond_to?(self.class.aasm.attribute_name) && send(self.class.aasm.attribute_name).blank? # Rails 4 - if attribute_names.include?(self.class.aasm.attribute_name.to_s) && send(self.class.aasm.attribute_name).blank? - aasm.enter_initial_state + AASM::StateMachine[self.class].keys.each do |state_machine_name| + # checking via respond_to? does not work in Rails <= 3 + # if respond_to?(self.class.aasm(state_machine_name).attribute_name) && send(self.class.aasm(state_machine_name).attribute_name).blank? # Rails 4 + if attribute_names.include?(self.class.aasm(state_machine_name).attribute_name.to_s) && send(self.class.aasm(state_machine_name).attribute_name).blank? + aasm(state_machine_name).enter_initial_state + end end end - def aasm_fire_event(name, options, *args, &block) - success = options[:persist] ? self.class.transaction(:requires_new => requires_new?) { super } : super + def aasm_fire_event(state_machine_name, name, options, *args, &block) + success = options[:persist] ? self.class.transaction(:requires_new => requires_new?(state_machine_name)) { super } : super if success && options[:persist] - event = self.class.aasm.state_machine.events[name] + event = self.class.aasm(state_machine_name).state_machine.events[name] event.fire_callbacks(:after_commit, self) end success end - def requires_new? - AASM::StateMachine[self.class].config.requires_new_transaction + def requires_new?(state_machine_name) + AASM::StateMachine[self.class][state_machine_name].config.requires_new_transaction end def aasm_validate_states - unless aasm_skipping_validations - if aasm.current_state && !aasm.states.include?(aasm.current_state) - self.errors.add(AASM::StateMachine[self.class].config.column , "is invalid") + AASM::StateMachine[self.class].keys.each do |state_machine_name| + unless aasm_skipping_validations(state_machine_name) + if aasm(state_machine_name).current_state && !aasm(state_machine_name).states.include?(aasm(state_machine_name).current_state) + self.errors.add(AASM::StateMachine[self.class][state_machine_name].config.column , "is invalid") + end end end end diff --git a/lib/aasm/persistence/base.rb b/lib/aasm/persistence/base.rb index d019fa4..83fd8da 100644 --- a/lib/aasm/persistence/base.rb +++ b/lib/aasm/persistence/base.rb @@ -32,10 +32,10 @@ module AASM # NOTE: intended to be called from an event # # This allows for nil aasm states - be sure to add validation to your model - def aasm_read_state - state = send(self.class.aasm.attribute_name) + def aasm_read_state(name=:default) + state = send(self.class.aasm(name).attribute_name) if new_record? - state.blank? ? aasm.determine_state_name(self.class.aasm.initial_state) : state.to_sym + state.blank? ? aasm.determine_state_name(self.class.aasm(name).initial_state) : state.to_sym else state.blank? ? nil : state.to_sym end @@ -55,10 +55,10 @@ module AASM # make sure to create a (named) scope for each state def state_with_scope(name, *args) state_without_scope(name, *args) - if AASM::StateMachine[@klass].config.create_scopes && !@klass.respond_to?(name) + if @state_machine.config.create_scopes && !@klass.respond_to?(name) if @klass.ancestors.map {|klass| klass.to_s}.include?("ActiveRecord::Base") - conditions = {"#{@klass.table_name}.#{@klass.aasm.attribute_name}" => name.to_s} + conditions = {"#{@klass.table_name}.#{@klass.aasm(@name).attribute_name}" => name.to_s} if ActiveRecord::VERSION::MAJOR >= 3 @klass.class_eval do scope name, lambda { where(conditions) } @@ -69,10 +69,10 @@ module AASM end end elsif @klass.ancestors.map {|klass| klass.to_s}.include?("Mongoid::Document") - scope_options = lambda { @klass.send(:where, {@klass.aasm.attribute_name.to_sym => name.to_s}) } + scope_options = lambda { @klass.send(:where, {@klass.aasm(@name).attribute_name.to_sym => name.to_s}) } @klass.send(:scope, name, scope_options) elsif @klass.ancestors.map {|klass| klass.to_s}.include?("MongoMapper::Document") - conditions = { @klass.aasm.attribute_name.to_sym => name.to_s } + conditions = { @klass.aasm(@name).attribute_name.to_sym => name.to_s } @klass.scope(name, lambda { @klass.where(conditions) }) end end diff --git a/lib/aasm/persistence/mongo_mapper_persistence.rb b/lib/aasm/persistence/mongo_mapper_persistence.rb index 8737aa0..26680d5 100644 --- a/lib/aasm/persistence/mongo_mapper_persistence.rb +++ b/lib/aasm/persistence/mongo_mapper_persistence.rb @@ -102,7 +102,7 @@ module AASM # Foo.find(1).aasm.current_state # => :closed # # NOTE: intended to be called from an event - def aasm_write_state_without_persistence(state) + def aasm_write_state_without_persistence(state, name=:default) aasm_write_attribute state end @@ -124,17 +124,17 @@ module AASM self.class.aasm.attribute_name.to_s.pluralize.to_sym end - def aasm_skipping_validations - AASM::StateMachine[self.class].config.skip_validation_on_save + def aasm_skipping_validations(state_machine_name) + AASM::StateMachine[self.class][state_machine_name].config.skip_validation_on_save end - def aasm_write_attribute(state) - write_attribute self.class.aasm.attribute_name, aasm_raw_attribute_value(state) + def aasm_write_attribute(state, name=:default) + write_attribute self.class.aasm(name).attribute_name, aasm_raw_attribute_value(state, name) end - def aasm_raw_attribute_value(state) - if aasm_enum - self.class.send(aasm_enum)[state] + def aasm_raw_attribute_value(state, name=:default) + if aasm_enum(name) + self.class.send(aasm_enum(name))[state] else state.to_s end @@ -156,14 +156,18 @@ module AASM # foo.aasm_state # => nil # def aasm_ensure_initial_state - return send("#{self.class.aasm.attribute_name}=", aasm.enter_initial_state.to_s) if send(self.class.aasm.attribute_name).blank? + AASM::StateMachine[self.class].keys.each do |state_machine_name| + send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s) if send(self.class.aasm(state_machine_name).attribute_name).blank? + end end def aasm_validate_states - send("#{self.class.aasm.attribute_name}=", aasm.enter_initial_state.to_s) if send(self.class.aasm.attribute_name).blank? - unless AASM::StateMachine[self.class].config.skip_validation_on_save - if aasm.current_state && !aasm.states.include?(aasm.current_state) - self.errors.add(AASM::StateMachine[self.class].config.column , "is invalid") + AASM::StateMachine[self.class].keys.each do |state_machine_name| + send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s) if send(self.class.aasm(state_machine_name).attribute_name).blank? + unless AASM::StateMachine[self.class][state_machine_name].config.skip_validation_on_save + if aasm(state_machine_name).current_state && !aasm(state_machine_name).states.include?(aasm(state_machine_name).current_state) + self.errors.add(AASM::StateMachine[self.class][state_machine_name].config.column , "is invalid") + end end end end diff --git a/lib/aasm/persistence/mongoid_persistence.rb b/lib/aasm/persistence/mongoid_persistence.rb index d42a82e..41f42e8 100644 --- a/lib/aasm/persistence/mongoid_persistence.rb +++ b/lib/aasm/persistence/mongoid_persistence.rb @@ -72,12 +72,12 @@ module AASM # Foo.find(1).aasm.current_state # => :closed # # NOTE: intended to be called from an event - def aasm_write_state(state) - old_value = read_attribute(self.class.aasm.attribute_name) - write_attribute(self.class.aasm.attribute_name, state.to_s) + def aasm_write_state(state, name=:default) + old_value = read_attribute(self.class.aasm(name).attribute_name) + write_attribute(self.class.aasm(name).attribute_name, state.to_s) unless self.save(:validate => false) - write_attribute(self.class.aasm.attribute_name, old_value) + write_attribute(self.class.aasm(name).attribute_name, old_value) return false end @@ -96,8 +96,8 @@ module AASM # Foo.find(1).aasm.current_state # => :closed # # NOTE: intended to be called from an event - def aasm_write_state_without_persistence(state) - write_attribute(self.class.aasm.attribute_name, state.to_s) + def aasm_write_state_without_persistence(state, name=:default) + write_attribute(self.class.aasm(name).attribute_name, state.to_s) end private @@ -118,16 +118,18 @@ module AASM # foo.aasm_state # => nil # def aasm_ensure_initial_state - send("#{self.class.aasm.attribute_name}=", aasm.enter_initial_state.to_s) if send(self.class.aasm.attribute_name).blank? + AASM::StateMachine[self.class].keys.each do |state_machine_name| + send("#{self.class.aasm(state_machine_name).attribute_name}=", aasm(state_machine_name).enter_initial_state.to_s) if send(self.class.aasm(state_machine_name).attribute_name).blank? + end end end # InstanceMethods - module NamedScopeMethods - def aasm_state_with_named_scope name, options = {} - aasm_state_without_named_scope name, options - self.named_scope name, :conditions => { "#{table_name}.#{self.aasm.attribute_name}" => name.to_s} unless self.respond_to?(name) - end - end + # module NamedScopeMethods + # def aasm_state_with_named_scope name, options = {} + # aasm_state_without_named_scope name, options + # self.named_scope name, :conditions => { "#{table_name}.#{self.aasm.attribute_name}" => name.to_s} unless self.respond_to?(name) + # end + # end end end # Persistence end # AASM diff --git a/lib/aasm/persistence/plain_persistence.rb b/lib/aasm/persistence/plain_persistence.rb index dd17d7e..2fa1011 100644 --- a/lib/aasm/persistence/plain_persistence.rb +++ b/lib/aasm/persistence/plain_persistence.rb @@ -2,21 +2,22 @@ module AASM module Persistence module PlainPersistence - def aasm_read_state + # may be overwritten by persistence mixins + def aasm_read_state(name=:default) # all the following lines behave like @current_state ||= aasm.enter_initial_state - current = aasm.instance_variable_get("@current_state") + current = aasm(name).instance_variable_get("@current_state_#{name}") return current if current - aasm.instance_variable_set("@current_state", aasm.enter_initial_state) + aasm(name).instance_variable_set("@current_state_#{name}", aasm.enter_initial_state) end # may be overwritten by persistence mixins - def aasm_write_state(new_state) + def aasm_write_state(new_state, name=:default) true end # may be overwritten by persistence mixins - def aasm_write_state_without_persistence(new_state) - true + def aasm_write_state_without_persistence(new_state, name=:default) + aasm(name).instance_variable_set("@current_state_#{name}", new_state) end end diff --git a/lib/aasm/persistence/sequel_persistence.rb b/lib/aasm/persistence/sequel_persistence.rb index aa64752..0dafde7 100644 --- a/lib/aasm/persistence/sequel_persistence.rb +++ b/lib/aasm/persistence/sequel_persistence.rb @@ -44,10 +44,10 @@ module AASM # NOTE: intended to be called from an event # # This allows for nil aasm states - be sure to add validation to your model - def aasm_read_state - state = send(self.class.aasm.attribute_name) + def aasm_read_state(name=:default) + state = send(self.class.aasm(name).attribute_name) if new? && state.to_s.strip.empty? - aasm.determine_state_name(self.class.aasm.initial_state) + aasm(name).determine_state_name(self.class.aasm(name).initial_state) elsif state.nil? nil else @@ -71,9 +71,11 @@ module AASM # foo.aasm_state # => nil # def aasm_ensure_initial_state - aasm.enter_initial_state if - (new? || values.key?(self.class.aasm.attribute_name)) && - send(self.class.aasm.attribute_name).to_s.strip.empty? + AASM::StateMachine[self.class].keys.each do |state_machine_name| + aasm(state_machine_name).enter_initial_state if + (new? || values.key?(self.class.aasm(state_machine_name).attribute_name)) && + send(self.class.aasm(state_machine_name).attribute_name).to_s.strip.empty? + end end # Writes state to the state column and persists it to the database @@ -85,8 +87,8 @@ module AASM # Foo[1].aasm.current_state # => :closed # # NOTE: intended to be called from an event - def aasm_write_state state - aasm_column = self.class.aasm.attribute_name + def aasm_write_state state, name=:default + aasm_column = self.class.aasm(name).attribute_name update_only({aasm_column => state.to_s}, aasm_column) end @@ -102,8 +104,8 @@ module AASM # Foo[1].aasm.current_state # => :closed # # NOTE: intended to be called from an event - def aasm_write_state_without_persistence state - send("#{self.class.aasm.attribute_name}=", state.to_s) + def aasm_write_state_without_persistence state, name=:default + send("#{self.class.aasm(name).attribute_name}=", state.to_s) end end end diff --git a/lib/aasm/state_machine.rb b/lib/aasm/state_machine.rb index 4f10f20..d327f5a 100644 --- a/lib/aasm/state_machine.rb +++ b/lib/aasm/state_machine.rb @@ -10,7 +10,7 @@ module AASM (@machines ||= {})[klass.to_s] = machine end - attr_accessor :states, :events, :initial_state, :config + attr_accessor :states, :events, :initial_state, :config, :name def initialize @initial_state = nil diff --git a/spec/models/active_record/provided_and_persisted_state.rb b/spec/models/active_record/provided_and_persisted_state.rb index 1f4e011..8f67058 100644 --- a/spec/models/active_record/provided_and_persisted_state.rb +++ b/spec/models/active_record/provided_and_persisted_state.rb @@ -10,15 +10,15 @@ class ProvidedAndPersistedState < ActiveRecord::Base end end - def aasm_read_state + def aasm_read_state(*args) :gamma end - def aasm_write_state(new_state) + def aasm_write_state(new_state, *args) @persisted_store = new_state end - def aasm_write_state_without_persistence(new_state) + def aasm_write_state_without_persistence(new_state, *args) @transient_store = new_state end end diff --git a/spec/models/parametrised_event.rb b/spec/models/parametrised_event.rb index 3a30d1c..23e013e 100644 --- a/spec/models/parametrised_event.rb +++ b/spec/models/parametrised_event.rb @@ -18,7 +18,7 @@ class ParametrisedEvent end end - def wear_clothes(shirt_color, trouser_type) + def wear_clothes(shirt_color, trouser_type=nil) end def condition_hair diff --git a/spec/models/provided_state.rb b/spec/models/provided_state.rb index 0013a8e..bd2c5fe 100644 --- a/spec/models/provided_state.rb +++ b/spec/models/provided_state.rb @@ -10,15 +10,15 @@ class ProvidedState end end - def aasm_read_state + def aasm_read_state(*args) :beta end - def aasm_write_state(new_state) + def aasm_write_state(new_state, *args) @persisted_store = new_state end - def aasm_write_state_without_persistence(new_state) + def aasm_write_state_without_persistence(new_state, *args) @transient_store = new_state end end diff --git a/spec/unit/persistence/active_record_persistence_spec.rb b/spec/unit/persistence/active_record_persistence_spec.rb index d8df5a3..29724a3 100644 --- a/spec/unit/persistence/active_record_persistence_spec.rb +++ b/spec/unit/persistence/active_record_persistence_spec.rb @@ -160,7 +160,7 @@ describe "instance methods" do gate.aasm_write_state state_sym - expect(gate).to have_received(:aasm_write_attribute).with(state_sym) + expect(gate).to have_received(:aasm_write_attribute).with(state_sym, :default) expect(gate).to_not have_received :write_attribute end end @@ -170,7 +170,7 @@ describe "instance methods" do it "delegates state update to the helper method" do gate.aasm_write_state_without_persistence state_sym - expect(gate).to have_received(:aasm_write_attribute).with(state_sym) + expect(gate).to have_received(:aasm_write_attribute).with(state_sym, :default) expect(gate).to_not have_received :write_attribute end end @@ -210,7 +210,7 @@ describe "instance methods" do end it "generates attribute value using a helper method" do - expect(gate).to have_received(:aasm_raw_attribute_value).with(sym) + expect(gate).to have_received(:aasm_raw_attribute_value).with(sym, :default) end it "writes attribute to the model" do @@ -413,7 +413,7 @@ describe 'transitions with persistence' do it "should only rollback changes in the main transaction not the nested one" do # change configuration to not require new transaction - AASM::StateMachine[Transactor].config.requires_new_transaction = false + AASM::StateMachine[Transactor][:default].config.requires_new_transaction = false expect(transactor).to be_sleeping expect(worker.status).to eq('sleeping') From b3113ceb9dff65eb96c06a6baf1ddd12d9919e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 5 May 2015 22:22:01 +1200 Subject: [PATCH 02/45] fix for MongoMapper --- .../persistence/mongo_mapper_persistence.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/aasm/persistence/mongo_mapper_persistence.rb b/lib/aasm/persistence/mongo_mapper_persistence.rb index 26680d5..871566f 100644 --- a/lib/aasm/persistence/mongo_mapper_persistence.rb +++ b/lib/aasm/persistence/mongo_mapper_persistence.rb @@ -107,21 +107,21 @@ module AASM end private - def aasm_enum - case AASM::StateMachine[self.class].config.enum + def aasm_enum(name=:default) + case AASM::StateMachine[self.class][name].config.enum when false then nil - when true then aasm_guess_enum_method - when nil then aasm_guess_enum_method if aasm_column_looks_like_enum - else AASM::StateMachine[self.class].config.enum + when true then aasm_guess_enum_method(name) + when nil then aasm_guess_enum_method(name) if aasm_column_looks_like_enum(name) + else AASM::StateMachine[self.class][name].config.enum end end - def aasm_column_looks_like_enum - self.class.keys[self.class.aasm.attribute_name.to_s].type == Integer + def aasm_column_looks_like_enum(name) + self.class.keys[self.class.aasm(name).attribute_name.to_s].type == Integer end - def aasm_guess_enum_method - self.class.aasm.attribute_name.to_s.pluralize.to_sym + def aasm_guess_enum_method(name) + self.class.aasm(name).attribute_name.to_s.pluralize.to_sym end def aasm_skipping_validations(state_machine_name) From cf7ac74664e9e696fe5c488875dcd35792b8984c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Wed, 13 May 2015 22:12:59 +1200 Subject: [PATCH 03/45] make sure to use parameterised aasm everywhere --- lib/aasm/aasm.rb | 18 ++++++------- lib/aasm/base.rb | 2 +- lib/aasm/core/event.rb | 11 ++++---- lib/aasm/core/state.rb | 5 ++-- lib/aasm/core/transition.rb | 9 ++++--- lib/aasm/errors.rb | 9 ++++--- lib/aasm/instance_base.rb | 2 +- lib/aasm/localizer.rb | 2 +- lib/aasm/state_machine.rb | 15 +++++++---- spec/unit/event_spec.rb | 17 +++++++----- spec/unit/state_spec.rb | 4 ++- spec/unit/transition_spec.rb | 50 ++++++++++++++++++++++-------------- 12 files changed, 86 insertions(+), 58 deletions(-) diff --git a/lib/aasm/aasm.rb b/lib/aasm/aasm.rb index d2d2af4..e9ffdc1 100644 --- a/lib/aasm/aasm.rb +++ b/lib/aasm/aasm.rb @@ -28,20 +28,20 @@ module AASM def aasm(*args, &block) if args[0].is_a?(Symbol) || args[0].is_a?(String) # using custom name - name = args[0].to_sym + state_machine_name = args[0].to_sym options = args[1] || {} else - # using the default name - name = :default + # using the default state_machine_name + state_machine_name = :default options = args[0] || {} end - AASM::StateMachine[self][name] ||= AASM::StateMachine.new + AASM::StateMachine[self][state_machine_name] ||= AASM::StateMachine.new(state_machine_name) @aasm ||= {} - @aasm[name] ||= AASM::Base.new(self, name, AASM::StateMachine[self][name], options) - @aasm[name].instance_eval(&block) if block # new DSL - @aasm[name] + @aasm[state_machine_name] ||= AASM::Base.new(self, state_machine_name, AASM::StateMachine[self][state_machine_name], options) + @aasm[state_machine_name].instance_eval(&block) if block # new DSL + @aasm[state_machine_name] end end # ClassMethods @@ -76,7 +76,7 @@ private event.fire_callbacks( :before, self, - *process_args(event, aasm.current_state, *args) + *process_args(event, aasm(state_machine_name).current_state, *args) ) if may_fire_to = event.may_fire?(self, *args) @@ -146,7 +146,7 @@ private end if AASM::StateMachine[self.class][state_machine_name].config.whiny_transitions - raise AASM::InvalidTransition.new(self, event_name) + raise AASM::InvalidTransition.new(self, event_name, state_machine_name) else false end diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index 071be04..85bb053 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -75,7 +75,7 @@ module AASM # define an event def event(name, options={}, &block) - @state_machine.events[name] = AASM::Core::Event.new(name, options, &block) + @state_machine.add_event(name, options, &block) if @klass.instance_methods.include?("may_#{name}?") warn "The event name #{name} is already used!" diff --git a/lib/aasm/core/event.rb b/lib/aasm/core/event.rb index aca4a1d..9aea427 100644 --- a/lib/aasm/core/event.rb +++ b/lib/aasm/core/event.rb @@ -2,10 +2,11 @@ module AASM::Core class Event include DslHelper - attr_reader :name, :options + attr_reader :name, :state_machine, :options - def initialize(name, options = {}, &block) + def initialize(name, state_machine, options = {}, &block) @name = name + @state_machine = state_machine @transitions = [] @guards = Array(options[:guard] || options[:guards] || options[:if]) @unless = Array(options[:unless]) #TODO: This could use a better name @@ -61,11 +62,11 @@ module AASM::Core if definitions # define new transitions # Create a separate transition for each from-state to the given state Array(definitions[:from]).each do |s| - @transitions << AASM::Core::Transition.new(attach_event_guards(definitions.merge(:from => s.to_sym)), &block) + @transitions << AASM::Core::Transition.new(self, attach_event_guards(definitions.merge(:from => s.to_sym)), &block) end # Create a transition if :to is specified without :from (transitions from ANY state) if @transitions.empty? && definitions[:to] - @transitions << AASM::Core::Transition.new(attach_event_guards(definitions), &block) + @transitions << AASM::Core::Transition.new(self, attach_event_guards(definitions), &block) end end @transitions @@ -89,7 +90,7 @@ module AASM::Core def _fire(obj, options={}, to_state=nil, *args) result = options[:test_only] ? false : nil if @transitions.map(&:from).any? - transitions = @transitions.select { |t| t.from == obj.aasm.current_state } + transitions = @transitions.select { |t| t.from == obj.aasm(state_machine.name).current_state } return result if transitions.size == 0 else transitions = @transitions diff --git a/lib/aasm/core/state.rb b/lib/aasm/core/state.rb index 20b549f..3ce3fab 100644 --- a/lib/aasm/core/state.rb +++ b/lib/aasm/core/state.rb @@ -1,10 +1,11 @@ module AASM::Core class State - attr_reader :name, :options + attr_reader :name, :state_machine, :options - def initialize(name, klass, options={}) + def initialize(name, klass, state_machine, options={}) @name = name @klass = klass + @state_machine = state_machine update(options) end diff --git a/lib/aasm/core/transition.rb b/lib/aasm/core/transition.rb index e4a80af..d13fa9a 100644 --- a/lib/aasm/core/transition.rb +++ b/lib/aasm/core/transition.rb @@ -2,12 +2,13 @@ module AASM::Core class Transition include DslHelper - attr_reader :from, :to, :opts + attr_reader :from, :to, :event, :opts alias_method :options, :opts - def initialize(opts, &block) + def initialize(event, opts, &block) add_options_from_dsl(opts, [:on_transition, :guard, :after], &block) if block + @event = event @from = opts[:from] @to = opts[:to] @guards = Array(opts[:guards]) + Array(opts[:guard]) + Array(opts[:if]) @@ -44,8 +45,8 @@ module AASM::Core def invoke_callbacks_compatible_with_guard(code, record, args, options={}) if record.respond_to?(:aasm) - record.aasm.from_state = @from if record.aasm.respond_to?(:from_state=) - record.aasm.to_state = @to if record.aasm.respond_to?(:to_state=) + record.aasm(event.state_machine.name).from_state = @from if record.aasm(event.state_machine.name).respond_to?(:from_state=) + record.aasm(event.state_machine.name).to_state = @to if record.aasm(event.state_machine.name).respond_to?(:to_state=) end case code diff --git a/lib/aasm/errors.rb b/lib/aasm/errors.rb index 7e36fd8..9c8637e 100644 --- a/lib/aasm/errors.rb +++ b/lib/aasm/errors.rb @@ -1,13 +1,14 @@ module AASM class InvalidTransition < RuntimeError - attr_reader :object, :event_name - def initialize(object, event_name) - @object, @event_name = object, event_name + attr_reader :object, :event_name, :state_machine_name + + def initialize(object, event_name, state_machine_name) + @object, @event_name, @state_machine_name = object, event_name, state_machine_name end def message - "Event '#{event_name}' cannot transition from '#{object.aasm.current_state}'" + "Event '#{event_name}' cannot transition from '#{object.aasm(state_machine_name).current_state}'" end end diff --git a/lib/aasm/instance_base.rb b/lib/aasm/instance_base.rb index a5c0b8e..64c7f58 100644 --- a/lib/aasm/instance_base.rb +++ b/lib/aasm/instance_base.rb @@ -30,7 +30,7 @@ module AASM end def human_state - AASM::Localizer.new.human_state_name(@instance.class, current_state) + AASM::Localizer.new.human_state_name(@instance.class, state_object_for_name(current_state)) end def states(options={}) diff --git a/lib/aasm/localizer.rb b/lib/aasm/localizer.rb index e1cd126..ec2ab44 100644 --- a/lib/aasm/localizer.rb +++ b/lib/aasm/localizer.rb @@ -21,7 +21,7 @@ module AASM def item_for(klass, state, ancestor, options={}) separator = options[:old_style] ? '.' : '/' - :"#{i18n_scope(klass)}.attributes.#{i18n_klass(ancestor)}.#{klass.aasm.attribute_name}#{separator}#{state}" + :"#{i18n_scope(klass)}.attributes.#{i18n_klass(ancestor)}.#{klass.aasm(state.state_machine.name).attribute_name}#{separator}#{state}" end def translate_queue(checklist) diff --git a/lib/aasm/state_machine.rb b/lib/aasm/state_machine.rb index d327f5a..001387a 100644 --- a/lib/aasm/state_machine.rb +++ b/lib/aasm/state_machine.rb @@ -12,11 +12,12 @@ module AASM attr_accessor :states, :events, :initial_state, :config, :name - def initialize + def initialize(name) @initial_state = nil @states = [] @events = {} @config = AASM::Configuration.new + @name = name end # called internally by Ruby 1.9 after clone() @@ -26,13 +27,17 @@ module AASM @events = @events.dup end - def add_state(name, klass, options) - set_initial_state(name, options) + def add_state(state_name, klass, options) + set_initial_state(state_name, options) # allow reloading, extending or redefining a state - @states.delete(name) if @states.include?(name) + @states.delete(state_name) if @states.include?(state_name) - @states << AASM::Core::State.new(name, klass, options) + @states << AASM::Core::State.new(state_name, klass, self, options) + end + + def add_event(name, options, &block) + @events[name] = AASM::Core::Event.new(name, self, options, &block) end private diff --git a/spec/unit/event_spec.rb b/spec/unit/event_spec.rb index fb0bc24..cddeea9 100644 --- a/spec/unit/event_spec.rb +++ b/spec/unit/event_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe 'adding an event' do + let(:state_machine) { AASM::StateMachine.new(:name) } let(:event) do - AASM::Core::Event.new(:close_order, {:success => :success_callback}) do + AASM::Core::Event.new(:close_order, state_machine, {:success => :success_callback}) do before :before_callback after :after_callback transitions :to => :closed, :from => [:open, :received] @@ -35,8 +36,9 @@ describe 'adding an event' do end describe 'transition inspection' do + let(:state_machine) { AASM::StateMachine.new(:name) } let(:event) do - AASM::Core::Event.new(:run) do + AASM::Core::Event.new(:run, state_machine) do transitions :to => :running, :from => :sleeping end end @@ -59,8 +61,9 @@ describe 'transition inspection' do end describe 'transition inspection without from' do + let(:state_machine) { AASM::StateMachine.new(:name) } let(:event) do - AASM::Core::Event.new(:run) do + AASM::Core::Event.new(:run, state_machine) do transitions :to => :running end end @@ -76,15 +79,17 @@ describe 'transition inspection without from' do end describe 'firing an event' do + let(:state_machine) { AASM::StateMachine.new(:name) } + it 'should return nil if the transitions are empty' do obj = double('object', :aasm => double('aasm', :current_state => 'open')) - event = AASM::Core::Event.new(:event) + event = AASM::Core::Event.new(:event, state_machine) expect(event.fire(obj)).to be_nil end it 'should return the state of the first matching transition it finds' do - event = AASM::Core::Event.new(:event) do + event = AASM::Core::Event.new(:event, state_machine) do transitions :to => :closed, :from => [:open, :received] end @@ -94,7 +99,7 @@ describe 'firing an event' do end it 'should call the guard with the params passed in' do - event = AASM::Core::Event.new(:event) do + event = AASM::Core::Event.new(:event, state_machine) do transitions :to => :closed, :from => [:open, :received], :guard => :guard_fn end diff --git a/spec/unit/state_spec.rb b/spec/unit/state_spec.rb index f4ab6a8..8897b6d 100644 --- a/spec/unit/state_spec.rb +++ b/spec/unit/state_spec.rb @@ -1,13 +1,15 @@ require 'spec_helper' describe AASM::Core::State do + let(:state_machine) { AASM::StateMachine.new(:name) } + before(:each) do @name = :astate @options = { :crazy_custom_key => 'key' } end def new_state(options={}) - AASM::Core::State.new(@name, Conversation, @options.merge(options)) + AASM::Core::State.new(@name, Conversation, state_machine, @options.merge(options)) end it 'should set the name' do diff --git a/spec/unit/transition_spec.rb b/spec/unit/transition_spec.rb index 6d9194c..d10fef5 100644 --- a/spec/unit/transition_spec.rb +++ b/spec/unit/transition_spec.rb @@ -57,9 +57,12 @@ describe 'blocks' do end describe AASM::Core::Transition do + let(:state_machine) { AASM::StateMachine.new(:name) } + let(:event) { AASM::Core::Event.new(:event, state_machine) } + it 'should set from, to, and opts attr readers' do opts = {:from => 'foo', :to => 'bar', :guard => 'g'} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) expect(st.from).to eq(opts[:from]) expect(st.to).to eq(opts[:to]) @@ -71,7 +74,7 @@ describe AASM::Core::Transition do st = AASM::Core::Transition.allocate expect(st).to receive(:warn).with('[DEPRECATION] :on_transition is deprecated, use :after instead') - st.send :initialize, opts do + st.send :initialize, event, opts do guard :gg on_transition :after_callback end @@ -81,7 +84,7 @@ describe AASM::Core::Transition do it 'should set after and guard from dsl' do opts = {:from => 'foo', :to => 'bar', :guard => 'g'} - st = AASM::Core::Transition.new(opts) do + st = AASM::Core::Transition.new(event, opts) do guard :gg after :after_callback end @@ -92,7 +95,7 @@ describe AASM::Core::Transition do it 'should pass equality check if from and to are the same' do opts = {:from => 'foo', :to => 'bar', :guard => 'g'} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) obj = double('object') allow(obj).to receive(:from).and_return(opts[:from]) @@ -103,7 +106,7 @@ describe AASM::Core::Transition do it 'should fail equality check if from are not the same' do opts = {:from => 'foo', :to => 'bar', :guard => 'g'} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) obj = double('object') allow(obj).to receive(:from).and_return('blah') @@ -114,7 +117,7 @@ describe AASM::Core::Transition do it 'should fail equality check if to are not the same' do opts = {:from => 'foo', :to => 'bar', :guard => 'g'} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) obj = double('object') allow(obj).to receive(:from).and_return(opts[:from]) @@ -125,16 +128,19 @@ describe AASM::Core::Transition do end describe AASM::Core::Transition, '- when performing guard checks' do + let(:state_machine) { AASM::StateMachine.new(:name) } + let(:event) { AASM::Core::Event.new(:event, state_machine) } + it 'should return true of there is no guard' do opts = {:from => 'foo', :to => 'bar'} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) expect(st.allowed?(nil)).to be_truthy end it 'should call the method on the object if guard is a symbol' do opts = {:from => 'foo', :to => 'bar', :guard => :test} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) obj = double('object') expect(obj).to receive(:test) @@ -144,7 +150,7 @@ describe AASM::Core::Transition, '- when performing guard checks' do it 'should call the method on the object if unless is a symbol' do opts = {:from => 'foo', :to => 'bar', :unless => :test} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) obj = double('object') expect(obj).to receive(:test) @@ -154,7 +160,7 @@ describe AASM::Core::Transition, '- when performing guard checks' do it 'should call the method on the object if guard is a string' do opts = {:from => 'foo', :to => 'bar', :guard => 'test'} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) obj = double('object') expect(obj).to receive(:test) @@ -164,7 +170,7 @@ describe AASM::Core::Transition, '- when performing guard checks' do it 'should call the method on the object if unless is a string' do opts = {:from => 'foo', :to => 'bar', :unless => 'test'} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) obj = double('object') expect(obj).to receive(:test) @@ -174,7 +180,7 @@ describe AASM::Core::Transition, '- when performing guard checks' do it 'should call the proc passing the object if the guard is a proc' do opts = {:from => 'foo', :to => 'bar', :guard => Proc.new { test }} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) obj = double('object') expect(obj).to receive(:test) @@ -184,9 +190,12 @@ describe AASM::Core::Transition, '- when performing guard checks' do end describe AASM::Core::Transition, '- when executing the transition with a Proc' do + let(:state_machine) { AASM::StateMachine.new(:name) } + let(:event) { AASM::Core::Event.new(:event, state_machine) } + it 'should call a Proc on the object with args' do opts = {:from => 'foo', :to => 'bar', :after => Proc.new {|a| test(a) }} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) args = {:arg1 => '1', :arg2 => '2'} obj = double('object', :aasm => 'aasm') @@ -202,7 +211,7 @@ describe AASM::Core::Transition, '- when executing the transition with a Proc' d prc = Proc.new { prc_object = self } opts = {:from => 'foo', :to => 'bar', :after => prc } - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) args = {:arg1 => '1', :arg2 => '2'} obj = double('object', :aasm => 'aasm') @@ -212,9 +221,12 @@ describe AASM::Core::Transition, '- when executing the transition with a Proc' d end describe AASM::Core::Transition, '- when executing the transition with an :after method call' do + let(:state_machine) { AASM::StateMachine.new(:name) } + let(:event) { AASM::Core::Event.new(:event, state_machine) } + it 'should accept a String for the method name' do opts = {:from => 'foo', :to => 'bar', :after => 'test'} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) args = {:arg1 => '1', :arg2 => '2'} obj = double('object', :aasm => 'aasm') @@ -225,7 +237,7 @@ describe AASM::Core::Transition, '- when executing the transition with an :after it 'should accept a Symbol for the method name' do opts = {:from => 'foo', :to => 'bar', :after => :test} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) args = {:arg1 => '1', :arg2 => '2'} obj = double('object', :aasm => 'aasm') @@ -236,7 +248,7 @@ describe AASM::Core::Transition, '- when executing the transition with an :after it 'should pass args if the target method accepts them' do opts = {:from => 'foo', :to => 'bar', :after => :test} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) args = {:arg1 => '1', :arg2 => '2'} obj = double('object', :aasm => 'aasm') @@ -251,7 +263,7 @@ describe AASM::Core::Transition, '- when executing the transition with an :after it 'should NOT pass args if the target method does NOT accept them' do opts = {:from => 'foo', :to => 'bar', :after => :test} - st = AASM::Core::Transition.new(opts) + st = AASM::Core::Transition.new(event, opts) args = {:arg1 => '1', :arg2 => '2'} obj = double('object', :aasm => 'aasm') @@ -266,7 +278,7 @@ describe AASM::Core::Transition, '- when executing the transition with an :after it 'should allow accessing the from_state and the to_state' do opts = {:from => 'foo', :to => 'bar', :after => :test} - transition = AASM::Core::Transition.new(opts) + transition = AASM::Core::Transition.new(event, opts) args = {:arg1 => '1', :arg2 => '2'} obj = double('object', :aasm => AASM::InstanceBase.new('object')) From f043bbb04d4b9225c62990d95d119661610e065d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Mon, 18 May 2015 21:27:56 +1200 Subject: [PATCH 04/45] first tests for state machines with multiple state attributes --- lib/aasm/base.rb | 2 +- lib/aasm/persistence/plain_persistence.rb | 4 +- spec/models/simple_multiple_example.rb | 30 +++++++++++ spec/unit/simple_multiple_example_spec.rb | 63 +++++++++++++++++++++++ 4 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 spec/models/simple_multiple_example.rb create mode 100644 spec/unit/simple_multiple_example_spec.rb diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index 85bb053..94e82d6 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -77,7 +77,7 @@ module AASM def event(name, options={}, &block) @state_machine.add_event(name, options, &block) - if @klass.instance_methods.include?("may_#{name}?") + if @klass.instance_methods.include?("may_#{name}?".to_sym) warn "The event name #{name} is already used!" end diff --git a/lib/aasm/persistence/plain_persistence.rb b/lib/aasm/persistence/plain_persistence.rb index 2fa1011..e3ebb6e 100644 --- a/lib/aasm/persistence/plain_persistence.rb +++ b/lib/aasm/persistence/plain_persistence.rb @@ -4,10 +4,10 @@ module AASM # may be overwritten by persistence mixins def aasm_read_state(name=:default) - # all the following lines behave like @current_state ||= aasm.enter_initial_state + # all the following lines behave like @current_state ||= aasm(name).enter_initial_state current = aasm(name).instance_variable_get("@current_state_#{name}") return current if current - aasm(name).instance_variable_set("@current_state_#{name}", aasm.enter_initial_state) + aasm(name).instance_variable_set("@current_state_#{name}", aasm(name).enter_initial_state) end # may be overwritten by persistence mixins diff --git a/spec/models/simple_multiple_example.rb b/spec/models/simple_multiple_example.rb new file mode 100644 index 0000000..81ef414 --- /dev/null +++ b/spec/models/simple_multiple_example.rb @@ -0,0 +1,30 @@ +class SimpleMultipleExample + include AASM + aasm(:move) do + state :standing, :initial => true + state :walking + state :running + + event :walk do + transitions :from => :standing, :to => :walking + end + event :run do + transitions :from => [:standing, :walking], :to => :running + end + event :hold do + transitions :from => [:walking, :running], :to => :standing + end + end + + aasm(:work) do + state :sleeping, :initial => true + state :processing + + event :start do + transitions :from => :sleeping, :to => :processing + end + event :stop do + transitions :from => :processing, :to => :sleeping + end + end +end diff --git a/spec/unit/simple_multiple_example_spec.rb b/spec/unit/simple_multiple_example_spec.rb new file mode 100644 index 0000000..7dc5ab4 --- /dev/null +++ b/spec/unit/simple_multiple_example_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe 'state machine' do + let(:simple) { SimpleMultipleExample.new } + + it 'starts with an initial state' do + expect(simple.aasm(:move).current_state).to eq(:standing) + expect(simple).to respond_to(:standing?) + expect(simple).to be_standing + + expect(simple.aasm(:work).current_state).to eq(:sleeping) + expect(simple).to respond_to(:sleeping?) + expect(simple).to be_sleeping + end + + it 'allows transitions to other states' do + expect(simple).to respond_to(:walk) + expect(simple).to respond_to(:walk!) + simple.walk! + expect(simple).to respond_to(:walking?) + expect(simple).to be_walking + + expect(simple).to respond_to(:run) + expect(simple).to respond_to(:run!) + simple.run + expect(simple).to respond_to(:running?) + expect(simple).to be_running + + expect(simple).to respond_to(:start) + expect(simple).to respond_to(:start!) + simple.start + expect(simple).to respond_to(:processing?) + expect(simple).to be_processing + end + + it 'denies transitions to other states' do + expect {simple.hold}.to raise_error(AASM::InvalidTransition) + expect {simple.hold!}.to raise_error(AASM::InvalidTransition) + simple.walk + expect {simple.walk}.to raise_error(AASM::InvalidTransition) + expect {simple.walk!}.to raise_error(AASM::InvalidTransition) + simple.run + expect {simple.walk}.to raise_error(AASM::InvalidTransition) + expect {simple.walk!}.to raise_error(AASM::InvalidTransition) + + expect {simple.stop}.to raise_error(AASM::InvalidTransition) + expect {simple.stop!}.to raise_error(AASM::InvalidTransition) + simple.start + expect {simple.start}.to raise_error(AASM::InvalidTransition) + expect {simple.start!}.to raise_error(AASM::InvalidTransition) + simple.stop + end + + it 'defines constants for each state name' do + expect(SimpleMultipleExample::STATE_STANDING).to eq(:standing) + expect(SimpleMultipleExample::STATE_WALKING).to eq(:walking) + expect(SimpleMultipleExample::STATE_RUNNING).to eq(:running) + + expect(SimpleMultipleExample::STATE_SLEEPING).to eq(:sleeping) + expect(SimpleMultipleExample::STATE_PROCESSING).to eq(:processing) + expect(SimpleMultipleExample::STATE_RUNNING).to eq(:running) + end +end From 6db4c04da8403b817dcec9ca8adc15cf33ccf74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Mon, 18 May 2015 21:36:31 +1200 Subject: [PATCH 05/45] first tests for state machines with multiple state attributes --- spec/models/complex_multiple_example.rb | 132 +++++++++++++++++++++ spec/unit/complex_multiple_example_spec.rb | 84 +++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 spec/models/complex_multiple_example.rb create mode 100644 spec/unit/complex_multiple_example_spec.rb diff --git a/spec/models/complex_multiple_example.rb b/spec/models/complex_multiple_example.rb new file mode 100644 index 0000000..e6c5282 --- /dev/null +++ b/spec/models/complex_multiple_example.rb @@ -0,0 +1,132 @@ +class ComplexMultipleExample + include AASM + + attr_accessor :activation_code, :activated_at, :deleted_at + + aasm(:left) do + state :passive + state :pending, :initial => true, :before_enter => :make_activation_code + state :active, :before_enter => :do_activate + state :suspended + state :deleted, :before_enter => :do_delete#, :exit => :do_undelete + state :waiting + + event :register do + transitions :from => :passive, :to => :pending do + guard do + can_register? + end + end + end + + event :activate do + transitions :from => :pending, :to => :active + end + + event :suspend do + transitions :from => [:passive, :pending, :active], :to => :suspended + end + + event :delete do + transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted + end + + # a dummy event that can never happen + event :unpassify do + transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } + end + + event :unsuspend do + transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } + transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? + transitions :from => :suspended, :to => :passive + end + + event :wait do + transitions :from => :suspended, :to => :waiting, :guard => :if_polite? + end + end + + # aasm(:right) do + # state :passive + # state :pending, :initial => true, :before_enter => :make_activation_code + # state :active, :before_enter => :do_activate + # state :suspended + # state :deleted, :before_enter => :do_delete#, :exit => :do_undelete + # state :waiting + + # event :register do + # transitions :from => :passive, :to => :pending do + # guard do + # can_register? + # end + # end + # end + + # event :activate do + # transitions :from => :pending, :to => :active + # end + + # event :suspend do + # transitions :from => [:passive, :pending, :active], :to => :suspended + # end + + # event :delete do + # transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted + # end + + # # a dummy event that can never happen + # event :unpassify do + # transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } + # end + + # event :unsuspend do + # transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } + # transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? + # transitions :from => :suspended, :to => :passive + # end + + # event :wait do + # transitions :from => :suspended, :to => :waiting, :guard => :if_polite? + # end + # end # right + + def initialize + # the AR backend uses a before_validate_on_create :aasm_ensure_initial_state + # lets do something similar here for testing purposes. + aasm(:left).enter_initial_state + end + + def make_activation_code + @activation_code = 'moo' + end + + def do_activate + @activated_at = Time.now + @activation_code = nil + end + + def do_delete + @deleted_at = Time.now + end + + def do_undelete + @deleted_at = false + end + + def can_register? + true + end + + def has_activated? + !!@activated_at + end + + def has_activation_code? + !!@activation_code + end + + def if_polite?(phrase = nil) + phrase == :please + end +end diff --git a/spec/unit/complex_multiple_example_spec.rb b/spec/unit/complex_multiple_example_spec.rb new file mode 100644 index 0000000..31c06f1 --- /dev/null +++ b/spec/unit/complex_multiple_example_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe 'on initialization' do + let(:auth) {ComplexMultipleExample.new} + + it 'should be in the pending state' do + expect(auth.aasm(:left).current_state).to eq(:pending) + end + + it 'should have an activation code' do + expect(auth.has_activation_code?).to be_truthy + expect(auth.activation_code).not_to be_nil + end +end + +describe 'when being unsuspended' do + let(:auth) {ComplexMultipleExample.new} + + it 'should be able to be unsuspended' do + auth.activate! + auth.suspend! + expect(auth.may_unsuspend?).to be true + end + + it 'should not be able to be unsuspended into active' do + auth.suspend! + expect(auth.may_unsuspend?(:active)).not_to be true + end + + it 'should be able to be unsuspended into active if polite' do + auth.suspend! + expect(auth.may_wait?(:waiting, :please)).to be true + auth.wait!(nil, :please) + end + + it 'should not be able to be unsuspended into active if not polite' do + auth.suspend! + expect(auth.may_wait?(:waiting)).not_to be true + expect(auth.may_wait?(:waiting, :rude)).not_to be true + expect {auth.wait!(nil, :rude)}.to raise_error(AASM::InvalidTransition) + expect {auth.wait!}.to raise_error(AASM::InvalidTransition) + end + + it 'should not be able to be unpassified' do + auth.activate! + auth.suspend! + auth.unsuspend! + + expect(auth.may_unpassify?).not_to be true + expect {auth.unpassify!}.to raise_error(AASM::InvalidTransition) + end + + it 'should be active if previously activated' do + auth.activate! + auth.suspend! + auth.unsuspend! + + expect(auth.aasm(:left).current_state).to eq(:active) + end + + it 'should be pending if not previously activated, but an activation code is present' do + auth.suspend! + auth.unsuspend! + + expect(auth.aasm(:left).current_state).to eq(:pending) + end + + it 'should be passive if not previously activated and there is no activation code' do + auth.activation_code = nil + auth.suspend! + auth.unsuspend! + + expect(auth.aasm(:left).current_state).to eq(:passive) + end + + it "should be able to fire known events" do + expect(auth.aasm(:left).may_fire_event?(:activate)).to be true + end + + it "should not be able to fire unknown events" do + expect(auth.aasm(:left).may_fire_event?(:unknown)).to be false + end + +end From 4b58b5829a2cdccb00bf4afd142e08c06e8ae6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Mon, 18 May 2015 21:43:18 +1200 Subject: [PATCH 06/45] first tests for state machines with multiple state attributes --- spec/models/complex_multiple_example.rb | 60 +++++++++++----------- spec/unit/complex_multiple_example_spec.rb | 58 ++++++++++----------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/spec/models/complex_multiple_example.rb b/spec/models/complex_multiple_example.rb index e6c5282..8d73bfd 100644 --- a/spec/models/complex_multiple_example.rb +++ b/spec/models/complex_multiple_example.rb @@ -1,49 +1,49 @@ class ComplexMultipleExample include AASM - attr_accessor :activation_code, :activated_at, :deleted_at + attr_accessor :left_activation_code, :left_activated_at, :left_deleted_at aasm(:left) do state :passive - state :pending, :initial => true, :before_enter => :make_activation_code - state :active, :before_enter => :do_activate + state :pending, :initial => true, :before_enter => :make_left_activation_code + state :active, :before_enter => :do_left_activate state :suspended - state :deleted, :before_enter => :do_delete#, :exit => :do_undelete + state :deleted, :before_enter => :do_left_delete#, :exit => :do_left_undelete state :waiting - event :register do + event :left_register do transitions :from => :passive, :to => :pending do guard do - can_register? + can_left_register? end end end - event :activate do + event :left_activate do transitions :from => :pending, :to => :active end - event :suspend do + event :left_suspend do transitions :from => [:passive, :pending, :active], :to => :suspended end - event :delete do + event :left_delete do transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted end # a dummy event that can never happen - event :unpassify do + event :left_unpassify do transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } end - event :unsuspend do - transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } - transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? + event :left_unsuspend do + transitions :from => :suspended, :to => :active, :guard => Proc.new { has_left_activated? } + transitions :from => :suspended, :to => :pending, :guard => :has_left_activation_code? transitions :from => :suspended, :to => :passive end - event :wait do - transitions :from => :suspended, :to => :waiting, :guard => :if_polite? + event :left_wait do + transitions :from => :suspended, :to => :waiting, :guard => :if_left_polite? end end @@ -97,36 +97,36 @@ class ComplexMultipleExample aasm(:left).enter_initial_state end - def make_activation_code - @activation_code = 'moo' + def make_left_activation_code + @left_activation_code = 'moo' end - def do_activate - @activated_at = Time.now - @activation_code = nil + def do_left_activate + @left_activated_at = Time.now + @left_activation_code = nil end - def do_delete - @deleted_at = Time.now + def do_left_delete + @left_deleted_at = Time.now end - def do_undelete - @deleted_at = false + def do_left_undelete + @left_deleted_at = false end - def can_register? + def can_left_register? true end - def has_activated? - !!@activated_at + def has_left_activated? + !!@left_activated_at end - def has_activation_code? - !!@activation_code + def has_left_activation_code? + !!@left_activation_code end - def if_polite?(phrase = nil) + def if_left_polite?(phrase = nil) phrase == :please end end diff --git a/spec/unit/complex_multiple_example_spec.rb b/spec/unit/complex_multiple_example_spec.rb index 31c06f1..88525e1 100644 --- a/spec/unit/complex_multiple_example_spec.rb +++ b/spec/unit/complex_multiple_example_spec.rb @@ -8,8 +8,8 @@ describe 'on initialization' do end it 'should have an activation code' do - expect(auth.has_activation_code?).to be_truthy - expect(auth.activation_code).not_to be_nil + expect(auth.has_left_activation_code?).to be_truthy + expect(auth.left_activation_code).not_to be_nil end end @@ -17,64 +17,64 @@ describe 'when being unsuspended' do let(:auth) {ComplexMultipleExample.new} it 'should be able to be unsuspended' do - auth.activate! - auth.suspend! - expect(auth.may_unsuspend?).to be true + auth.left_activate! + auth.left_suspend! + expect(auth.may_left_unsuspend?).to be true end it 'should not be able to be unsuspended into active' do - auth.suspend! - expect(auth.may_unsuspend?(:active)).not_to be true + auth.left_suspend! + expect(auth.may_left_unsuspend?(:active)).not_to be true end it 'should be able to be unsuspended into active if polite' do - auth.suspend! - expect(auth.may_wait?(:waiting, :please)).to be true - auth.wait!(nil, :please) + auth.left_suspend! + expect(auth.may_left_wait?(:waiting, :please)).to be true + auth.left_wait!(nil, :please) end it 'should not be able to be unsuspended into active if not polite' do - auth.suspend! - expect(auth.may_wait?(:waiting)).not_to be true - expect(auth.may_wait?(:waiting, :rude)).not_to be true - expect {auth.wait!(nil, :rude)}.to raise_error(AASM::InvalidTransition) - expect {auth.wait!}.to raise_error(AASM::InvalidTransition) + auth.left_suspend! + expect(auth.may_left_wait?(:waiting)).not_to be true + expect(auth.may_left_wait?(:waiting, :rude)).not_to be true + expect {auth.left_wait!(nil, :rude)}.to raise_error(AASM::InvalidTransition) + expect {auth.left_wait!}.to raise_error(AASM::InvalidTransition) end it 'should not be able to be unpassified' do - auth.activate! - auth.suspend! - auth.unsuspend! + auth.left_activate! + auth.left_suspend! + auth.left_unsuspend! - expect(auth.may_unpassify?).not_to be true - expect {auth.unpassify!}.to raise_error(AASM::InvalidTransition) + expect(auth.may_left_unpassify?).not_to be true + expect {auth.left_unpassify!}.to raise_error(AASM::InvalidTransition) end it 'should be active if previously activated' do - auth.activate! - auth.suspend! - auth.unsuspend! + auth.left_activate! + auth.left_suspend! + auth.left_unsuspend! expect(auth.aasm(:left).current_state).to eq(:active) end it 'should be pending if not previously activated, but an activation code is present' do - auth.suspend! - auth.unsuspend! + auth.left_suspend! + auth.left_unsuspend! expect(auth.aasm(:left).current_state).to eq(:pending) end it 'should be passive if not previously activated and there is no activation code' do - auth.activation_code = nil - auth.suspend! - auth.unsuspend! + auth.left_activation_code = nil + auth.left_suspend! + auth.left_unsuspend! expect(auth.aasm(:left).current_state).to eq(:passive) end it "should be able to fire known events" do - expect(auth.aasm(:left).may_fire_event?(:activate)).to be true + expect(auth.aasm(:left).may_fire_event?(:left_activate)).to be true end it "should not be able to fire unknown events" do From 3ea60681f3a645637bb951fc6d7d4ce9f79c470f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Mon, 18 May 2015 21:52:36 +1200 Subject: [PATCH 07/45] don't left too much --- spec/models/complex_multiple_example.rb | 46 +++++++++++----------- spec/unit/complex_multiple_example_spec.rb | 6 +-- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/spec/models/complex_multiple_example.rb b/spec/models/complex_multiple_example.rb index 8d73bfd..e56eb06 100644 --- a/spec/models/complex_multiple_example.rb +++ b/spec/models/complex_multiple_example.rb @@ -1,20 +1,20 @@ class ComplexMultipleExample include AASM - attr_accessor :left_activation_code, :left_activated_at, :left_deleted_at + attr_accessor :activation_code, :activated_at, :deleted_at aasm(:left) do state :passive - state :pending, :initial => true, :before_enter => :make_left_activation_code - state :active, :before_enter => :do_left_activate + state :pending, :initial => true, :before_enter => :make_activation_code + state :active, :before_enter => :do_activate state :suspended - state :deleted, :before_enter => :do_left_delete#, :exit => :do_left_undelete + state :deleted, :before_enter => :do_delete#, :exit => :do_undelete state :waiting event :left_register do transitions :from => :passive, :to => :pending do guard do - can_left_register? + can_register? end end end @@ -37,13 +37,13 @@ class ComplexMultipleExample end event :left_unsuspend do - transitions :from => :suspended, :to => :active, :guard => Proc.new { has_left_activated? } - transitions :from => :suspended, :to => :pending, :guard => :has_left_activation_code? + transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } + transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? transitions :from => :suspended, :to => :passive end event :left_wait do - transitions :from => :suspended, :to => :waiting, :guard => :if_left_polite? + transitions :from => :suspended, :to => :waiting, :guard => :if_polite? end end @@ -97,36 +97,36 @@ class ComplexMultipleExample aasm(:left).enter_initial_state end - def make_left_activation_code - @left_activation_code = 'moo' + def make_activation_code + @activation_code = 'moo' end - def do_left_activate - @left_activated_at = Time.now - @left_activation_code = nil + def do_activate + @activated_at = Time.now + @activation_code = nil end - def do_left_delete - @left_deleted_at = Time.now + def do_delete + @deleted_at = Time.now end - def do_left_undelete - @left_deleted_at = false + def do_undelete + @deleted_at = false end - def can_left_register? + def can_register? true end - def has_left_activated? - !!@left_activated_at + def has_activated? + !!@activated_at end - def has_left_activation_code? - !!@left_activation_code + def has_activation_code? + !!@activation_code end - def if_left_polite?(phrase = nil) + def if_polite?(phrase = nil) phrase == :please end end diff --git a/spec/unit/complex_multiple_example_spec.rb b/spec/unit/complex_multiple_example_spec.rb index 88525e1..7dd929d 100644 --- a/spec/unit/complex_multiple_example_spec.rb +++ b/spec/unit/complex_multiple_example_spec.rb @@ -8,8 +8,8 @@ describe 'on initialization' do end it 'should have an activation code' do - expect(auth.has_left_activation_code?).to be_truthy - expect(auth.left_activation_code).not_to be_nil + expect(auth.has_activation_code?).to be_truthy + expect(auth.activation_code).not_to be_nil end end @@ -66,7 +66,7 @@ describe 'when being unsuspended' do end it 'should be passive if not previously activated and there is no activation code' do - auth.left_activation_code = nil + auth.activation_code = nil auth.left_suspend! auth.left_unsuspend! From 8d0cc35547cc5ef351e0b0cc692af1de6d96c865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Mon, 18 May 2015 22:05:18 +1200 Subject: [PATCH 08/45] first tests for state machines with multiple state attributes --- spec/models/complex_multiple_example.rb | 75 +++++++++++----------- spec/unit/complex_multiple_example_spec.rb | 23 +++++-- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/spec/models/complex_multiple_example.rb b/spec/models/complex_multiple_example.rb index e56eb06..53e94be 100644 --- a/spec/models/complex_multiple_example.rb +++ b/spec/models/complex_multiple_example.rb @@ -47,58 +47,59 @@ class ComplexMultipleExample end end - # aasm(:right) do - # state :passive - # state :pending, :initial => true, :before_enter => :make_activation_code - # state :active, :before_enter => :do_activate - # state :suspended - # state :deleted, :before_enter => :do_delete#, :exit => :do_undelete - # state :waiting + aasm(:right) do + state :passive + state :pending, :initial => true, :before_enter => :make_activation_code + state :active, :before_enter => :do_activate + state :suspended + state :deleted, :before_enter => :do_delete#, :exit => :do_undelete + state :waiting - # event :register do - # transitions :from => :passive, :to => :pending do - # guard do - # can_register? - # end - # end - # end + event :right_register do + transitions :from => :passive, :to => :pending do + guard do + can_register? + end + end + end - # event :activate do - # transitions :from => :pending, :to => :active - # end + event :right_activate do + transitions :from => :pending, :to => :active + end - # event :suspend do - # transitions :from => [:passive, :pending, :active], :to => :suspended - # end + event :right_suspend do + transitions :from => [:passive, :pending, :active], :to => :suspended + end - # event :delete do - # transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted - # end + event :right_delete do + transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted + end - # # a dummy event that can never happen - # event :unpassify do - # transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } - # end + # a dummy event that can never happen + event :right_unpassify do + transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } + end - # event :unsuspend do - # transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } - # transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? - # transitions :from => :suspended, :to => :passive - # end + event :right_unsuspend do + transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } + transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? + transitions :from => :suspended, :to => :passive + end - # event :wait do - # transitions :from => :suspended, :to => :waiting, :guard => :if_polite? - # end - # end # right + event :right_wait do + transitions :from => :suspended, :to => :waiting, :guard => :if_polite? + end + end # right def initialize # the AR backend uses a before_validate_on_create :aasm_ensure_initial_state # lets do something similar here for testing purposes. aasm(:left).enter_initial_state + aasm(:right).enter_initial_state end def make_activation_code - @activation_code = 'moo' + @activation_code = @activation_code ? @activation_code + '2' : '1' end def do_activate diff --git a/spec/unit/complex_multiple_example_spec.rb b/spec/unit/complex_multiple_example_spec.rb index 7dd929d..2cef3e7 100644 --- a/spec/unit/complex_multiple_example_spec.rb +++ b/spec/unit/complex_multiple_example_spec.rb @@ -5,32 +5,45 @@ describe 'on initialization' do it 'should be in the pending state' do expect(auth.aasm(:left).current_state).to eq(:pending) + expect(auth.aasm(:right).current_state).to eq(:pending) end it 'should have an activation code' do expect(auth.has_activation_code?).to be_truthy - expect(auth.activation_code).not_to be_nil + expect(auth.activation_code).to eq '12' end end describe 'when being unsuspended' do let(:auth) {ComplexMultipleExample.new} - it 'should be able to be unsuspended' do + it 'should be able to unsuspend' do auth.left_activate! auth.left_suspend! expect(auth.may_left_unsuspend?).to be true + + auth.right_activate! + auth.right_suspend! + expect(auth.may_right_unsuspend?).to be true end - it 'should not be able to be unsuspended into active' do + it 'should not be able to unsuspend into active' do auth.left_suspend! expect(auth.may_left_unsuspend?(:active)).not_to be true + + auth.right_activate! + auth.right_suspend! + expect(auth.may_right_unsuspend?(:active)).to be true end - it 'should be able to be unsuspended into active if polite' do + it 'should be able to wait into waiting if polite' do auth.left_suspend! expect(auth.may_left_wait?(:waiting, :please)).to be true auth.left_wait!(nil, :please) + + auth.right_suspend! + expect(auth.may_right_wait?(:waiting)).to be false + auth.right_wait!(nil, :please) end it 'should not be able to be unsuspended into active if not polite' do @@ -75,10 +88,12 @@ describe 'when being unsuspended' do it "should be able to fire known events" do expect(auth.aasm(:left).may_fire_event?(:left_activate)).to be true + expect(auth.aasm(:right).may_fire_event?(:right_activate)).to be true end it "should not be able to fire unknown events" do expect(auth.aasm(:left).may_fire_event?(:unknown)).to be false + expect(auth.aasm(:right).may_fire_event?(:unknown)).to be false end end From 2280f03e88b23f128ea45fbf050076bbeaddd81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Mon, 18 May 2015 22:39:24 +1200 Subject: [PATCH 09/45] first tests for state machines with multiple state attributes (callbacks, part 1) --- spec/models/callbacks/basic_multiple.rb | 75 ++++++ spec/unit/callback_multiple_spec.rb | 295 ++++++++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 spec/models/callbacks/basic_multiple.rb create mode 100644 spec/unit/callback_multiple_spec.rb diff --git a/spec/models/callbacks/basic_multiple.rb b/spec/models/callbacks/basic_multiple.rb new file mode 100644 index 0000000..af0170d --- /dev/null +++ b/spec/models/callbacks/basic_multiple.rb @@ -0,0 +1,75 @@ +module Callbacks + class BasicMultiple + include AASM + + def initialize(options={}) + @fail_event_guard = options[:fail_event_guard] + @fail_transition_guard = options[:fail_transition_guard] + @log = options[:log] + reset_data + end + + def reset_data + @data = [] + end + + def data + @data.join(' ') + end + + aasm(:left) do + state :open, :initial => true, + :before_enter => :before_enter_open, + :enter => :enter_open, + :after_enter => :after_enter_open, + :before_exit => :before_exit_open, + :exit => :exit_open, + :after_exit => :after_exit_open + + state :closed, + :before_enter => :before_enter_closed, + :enter => :enter_closed, + :after_enter => :after_enter_closed, + :before_exit => :before_exit_closed, + :exit => :exit_closed, + :after_exit => :after_exit_closed + + event :left_close, :before => :before_event, :after => :after_event, :guard => :event_guard do + transitions :to => :closed, :from => [:open], :guard => :transition_guard, :after => :after_transition + end + + event :left_open, :before => :before_event, :after => :after_event do + transitions :to => :open, :from => :closed + end + end + + def log(text) + @data << text + puts text if @log + end + + def aasm_write_state(*args); log("aasm_write_state(#{args.inspect})"); true; end + + def before_enter_open; log('before_enter_open'); end + def enter_open; log('enter_open'); end + def before_exit_open; log('before_exit_open'); end + def after_enter_open; log('after_enter_open'); end + def exit_open; log('exit_open'); end + def after_exit_open; log('after_exit_open'); end + + def before_enter_closed; log('before_enter_closed'); end + def enter_closed; log('enter_closed'); end + def before_exit_closed; log('before_exit_closed'); end + def exit_closed; log('exit_closed'); end + def after_enter_closed; log('after_enter_closed'); end + def after_exit_closed; log('after_exit_closed'); end + + def event_guard; log('event_guard'); !@fail_event_guard; end + def transition_guard; log('transition_guard'); !@fail_transition_guard; end + + def after_transition; log('after_transition'); end + + def before_event; log('before_event'); end + def after_event; log('after_event'); end + end +end diff --git a/spec/unit/callback_multiple_spec.rb b/spec/unit/callback_multiple_spec.rb new file mode 100644 index 0000000..98f849f --- /dev/null +++ b/spec/unit/callback_multiple_spec.rb @@ -0,0 +1,295 @@ +require 'spec_helper' +Dir[File.dirname(__FILE__) + "/../models/callbacks/*.rb"].sort.each { |f| require File.expand_path(f) } + +describe 'callbacks for the new DSL' do + + it "be called in order" do + show_debug_log = false + + callback = Callbacks::BasicMultiple.new(:log => show_debug_log) + callback.aasm(:left).current_state + + unless show_debug_log + expect(callback).to receive(:before_event).once.ordered + expect(callback).to receive(:event_guard).once.ordered.and_return(true) + expect(callback).to receive(:transition_guard).once.ordered.and_return(true) + expect(callback).to receive(:before_exit_open).once.ordered # these should be before the state changes + expect(callback).to receive(:exit_open).once.ordered + # expect(callback).to receive(:event_guard).once.ordered.and_return(true) + # expect(callback).to receive(:transition_guard).once.ordered.and_return(true) + expect(callback).to receive(:after_transition).once.ordered + expect(callback).to receive(:before_enter_closed).once.ordered + expect(callback).to receive(:enter_closed).once.ordered + expect(callback).to receive(:aasm_write_state).with(:closed, :left).once.ordered.and_return(true) # this is when the state changes + expect(callback).to receive(:after_exit_open).once.ordered # these should be after the state changes + expect(callback).to receive(:after_enter_closed).once.ordered + expect(callback).to receive(:after_event).once.ordered + end + + # puts "------- close!" + callback.left_close! + end + + it "does not run any state callback if the event guard fails" do + callback = Callbacks::BasicMultiple.new(:log => false) + callback.aasm(:left).current_state + + expect(callback).to receive(:before_event).once.ordered + expect(callback).to receive(:event_guard).once.ordered.and_return(false) + expect(callback).to_not receive(:transition_guard) + expect(callback).to_not receive(:before_exit_open) + expect(callback).to_not receive(:exit_open) + expect(callback).to_not receive(:after_transition) + expect(callback).to_not receive(:before_enter_closed) + expect(callback).to_not receive(:enter_closed) + expect(callback).to_not receive(:aasm_write_state) + expect(callback).to_not receive(:after_exit_open) + expect(callback).to_not receive(:after_enter_closed) + expect(callback).to_not receive(:after_event) + + expect { + callback.left_close! + }.to raise_error(AASM::InvalidTransition) + end + + # it "it handles private callback methods as well" do + # show_debug_log = false + + # callback = Callbacks::PrivateMethod.new(:log => show_debug_log) + # callback.aasm.current_state + + # # puts "------- close!" + # expect { + # callback.close! + # }.to_not raise_error + # end + + context "if the transition guard fails" do + it "does not run any state callback if guard is defined inline" do + show_debug_log = false + callback = Callbacks::BasicMultiple.new(:log => show_debug_log, :fail_transition_guard => true) + callback.aasm(:left).current_state + + unless show_debug_log + expect(callback).to receive(:before_event).once.ordered + expect(callback).to receive(:event_guard).once.ordered.and_return(true) + expect(callback).to receive(:transition_guard).once.ordered.and_return(false) + expect(callback).to_not receive(:before_exit_open) + expect(callback).to_not receive(:exit_open) + expect(callback).to_not receive(:after_transition) + expect(callback).to_not receive(:before_enter_closed) + expect(callback).to_not receive(:enter_closed) + expect(callback).to_not receive(:aasm_write_state) + expect(callback).to_not receive(:after_exit_open) + expect(callback).to_not receive(:after_enter_closed) + expect(callback).to_not receive(:after_event) + end + + expect { + callback.left_close! + }.to raise_error(AASM::InvalidTransition) + end + + # it "does not run transition_guard twice for multiple permitted transitions" do + # show_debug_log = false + # callback = Callbacks::MultipleTransitionsTransitionGuard.new(:log => show_debug_log, :fail_transition_guard => true) + # callback.aasm.current_state + + # unless show_debug_log + # expect(callback).to receive(:before).once.ordered + # expect(callback).to receive(:event_guard).once.ordered.and_return(true) + # expect(callback).to receive(:transition_guard).once.ordered.and_return(false) + # expect(callback).to receive(:event_guard).once.ordered.and_return(true) + # expect(callback).to receive(:before_exit_open).once.ordered + # expect(callback).to receive(:exit_open).once.ordered + # expect(callback).to receive(:aasm_write_state).once.ordered.and_return(true) # this is when the state changes + # expect(callback).to receive(:after_exit_open).once.ordered + # expect(callback).to receive(:after).once.ordered + + # expect(callback).to_not receive(:transitioning) + # expect(callback).to_not receive(:before_enter_closed) + # expect(callback).to_not receive(:enter_closed) + # expect(callback).to_not receive(:after_enter_closed) + # end + + # callback.close! + # expect(callback.aasm.current_state).to eql :failed + # end + + # it "does not run any state callback if guard is defined with block" do + # callback = Callbacks::GuardWithinBlock.new #(:log => true, :fail_transition_guard => true) + # callback.aasm.current_state + + # expect(callback).to receive(:before).once.ordered + # expect(callback).to receive(:event_guard).once.ordered.and_return(true) + # expect(callback).to receive(:transition_guard).once.ordered.and_return(false) + # expect(callback).to_not receive(:before_exit_open) + # expect(callback).to_not receive(:exit_open) + # expect(callback).to_not receive(:transitioning) + # expect(callback).to_not receive(:before_enter_closed) + # expect(callback).to_not receive(:enter_closed) + # expect(callback).to_not receive(:aasm_write_state) + # expect(callback).to_not receive(:after_exit_open) + # expect(callback).to_not receive(:after_enter_closed) + # expect(callback).to_not receive(:after) + + # expect { + # callback.close! + # }.to raise_error(AASM::InvalidTransition) + # end + end + + # it "should properly pass arguments" do + # cb = Callbacks::WithArgs.new(:log => false) + # cb.aasm.current_state + + # cb.reset_data + # cb.close!(:arg1, :arg2) + # expect(cb.data).to eql 'before(:arg1,:arg2) before_exit_open(:arg1,:arg2) transition_proc(:arg1,:arg2) before_enter_closed(:arg1,:arg2) aasm_write_state after_exit_open(:arg1,:arg2) after_enter_closed(:arg1,:arg2) after(:arg1,:arg2)' + # end + + # it "should call the callbacks given the to-state as argument" do + # cb = Callbacks::WithStateArg.new + # expect(cb).to receive(:before_method).with(:arg1).once.ordered + # expect(cb).to receive(:transition_method).never + # expect(cb).to receive(:transition_method2).with(:arg1).once.ordered + # expect(cb).to receive(:after_method).with(:arg1).once.ordered + # cb.close!(:out_to_lunch, :arg1) + + # cb = Callbacks::WithStateArg.new + # some_object = double('some object') + # expect(cb).to receive(:before_method).with(some_object).once.ordered + # expect(cb).to receive(:transition_method2).with(some_object).once.ordered + # expect(cb).to receive(:after_method).with(some_object).once.ordered + # cb.close!(:out_to_lunch, some_object) + # end + + # it "should call the proper methods just with arguments" do + # cb = Callbacks::WithStateArg.new + # expect(cb).to receive(:before_method).with(:arg1).once.ordered + # expect(cb).to receive(:transition_method).with(:arg1).once.ordered + # expect(cb).to receive(:transition_method).never + # expect(cb).to receive(:after_method).with(:arg1).once.ordered + # cb.close!(:arg1) + + # cb = Callbacks::WithStateArg.new + # some_object = double('some object') + # expect(cb).to receive(:before_method).with(some_object).once.ordered + # expect(cb).to receive(:transition_method).with(some_object).once.ordered + # expect(cb).to receive(:transition_method).never + # expect(cb).to receive(:after_method).with(some_object).once.ordered + # cb.close!(some_object) + # end +end + +# describe 'event callbacks' do +# describe "with an error callback defined" do +# before do +# class Foo +# # this hack is needed to allow testing of parameters, since RSpec +# # destroys a method's arity when mocked +# attr_accessor :data + +# aasm do +# event :safe_close, :success => :success_callback, :error => :error_callback do +# transitions :to => :closed, :from => [:open] +# end +# end +# end + +# @foo = Foo.new +# end + +# context "error_callback defined" do +# it "should run error_callback if an exception is raised" do +# def @foo.error_callback(e) +# @data = [e] +# end + +# allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) + +# @foo.safe_close! +# expect(@foo.data).to eql [e] +# end + +# it "should run error_callback without parameters if callback does not support any" do +# def @foo.error_callback(e) +# @data = [] +# end + +# allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) + +# @foo.safe_close!('arg1', 'arg2') +# expect(@foo.data).to eql [] +# end + +# it "should run error_callback with parameters if callback supports them" do +# def @foo.error_callback(e, arg1, arg2) +# @data = [arg1, arg2] +# end + +# allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) + +# @foo.safe_close!('arg1', 'arg2') +# expect(@foo.data).to eql ['arg1', 'arg2'] +# end +# end + +# it "should raise NoMethodError if exception is raised and error_callback is declared but not defined" do +# allow(@foo).to receive(:before_enter).and_raise(StandardError) +# expect{@foo.safe_close!}.to raise_error(NoMethodError) +# end + +# it "should propagate an error if no error callback is declared" do +# allow(@foo).to receive(:before_enter).and_raise("Cannot enter safe") +# expect{@foo.close!}.to raise_error(StandardError, "Cannot enter safe") +# end +# end + +# describe "with aasm_event_fired defined" do +# before do +# @foo = Foo.new +# def @foo.aasm_event_fired(event, from, to); end +# end + +# it 'should call it for successful bang fire' do +# expect(@foo).to receive(:aasm_event_fired).with(:close, :open, :closed) +# @foo.close! +# end + +# it 'should call it for successful non-bang fire' do +# expect(@foo).to receive(:aasm_event_fired) +# @foo.close +# end + +# it 'should not call it for failing bang fire' do +# allow(@foo.aasm).to receive(:set_current_state_with_persistence).and_return(false) +# expect(@foo).not_to receive(:aasm_event_fired) +# @foo.close! +# end +# end + +# describe "with aasm_event_failed defined" do +# before do +# @foo = Foo.new +# def @foo.aasm_event_failed(event, from); end +# end + +# it 'should call it when transition failed for bang fire' do +# expect(@foo).to receive(:aasm_event_failed).with(:null, :open) +# expect {@foo.null!}.to raise_error(AASM::InvalidTransition) +# end + +# it 'should call it when transition failed for non-bang fire' do +# expect(@foo).to receive(:aasm_event_failed).with(:null, :open) +# expect {@foo.null}.to raise_error(AASM::InvalidTransition) +# end + +# it 'should not call it if persist fails for bang fire' do +# allow(@foo.aasm).to receive(:set_current_state_with_persistence).and_return(false) +# expect(@foo).to receive(:aasm_event_failed) +# @foo.close! +# end +# end + +# end From 69997ffec875d8d59758e01d2f08f8489ec86bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 26 May 2015 21:19:54 +1200 Subject: [PATCH 10/45] little changes --- spec/models/callbacks/{with_state_args.rb => with_state_arg.rb} | 0 spec/unit/callbacks_spec.rb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename spec/models/callbacks/{with_state_args.rb => with_state_arg.rb} (100%) diff --git a/spec/models/callbacks/with_state_args.rb b/spec/models/callbacks/with_state_arg.rb similarity index 100% rename from spec/models/callbacks/with_state_args.rb rename to spec/models/callbacks/with_state_arg.rb diff --git a/spec/unit/callbacks_spec.rb b/spec/unit/callbacks_spec.rb index 9d60e7d..1cee0a6 100644 --- a/spec/unit/callbacks_spec.rb +++ b/spec/unit/callbacks_spec.rb @@ -52,7 +52,7 @@ describe 'callbacks for the new DSL' do }.to raise_error(AASM::InvalidTransition) end - it "it handles private callback methods as well" do + it "handles private callback methods as well" do show_debug_log = false callback = Callbacks::PrivateMethod.new(:log => show_debug_log) From d0172e986336629e13011c0f77b070755f3ebbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 26 May 2015 21:21:04 +1200 Subject: [PATCH 11/45] more tests for multiple state machines (callbacks, rest) --- .../callbacks/guard_within_block_multiple.rb | 66 ++++ ...e_transitions_transition_guard_multiple.rb | 65 ++++ .../callbacks/private_method_multiple.rb | 44 +++ spec/models/callbacks/with_args_multiple.rb | 61 +++ .../callbacks/with_state_arg_multiple.rb | 26 ++ spec/models/foo_multiple.rb | 35 ++ spec/unit/callback_multiple_spec.rb | 352 +++++++++--------- 7 files changed, 473 insertions(+), 176 deletions(-) create mode 100644 spec/models/callbacks/guard_within_block_multiple.rb create mode 100644 spec/models/callbacks/multiple_transitions_transition_guard_multiple.rb create mode 100644 spec/models/callbacks/private_method_multiple.rb create mode 100644 spec/models/callbacks/with_args_multiple.rb create mode 100644 spec/models/callbacks/with_state_arg_multiple.rb create mode 100644 spec/models/foo_multiple.rb diff --git a/spec/models/callbacks/guard_within_block_multiple.rb b/spec/models/callbacks/guard_within_block_multiple.rb new file mode 100644 index 0000000..cb817f7 --- /dev/null +++ b/spec/models/callbacks/guard_within_block_multiple.rb @@ -0,0 +1,66 @@ +module Callbacks + class GuardWithinBlockMultiple + include AASM + + def initialize(options={}) + @fail_event_guard = options[:fail_event_guard] + @fail_transition_guard = options[:fail_transition_guard] + @log = options[:log] + end + + aasm(:left) do + state :open, :initial => true, + :before_enter => :before_enter_open, + :enter => :enter_open, + :after_enter => :after_enter_open, + :before_exit => :before_exit_open, + :exit => :exit_open, + :after_exit => :after_exit_open + + state :closed, + :before_enter => :before_enter_closed, + :enter => :enter_closed, + :after_enter => :after_enter_closed, + :before_exit => :before_exit_closed, + :exit => :exit_closed, + :after_exit => :after_exit_closed + + event :close, :before => :before, :after => :after, :guard => :event_guard do + transitions :to => :closed, :from => [:open], :after => :transitioning do + guard do + transition_guard + end + end + end + + event :open, :before => :before, :after => :after do + transitions :to => :open, :from => :closed + end + end + + def log(text) + puts text if @log + end + + def before_enter_open; log('before_enter_open'); end + def enter_open; log('enter_open'); end + def before_exit_open; log('before_exit_open'); end + def after_enter_open; log('after_enter_open'); end + def exit_open; log('exit_open'); end + def after_exit_open; log('after_exit_open'); end + + def before_enter_closed; log('before_enter_closed'); end + def enter_closed; log('enter_closed'); end + def before_exit_closed; log('before_exit_closed'); end + def exit_closed; log('exit_closed'); end + def after_enter_closed; log('after_enter_closed'); end + def after_exit_closed; log('after_exit_closed'); end + + def event_guard; log('event_guard'); !@fail_event_guard; end + def transition_guard; log('transition_guard'); !@fail_transition_guard; end + def transitioning; log('transitioning'); end + + def before; log('before'); end + def after; log('after'); end + end +end diff --git a/spec/models/callbacks/multiple_transitions_transition_guard_multiple.rb b/spec/models/callbacks/multiple_transitions_transition_guard_multiple.rb new file mode 100644 index 0000000..5c95df4 --- /dev/null +++ b/spec/models/callbacks/multiple_transitions_transition_guard_multiple.rb @@ -0,0 +1,65 @@ +module Callbacks + class MultipleTransitionsTransitionGuardMultiple + include AASM + + def initialize(options={}) + @fail_event_guard = options[:fail_event_guard] + @fail_transition_guard = options[:fail_transition_guard] + @log = options[:log] + end + + aasm(:left) do + state :open, :initial => true, + :before_enter => :before_enter_open, + :enter => :enter_open, + :after_enter => :after_enter_open, + :before_exit => :before_exit_open, + :exit => :exit_open, + :after_exit => :after_exit_open + + state :closed, + :before_enter => :before_enter_closed, + :enter => :enter_closed, + :after_enter => :after_enter_closed, + :before_exit => :before_exit_closed, + :exit => :exit_closed, + :after_exit => :after_exit_closed + + state :failed + + event :close, :before => :before, :after => :after, :guard => :event_guard do + transitions :to => :closed, :from => [:open], :guard => :transition_guard, :after => :transitioning + transitions :to => :failed, :from => [:open] + end + + event :open, :before => :before, :after => :after do + transitions :to => :open, :from => :closed + end + end + + def log(text) + puts text if @log + end + + def before_enter_open; log('before_enter_open'); end + def enter_open; log('enter_open'); end + def before_exit_open; log('before_exit_open'); end + def after_enter_open; log('after_enter_open'); end + def exit_open; log('exit_open'); end + def after_exit_open; log('after_exit_open'); end + + def before_enter_closed; log('before_enter_closed'); end + def enter_closed; log('enter_closed'); end + def before_exit_closed; log('before_exit_closed'); end + def exit_closed; log('exit_closed'); end + def after_enter_closed; log('after_enter_closed'); end + def after_exit_closed; log('after_exit_closed'); end + + def event_guard; log('event_guard'); !@fail_event_guard; end + def transition_guard; log('transition_guard'); !@fail_transition_guard; end + def transitioning; log('transitioning'); end + + def before; log('before'); end + def after; log('after'); end + end +end diff --git a/spec/models/callbacks/private_method_multiple.rb b/spec/models/callbacks/private_method_multiple.rb new file mode 100644 index 0000000..24d963d --- /dev/null +++ b/spec/models/callbacks/private_method_multiple.rb @@ -0,0 +1,44 @@ +module Callbacks + class PrivateMethodMultiple + include AASM + + def initialize(options={}) + @fail_event_guard = options[:fail_event_guard] + @fail_transition_guard = options[:fail_transition_guard] + @log = options[:log] + reset_data + end + + def reset_data + @data = [] + end + + def data + @data.join(' ') + end + + aasm(:left) do + state :open, :initial => true + state :closed + + event :close, :after => :after_event do + transitions :to => :closed, :from => [:open] + end + + event :open, :after => :after_event do + transitions :to => :open, :from => :closed + end + end + + def log(text) + @data << text + puts text if @log + end + + def aasm_write_state(*args); log('aasm_write_state'); true; end + + private + + def after_event; log('after_event'); end + end +end diff --git a/spec/models/callbacks/with_args_multiple.rb b/spec/models/callbacks/with_args_multiple.rb new file mode 100644 index 0000000..6ec9fc6 --- /dev/null +++ b/spec/models/callbacks/with_args_multiple.rb @@ -0,0 +1,61 @@ +module Callbacks + class WithArgsMultiple + include AASM + + def initialize(options={}) + @log = options[:log] + reset_data + end + + def reset_data + @data = [] + end + + def data + @data.join(' ') + end + + aasm(:left) do + state :open, :initial => true, + :before_enter => :before_enter_open, + :after_enter => :after_enter_open, + :before_exit => :before_exit_open, + :after_exit => :after_exit_open + + state :closed, + :before_enter => :before_enter_closed, + :after_enter => :after_enter_closed, + :before_exit => :before_exit_closed, + :after_exit => :after_exit_closed + + event :close, :before => :before, :after => :after do + transitions :to => :closed, :from => [:open], :after => :transition_proc + end + + event :open, :before => :before, :after => :after do + transitions :to => :open, :from => :closed + end + end + + def log(text) + @data << text + puts text if @log + end + + def aasm_write_state(*args); log('aasm_write_state'); true; end + + def before_enter_open(*args); log("before_enter_open(#{args.map(&:inspect).join(',')})"); end + def before_exit_open(*args); log("before_exit_open(#{args.map(&:inspect).join(',')})"); end + def after_enter_open(*args); log("after_enter_open(#{args.map(&:inspect).join(',')})"); end + def after_exit_open(*args); log("after_exit_open(#{args.map(&:inspect).join(',')})"); end + + def before_enter_closed(*args); log("before_enter_closed(#{args.map(&:inspect).join(',')})"); end + def before_exit_closed(*args); log("before_enter_closed(#{args.map(&:inspect).join(',')})"); end + def after_enter_closed(*args); log("after_enter_closed(#{args.map(&:inspect).join(',')})"); end + def after_exit_closed(*args); log("after_exit_closed(#{args.map(&:inspect).join(',')})"); end + + def before(arg1, *args); log("before(#{arg1.inspect},#{args.map(&:inspect).join(',')})"); end + def transition_proc(arg1, arg2); log("transition_proc(#{arg1.inspect},#{arg2.inspect})"); end + def after(*args); log("after(#{args.map(&:inspect).join(',')})"); end + end +end diff --git a/spec/models/callbacks/with_state_arg_multiple.rb b/spec/models/callbacks/with_state_arg_multiple.rb new file mode 100644 index 0000000..29b9fed --- /dev/null +++ b/spec/models/callbacks/with_state_arg_multiple.rb @@ -0,0 +1,26 @@ +module Callbacks + class WithStateArgMultiple + + include AASM + + aasm(:left) do + state :open, :inital => true + state :closed + state :out_to_lunch + + event :close, :before => :before_method, :after => :after_method do + transitions :to => :closed, :from => [:open], :after => :transition_method + transitions :to => :out_to_lunch, :from => [:open], :after => :transition_method2 + end + end + + def before_method(arg); end + + def after_method(arg); end + + def transition_method(arg); end + + def transition_method2(arg); end + + end +end diff --git a/spec/models/foo_multiple.rb b/spec/models/foo_multiple.rb new file mode 100644 index 0000000..c337dcb --- /dev/null +++ b/spec/models/foo_multiple.rb @@ -0,0 +1,35 @@ +class FooMultiple + include AASM + aasm(:left) do + state :open, :initial => true, :before_exit => :before_exit + state :closed, :before_enter => :before_enter + state :final + + event :close, :success => :success_callback do + transitions :from => [:open], :to => [:closed] + end + + event :null do + transitions :from => [:open], :to => [:closed, :final], :guard => :always_false + end + end + + def always_false + false + end + + def success_callback + end + + def before_enter + end + def before_exit + end +end + +class FooTwoMultiple < FooMultiple + include AASM + aasm(:left) do + state :foo + end +end diff --git a/spec/unit/callback_multiple_spec.rb b/spec/unit/callback_multiple_spec.rb index 98f849f..9ffb41d 100644 --- a/spec/unit/callback_multiple_spec.rb +++ b/spec/unit/callback_multiple_spec.rb @@ -52,17 +52,17 @@ describe 'callbacks for the new DSL' do }.to raise_error(AASM::InvalidTransition) end - # it "it handles private callback methods as well" do - # show_debug_log = false + it "handles private callback methods as well" do + show_debug_log = false - # callback = Callbacks::PrivateMethod.new(:log => show_debug_log) - # callback.aasm.current_state + callback = Callbacks::PrivateMethodMultiple.new(:log => show_debug_log) + callback.aasm(:left).current_state - # # puts "------- close!" - # expect { - # callback.close! - # }.to_not raise_error - # end + # puts "------- close!" + expect { + callback.close! + }.to_not raise_error + end context "if the transition guard fails" do it "does not run any state callback if guard is defined inline" do @@ -90,206 +90,206 @@ describe 'callbacks for the new DSL' do }.to raise_error(AASM::InvalidTransition) end - # it "does not run transition_guard twice for multiple permitted transitions" do - # show_debug_log = false - # callback = Callbacks::MultipleTransitionsTransitionGuard.new(:log => show_debug_log, :fail_transition_guard => true) - # callback.aasm.current_state + it "does not run transition_guard twice for multiple permitted transitions" do + show_debug_log = false + callback = Callbacks::MultipleTransitionsTransitionGuardMultiple.new(:log => show_debug_log, :fail_transition_guard => true) + callback.aasm(:left).current_state - # unless show_debug_log - # expect(callback).to receive(:before).once.ordered - # expect(callback).to receive(:event_guard).once.ordered.and_return(true) - # expect(callback).to receive(:transition_guard).once.ordered.and_return(false) - # expect(callback).to receive(:event_guard).once.ordered.and_return(true) - # expect(callback).to receive(:before_exit_open).once.ordered - # expect(callback).to receive(:exit_open).once.ordered - # expect(callback).to receive(:aasm_write_state).once.ordered.and_return(true) # this is when the state changes - # expect(callback).to receive(:after_exit_open).once.ordered - # expect(callback).to receive(:after).once.ordered + unless show_debug_log + expect(callback).to receive(:before).once.ordered + expect(callback).to receive(:event_guard).once.ordered.and_return(true) + expect(callback).to receive(:transition_guard).once.ordered.and_return(false) + expect(callback).to receive(:event_guard).once.ordered.and_return(true) + expect(callback).to receive(:before_exit_open).once.ordered + expect(callback).to receive(:exit_open).once.ordered + expect(callback).to receive(:aasm_write_state).once.ordered.and_return(true) # this is when the state changes + expect(callback).to receive(:after_exit_open).once.ordered + expect(callback).to receive(:after).once.ordered - # expect(callback).to_not receive(:transitioning) - # expect(callback).to_not receive(:before_enter_closed) - # expect(callback).to_not receive(:enter_closed) - # expect(callback).to_not receive(:after_enter_closed) - # end + expect(callback).to_not receive(:transitioning) + expect(callback).to_not receive(:before_enter_closed) + expect(callback).to_not receive(:enter_closed) + expect(callback).to_not receive(:after_enter_closed) + end - # callback.close! - # expect(callback.aasm.current_state).to eql :failed - # end + callback.close! + expect(callback.aasm(:left).current_state).to eql :failed + end - # it "does not run any state callback if guard is defined with block" do - # callback = Callbacks::GuardWithinBlock.new #(:log => true, :fail_transition_guard => true) - # callback.aasm.current_state + it "does not run any state callback if guard is defined with block" do + callback = Callbacks::GuardWithinBlockMultiple.new #(:log => true, :fail_transition_guard => true) + callback.aasm(:left).current_state - # expect(callback).to receive(:before).once.ordered - # expect(callback).to receive(:event_guard).once.ordered.and_return(true) - # expect(callback).to receive(:transition_guard).once.ordered.and_return(false) - # expect(callback).to_not receive(:before_exit_open) - # expect(callback).to_not receive(:exit_open) - # expect(callback).to_not receive(:transitioning) - # expect(callback).to_not receive(:before_enter_closed) - # expect(callback).to_not receive(:enter_closed) - # expect(callback).to_not receive(:aasm_write_state) - # expect(callback).to_not receive(:after_exit_open) - # expect(callback).to_not receive(:after_enter_closed) - # expect(callback).to_not receive(:after) + expect(callback).to receive(:before).once.ordered + expect(callback).to receive(:event_guard).once.ordered.and_return(true) + expect(callback).to receive(:transition_guard).once.ordered.and_return(false) + expect(callback).to_not receive(:before_exit_open) + expect(callback).to_not receive(:exit_open) + expect(callback).to_not receive(:transitioning) + expect(callback).to_not receive(:before_enter_closed) + expect(callback).to_not receive(:enter_closed) + expect(callback).to_not receive(:aasm_write_state) + expect(callback).to_not receive(:after_exit_open) + expect(callback).to_not receive(:after_enter_closed) + expect(callback).to_not receive(:after) - # expect { - # callback.close! - # }.to raise_error(AASM::InvalidTransition) - # end + expect { + callback.close! + }.to raise_error(AASM::InvalidTransition) + end end - # it "should properly pass arguments" do - # cb = Callbacks::WithArgs.new(:log => false) - # cb.aasm.current_state + it "should properly pass arguments" do + cb = Callbacks::WithArgsMultiple.new(:log => false) + cb.aasm(:left).current_state - # cb.reset_data - # cb.close!(:arg1, :arg2) - # expect(cb.data).to eql 'before(:arg1,:arg2) before_exit_open(:arg1,:arg2) transition_proc(:arg1,:arg2) before_enter_closed(:arg1,:arg2) aasm_write_state after_exit_open(:arg1,:arg2) after_enter_closed(:arg1,:arg2) after(:arg1,:arg2)' - # end + cb.reset_data + cb.close!(:arg1, :arg2) + expect(cb.data).to eql 'before(:arg1,:arg2) before_exit_open(:arg1,:arg2) transition_proc(:arg1,:arg2) before_enter_closed(:arg1,:arg2) aasm_write_state after_exit_open(:arg1,:arg2) after_enter_closed(:arg1,:arg2) after(:arg1,:arg2)' + end - # it "should call the callbacks given the to-state as argument" do - # cb = Callbacks::WithStateArg.new - # expect(cb).to receive(:before_method).with(:arg1).once.ordered - # expect(cb).to receive(:transition_method).never - # expect(cb).to receive(:transition_method2).with(:arg1).once.ordered - # expect(cb).to receive(:after_method).with(:arg1).once.ordered - # cb.close!(:out_to_lunch, :arg1) + it "should call the callbacks given the to-state as argument" do + cb = Callbacks::WithStateArgMultiple.new + expect(cb).to receive(:before_method).with(:arg1).once.ordered + expect(cb).to receive(:transition_method).never + expect(cb).to receive(:transition_method2).with(:arg1).once.ordered + expect(cb).to receive(:after_method).with(:arg1).once.ordered + cb.close!(:out_to_lunch, :arg1) - # cb = Callbacks::WithStateArg.new - # some_object = double('some object') - # expect(cb).to receive(:before_method).with(some_object).once.ordered - # expect(cb).to receive(:transition_method2).with(some_object).once.ordered - # expect(cb).to receive(:after_method).with(some_object).once.ordered - # cb.close!(:out_to_lunch, some_object) - # end + cb = Callbacks::WithStateArgMultiple.new + some_object = double('some object') + expect(cb).to receive(:before_method).with(some_object).once.ordered + expect(cb).to receive(:transition_method2).with(some_object).once.ordered + expect(cb).to receive(:after_method).with(some_object).once.ordered + cb.close!(:out_to_lunch, some_object) + end - # it "should call the proper methods just with arguments" do - # cb = Callbacks::WithStateArg.new - # expect(cb).to receive(:before_method).with(:arg1).once.ordered - # expect(cb).to receive(:transition_method).with(:arg1).once.ordered - # expect(cb).to receive(:transition_method).never - # expect(cb).to receive(:after_method).with(:arg1).once.ordered - # cb.close!(:arg1) + it "should call the proper methods just with arguments" do + cb = Callbacks::WithStateArgMultiple.new + expect(cb).to receive(:before_method).with(:arg1).once.ordered + expect(cb).to receive(:transition_method).with(:arg1).once.ordered + expect(cb).to receive(:transition_method).never + expect(cb).to receive(:after_method).with(:arg1).once.ordered + cb.close!(:arg1) - # cb = Callbacks::WithStateArg.new - # some_object = double('some object') - # expect(cb).to receive(:before_method).with(some_object).once.ordered - # expect(cb).to receive(:transition_method).with(some_object).once.ordered - # expect(cb).to receive(:transition_method).never - # expect(cb).to receive(:after_method).with(some_object).once.ordered - # cb.close!(some_object) - # end -end + cb = Callbacks::WithStateArgMultiple.new + some_object = double('some object') + expect(cb).to receive(:before_method).with(some_object).once.ordered + expect(cb).to receive(:transition_method).with(some_object).once.ordered + expect(cb).to receive(:transition_method).never + expect(cb).to receive(:after_method).with(some_object).once.ordered + cb.close!(some_object) + end +end # callbacks for the new DSL -# describe 'event callbacks' do -# describe "with an error callback defined" do -# before do -# class Foo -# # this hack is needed to allow testing of parameters, since RSpec -# # destroys a method's arity when mocked -# attr_accessor :data +describe 'event callbacks' do + describe "with an error callback defined" do + before do + class FooMultiple + # this hack is needed to allow testing of parameters, since RSpec + # destroys a method's arity when mocked + attr_accessor :data -# aasm do -# event :safe_close, :success => :success_callback, :error => :error_callback do -# transitions :to => :closed, :from => [:open] -# end -# end -# end + aasm(:left) do + event :safe_close, :success => :success_callback, :error => :error_callback do + transitions :to => :closed, :from => [:open] + end + end + end -# @foo = Foo.new -# end + @foo = FooMultiple.new + end -# context "error_callback defined" do -# it "should run error_callback if an exception is raised" do -# def @foo.error_callback(e) -# @data = [e] -# end + context "error_callback defined" do + it "should run error_callback if an exception is raised" do + def @foo.error_callback(e) + @data = [e] + end -# allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) + allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) -# @foo.safe_close! -# expect(@foo.data).to eql [e] -# end + @foo.safe_close! + expect(@foo.data).to eql [e] + end -# it "should run error_callback without parameters if callback does not support any" do -# def @foo.error_callback(e) -# @data = [] -# end + it "should run error_callback without parameters if callback does not support any" do + def @foo.error_callback(e) + @data = [] + end -# allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) + allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) -# @foo.safe_close!('arg1', 'arg2') -# expect(@foo.data).to eql [] -# end + @foo.safe_close!('arg1', 'arg2') + expect(@foo.data).to eql [] + end -# it "should run error_callback with parameters if callback supports them" do -# def @foo.error_callback(e, arg1, arg2) -# @data = [arg1, arg2] -# end + it "should run error_callback with parameters if callback supports them" do + def @foo.error_callback(e, arg1, arg2) + @data = [arg1, arg2] + end -# allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) + allow(@foo).to receive(:before_enter).and_raise(e = StandardError.new) -# @foo.safe_close!('arg1', 'arg2') -# expect(@foo.data).to eql ['arg1', 'arg2'] -# end -# end + @foo.safe_close!('arg1', 'arg2') + expect(@foo.data).to eql ['arg1', 'arg2'] + end + end -# it "should raise NoMethodError if exception is raised and error_callback is declared but not defined" do -# allow(@foo).to receive(:before_enter).and_raise(StandardError) -# expect{@foo.safe_close!}.to raise_error(NoMethodError) -# end + it "should raise NoMethodError if exception is raised and error_callback is declared but not defined" do + allow(@foo).to receive(:before_enter).and_raise(StandardError) + expect{@foo.safe_close!}.to raise_error(NoMethodError) + end -# it "should propagate an error if no error callback is declared" do -# allow(@foo).to receive(:before_enter).and_raise("Cannot enter safe") -# expect{@foo.close!}.to raise_error(StandardError, "Cannot enter safe") -# end -# end + it "should propagate an error if no error callback is declared" do + allow(@foo).to receive(:before_enter).and_raise("Cannot enter safe") + expect{@foo.close!}.to raise_error(StandardError, "Cannot enter safe") + end + end -# describe "with aasm_event_fired defined" do -# before do -# @foo = Foo.new -# def @foo.aasm_event_fired(event, from, to); end -# end + describe "with aasm_event_fired defined" do + before do + @foo = FooMultiple.new + def @foo.aasm_event_fired(event, from, to); end + end -# it 'should call it for successful bang fire' do -# expect(@foo).to receive(:aasm_event_fired).with(:close, :open, :closed) -# @foo.close! -# end + it 'should call it for successful bang fire' do + expect(@foo).to receive(:aasm_event_fired).with(:close, :open, :closed) + @foo.close! + end -# it 'should call it for successful non-bang fire' do -# expect(@foo).to receive(:aasm_event_fired) -# @foo.close -# end + it 'should call it for successful non-bang fire' do + expect(@foo).to receive(:aasm_event_fired) + @foo.close + end -# it 'should not call it for failing bang fire' do -# allow(@foo.aasm).to receive(:set_current_state_with_persistence).and_return(false) -# expect(@foo).not_to receive(:aasm_event_fired) -# @foo.close! -# end -# end + it 'should not call it for failing bang fire' do + allow(@foo.aasm(:left)).to receive(:set_current_state_with_persistence).and_return(false) + expect(@foo).not_to receive(:aasm_event_fired) + @foo.close! + end + end -# describe "with aasm_event_failed defined" do -# before do -# @foo = Foo.new -# def @foo.aasm_event_failed(event, from); end -# end + describe "with aasm_event_failed defined" do + before do + @foo = FooMultiple.new + def @foo.aasm_event_failed(event, from); end + end -# it 'should call it when transition failed for bang fire' do -# expect(@foo).to receive(:aasm_event_failed).with(:null, :open) -# expect {@foo.null!}.to raise_error(AASM::InvalidTransition) -# end + it 'should call it when transition failed for bang fire' do + expect(@foo).to receive(:aasm_event_failed).with(:null, :open) + expect {@foo.null!}.to raise_error(AASM::InvalidTransition) + end -# it 'should call it when transition failed for non-bang fire' do -# expect(@foo).to receive(:aasm_event_failed).with(:null, :open) -# expect {@foo.null}.to raise_error(AASM::InvalidTransition) -# end + it 'should call it when transition failed for non-bang fire' do + expect(@foo).to receive(:aasm_event_failed).with(:null, :open) + expect {@foo.null}.to raise_error(AASM::InvalidTransition) + end -# it 'should not call it if persist fails for bang fire' do -# allow(@foo.aasm).to receive(:set_current_state_with_persistence).and_return(false) -# expect(@foo).to receive(:aasm_event_failed) -# @foo.close! -# end -# end + it 'should not call it if persist fails for bang fire' do + allow(@foo.aasm(:left)).to receive(:set_current_state_with_persistence).and_return(false) + expect(@foo).to receive(:aasm_event_failed) + @foo.close! + end + end -# end +end # event callbacks From 757465a49c0e45bac7cccea72428af1802d7e355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 26 May 2015 21:26:36 +1200 Subject: [PATCH 12/45] plan changes for 4.2 --- PLANNED_CHANGES.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index e74feae..336fac6 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -1,7 +1,5 @@ -# Planned changes for AASM 4.1 +# Planned changes for AASM 4.2 - * remove support for `:on_transition` callback - -# Planned changes for AASM 4.0 - - * nothing left + * add support for multiple state machines per class + * events are chained + * :default state machine not provided (if state machine name is used) From f17503f5c5a39362b8cb94e223e7cd1ed39f7ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Thu, 25 Jun 2015 17:49:24 +1200 Subject: [PATCH 13/45] multiple state machine per class has been moved to version 4.3 --- PLANNED_CHANGES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 336fac6..dbc9dce 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -1,5 +1,7 @@ -# Planned changes for AASM 4.2 +# Planned changes + +## version 4.3 * add support for multiple state machines per class * events are chained - * :default state machine not provided (if state machine name is used) + * :default state machine won't be provided, if state machine name is used From 719dadf1ffb7f2fc585cc57c8e1ca4e041a9c32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Thu, 25 Jun 2015 18:12:45 +1200 Subject: [PATCH 14/45] more planned changes for 4.3 --- PLANNED_CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index dbc9dce..0709b7a 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -3,5 +3,7 @@ ## version 4.3 * add support for multiple state machines per class + * documentation * events are chained * :default state machine won't be provided, if state machine name is used + * if used raise an error From 206b1991bcdac2d059c4942900f79f00823e4e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Fri, 26 Jun 2015 19:46:58 +1200 Subject: [PATCH 15/45] more tests --- PLANNED_CHANGES.md | 22 +- lib/aasm/base.rb | 4 +- spec/models/complex_example.rb | 134 ++++++++++++ spec/models/complex_multiple_example.rb | 133 ------------ spec/models/conversation.rb | 50 ++++- spec/models/foo.rb | 57 +++++ ...o_multiple.rb => foo_callback_multiple.rb} | 26 ++- spec/models/guardian_multiple.rb | 48 +++++ spec/models/initial_state_proc.rb | 16 ++ spec/models/no_initial_state.rb | 13 ++ spec/models/parametrised_event_multiple.rb | 29 +++ spec/models/valid_state_name.rb | 12 ++ spec/unit/callback_multiple_spec.rb | 4 +- spec/unit/complex_multiple_example_spec.rb | 4 +- spec/unit/event_multiple_spec.rb | 73 +++++++ spec/unit/guard_multiple_spec.rb | 60 ++++++ spec/unit/initial_state_multiple_spec.rb | 15 ++ spec/unit/inspection_multiple_spec.rb | 202 ++++++++++++++++++ 18 files changed, 750 insertions(+), 152 deletions(-) delete mode 100644 spec/models/complex_multiple_example.rb rename spec/models/{foo_multiple.rb => foo_callback_multiple.rb} (58%) create mode 100644 spec/models/guardian_multiple.rb create mode 100644 spec/models/parametrised_event_multiple.rb create mode 100644 spec/unit/event_multiple_spec.rb create mode 100644 spec/unit/guard_multiple_spec.rb create mode 100644 spec/unit/initial_state_multiple_spec.rb create mode 100644 spec/unit/inspection_multiple_spec.rb diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 0709b7a..4939dce 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -3,7 +3,23 @@ ## version 4.3 * add support for multiple state machines per class + * check all tests + * what happen's if someone accesses `aasm`, but has defined a + state machine for `aasm(:my_name)`? + * persistence + * _ActiveRecord_ + * _Mongoid_ + * _MongoMapper_ + * _Sequel_ * documentation - * events are chained - * :default state machine won't be provided, if state machine name is used - * if used raise an error + * silence warnings? + +# Changes so far + +## version 4.3 + + * add support for multiple state machines per class + * class- and instance-level `aasm` methods accept a state machine selector + (aka the state machine _name_) + * if no selector/name is provided, `:default` will be used + * duplicate definitions of states and events will issue warnings diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index cdf78fb..3e0bc3a 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -67,7 +67,7 @@ module AASM @state_machine.add_state(name, @klass, options) if @klass.instance_methods.include?("#{name}?") - warn "The state name #{name} is already used!" + warn "#{@klass.name}: The state name #{name} is already used!" end @klass.class_eval <<-EORUBY, __FILE__, __LINE__ + 1 @@ -86,7 +86,7 @@ module AASM @state_machine.add_event(name, options, &block) if @klass.instance_methods.include?("may_#{name}?".to_sym) - warn "The event name #{name} is already used!" + warn "#{@klass.name}: The event name #{name} is already used!" end # an addition over standard aasm so that, before firing an event, you can ask diff --git a/spec/models/complex_example.rb b/spec/models/complex_example.rb index 0844aed..540e062 100644 --- a/spec/models/complex_example.rb +++ b/spec/models/complex_example.rb @@ -86,3 +86,137 @@ class ComplexExample phrase == :please end end + +class ComplexExampleMultiple + include AASM + + attr_accessor :activation_code, :activated_at, :deleted_at + + aasm(:left) do + state :passive + state :pending, :initial => true, :before_enter => :make_activation_code + state :active, :before_enter => :do_activate + state :suspended + state :deleted, :before_enter => :do_delete#, :exit => :do_undelete + state :waiting + + event :left_register do + transitions :from => :passive, :to => :pending do + guard do + can_register? + end + end + end + + event :left_activate do + transitions :from => :pending, :to => :active + end + + event :left_suspend do + transitions :from => [:passive, :pending, :active], :to => :suspended + end + + event :left_delete do + transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted + end + + # a dummy event that can never happen + event :left_unpassify do + transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } + end + + event :left_unsuspend do + transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } + transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? + transitions :from => :suspended, :to => :passive + end + + event :left_wait do + transitions :from => :suspended, :to => :waiting, :guard => :if_polite? + end + end + + aasm(:right) do + state :passive + state :pending, :initial => true, :before_enter => :make_activation_code + state :active, :before_enter => :do_activate + state :suspended + state :deleted, :before_enter => :do_delete#, :exit => :do_undelete + state :waiting + + event :right_register do + transitions :from => :passive, :to => :pending do + guard do + can_register? + end + end + end + + event :right_activate do + transitions :from => :pending, :to => :active + end + + event :right_suspend do + transitions :from => [:passive, :pending, :active], :to => :suspended + end + + event :right_delete do + transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted + end + + # a dummy event that can never happen + event :right_unpassify do + transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } + end + + event :right_unsuspend do + transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } + transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? + transitions :from => :suspended, :to => :passive + end + + event :right_wait do + transitions :from => :suspended, :to => :waiting, :guard => :if_polite? + end + end # right + + def initialize + # the AR backend uses a before_validate_on_create :aasm_ensure_initial_state + # lets do something similar here for testing purposes. + aasm(:left).enter_initial_state + aasm(:right).enter_initial_state + end + + def make_activation_code + @activation_code = @activation_code ? @activation_code + '2' : '1' + end + + def do_activate + @activated_at = Time.now + @activation_code = nil + end + + def do_delete + @deleted_at = Time.now + end + + def do_undelete + @deleted_at = false + end + + def can_register? + true + end + + def has_activated? + !!@activated_at + end + + def has_activation_code? + !!@activation_code + end + + def if_polite?(phrase = nil) + phrase == :please + end +end diff --git a/spec/models/complex_multiple_example.rb b/spec/models/complex_multiple_example.rb deleted file mode 100644 index 53e94be..0000000 --- a/spec/models/complex_multiple_example.rb +++ /dev/null @@ -1,133 +0,0 @@ -class ComplexMultipleExample - include AASM - - attr_accessor :activation_code, :activated_at, :deleted_at - - aasm(:left) do - state :passive - state :pending, :initial => true, :before_enter => :make_activation_code - state :active, :before_enter => :do_activate - state :suspended - state :deleted, :before_enter => :do_delete#, :exit => :do_undelete - state :waiting - - event :left_register do - transitions :from => :passive, :to => :pending do - guard do - can_register? - end - end - end - - event :left_activate do - transitions :from => :pending, :to => :active - end - - event :left_suspend do - transitions :from => [:passive, :pending, :active], :to => :suspended - end - - event :left_delete do - transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted - end - - # a dummy event that can never happen - event :left_unpassify do - transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } - end - - event :left_unsuspend do - transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } - transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? - transitions :from => :suspended, :to => :passive - end - - event :left_wait do - transitions :from => :suspended, :to => :waiting, :guard => :if_polite? - end - end - - aasm(:right) do - state :passive - state :pending, :initial => true, :before_enter => :make_activation_code - state :active, :before_enter => :do_activate - state :suspended - state :deleted, :before_enter => :do_delete#, :exit => :do_undelete - state :waiting - - event :right_register do - transitions :from => :passive, :to => :pending do - guard do - can_register? - end - end - end - - event :right_activate do - transitions :from => :pending, :to => :active - end - - event :right_suspend do - transitions :from => [:passive, :pending, :active], :to => :suspended - end - - event :right_delete do - transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted - end - - # a dummy event that can never happen - event :right_unpassify do - transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } - end - - event :right_unsuspend do - transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? } - transitions :from => :suspended, :to => :pending, :guard => :has_activation_code? - transitions :from => :suspended, :to => :passive - end - - event :right_wait do - transitions :from => :suspended, :to => :waiting, :guard => :if_polite? - end - end # right - - def initialize - # the AR backend uses a before_validate_on_create :aasm_ensure_initial_state - # lets do something similar here for testing purposes. - aasm(:left).enter_initial_state - aasm(:right).enter_initial_state - end - - def make_activation_code - @activation_code = @activation_code ? @activation_code + '2' : '1' - end - - def do_activate - @activated_at = Time.now - @activation_code = nil - end - - def do_delete - @deleted_at = Time.now - end - - def do_undelete - @deleted_at = false - end - - def can_register? - true - end - - def has_activated? - !!@activated_at - end - - def has_activation_code? - !!@activation_code - end - - def if_polite?(phrase = nil) - phrase == :please - end -end diff --git a/spec/models/conversation.rb b/spec/models/conversation.rb index a14a537..679aa9a 100644 --- a/spec/models/conversation.rb +++ b/spec/models/conversation.rb @@ -34,8 +34,55 @@ class Conversation @persister = persister end - private + + def aasm_read_state + @persister.read_state + end + + def aasm_write_state(state) + @persister.write_state(state) + end +end + +class ConversationMultiple + include AASM + + aasm(:left) do + state :needs_attention, :initial => true + state :read + state :closed + state :awaiting_response + state :junk + + event :new_message do + end + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + + event :reply do + end + + event :close do + transitions :to => :closed, :from => [:read, :awaiting_response] + end + + event :junk do + transitions :to => :junk, :from => [:read] + end + + event :unjunk do + end + end + + def initialize(persister) + @persister = persister + end + + private + def aasm_read_state @persister.read_state end @@ -43,5 +90,4 @@ class Conversation def aasm_write_state(state) @persister.write_state(state) end - end diff --git a/spec/models/foo.rb b/spec/models/foo.rb index b299979..3894fd0 100644 --- a/spec/models/foo.rb +++ b/spec/models/foo.rb @@ -33,3 +33,60 @@ class FooTwo < Foo state :foo end end + +class FooMultiple + include AASM + + aasm(:left) do + state :open, :initial => true, :before_exit => :before_exit + state :closed, :before_enter => :before_enter + state :final + + event :close, :success => :success_callback do + transitions :from => [:open], :to => [:closed] + end + + event :null do + transitions :from => [:open], :to => [:closed, :final], :guard => :always_false + end + end + + aasm(:right, :column => :right) do + state :green, :initial => true + state :yellow + state :red + + event :green do + transitions :from => [:yellow], :to => :green + end + event :yellow do + transitions :from => [:green, :red], :to => :yellow + end + event :red do + transitions :from => [:yellow], :to => :red + end + end + + def always_false + false + end + + def success_callback + end + + def before_enter + end + def before_exit + end +end + +class FooTwoMultiple < FooMultiple + include AASM + aasm(:left) do + state :foo + end + + aasm(:right) do + state :bar + end +end diff --git a/spec/models/foo_multiple.rb b/spec/models/foo_callback_multiple.rb similarity index 58% rename from spec/models/foo_multiple.rb rename to spec/models/foo_callback_multiple.rb index c337dcb..e8e9977 100644 --- a/spec/models/foo_multiple.rb +++ b/spec/models/foo_callback_multiple.rb @@ -1,5 +1,6 @@ -class FooMultiple +class FooCallbackMultiple include AASM + aasm(:left) do state :open, :initial => true, :before_exit => :before_exit state :closed, :before_enter => :before_enter @@ -14,6 +15,22 @@ class FooMultiple end end + aasm(:right, :column => :right) do + state :green, :initial => true + state :yellow + state :red + + event :green do + transitions :from => [:yellow], :to => :green + end + event :yellow do + transitions :from => [:green, :red], :to => :yellow + end + event :red do + transitions :from => [:yellow], :to => :red + end + end + def always_false false end @@ -26,10 +43,3 @@ class FooMultiple def before_exit end end - -class FooTwoMultiple < FooMultiple - include AASM - aasm(:left) do - state :foo - end -end diff --git a/spec/models/guardian_multiple.rb b/spec/models/guardian_multiple.rb new file mode 100644 index 0000000..8a7df5c --- /dev/null +++ b/spec/models/guardian_multiple.rb @@ -0,0 +1,48 @@ +class GuardianMultiple + include AASM + + aasm(:left) do + state :alpha, :initial => true + state :beta + + event :use_one_guard_that_succeeds do + transitions :from => :alpha, :to => :beta, :guard => :succeed + end + event :use_one_guard_that_fails do + transitions :from => :alpha, :to => :beta, :guard => :fail + end + + event :use_guards_that_succeed do + transitions :from => :alpha, :to => :beta, :guards => [:succeed, :another_succeed] + end + event :use_guards_where_the_first_fails do + transitions :from => :alpha, :to => :beta, :guards => [:succeed, :fail] + end + event :use_guards_where_the_second_fails do + transitions :from => :alpha, :to => :beta, :guards => [:fail, :succeed] + end + + event :use_event_guards_that_succeed, :guards => [:succeed, :another_succeed] do + transitions :from => :alpha, :to => :beta + end + event :use_event_and_transition_guards_that_succeed, :guards => [:succeed, :another_succeed] do + transitions :from => :alpha, :to => :beta, :guards => [:more_succeed] + end + event :use_event_guards_where_the_first_fails, :guards => [:succeed, :fail] do + transitions :from => :alpha, :to => :beta + end + event :use_event_guards_where_the_second_fails, :guards => [:fail, :succeed] do + transitions :from => :alpha, :to => :beta, :guard => :another_succeed + end + event :use_event_and_transition_guards_where_third_fails, :guards => [:succeed, :another_succeed] do + transitions :from => :alpha, :to => :beta, :guards => [:fail] + end + end + + def fail; false; end + + def succeed; true; end + def another_succeed; true; end + def more_succeed; true; end + +end diff --git a/spec/models/initial_state_proc.rb b/spec/models/initial_state_proc.rb index 0fe72eb..472b0d3 100644 --- a/spec/models/initial_state_proc.rb +++ b/spec/models/initial_state_proc.rb @@ -13,3 +13,19 @@ class InitialStateProc def initialize(balance = 0); self.balance = balance; end def rich?; self.balance >= RICH; end end + +class InitialStateProcMultiple + RICH = 1_000_000 + + attr_accessor :balance + + include AASM + aasm(:left) do + state :retired + state :selling_bad_mortgages + initial_state Proc.new { |banker| banker.rich? ? :retired : :selling_bad_mortgages } + end + + def initialize(balance = 0); self.balance = balance; end + def rich?; self.balance >= RICH; end +end diff --git a/spec/models/no_initial_state.rb b/spec/models/no_initial_state.rb index 07e72ef..5e86671 100644 --- a/spec/models/no_initial_state.rb +++ b/spec/models/no_initial_state.rb @@ -10,3 +10,16 @@ class NoInitialState end end end + +class NoInitialStateMultiple + include AASM + + aasm(:left) do + state :read + state :ended + + event :foo do + transitions :to => :ended, :from => [:read] + end + end +end diff --git a/spec/models/parametrised_event_multiple.rb b/spec/models/parametrised_event_multiple.rb new file mode 100644 index 0000000..e9fcb07 --- /dev/null +++ b/spec/models/parametrised_event_multiple.rb @@ -0,0 +1,29 @@ +class ParametrisedEventMultiple + include AASM + aasm(:left) do + state :sleeping, :initial => true + state :showering + state :working + state :dating + state :prettying_up + + event :wakeup do + transitions :from => :sleeping, :to => [:showering, :working] + end + + event :dress do + transitions :from => :sleeping, :to => :working, :after => :wear_clothes + transitions :from => :showering, :to => [:working, :dating], :after => Proc.new { |*args| wear_clothes(*args) } + transitions :from => :showering, :to => :prettying_up, :after => [:condition_hair, :fix_hair] + end + end + + def wear_clothes(shirt_color, trouser_type=nil) + end + + def condition_hair + end + + def fix_hair + end +end diff --git a/spec/models/valid_state_name.rb b/spec/models/valid_state_name.rb index 8097f19..db83a86 100644 --- a/spec/models/valid_state_name.rb +++ b/spec/models/valid_state_name.rb @@ -9,3 +9,15 @@ class ValidStateName end end end + +class ValidStateNameMultiple + include AASM + aasm(:left) do + state :invalid, :initial => true + state :valid + + event :valid do + transitions :to => :valid, :from => [:invalid] + end + end +end diff --git a/spec/unit/callback_multiple_spec.rb b/spec/unit/callback_multiple_spec.rb index 9ffb41d..c5c6ef2 100644 --- a/spec/unit/callback_multiple_spec.rb +++ b/spec/unit/callback_multiple_spec.rb @@ -185,7 +185,7 @@ end # callbacks for the new DSL describe 'event callbacks' do describe "with an error callback defined" do before do - class FooMultiple + class FooCallbackMultiple # this hack is needed to allow testing of parameters, since RSpec # destroys a method's arity when mocked attr_accessor :data @@ -197,7 +197,7 @@ describe 'event callbacks' do end end - @foo = FooMultiple.new + @foo = FooCallbackMultiple.new end context "error_callback defined" do diff --git a/spec/unit/complex_multiple_example_spec.rb b/spec/unit/complex_multiple_example_spec.rb index 2cef3e7..334406e 100644 --- a/spec/unit/complex_multiple_example_spec.rb +++ b/spec/unit/complex_multiple_example_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'on initialization' do - let(:auth) {ComplexMultipleExample.new} + let(:auth) {ComplexExampleMultiple.new} it 'should be in the pending state' do expect(auth.aasm(:left).current_state).to eq(:pending) @@ -15,7 +15,7 @@ describe 'on initialization' do end describe 'when being unsuspended' do - let(:auth) {ComplexMultipleExample.new} + let(:auth) {ComplexExampleMultiple.new} it 'should be able to unsuspend' do auth.left_activate! diff --git a/spec/unit/event_multiple_spec.rb b/spec/unit/event_multiple_spec.rb new file mode 100644 index 0000000..219be9a --- /dev/null +++ b/spec/unit/event_multiple_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe 'current event' do + let(:pe) {ParametrisedEventMultiple.new} + + it 'if no event has been triggered' do + expect(pe.aasm(:left).current_event).to be_nil + end + + it 'if a event has been triggered' do + pe.wakeup + expect(pe.aasm(:left).current_event).to eql :wakeup + end + + it 'if no event has been triggered' do + pe.wakeup! + expect(pe.aasm(:left).current_event).to eql :wakeup! + end +end + +describe 'parametrised events' do + let(:pe) {ParametrisedEventMultiple.new} + + it 'should transition to specified next state (sleeping to showering)' do + pe.wakeup!(:showering) + expect(pe.aasm(:left).current_state).to eq(:showering) + end + + it 'should transition to specified next state (sleeping to working)' do + pe.wakeup!(:working) + expect(pe.aasm(:left).current_state).to eq(:working) + end + + it 'should transition to default (first or showering) state' do + pe.wakeup! + expect(pe.aasm(:left).current_state).to eq(:showering) + end + + it 'should transition to default state when :after transition invoked' do + pe.dress!(nil, 'purple', 'dressy') + expect(pe.aasm(:left).current_state).to eq(:working) + end + + it 'should call :after transition method with args' do + pe.wakeup!(:showering) + expect(pe).to receive(:wear_clothes).with('blue', 'jeans') + pe.dress!(:working, 'blue', 'jeans') + end + + it 'should call :after transition proc' do + pe.wakeup!(:showering) + expect(pe).to receive(:wear_clothes).with('purple', 'slacks') + pe.dress!(:dating, 'purple', 'slacks') + end + + it 'should call :after transition with an array of methods' do + pe.wakeup!(:showering) + expect(pe).to receive(:condition_hair) + expect(pe).to receive(:fix_hair) + pe.dress!(:prettying_up) + end +end + +describe 'event firing without persistence' do + it 'should attempt to persist if aasm_write_state is defined' do + foo = Foo.new + def foo.aasm_write_state; end + expect(foo).to be_open + + expect(foo).to receive(:aasm_write_state_without_persistence) + foo.close + end +end diff --git a/spec/unit/guard_multiple_spec.rb b/spec/unit/guard_multiple_spec.rb new file mode 100644 index 0000000..cd93861 --- /dev/null +++ b/spec/unit/guard_multiple_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe "per-transition guards" do + let(:guardian) { GuardianMultiple.new } + + it "allows the transition if the guard succeeds" do + expect { guardian.use_one_guard_that_succeeds! }.to_not raise_error + expect(guardian).to be_beta + end + + it "stops the transition if the guard fails" do + expect { guardian.use_one_guard_that_fails! }.to raise_error(AASM::InvalidTransition) + expect(guardian).to be_alpha + end + + it "allows the transition if all guards succeeds" do + expect { guardian.use_guards_that_succeed! }.to_not raise_error + expect(guardian).to be_beta + end + + it "stops the transition if the first guard fails" do + expect { guardian.use_guards_where_the_first_fails! }.to raise_error(AASM::InvalidTransition) + expect(guardian).to be_alpha + end + + it "stops the transition if the second guard fails" do + expect { guardian.use_guards_where_the_second_fails! }.to raise_error(AASM::InvalidTransition) + expect(guardian).to be_alpha + end +end + +describe "event guards" do + let(:guardian) { GuardianMultiple.new } + + it "allows the transition if the event guards succeed" do + expect { guardian.use_event_guards_that_succeed! }.to_not raise_error + expect(guardian).to be_beta + end + + it "allows the transition if the event and transition guards succeed" do + expect { guardian.use_event_and_transition_guards_that_succeed! }.to_not raise_error + expect(guardian).to be_beta + end + + it "stops the transition if the first event guard fails" do + expect { guardian.use_event_guards_where_the_first_fails! }.to raise_error(AASM::InvalidTransition) + expect(guardian).to be_alpha + end + + it "stops the transition if the second event guard fails" do + expect { guardian.use_event_guards_where_the_second_fails! }.to raise_error(AASM::InvalidTransition) + expect(guardian).to be_alpha + end + + it "stops the transition if the transition guard fails" do + expect { guardian.use_event_and_transition_guards_where_third_fails! }.to raise_error(AASM::InvalidTransition) + expect(guardian).to be_alpha + end + +end diff --git a/spec/unit/initial_state_multiple_spec.rb b/spec/unit/initial_state_multiple_spec.rb new file mode 100644 index 0000000..22e8f19 --- /dev/null +++ b/spec/unit/initial_state_multiple_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'initial states' do + it 'should use the first state defined if no initial state is given' do + expect(NoInitialStateMultiple.new.aasm(:left).current_state).to eq(:read) + end + + it 'should determine initial state from the Proc results' do + balance = InitialStateProcMultiple::RICH - 1 + expect(InitialStateProcMultiple.new(balance).aasm(:left).current_state).to eq(:selling_bad_mortgages) + + balance = InitialStateProcMultiple::RICH + 1 + expect(InitialStateProcMultiple.new(balance).aasm(:left).current_state).to eq(:retired) + end +end diff --git a/spec/unit/inspection_multiple_spec.rb b/spec/unit/inspection_multiple_spec.rb new file mode 100644 index 0000000..a6f32c0 --- /dev/null +++ b/spec/unit/inspection_multiple_spec.rb @@ -0,0 +1,202 @@ +require 'spec_helper' + +describe 'inspection for common cases' do + it 'should support the new DSL' do + # 1st state machine + expect(FooMultiple.aasm(:left)).to respond_to(:states) + expect(FooMultiple.aasm(:left).states.size).to eql 3 + expect(FooMultiple.aasm(:left).states).to include(:open) + expect(FooMultiple.aasm(:left).states).to include(:closed) + expect(FooMultiple.aasm(:left).states).to include(:final) + + expect(FooMultiple.aasm(:left)).to respond_to(:initial_state) + expect(FooMultiple.aasm(:left).initial_state).to eq(:open) + + expect(FooMultiple.aasm(:left)).to respond_to(:events) + puts FooMultiple.aasm(:left).events.map(&:name).inspect + expect(FooMultiple.aasm(:left).events.size).to eql 2 + expect(FooMultiple.aasm(:left).events).to include(:close) + expect(FooMultiple.aasm(:left).events).to include(:null) + + # 2nd state machine + expect(FooMultiple.aasm(:right)).to respond_to(:states) + expect(FooMultiple.aasm(:right).states.size).to eql 3 + expect(FooMultiple.aasm(:right).states).to include(:green) + expect(FooMultiple.aasm(:right).states).to include(:yellow) + expect(FooMultiple.aasm(:right).states).to include(:red) + + expect(FooMultiple.aasm(:right)).to respond_to(:initial_state) + expect(FooMultiple.aasm(:right).initial_state).to eq(:green) + + expect(FooMultiple.aasm(:right)).to respond_to(:events) + expect(FooMultiple.aasm(:right).events.size).to eql 3 + expect(FooMultiple.aasm(:right).events).to include(:green) + expect(FooMultiple.aasm(:right).events).to include(:yellow) + expect(FooMultiple.aasm(:right).events).to include(:red) + end + + context "instance level inspection" do + let(:foo) { FooMultiple.new } + let(:two) { FooTwoMultiple.new } + + it "delivers all states" do + # 1st state machine + states = foo.aasm(:left).states + expect(states.size).to eql 3 + expect(states).to include(:open) + expect(states).to include(:closed) + expect(states).to include(:final) + + states = foo.aasm(:left).states(:permitted => true) + expect(states.size).to eql 1 + expect(states).to include(:closed) + expect(states).not_to include(:open) + expect(states).not_to include(:final) + + foo.close + expect(foo.aasm(:left).states(:permitted => true)).to be_empty + + # 2nd state machine + states = foo.aasm(:right).states + expect(states.size).to eql 3 + expect(states).to include(:green) + expect(states).to include(:yellow) + expect(states).to include(:red) + + states = foo.aasm(:right).states(:permitted => true) + expect(states.size).to eql 1 + expect(states).to include(:yellow) + expect(states).not_to include(:green) + expect(states).not_to include(:red) + + foo.yellow + states = foo.aasm(:right).states(:permitted => true) + expect(states.size).to eql 2 + expect(states).to include(:red) + expect(states).to include(:green) + expect(states).not_to include(:yellow) + end + + it "delivers all states for subclasses" do + # 1st state machine + states = two.aasm(:left).states + expect(states.size).to eql 4 + expect(states).to include(:open) + expect(states).to include(:closed) + expect(states).to include(:final) + expect(states).to include(:foo) + + states = two.aasm(:left).states(:permitted => true) + expect(states.size).to eql 1 + expect(states).to include(:closed) + expect(states).not_to include(:open) + + two.close + expect(two.aasm(:left).states(:permitted => true)).to be_empty + + # 2nd state machine + states = two.aasm(:right).states + expect(states.size).to eql 4 + expect(states).to include(:green) + expect(states).to include(:yellow) + expect(states).to include(:red) + expect(states).to include(:bar) + + states = two.aasm(:right).states(:permitted => true) + expect(states.size).to eql 1 + expect(states).to include(:yellow) + expect(states).not_to include(:red) + expect(states).not_to include(:green) + expect(states).not_to include(:bar) + + two.yellow + states = two.aasm(:right).states(:permitted => true) + expect(states.size).to eql 2 + expect(states).to include(:green) + expect(states).to include(:red) + expect(states).not_to include(:yellow) + expect(states).not_to include(:bar) + end + + it "delivers all events" do + # 1st state machine + events = foo.aasm(:left).events + expect(events.size).to eql 2 + expect(events).to include(:close) + expect(events).to include(:null) + + foo.close + expect(foo.aasm(:left).events).to be_empty + + # 2nd state machine + events = foo.aasm(:right).events + expect(events.size).to eql 1 + expect(events).to include(:yellow) + expect(events).not_to include(:green) + expect(events).not_to include(:red) + + foo.yellow + events = foo.aasm(:right).events + expect(events.size).to eql 2 + expect(events).to include(:green) + expect(events).to include(:red) + expect(events).not_to include(:yellow) + end + end + + it 'should list states in the order they have been defined' do + expect(ConversationMultiple.aasm(:left).states).to eq([ + :needs_attention, :read, :closed, :awaiting_response, :junk + ]) + end +end + +describe "special cases" do + it "should support valid as state name" do + expect(ValidStateNameMultiple.aasm(:left).states).to include(:invalid) + expect(ValidStateNameMultiple.aasm(:left).states).to include(:valid) + + argument = ValidStateNameMultiple.new + expect(argument.invalid?).to be_truthy + expect(argument.aasm(:left).current_state).to eq(:invalid) + + argument.valid! + expect(argument.valid?).to be_truthy + expect(argument.aasm(:left).current_state).to eq(:valid) + end +end + +describe 'aasm.states_for_select' do + it "should return a select friendly array of states" do + expect(FooMultiple.aasm(:left)).to respond_to(:states_for_select) + expect(FooMultiple.aasm(:left).states_for_select).to eq( + [['Open', 'open'], ['Closed', 'closed'], ['Final', 'final']] + ) + end +end + +describe 'aasm.from_states_for_state' do + it "should return all from states for a state" do + expect(ComplexExampleMultiple.aasm(:left)).to respond_to(:from_states_for_state) + froms = ComplexExampleMultiple.aasm(:left).from_states_for_state(:active) + [:pending, :passive, :suspended].each {|from| expect(froms).to include(from)} + end + + it "should return from states for a state for a particular transition only" do + froms = ComplexExampleMultiple.aasm(:left).from_states_for_state(:active, :transition => :left_unsuspend) + [:suspended].each {|from| expect(froms).to include(from)} + end +end + +describe 'permitted events' do + let(:foo) {FooMultiple.new} + + it 'work' do + expect(foo.aasm(:left).events(:permitted => true)).to include(:close) + expect(foo.aasm(:left).events(:permitted => true)).not_to include(:null) + + expect(foo.aasm(:right).events(:permitted => true)).to include(:yellow) + expect(foo.aasm(:right).events(:permitted => true)).not_to include(:green) + expect(foo.aasm(:right).events(:permitted => true)).not_to include(:red) + end +end From 7c02bafcad1be2b325673eb4faaface9cbebd5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Wed, 1 Jul 2015 21:35:04 +1200 Subject: [PATCH 16/45] add tests for subclassing classes with multiple state machines --- spec/models/sub_class.rb | 4 +++ spec/models/sub_class_with_more_states.rb | 11 +++++++ spec/models/super_class.rb | 28 ++++++++++++++++ spec/unit/subclassing_multiple_spec.rb | 39 +++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 spec/unit/subclassing_multiple_spec.rb diff --git a/spec/models/sub_class.rb b/spec/models/sub_class.rb index 94f8ef1..0a68e43 100644 --- a/spec/models/sub_class.rb +++ b/spec/models/sub_class.rb @@ -1,3 +1,7 @@ require_relative 'super_class' + class SubClass < SuperClass end + +class SubClassMultiple < SuperClassMultiple +end diff --git a/spec/models/sub_class_with_more_states.rb b/spec/models/sub_class_with_more_states.rb index aa7b8ec..9978b86 100644 --- a/spec/models/sub_class_with_more_states.rb +++ b/spec/models/sub_class_with_more_states.rb @@ -1,7 +1,18 @@ require_relative 'super_class' + class SubClassWithMoreStates < SuperClass include AASM aasm do state :foo end end + +class SubClassWithMoreStatesMultiple < SuperClassMultiple + include AASM + aasm(:left) do + state :foo + end + aasm(:right) do + state :archived + end +end diff --git a/spec/models/super_class.rb b/spec/models/super_class.rb index ad53327..b55fc93 100644 --- a/spec/models/super_class.rb +++ b/spec/models/super_class.rb @@ -16,3 +16,31 @@ class SuperClass end end end + +class SuperClassMultiple + include AASM + + aasm(:left) do + state :read + state :ended + + event :foo do + transitions :to => :ended, :from => [:read] + end + end + + aasm(:right) do + state :opened + state :closed + + event :close do + transitions :to => :closed, :from => [:opened] + end + end + + def update_state + if may_foo? + foo! + end + end +end diff --git a/spec/unit/subclassing_multiple_spec.rb b/spec/unit/subclassing_multiple_spec.rb new file mode 100644 index 0000000..f0f0ac9 --- /dev/null +++ b/spec/unit/subclassing_multiple_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'subclassing with multiple state machines' do + + it 'should have the parent states' do + SuperClassMultiple.aasm(:left).states.each do |state| + expect(SubClassWithMoreStatesMultiple.aasm(:left).states).to include(state) + end + expect(SubClassMultiple.aasm(:left).states).to eq(SuperClassMultiple.aasm(:left).states) + + SuperClassMultiple.aasm(:right).states.each do |state| + expect(SubClassWithMoreStatesMultiple.aasm(:right).states).to include(state) + end + expect(SubClassMultiple.aasm(:right).states).to eq(SuperClassMultiple.aasm(:right).states) + end + + it 'should not add the child states to the parent machine' do + expect(SuperClassMultiple.aasm(:left).states).not_to include(:foo) + expect(SuperClassMultiple.aasm(:right).states).not_to include(:archived) + end + + 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 + + it 'should know how to respond to question methods' do + expect(SubClassMultiple.new.may_foo?).to be_truthy + expect(SubClassMultiple.new.may_close?).to be_truthy + end + + it 'should not break if I call methods from super class' do + son = SubClassMultiple.new + son.update_state + expect(son.aasm(:left).current_state).to eq(:ended) + end + +end + From 64637d8aa0f415edeeba8fb1c94a2ced3f667992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Wed, 1 Jul 2015 21:35:18 +1200 Subject: [PATCH 17/45] improve test description --- spec/unit/reloading_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/reloading_spec.rb b/spec/unit/reloading_spec.rb index 322bd14..e8d7827 100644 --- a/spec/unit/reloading_spec.rb +++ b/spec/unit/reloading_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'callbacks for the new DSL' do +describe 'when redefining states' do let(:definer) { DoubleDefiner.new } it "allows extending states" do From a5fe043e32f24a787113a768c4f78cd4d879aeb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Wed, 1 Jul 2015 21:37:19 +1200 Subject: [PATCH 18/45] remove unused test block --- spec/unit/transition_spec.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/unit/transition_spec.rb b/spec/unit/transition_spec.rb index d10fef5..3fcbca1 100644 --- a/spec/unit/transition_spec.rb +++ b/spec/unit/transition_spec.rb @@ -53,9 +53,6 @@ describe 'transitions' do end -describe 'blocks' do -end - describe AASM::Core::Transition do let(:state_machine) { AASM::StateMachine.new(:name) } let(:event) { AASM::Core::Event.new(:event, state_machine) } From 5c94b624d16c4b089a67c78e457b83a94557ffc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Wed, 1 Jul 2015 23:41:44 +1200 Subject: [PATCH 19/45] first set of ActiveRecord tests --- PLANNED_CHANGES.md | 1 + lib/aasm/base.rb | 2 +- .../persistence/active_record_persistence.rb | 12 +- spec/database.rb | 18 +- spec/models/active_record/derivate_new_dsl.rb | 4 + spec/models/active_record/false_state.rb | 18 + spec/models/active_record/gate.rb | 20 + .../active_record/no_direct_assignment.rb | 11 + spec/models/active_record/no_scope.rb | 11 + spec/models/active_record/simple_new_dsl.rb | 9 + spec/models/active_record/thief.rb | 15 + spec/models/active_record/with_enum.rb | 20 + spec/models/active_record/with_false_enum.rb | 16 + spec/models/active_record/with_true_enum.rb | 20 + spec/models/invalid_persistor.rb | 15 + spec/models/transactor.rb | 27 + spec/models/validator.rb | 39 ++ ...active_record_persistence_multiple_spec.rb | 512 ++++++++++++++++++ .../active_record_persistence_spec.rb | 12 +- 19 files changed, 766 insertions(+), 16 deletions(-) create mode 100644 spec/unit/persistence/active_record_persistence_multiple_spec.rb diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 4939dce..beed22d 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -4,6 +4,7 @@ * add support for multiple state machines per class * check all tests + * _ActiveRecord_ when :right state machine is defined as well * what happen's if someone accesses `aasm`, but has defined a state machine for `aasm(:my_name)`? * persistence diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index 3e0bc3a..a9c30b3 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -33,7 +33,7 @@ module AASM # and attribute is directly assigned though @klass.class_eval %Q( def #{@state_machine.config.column}=(state_name) - if self.class.aasm.state_machine.config.no_direct_assignment + if self.class.aasm(:#{@name}).state_machine.config.no_direct_assignment raise AASM::NoDirectAssignmentError.new( 'direct assignment of AASM column has been disabled (see AASM configuration for this class)' ) diff --git a/lib/aasm/persistence/active_record_persistence.rb b/lib/aasm/persistence/active_record_persistence.rb index 4c80710..c122f69 100644 --- a/lib/aasm/persistence/active_record_persistence.rb +++ b/lib/aasm/persistence/active_record_persistence.rb @@ -118,18 +118,18 @@ module AASM def aasm_enum(name=:default) case AASM::StateMachine[self.class][name].config.enum when false then nil - when true then aasm_guess_enum_method - when nil then aasm_guess_enum_method if aasm_column_looks_like_enum + when true then aasm_guess_enum_method(name) + when nil then aasm_guess_enum_method(name) if aasm_column_looks_like_enum(name) else AASM::StateMachine[self.class][name].config.enum end end - def aasm_column_looks_like_enum - self.class.columns_hash[self.class.aasm.attribute_name.to_s].type == :integer + def aasm_column_looks_like_enum(name=:default) + self.class.columns_hash[self.class.aasm(name).attribute_name.to_s].type == :integer end - def aasm_guess_enum_method - self.class.aasm.attribute_name.to_s.pluralize.to_sym + def aasm_guess_enum_method(name=:default) + self.class.aasm(name).attribute_name.to_s.pluralize.to_sym end def aasm_skipping_validations(state_machine_name) diff --git a/spec/database.rb b/spec/database.rb index 4e7ccbd..52594aa 100644 --- a/spec/database.rb +++ b/spec/database.rb @@ -1,5 +1,5 @@ ActiveRecord::Migration.suppress_messages do - %w{gates readers writers transients simples no_scopes no_direct_assignments thieves localizer_test_models persisted_states provided_and_persisted_states with_enums with_true_enums with_false_enums false_states}.each do |table_name| + %w{gates multiple_gates readers writers transients simples no_scopes multiple_no_scopes no_direct_assignments multiple_no_direct_assignments thieves multiple_thieves localizer_test_models persisted_states provided_and_persisted_states with_enums with_true_enums with_false_enums false_states multiple_with_enums multiple_with_true_enums multiple_with_false_enums multiple_false_states}.each do |table_name| ActiveRecord::Migration.create_table table_name, :force => true do |t| t.string "aasm_state" end @@ -8,17 +8,29 @@ ActiveRecord::Migration.suppress_messages do ActiveRecord::Migration.create_table "simple_new_dsls", :force => true do |t| t.string "status" end + ActiveRecord::Migration.create_table "multiple_simple_new_dsls", :force => true do |t| + t.string "status" + end ActiveRecord::Migration.create_table "validators", :force => true do |t| t.string "name" t.string "status" end + ActiveRecord::Migration.create_table "multiple_validators", :force => true do |t| + t.string "name" + t.string "status" + end ActiveRecord::Migration.create_table "transactors", :force => true do |t| t.string "name" t.string "status" t.integer "worker_id" end + ActiveRecord::Migration.create_table "multiple_transactors", :force => true do |t| + t.string "name" + t.string "status" + t.integer "worker_id" + end ActiveRecord::Migration.create_table "workers", :force => true do |t| t.string "name" @@ -29,6 +41,10 @@ ActiveRecord::Migration.suppress_messages do t.string "name" t.string "status" end + ActiveRecord::Migration.create_table "multiple_invalid_persistors", :force => true do |t| + t.string "name" + t.string "status" + end ActiveRecord::Migration.create_table "fathers", :force => true do |t| t.string "aasm_state" diff --git a/spec/models/active_record/derivate_new_dsl.rb b/spec/models/active_record/derivate_new_dsl.rb index 532d0b3..2aa6cbd 100644 --- a/spec/models/active_record/derivate_new_dsl.rb +++ b/spec/models/active_record/derivate_new_dsl.rb @@ -1,3 +1,7 @@ require_relative 'simple_new_dsl' + class DerivateNewDsl < SimpleNewDsl end + +class MultipleDerivateNewDsl < MultipleSimpleNewDsl +end diff --git a/spec/models/active_record/false_state.rb b/spec/models/active_record/false_state.rb index 3c19aed..70e0797 100644 --- a/spec/models/active_record/false_state.rb +++ b/spec/models/active_record/false_state.rb @@ -15,3 +15,21 @@ class FalseState < ActiveRecord::Base end end end + +class MultipleFalseState < ActiveRecord::Base + include AASM + + def initialize(*args) + super + self.aasm_state = false + end + + aasm :left do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/active_record/gate.rb b/spec/models/active_record/gate.rb index b89daa2..7fcc70a 100644 --- a/spec/models/active_record/gate.rb +++ b/spec/models/active_record/gate.rb @@ -17,3 +17,23 @@ class Gate < ActiveRecord::Base end end end + +class MultipleGate < ActiveRecord::Base + include AASM + + # Fake this column for testing purposes + # attr_accessor :aasm_state + + def value + 'value' + end + + aasm :left do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/active_record/no_direct_assignment.rb b/spec/models/active_record/no_direct_assignment.rb index a3fea8e..fc942c1 100644 --- a/spec/models/active_record/no_direct_assignment.rb +++ b/spec/models/active_record/no_direct_assignment.rb @@ -8,3 +8,14 @@ class NoDirectAssignment < ActiveRecord::Base end end end + +class MultipleNoDirectAssignment < ActiveRecord::Base + include AASM + aasm :left, :no_direct_assignment => true do + state :pending, :initial => true + state :running + event :run do + transitions :from => :pending, :to => :running + end + end +end diff --git a/spec/models/active_record/no_scope.rb b/spec/models/active_record/no_scope.rb index dfa5380..13d7169 100644 --- a/spec/models/active_record/no_scope.rb +++ b/spec/models/active_record/no_scope.rb @@ -8,3 +8,14 @@ class NoScope < ActiveRecord::Base end end end + +class MultipleNoScope < ActiveRecord::Base + include AASM + aasm :left, :create_scopes => false do + state :pending, :initial => true + state :running + event :run do + transitions :from => :pending, :to => :running + end + end +end diff --git a/spec/models/active_record/simple_new_dsl.rb b/spec/models/active_record/simple_new_dsl.rb index 61a7f2a..7e63f57 100644 --- a/spec/models/active_record/simple_new_dsl.rb +++ b/spec/models/active_record/simple_new_dsl.rb @@ -6,3 +6,12 @@ class SimpleNewDsl < ActiveRecord::Base state :new end end + +class MultipleSimpleNewDsl < ActiveRecord::Base + include AASM + aasm :left, :column => :status + aasm :left do + state :unknown_scope + state :new + end +end diff --git a/spec/models/active_record/thief.rb b/spec/models/active_record/thief.rb index 7d84be6..9dd90d9 100644 --- a/spec/models/active_record/thief.rb +++ b/spec/models/active_record/thief.rb @@ -12,3 +12,18 @@ class Thief < ActiveRecord::Base end attr_accessor :skilled, :aasm_state end + +class MultipleThief < ActiveRecord::Base + if ActiveRecord::VERSION::MAJOR >= 3 + self.table_name = 'multiple_thieves' + else + set_table_name "multiple_thieves" + end + include AASM + aasm :left do + state :rich + state :jailed + initial_state Proc.new {|thief| thief.skilled ? :rich : :jailed } + end + attr_accessor :skilled, :aasm_state +end diff --git a/spec/models/active_record/with_enum.rb b/spec/models/active_record/with_enum.rb index 70ce1aa..83500e7 100644 --- a/spec/models/active_record/with_enum.rb +++ b/spec/models/active_record/with_enum.rb @@ -17,3 +17,23 @@ class WithEnum < ActiveRecord::Base end end end + +class MultipleWithEnum < ActiveRecord::Base + include AASM + + # Fake this column for testing purposes + attr_accessor :aasm_state + + def self.test + {} + end + + aasm :left, :enum => :test do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/active_record/with_false_enum.rb b/spec/models/active_record/with_false_enum.rb index 074589b..428276b 100644 --- a/spec/models/active_record/with_false_enum.rb +++ b/spec/models/active_record/with_false_enum.rb @@ -13,3 +13,19 @@ class WithFalseEnum < ActiveRecord::Base end end end + +class MultipleWithFalseEnum < ActiveRecord::Base + include AASM + + # Fake this column for testing purposes + attr_accessor :aasm_state + + aasm :left, :enum => false do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/active_record/with_true_enum.rb b/spec/models/active_record/with_true_enum.rb index 56a3be3..fea1687 100644 --- a/spec/models/active_record/with_true_enum.rb +++ b/spec/models/active_record/with_true_enum.rb @@ -17,3 +17,23 @@ class WithTrueEnum < ActiveRecord::Base end end end + +class MultipleWithTrueEnum < ActiveRecord::Base + include AASM + + # Fake this column for testing purposes + attr_accessor :aasm_state + + def value + 'value' + end + + aasm :left, :enum => true do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/invalid_persistor.rb b/spec/models/invalid_persistor.rb index 2888ac1..aebd071 100644 --- a/spec/models/invalid_persistor.rb +++ b/spec/models/invalid_persistor.rb @@ -14,3 +14,18 @@ class InvalidPersistor < ActiveRecord::Base end validates_presence_of :name end + +class MultipleInvalidPersistor < ActiveRecord::Base + include AASM + aasm :left, :column => :status, :skip_validation_on_save => true do + state :sleeping, :initial => true + state :running + event :run do + transitions :to => :running, :from => :sleeping + end + event :sleep do + transitions :to => :sleeping, :from => :running + end + end + validates_presence_of :name +end diff --git a/spec/models/transactor.rb b/spec/models/transactor.rb index 0ffda5c..bca819e 100644 --- a/spec/models/transactor.rb +++ b/spec/models/transactor.rb @@ -1,4 +1,5 @@ require 'active_record' + class Transactor < ActiveRecord::Base belongs_to :worker @@ -24,3 +25,29 @@ private end end + +class MultipleTransactor < ActiveRecord::Base + + belongs_to :worker + + include AASM + aasm :left, :column => :status do + state :sleeping, :initial => true + state :running, :before_enter => :start_worker, :after_enter => :fail + + event :run do + transitions :to => :running, :from => :sleeping + end + end + +private + + def start_worker + worker.update_attribute(:status, 'running') + end + + def fail + raise StandardError.new('failed on purpose') + end + +end diff --git a/spec/models/validator.rb b/spec/models/validator.rb index c23bd52..133d8e9 100644 --- a/spec/models/validator.rb +++ b/spec/models/validator.rb @@ -38,3 +38,42 @@ class Validator < ActiveRecord::Base raise StandardError.new('failed on purpose') end end + +class MultipleValidator < ActiveRecord::Base + + include AASM + aasm :left, :column => :status do + state :sleeping, :initial => true + state :running + state :failed, :after_enter => :fail + + event :run, :after_commit => :change_name! do + transitions :to => :running, :from => :sleeping + end + event :sleep do + after_commit do |name| + change_name_on_sleep name + end + transitions :to => :sleeping, :from => :running + end + event :fail do + transitions :to => :failed, :from => [:sleeping, :running] + end + end + + validates_presence_of :name + + def change_name! + self.name = "name changed" + save! + end + + def change_name_on_sleep name + self.name = name + save! + end + + def fail + raise StandardError.new('failed on purpose') + end +end diff --git a/spec/unit/persistence/active_record_persistence_multiple_spec.rb b/spec/unit/persistence/active_record_persistence_multiple_spec.rb new file mode 100644 index 0000000..54762de --- /dev/null +++ b/spec/unit/persistence/active_record_persistence_multiple_spec.rb @@ -0,0 +1,512 @@ +require 'active_record' +require 'spec_helper' +Dir[File.dirname(__FILE__) + "/../../models/active_record/*.rb"].sort.each do |f| + require File.expand_path(f) +end + +load_schema + +# if you want to see the statements while running the spec enable the following line +# require 'logger' +# ActiveRecord::Base.logger = Logger.new(STDERR) + +describe "instance methods" do + let(:gate) {MultipleGate.new} + + it "should respond to aasm persistence methods" do + expect(gate).to respond_to(:aasm_read_state) + expect(gate).to respond_to(:aasm_write_state) + expect(gate).to respond_to(:aasm_write_state_without_persistence) + end + + describe "aasm_column_looks_like_enum" do + subject { lambda{ gate.send(:aasm_column_looks_like_enum, :left) } } + + let(:column_name) { "value" } + let(:columns_hash) { Hash[column_name, column] } + + before :each do + allow(gate.class.aasm(:left)).to receive(:attribute_name).and_return(column_name.to_sym) + allow(gate.class).to receive(:columns_hash).and_return(columns_hash) + end + + context "when AASM column has integer type" do + let(:column) { double(Object, type: :integer) } + + it "returns true" do + expect(subject.call).to be_truthy + end + end + + context "when AASM column has string type" do + let(:column) { double(Object, type: :string) } + + it "returns false" do + expect(subject.call).to be_falsey + end + end + end + + describe "aasm_guess_enum_method" do + subject { lambda{ gate.send(:aasm_guess_enum_method, :left) } } + + before :each do + allow(gate.class.aasm(:left)).to receive(:attribute_name).and_return(:value) + end + + it "pluralizes AASM column name" do + expect(subject.call).to eq :values + end + end + + describe "aasm_enum" do + context "when AASM enum setting contains an explicit enum method name" do + let(:with_enum) { MultipleWithEnum.new } + + it "returns whatever value was set in AASM config" do + expect(with_enum.send(:aasm_enum, :left)).to eq :test + end + end + + context "when AASM enum setting is simply set to true" do + let(:with_true_enum) { MultipleWithTrueEnum.new } + before :each do + allow(MultipleWithTrueEnum.aasm(:left)).to receive(:attribute_name).and_return(:value) + end + + it "infers enum method name from pluralized column name" do + expect(with_true_enum.send(:aasm_enum, :left)).to eq :values + end + end + + context "when AASM enum setting is explicitly disabled" do + let(:with_false_enum) { MultipleWithFalseEnum.new } + + it "returns nil" do + expect(with_false_enum.send(:aasm_enum, :left)).to be_nil + end + end + + context "when AASM enum setting is not enabled" do + before :each do + allow(MultipleGate.aasm(:left)).to receive(:attribute_name).and_return(:value) + end + + context "when AASM column looks like enum" do + before :each do + allow(gate).to receive(:aasm_column_looks_like_enum).with(:left).and_return(true) + end + + it "infers enum method name from pluralized column name" do + expect(gate.send(:aasm_enum, :left)).to eq :values + end + end + + context "when AASM column doesn't look like enum'" do + before :each do + allow(gate).to receive(:aasm_column_looks_like_enum) + .and_return(false) + end + + it "returns nil, as we're not using enum" do + expect(gate.send(:aasm_enum, :left)).to be_nil + end + end + end + end + + context "when AASM is configured to use enum" do + let(:state_sym) { :running } + let(:state_code) { 2 } + let(:enum_name) { :states } + let(:enum) { Hash[state_sym, state_code] } + + before :each do + allow(gate).to receive(:aasm_enum).and_return(enum_name) + allow(gate).to receive(:aasm_write_attribute) + allow(gate).to receive(:write_attribute) + + allow(MultipleGate).to receive(enum_name).and_return(enum) + end + + describe "aasm_write_state" do + context "when AASM is configured to skip validations on save" do + before :each do + allow(gate).to receive(:aasm_skipping_validations).and_return(true) + end + + it "passes state code instead of state symbol to update_all" do + # stub_chain does not allow us to give expectations on call + # parameters in the middle of the chain, so we need to use + # intermediate object instead. + obj = double(Object, update_all: 1) + allow(MultipleGate).to receive(:where).and_return(obj) + + gate.aasm_write_state state_sym, :left + + expect(obj).to have_received(:update_all) + .with(Hash[gate.class.aasm(:left).attribute_name, state_code]) + end + end + + context "when AASM is not skipping validations" do + it "delegates state update to the helper method" do + # Let's pretend that validation is passed + allow(gate).to receive(:save).and_return(true) + + gate.aasm_write_state state_sym, :left + + expect(gate).to have_received(:aasm_write_attribute).with(state_sym, :left) + expect(gate).to_not have_received :write_attribute + end + end + end + + describe "aasm_write_state_without_persistence" do + it "delegates state update to the helper method" do + gate.aasm_write_state_without_persistence state_sym, :left + + expect(gate).to have_received(:aasm_write_attribute).with(state_sym, :left) + expect(gate).to_not have_received :write_attribute + end + end + + describe "aasm_raw_attribute_value" do + it "converts state symbol to state code" do + expect(gate.send(:aasm_raw_attribute_value, state_sym)) + .to eq state_code + end + end + end + + context "when AASM is configured to use string field" do + let(:state_sym) { :running } + + before :each do + allow(gate).to receive(:aasm_enum).and_return(nil) + end + + describe "aasm_raw_attribute_value" do + it "converts state symbol to string" do + expect(gate.send(:aasm_raw_attribute_value, state_sym)) + .to eq state_sym.to_s + end + end + end + + describe "aasm_write_attribute helper method" do + let(:sym) { :sym } + let(:value) { 42 } + + before :each do + allow(gate).to receive(:write_attribute) + allow(gate).to receive(:aasm_raw_attribute_value).and_return(value) + + gate.send(:aasm_write_attribute, sym, :left) + end + + it "generates attribute value using a helper method" do + expect(gate).to have_received(:aasm_raw_attribute_value).with(sym, :left) + end + + it "writes attribute to the model" do + expect(gate).to have_received(:write_attribute).with(:aasm_state, value) + end + end + + it "should return the initial state when new and the aasm field is nil" do + expect(gate.aasm(:left).current_state).to eq(:opened) + end + + it "should return the aasm column when new and the aasm field is not nil" do + gate.aasm_state = "closed" + expect(gate.aasm(:left).current_state).to eq(:closed) + end + + it "should return the aasm column when not new and the aasm.attribute_name is not nil" do + allow(gate).to receive(:new_record?).and_return(false) + gate.aasm_state = "state" + expect(gate.aasm(:left).current_state).to eq(:state) + end + + it "should allow a nil state" do + allow(gate).to receive(:new_record?).and_return(false) + gate.aasm_state = nil + expect(gate.aasm(:left).current_state).to be_nil + end + + context 'on initialization' do + it "should initialize the aasm state" do + expect(MultipleGate.new.aasm_state).to eql 'opened' + expect(MultipleGate.new.aasm(:left).current_state).to eql :opened + end + + it "should not initialize the aasm state if it has not been loaded" do + # we have to create a gate in the database, for which we only want to + # load the id, and not the state + gate = MultipleGate.create! + + # then we just load the gate ids + MultipleGate.select(:id).where(id: gate.id).first + end + end + +end + +if ActiveRecord::VERSION::MAJOR < 4 && ActiveRecord::VERSION::MINOR < 2 # won't work with Rails >= 4.2 +describe "direct state column access" do + it "accepts false states" do + f = MultipleFalseState.create! + expect(f.aasm_state).to eql false + expect { + f.aasm(:left).events.map(&:name) + }.to_not raise_error + end +end +end + +describe 'subclasses' do + it "should have the same states as its parent class" do + expect(MultipleDerivateNewDsl.aasm(:left).states).to eq(MultipleSimpleNewDsl.aasm(:left).states) + end + + it "should have the same events as its parent class" do + expect(MultipleDerivateNewDsl.aasm(:left).events).to eq(MultipleSimpleNewDsl.aasm(:left).events) + end + + it "should have the same column as its parent even for the new dsl" do + expect(MultipleSimpleNewDsl.aasm(:left).attribute_name).to eq(:status) + expect(MultipleDerivateNewDsl.aasm(:left).attribute_name).to eq(:status) + end +end + +describe "named scopes with the new DSL" do + context "Does not already respond_to? the scope name" do + it "should add a scope" do + expect(MultipleSimpleNewDsl).to respond_to(:unknown_scope) + expect(MultipleSimpleNewDsl.unknown_scope.is_a?(ActiveRecord::Relation)).to be_truthy + end + end + + context "Already respond_to? the scope name" do + it "should not add a scope" do + expect(MultipleSimpleNewDsl).to respond_to(:new) + expect(MultipleSimpleNewDsl.new.class).to eq(MultipleSimpleNewDsl) + end + end + + it "does not create scopes if requested" do + expect(MultipleNoScope).not_to respond_to(:pending) + end +end # scopes + +describe "direct assignment" do + it "is allowed by default" do + obj = MultipleNoScope.create + expect(obj.aasm_state.to_sym).to eql :pending + + obj.aasm_state = :running + expect(obj.aasm_state.to_sym).to eql :running + end + + it "is forbidden if configured" do + obj = MultipleNoDirectAssignment.create + expect(obj.aasm_state.to_sym).to eql :pending + + expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError) + expect(obj.aasm_state.to_sym).to eql :pending + end + + it 'can be turned off and on again' do + obj = MultipleNoDirectAssignment.create + expect(obj.aasm_state.to_sym).to eql :pending + + expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError) + expect(obj.aasm_state.to_sym).to eql :pending + + # allow it temporarily + MultipleNoDirectAssignment.aasm(:left).state_machine.config.no_direct_assignment = false + obj.aasm_state = :pending + expect(obj.aasm_state.to_sym).to eql :pending + + # and forbid it again + MultipleNoDirectAssignment.aasm(:left).state_machine.config.no_direct_assignment = true + expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError) + expect(obj.aasm_state.to_sym).to eql :pending + end +end # direct assignment + +describe 'initial states' do + it 'should support conditions' do + expect(MultipleThief.new(:skilled => true).aasm(:left).current_state).to eq(:rich) + expect(MultipleThief.new(:skilled => false).aasm(:left).current_state).to eq(:jailed) + end +end + +describe 'transitions with persistence' do + + it "should work for valid models" do + valid_object = MultipleValidator.create(:name => 'name') + expect(valid_object).to be_sleeping + valid_object.status = :running + expect(valid_object).to be_running + end + + it 'should not store states for invalid models' do + validator = MultipleValidator.create(:name => 'name') + expect(validator).to be_valid + expect(validator).to be_sleeping + + validator.name = nil + expect(validator).not_to be_valid + expect(validator.run!).to be_falsey + expect(validator).to be_sleeping + + validator.reload + expect(validator).not_to be_running + expect(validator).to be_sleeping + + validator.name = 'another name' + expect(validator).to be_valid + expect(validator.run!).to be_truthy + expect(validator).to be_running + + validator.reload + expect(validator).to be_running + expect(validator).not_to be_sleeping + end + + it 'should store states for invalid models if configured' do + persistor = MultipleInvalidPersistor.create(:name => 'name') + expect(persistor).to be_valid + expect(persistor).to be_sleeping + + persistor.name = nil + expect(persistor).not_to be_valid + expect(persistor.run!).to be_truthy + expect(persistor).to be_running + + persistor = MultipleInvalidPersistor.find(persistor.id) + persistor.valid? + expect(persistor).to be_valid + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + + persistor.reload + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + end + + describe 'transactions' do + let(:worker) { Worker.create!(:name => 'worker', :status => 'sleeping') } + let(:transactor) { MultipleTransactor.create!(:name => 'transactor', :worker => worker) } + + it 'should rollback all changes' do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + expect {transactor.run!}.to raise_error(StandardError, 'failed on purpose') + expect(transactor).to be_running + expect(worker.reload.status).to eq('sleeping') + end + + context "nested transactions" do + it "should rollback all changes in nested transaction" do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + Worker.transaction do + expect { transactor.run! }.to raise_error(StandardError, 'failed on purpose') + end + + expect(transactor).to be_running + expect(worker.reload.status).to eq('sleeping') + end + + it "should only rollback changes in the main transaction not the nested one" do + # change configuration to not require new transaction + AASM::StateMachine[MultipleTransactor][:left].config.requires_new_transaction = false + + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + Worker.transaction do + expect { transactor.run! }.to raise_error(StandardError, 'failed on purpose') + end + + expect(transactor).to be_running + expect(worker.reload.status).to eq('running') + end + end + + describe "after_commit callback" do + it "should fire :after_commit if transaction was successful" do + validator = MultipleValidator.create(:name => 'name') + expect(validator).to be_sleeping + + validator.run! + expect(validator).to be_running + expect(validator.name).to eq("name changed") + + validator.sleep!("sleeper") + expect(validator).to be_sleeping + expect(validator.name).to eq("sleeper") + end + + it "should not fire :after_commit if transaction failed" do + validator = MultipleValidator.create(:name => 'name') + expect { validator.fail! }.to raise_error(StandardError, 'failed on purpose') + expect(validator.name).to eq("name") + end + + it "should not fire if not saving" do + validator = MultipleValidator.create(:name => 'name') + expect(validator).to be_sleeping + validator.run + expect(validator).to be_running + expect(validator.name).to eq("name") + end + + end + + context "when not persisting" do + it 'should not rollback all changes' do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + # Notice here we're calling "run" and not "run!" with a bang. + expect {transactor.run}.to raise_error(StandardError, 'failed on purpose') + expect(transactor).to be_running + expect(worker.reload.status).to eq('running') + end + + it 'should not create a database transaction' do + expect(transactor.class).not_to receive(:transaction) + expect {transactor.run}.to raise_error(StandardError, 'failed on purpose') + end + end + end +end + +describe "invalid states with persistence" do + + it "should not store states" do + validator = MultipleValidator.create(:name => 'name') + validator.status = 'invalid_state' + expect(validator.save).to be_falsey + expect {validator.save!}.to raise_error(ActiveRecord::RecordInvalid) + + validator.reload + expect(validator).to be_sleeping + end + + it "should store invalid states if configured" do + persistor = MultipleInvalidPersistor.create(:name => 'name') + persistor.status = 'invalid_state' + expect(persistor.save).to be_truthy + + persistor.reload + expect(persistor.status).to eq('invalid_state') + end + +end diff --git a/spec/unit/persistence/active_record_persistence_spec.rb b/spec/unit/persistence/active_record_persistence_spec.rb index 057e337..0939b3a 100644 --- a/spec/unit/persistence/active_record_persistence_spec.rb +++ b/spec/unit/persistence/active_record_persistence_spec.rb @@ -1,19 +1,15 @@ require 'active_record' require 'spec_helper' -Dir[File.dirname(__FILE__) + "/../../models/active_record/*.rb"].sort.each { |f| require File.expand_path(f) } +Dir[File.dirname(__FILE__) + "/../../models/active_record/*.rb"].sort.each do |f| + require File.expand_path(f) +end + load_schema # if you want to see the statements while running the spec enable the following line # require 'logger' # ActiveRecord::Base.logger = Logger.new(STDERR) -shared_examples_for "aasm model" do - it "should include persistence mixins" do - expect(klass.included_modules).to be_include(AASM::Persistence::ActiveRecordPersistence) - expect(klass.included_modules).to be_include(AASM::Persistence::ActiveRecordPersistence::InstanceMethods) - end -end - describe "instance methods" do let(:gate) {Gate.new} From e72f00f5b1b2e60c4bb8818a9548decb7ac3de0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Fri, 10 Jul 2015 22:47:04 +1200 Subject: [PATCH 20/45] add more complex example for ActiveRecord, when two state machines are defined in a class --- PLANNED_CHANGES.md | 2 - spec/database.rb | 5 ++ .../complex_active_record_example.rb | 33 ++++++++++++ spec/unit/inspection_multiple_spec.rb | 1 - ...active_record_persistence_multiple_spec.rb | 52 ++++++++++++++++++- 5 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 spec/models/active_record/complex_active_record_example.rb diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index beed22d..27e97ac 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -3,8 +3,6 @@ ## version 4.3 * add support for multiple state machines per class - * check all tests - * _ActiveRecord_ when :right state machine is defined as well * what happen's if someone accesses `aasm`, but has defined a state machine for `aasm(:my_name)`? * persistence diff --git a/spec/database.rb b/spec/database.rb index 52594aa..ca82afd 100644 --- a/spec/database.rb +++ b/spec/database.rb @@ -12,6 +12,11 @@ ActiveRecord::Migration.suppress_messages do t.string "status" end + ActiveRecord::Migration.create_table "complex_active_record_examples", :force => true do |t| + t.string "left" + t.string "right" + end + ActiveRecord::Migration.create_table "validators", :force => true do |t| t.string "name" t.string "status" diff --git a/spec/models/active_record/complex_active_record_example.rb b/spec/models/active_record/complex_active_record_example.rb new file mode 100644 index 0000000..e02aa37 --- /dev/null +++ b/spec/models/active_record/complex_active_record_example.rb @@ -0,0 +1,33 @@ +class ComplexActiveRecordExample < ActiveRecord::Base + include AASM + + aasm :left, :column => 'left' do + state :one, :initial => true + state :two + state :three + + event :increment do + transitions :from => :one, :to => :two + transitions :from => :two, :to => :three + end + event :reset do + transitions :from => :three, :to => :one + end + end + + aasm :right, :column => 'right' do + state :alpha, :initial => true + state :beta + state :gamma + + event :level_up do + transitions :from => :alpha, :to => :beta + transitions :from => :beta, :to => :gamma + end + event :level_down do + transitions :from => :gamma, :to => :beta + transitions :from => :beta, :to => :alpha + end + end + +end diff --git a/spec/unit/inspection_multiple_spec.rb b/spec/unit/inspection_multiple_spec.rb index a6f32c0..ee8f478 100644 --- a/spec/unit/inspection_multiple_spec.rb +++ b/spec/unit/inspection_multiple_spec.rb @@ -13,7 +13,6 @@ describe 'inspection for common cases' do expect(FooMultiple.aasm(:left).initial_state).to eq(:open) expect(FooMultiple.aasm(:left)).to respond_to(:events) - puts FooMultiple.aasm(:left).events.map(&:name).inspect expect(FooMultiple.aasm(:left).events.size).to eql 2 expect(FooMultiple.aasm(:left).events).to include(:close) expect(FooMultiple.aasm(:left).events).to include(:null) diff --git a/spec/unit/persistence/active_record_persistence_multiple_spec.rb b/spec/unit/persistence/active_record_persistence_multiple_spec.rb index 54762de..f76963d 100644 --- a/spec/unit/persistence/active_record_persistence_multiple_spec.rb +++ b/spec/unit/persistence/active_record_persistence_multiple_spec.rb @@ -489,7 +489,6 @@ describe 'transitions with persistence' do end describe "invalid states with persistence" do - it "should not store states" do validator = MultipleValidator.create(:name => 'name') validator.status = 'invalid_state' @@ -508,5 +507,54 @@ describe "invalid states with persistence" do persistor.reload expect(persistor.status).to eq('invalid_state') end - +end + +describe "complex example" do + it "works" do + record = ComplexActiveRecordExample.new + expect_aasm_states record, :one, :alpha + + record.save! + expect_aasm_states record, :one, :alpha + record.reload + expect_aasm_states record, :one, :alpha + + record.increment! + expect_aasm_states record, :two, :alpha + record.reload + expect_aasm_states record, :two, :alpha + + record.level_up! + expect_aasm_states record, :two, :beta + record.reload + expect_aasm_states record, :two, :beta + + record.increment! + expect { record.increment! }.to raise_error(AASM::InvalidTransition) + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :beta + + record.level_up! + expect_aasm_states record, :three, :gamma + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reset! + expect_aasm_states record, :one, :beta + end + + def expect_aasm_states(record, left_state, right_state) + expect(record.aasm(:left).current_state).to eql left_state.to_sym + expect(record.left).to eql left_state.to_s + expect(record.aasm(:right).current_state).to eql right_state.to_sym + expect(record.right).to eql right_state.to_s + end end From 3eae2553eacd017d08fae82725a3b0a76a47964f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Fri, 10 Jul 2015 22:58:23 +1200 Subject: [PATCH 21/45] update planning --- PLANNED_CHANGES.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 27e97ac..544b38d 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -3,13 +3,12 @@ ## version 4.3 * add support for multiple state machines per class - * what happen's if someone accesses `aasm`, but has defined a - state machine for `aasm(:my_name)`? * persistence - * _ActiveRecord_ * _Mongoid_ * _MongoMapper_ * _Sequel_ + * what happen's if someone accesses `aasm`, but has defined a + state machine for `aasm(:my_name)`? * documentation * silence warnings? @@ -22,3 +21,5 @@ (aka the state machine _name_) * if no selector/name is provided, `:default` will be used * duplicate definitions of states and events will issue warnings + * check all tests + * _ActiveRecord_ From 85905c98752322863091de0df9ecfc0a93d35008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 00:13:27 +1200 Subject: [PATCH 22/45] remove unused methods find_in_state, count_in_state, calculate_in_state, with_state_scope for ActiveRecord --- .../persistence/active_record_persistence.rb | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/lib/aasm/persistence/active_record_persistence.rb b/lib/aasm/persistence/active_record_persistence.rb index c122f69..0193212 100644 --- a/lib/aasm/persistence/active_record_persistence.rb +++ b/lib/aasm/persistence/active_record_persistence.rb @@ -30,7 +30,6 @@ module AASM # def self.included(base) base.send(:include, AASM::Persistence::Base) - base.extend AASM::Persistence::ActiveRecordPersistence::ClassMethods base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods) base.after_initialize do @@ -41,34 +40,6 @@ module AASM base.validate :aasm_validate_states end - module ClassMethods - - def find_in_state(number, state, *args) - with_state_scope state do - find(number, *args) - end - end - - def count_in_state(state, *args) - with_state_scope state do - count(*args) - end - end - - def calculate_in_state(state, *args) - with_state_scope state do - calculate(*args) - end - end - - protected - def with_state_scope(state) - with_scope :find => {:conditions => ["#{table_name}.#{aasm_column} = ?", state.to_s]} do - yield if block_given? - end - end - end - module InstanceMethods # Writes state to the state column and persists it to the database From ebf81091a874a688b5a6cf9c94231f1aedc7dcaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 00:15:47 +1200 Subject: [PATCH 23/45] remove unused methods find_in_state, count_in_state, with_state_scope for Mongoid --- lib/aasm/persistence/mongoid_persistence.rb | 23 ------ .../persistence/mongoid_persistance_spec.rb | 77 +------------------ 2 files changed, 4 insertions(+), 96 deletions(-) diff --git a/lib/aasm/persistence/mongoid_persistence.rb b/lib/aasm/persistence/mongoid_persistence.rb index 41f42e8..b21a152 100644 --- a/lib/aasm/persistence/mongoid_persistence.rb +++ b/lib/aasm/persistence/mongoid_persistence.rb @@ -32,34 +32,11 @@ module AASM # def self.included(base) base.send(:include, AASM::Persistence::Base) - base.extend AASM::Persistence::MongoidPersistence::ClassMethods base.send(:include, AASM::Persistence::MongoidPersistence::InstanceMethods) base.after_initialize :aasm_ensure_initial_state end - module ClassMethods - - def find_in_state(number, state, *args) - with_state_scope state do - find(number, *args) - end - end - - def count_in_state(state, *args) - with_state_scope state do - count(*args) - end - end - - def with_state_scope(state) - with_scope where(aasm.attribute_name.to_sym => state.to_s) do - yield if block_given? - end - end - - end - module InstanceMethods # Writes state to the state column and persists it to the database diff --git a/spec/unit/persistence/mongoid_persistance_spec.rb b/spec/unit/persistence/mongoid_persistance_spec.rb index faba878..7831de0 100644 --- a/spec/unit/persistence/mongoid_persistance_spec.rb +++ b/spec/unit/persistence/mongoid_persistance_spec.rb @@ -5,15 +5,15 @@ describe 'mongoid' do require 'spec_helper' before(:all) do - Dir[File.dirname(__FILE__) + "/../../models/mongoid/*.rb"].sort.each { |f| require File.expand_path(f) } + Dir[File.dirname(__FILE__) + "/../../models/mongoid/*.rb"].sort.each do |f| + require File.expand_path(f) + end # if you want to see the statements while running the spec enable the following line # Mongoid.logger = Logger.new(STDERR) - DATABASE_NAME = "mongoid_#{Process.pid}" - Mongoid.configure do |config| - config.connect_to DATABASE_NAME + config.connect_to "mongoid_#{Process.pid}" end end @@ -61,75 +61,6 @@ describe 'mongoid' do end - describe "#find_in_state" do - - let!(:model) { SimpleNewDslMongoid.create!(:status => :unknown_scope) } - let!(:model_id) { model._id } - - it "should respond to method" do - expect(SimpleNewDslMongoid).to respond_to(:find_in_state) - end - - it "should find the model when given the correct scope and model id" do - expect(SimpleNewDslMongoid.find_in_state(model_id, 'unknown_scope').class).to eq(SimpleNewDslMongoid) - expect(SimpleNewDslMongoid.find_in_state(model_id, 'unknown_scope')).to eq(model) - end - - it "should raise DocumentNotFound error when given incorrect scope" do - expect {SimpleNewDslMongoid.find_in_state(model_id, 'new')}.to raise_error Mongoid::Errors::DocumentNotFound - end - - it "should raise DocumentNotFound error when given incorrect model id" do - expect {SimpleNewDslMongoid.find_in_state('bad_id', 'unknown_scope')}.to raise_error Mongoid::Errors::DocumentNotFound - end - - end - - describe "#count_in_state" do - - before do - 3.times { SimpleNewDslMongoid.create!(:status => :unknown_scope) } - end - - it "should respond to method" do - expect(SimpleNewDslMongoid).to respond_to(:count_in_state) - end - - it "should return n for a scope with n records persisted" do - expect(SimpleNewDslMongoid.count_in_state('unknown_scope').class).to eq(Fixnum) - expect(SimpleNewDslMongoid.count_in_state('unknown_scope')).to eq(3) - end - - it "should return zero for a scope without records persisted" do - expect(SimpleNewDslMongoid.count_in_state('new').class).to eq(Fixnum) - expect(SimpleNewDslMongoid.count_in_state('new')).to eq(0) - end - - end - - describe "#with_state_scope" do - - before do - 3.times { SimpleNewDslMongoid.create!(:status => :unknown_scope) } - 2.times { SimpleNewDslMongoid.create!(:status => :new) } - end - - it "should respond to method" do - expect(SimpleNewDslMongoid).to respond_to(:with_state_scope) - end - - it "should correctly process block" do - expect(SimpleNewDslMongoid.with_state_scope('unknown_scope') do - SimpleNewDslMongoid.count - end).to eq(3) - expect(SimpleNewDslMongoid.with_state_scope('new') do - SimpleNewDslMongoid.count - end).to eq(2) - end - - end - - describe "instance methods" do let(:simple) {SimpleNewDslMongoid.new} From 3970cc45b03f76bb290385d818234bc2e362e0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 00:16:58 +1200 Subject: [PATCH 24/45] test support for Mongoid and multiple state machines per class --- PLANNED_CHANGES.md | 2 +- spec/models/mongoid/no_scope_mongoid.rb | 11 +++ spec/models/mongoid/simple_mongoid.rb | 12 +++ spec/models/mongoid/simple_new_dsl_mongoid.rb | 13 ++++ .../mongoid_persistence_multiple_spec.rb | 75 +++++++++++++++++++ 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 spec/unit/persistence/mongoid_persistence_multiple_spec.rb diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 544b38d..645889f 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -4,7 +4,6 @@ * add support for multiple state machines per class * persistence - * _Mongoid_ * _MongoMapper_ * _Sequel_ * what happen's if someone accesses `aasm`, but has defined a @@ -23,3 +22,4 @@ * duplicate definitions of states and events will issue warnings * check all tests * _ActiveRecord_ + * _Mongoid_ diff --git a/spec/models/mongoid/no_scope_mongoid.rb b/spec/models/mongoid/no_scope_mongoid.rb index b022f6f..04198ea 100644 --- a/spec/models/mongoid/no_scope_mongoid.rb +++ b/spec/models/mongoid/no_scope_mongoid.rb @@ -8,3 +8,14 @@ class NoScopeMongoid state :ignored_scope end end + +class NoScopeMongoidMultiple + include Mongoid::Document + include AASM + + field :status, :type => String + + aasm :left, :create_scopes => false, :column => :status do + state :ignored_scope + end +end diff --git a/spec/models/mongoid/simple_mongoid.rb b/spec/models/mongoid/simple_mongoid.rb index dc8c557..996dec2 100644 --- a/spec/models/mongoid/simple_mongoid.rb +++ b/spec/models/mongoid/simple_mongoid.rb @@ -9,3 +9,15 @@ class SimpleMongoid state :new end end + +class SimpleMongoidMultiple + include Mongoid::Document + include AASM + + field :status, :type => String + + aasm :left, column: :status do + state :unknown_scope + state :new + end +end diff --git a/spec/models/mongoid/simple_new_dsl_mongoid.rb b/spec/models/mongoid/simple_new_dsl_mongoid.rb index a351dfb..bf7daf0 100644 --- a/spec/models/mongoid/simple_new_dsl_mongoid.rb +++ b/spec/models/mongoid/simple_new_dsl_mongoid.rb @@ -10,3 +10,16 @@ class SimpleNewDslMongoid state :new end end + +class SimpleNewDslMongoidMultiple + include Mongoid::Document + include AASM + + field :status, :type => String + + aasm :left, :column => :status + aasm :left do + state :unknown_scope, :initial => true + state :new + end +end diff --git a/spec/unit/persistence/mongoid_persistence_multiple_spec.rb b/spec/unit/persistence/mongoid_persistence_multiple_spec.rb new file mode 100644 index 0000000..90934d6 --- /dev/null +++ b/spec/unit/persistence/mongoid_persistence_multiple_spec.rb @@ -0,0 +1,75 @@ +describe 'mongoid' do + begin + require 'mongoid' + require 'logger' + require 'spec_helper' + + before(:all) do + Dir[File.dirname(__FILE__) + "/../../models/mongoid/*.rb"].sort.each do |f| + require File.expand_path(f) + end + + # if you want to see the statements while running the spec enable the following line + # Mongoid.logger = Logger.new(STDERR) + + Mongoid.configure do |config| + config.connect_to "mongoid_#{Process.pid}" + end + end + + after do + Mongoid.purge! + end + + describe "named scopes with the old DSL" do + + context "Does not already respond_to? the scope name" do + it "should add a scope" do + expect(SimpleMongoidMultiple).to respond_to(:unknown_scope) + expect(SimpleMongoidMultiple.unknown_scope.class).to eq(Mongoid::Criteria) + end + end + + context "Already respond_to? the scope name" do + it "should not add a scope" do + expect(SimpleMongoidMultiple).to respond_to(:new) + expect(SimpleMongoidMultiple.new.class).to eq(SimpleMongoidMultiple) + end + end + + end + + describe "named scopes with the new DSL" do + context "Does not already respond_to? the scope name" do + it "should add a scope" do + expect(SimpleNewDslMongoidMultiple).to respond_to(:unknown_scope) + expect(SimpleNewDslMongoidMultiple.unknown_scope.class).to eq(Mongoid::Criteria) + end + end + + context "Already respond_to? the scope name" do + it "should not add a scope" do + expect(SimpleNewDslMongoidMultiple).to respond_to(:new) + expect(SimpleNewDslMongoidMultiple.new.class).to eq(SimpleNewDslMongoidMultiple) + end + end + + it "does not create scopes if requested" do + expect(NoScopeMongoidMultiple).not_to respond_to(:ignored_scope) + end + end + + describe "instance methods" do + let(:simple) {SimpleNewDslMongoidMultiple.new} + + it "should initialize the aasm state on instantiation" do + expect(SimpleNewDslMongoidMultiple.new.status).to eql 'unknown_scope' + expect(SimpleNewDslMongoidMultiple.new.aasm(:left).current_state).to eql :unknown_scope + end + + end + + rescue LoadError + puts "Not running Mongoid specs because mongoid gem is not installed!!!" + end +end From 68bf3fb4742a009d39d552e8d12bd7080e28e9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 00:19:59 +1200 Subject: [PATCH 25/45] remove unused methods find_in_state, count_in_state, with_state_scope for MongoMapper --- PLANNED_CHANGES.md | 1 + .../persistence/mongo_mapper_persistence.rb | 21 --------- .../mongo_mapper_persistance_spec.rb | 46 ------------------- 3 files changed, 1 insertion(+), 67 deletions(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 645889f..ca433e1 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -23,3 +23,4 @@ * check all tests * _ActiveRecord_ * _Mongoid_ + * drop support for find_in_state, count_in_state, calculate_in_state, with_state_scope diff --git a/lib/aasm/persistence/mongo_mapper_persistence.rb b/lib/aasm/persistence/mongo_mapper_persistence.rb index 871566f..2dc41d7 100644 --- a/lib/aasm/persistence/mongo_mapper_persistence.rb +++ b/lib/aasm/persistence/mongo_mapper_persistence.rb @@ -32,7 +32,6 @@ module AASM # def self.included(base) base.send(:include, AASM::Persistence::Base) - base.extend AASM::Persistence::MongoMapperPersistence::ClassMethods base.send(:include, AASM::Persistence::MongoMapperPersistence::InstanceMethods) base.before_create :aasm_ensure_initial_state @@ -41,26 +40,6 @@ module AASM base.validate :aasm_validate_states end - module ClassMethods - - def find_in_state(number, state, *args) - with_state_scope(state).find!(number, *args) - end - - def count_in_state(state, *args) - with_state_scope(state).count(*args) - end - - def calculate_in_state(state, *args) - with_state_scope(state).calculate(*args) - end - - protected - def with_state_scope(state) - where(aasm.attribute_name.to_sym => state.to_s) - end - end - module InstanceMethods # Writes state to the state column and persists it to the database diff --git a/spec/unit/persistence/mongo_mapper_persistance_spec.rb b/spec/unit/persistence/mongo_mapper_persistance_spec.rb index d7ce651..e33a2d2 100644 --- a/spec/unit/persistence/mongo_mapper_persistance_spec.rb +++ b/spec/unit/persistence/mongo_mapper_persistance_spec.rb @@ -64,52 +64,6 @@ describe 'mongo_mapper' do end - describe "#find_in_state" do - - let!(:model) { SimpleNewDslMongoMapper.create!(:status => :unknown_scope) } - let!(:model_id) { model._id } - - it "should respond to method" do - expect(SimpleNewDslMongoMapper).to respond_to(:find_in_state) - end - - it "should find the model when given the correct scope and model id" do - expect(SimpleNewDslMongoMapper.find_in_state(model_id, 'unknown_scope').class).to eq(SimpleNewDslMongoMapper) - expect(SimpleNewDslMongoMapper.find_in_state(model_id, 'unknown_scope')).to eq(model) - end - - it "should raise DocumentNotFound error when given incorrect scope" do - expect {SimpleNewDslMongoMapper.find_in_state(model_id, 'next')}.to raise_error MongoMapper::DocumentNotFound - end - - it "should raise DocumentNotFound error when given incorrect model id" do - expect {SimpleNewDslMongoMapper.find_in_state('bad_id', 'unknown_scope')}.to raise_error MongoMapper::DocumentNotFound - end - - end - - describe "#count_in_state" do - - before do - 3.times { SimpleNewDslMongoMapper.create!(:status => :unknown_scope) } - end - - it "should respond to method" do - expect(SimpleNewDslMongoMapper).to respond_to(:count_in_state) - end - - it "should return n for a scope with n records persisted" do - expect(SimpleNewDslMongoMapper.count_in_state('unknown_scope').class).to eq(Fixnum) - expect(SimpleNewDslMongoMapper.count_in_state('unknown_scope')).to eq(3) - end - - it "should return zero for a scope without records persisted" do - expect(SimpleNewDslMongoMapper.count_in_state('next').class).to eq(Fixnum) - expect(SimpleNewDslMongoMapper.count_in_state('next')).to eq(0) - end - - end - describe "instance methods" do let(:simple) {SimpleNewDslMongoMapper.new} From f638333313ffdbde5983dc65c2dac27c82888fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 21:45:31 +1200 Subject: [PATCH 26/45] correct spelling of filenames --- ...apper_persistance_spec.rb => mongo_mapper_persistence_spec.rb} | 0 .../{mongoid_persistance_spec.rb => mongoid_persistence_spec.rb} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename spec/unit/persistence/{mongo_mapper_persistance_spec.rb => mongo_mapper_persistence_spec.rb} (100%) rename spec/unit/persistence/{mongoid_persistance_spec.rb => mongoid_persistence_spec.rb} (100%) diff --git a/spec/unit/persistence/mongo_mapper_persistance_spec.rb b/spec/unit/persistence/mongo_mapper_persistence_spec.rb similarity index 100% rename from spec/unit/persistence/mongo_mapper_persistance_spec.rb rename to spec/unit/persistence/mongo_mapper_persistence_spec.rb diff --git a/spec/unit/persistence/mongoid_persistance_spec.rb b/spec/unit/persistence/mongoid_persistence_spec.rb similarity index 100% rename from spec/unit/persistence/mongoid_persistance_spec.rb rename to spec/unit/persistence/mongoid_persistence_spec.rb From 44cf3f32209a33e97948a32ee4ab09b525a142c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 22:10:04 +1200 Subject: [PATCH 27/45] test support for MongoMapper and multiple state machines per class --- Gemfile | 4 +- .../persistence/mongo_mapper_persistence.rb | 16 +- .../complex_mongo_mapper_example.rb | 37 +++++ .../mongo_mapper/no_scope_mongo_mapper.rb | 11 ++ .../mongo_mapper/simple_mongo_mapper.rb | 12 ++ .../simple_new_dsl_mongo_mapper.rb | 13 ++ .../mongo_mapper_persistence_multiple_spec.rb | 144 ++++++++++++++++++ .../mongo_mapper_persistence_spec.rb | 4 +- 8 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 spec/models/mongo_mapper/complex_mongo_mapper_example.rb create mode 100644 spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb diff --git a/Gemfile b/Gemfile index 95c7aea..e487ce0 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ gem 'mongoid', '~>4.0' if Gem::Version.create(RUBY_VERSION.dup) >= Gem::Version. gem 'sequel' # Since mongoid V4 requires incompatible bson V2, cannot have mongoid (V4 or greater) # and mongo_mapper ( or mongo ) in the same application -#gem 'mongo_mapper', '~> 0.13' -#gem 'bson_ext', :platforms => :ruby +# gem 'mongo_mapper', '~> 0.13' +# gem 'bson_ext', :platforms => :ruby gemspec diff --git a/lib/aasm/persistence/mongo_mapper_persistence.rb b/lib/aasm/persistence/mongo_mapper_persistence.rb index 2dc41d7..9199b1f 100644 --- a/lib/aasm/persistence/mongo_mapper_persistence.rb +++ b/lib/aasm/persistence/mongo_mapper_persistence.rb @@ -51,18 +51,18 @@ module AASM # Foo.find(1).aasm.current_state # => :closed # # NOTE: intended to be called from an event - def aasm_write_state(state) - old_value = read_attribute(self.class.aasm.attribute_name) - aasm_write_attribute state + def aasm_write_state(state, name=:default) + old_value = read_attribute(self.class.aasm(name).attribute_name) + write_attribute(self.class.aasm(name).attribute_name, state) - success = if aasm_skipping_validations - value = aasm_raw_attribute_value state - self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm.attribute_name => value) == 1 + success = if aasm_skipping_validations(name) + value = aasm_raw_attribute_value(state, name) + self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm(name).attribute_name => value) == 1 else self.save end unless success - write_attribute(self.class.aasm.attribute_name, old_value) + write_attribute(self.class.aasm(name).attribute_name, old_value) return false end @@ -82,7 +82,7 @@ module AASM # # NOTE: intended to be called from an event def aasm_write_state_without_persistence(state, name=:default) - aasm_write_attribute state + aasm_write_attribute(state, name) end private diff --git a/spec/models/mongo_mapper/complex_mongo_mapper_example.rb b/spec/models/mongo_mapper/complex_mongo_mapper_example.rb new file mode 100644 index 0000000..efb1e5b --- /dev/null +++ b/spec/models/mongo_mapper/complex_mongo_mapper_example.rb @@ -0,0 +1,37 @@ +class ComplexMongoMapperExample + include MongoMapper::Document + include AASM + + key :left, String + key :right, String + + aasm :left, :column => 'left' do + state :one, :initial => true + state :two + state :three + + event :increment do + transitions :from => :one, :to => :two + transitions :from => :two, :to => :three + end + event :reset do + transitions :from => :three, :to => :one + end + end + + aasm :right, :column => 'right' do + state :alpha, :initial => true + state :beta + state :gamma + + event :level_up do + transitions :from => :alpha, :to => :beta + transitions :from => :beta, :to => :gamma + end + event :level_down do + transitions :from => :gamma, :to => :beta + transitions :from => :beta, :to => :alpha + end + end + +end diff --git a/spec/models/mongo_mapper/no_scope_mongo_mapper.rb b/spec/models/mongo_mapper/no_scope_mongo_mapper.rb index 7d7454a..d79d3f8 100644 --- a/spec/models/mongo_mapper/no_scope_mongo_mapper.rb +++ b/spec/models/mongo_mapper/no_scope_mongo_mapper.rb @@ -8,3 +8,14 @@ class NoScopeMongoMapper state :ignored_scope end end + +class NoScopeMongoMapperMultiple + include MongoMapper::Document + include AASM + + key :status, String + + aasm :left, :create_scopes => false, :column => :status do + state :ignored_scope + end +end diff --git a/spec/models/mongo_mapper/simple_mongo_mapper.rb b/spec/models/mongo_mapper/simple_mongo_mapper.rb index 0df4f14..b8bdc56 100644 --- a/spec/models/mongo_mapper/simple_mongo_mapper.rb +++ b/spec/models/mongo_mapper/simple_mongo_mapper.rb @@ -9,3 +9,15 @@ class SimpleMongoMapper state :next end end + +class SimpleMongoMapperMultiple + include MongoMapper::Document + include AASM + + key :status, String + + aasm :left, column: :status do + state :unknown_scope + state :next + end +end diff --git a/spec/models/mongo_mapper/simple_new_dsl_mongo_mapper.rb b/spec/models/mongo_mapper/simple_new_dsl_mongo_mapper.rb index 5d047a2..87bd7b5 100644 --- a/spec/models/mongo_mapper/simple_new_dsl_mongo_mapper.rb +++ b/spec/models/mongo_mapper/simple_new_dsl_mongo_mapper.rb @@ -10,3 +10,16 @@ class SimpleNewDslMongoMapper state :next end end + +class SimpleNewDslMongoMapperMultiple + include MongoMapper::Document + include AASM + + key :status, String + + aasm :left, :column => :status + aasm :left do + state :unknown_scope + state :next + end +end diff --git a/spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb b/spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb new file mode 100644 index 0000000..90fc0a1 --- /dev/null +++ b/spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb @@ -0,0 +1,144 @@ +describe 'mongo_mapper' do + begin + require 'mongo_mapper' + require 'logger' + require 'spec_helper' + + before(:all) do + Dir[File.dirname(__FILE__) + "/../../models/mongo_mapper/*.rb"].sort.each { |f| require File.expand_path(f) } + + config = { + 'test' => { + 'database' => "mongo_mapper_#{Process.pid}" + } + } + + MongoMapper.setup(config, 'test') #, :logger => Logger.new(STDERR)) + end + + after do + # Clear Out all non-system Mongo collections between tests + MongoMapper.database.collections.each do |collection| + collection.drop unless collection.capped? || (collection.name =~ /\Asystem/) + end + end + + describe "named scopes with the old DSL" do + + context "Does not already respond_to? the scope name" do + it "should add a scope" do + expect(SimpleMongoMapperMultiple).to respond_to(:unknown_scope) + expect(SimpleMongoMapperMultiple.unknown_scope.class).to eq(MongoMapper::Plugins::Querying::DecoratedPluckyQuery) + #expect(SimpleMongoMapperMultiple.unknown_scope.is_a?(ActiveRecord::Relation)).to be_truthy + end + end + + context "Already respond_to? the scope name" do + it "should not add a scope" do + expect(SimpleMongoMapperMultiple).to respond_to(:next) + expect(SimpleMongoMapperMultiple.new.class).to eq(SimpleMongoMapperMultiple) + end + end + + end + + describe "named scopes with the new DSL" do + + context "Does not already respond_to? the scope name" do + it "should add a scope" do + expect(SimpleNewDslMongoMapperMultiple).to respond_to(:unknown_scope) + expect(SimpleNewDslMongoMapperMultiple.unknown_scope.class).to eq(MongoMapper::Plugins::Querying::DecoratedPluckyQuery) + end + end + + context "Already respond_to? the scope name" do + it "should not add a scope" do + expect(SimpleNewDslMongoMapperMultiple).to respond_to(:next) + expect(SimpleNewDslMongoMapperMultiple.new.class).to eq(SimpleNewDslMongoMapperMultiple) + end + end + + it "does not create scopes if requested" do + expect(NoScopeMongoMapperMultiple).not_to respond_to(:ignored_scope) + end + + end + + describe "instance methods" do + + let(:simple) {SimpleNewDslMongoMapperMultiple.new} + + it "should call aasm_ensure_initial_state on validation before create" do + expect(SimpleNewDslMongoMapperMultiple.aasm(:left).initial_state).to eq(:unknown_scope) + expect(SimpleNewDslMongoMapperMultiple.aasm(:left).attribute_name).to eq(:status) + expect(simple.status).to eq(nil) + simple.valid? + expect(simple.status).to eq('unknown_scope') + end + + it "should call aasm_ensure_initial_state before create, even if skipping validations" do + expect(simple.status).to eq(nil) + simple.save(:validate => false) + expect(simple.status).to eq('unknown_scope') + end + end + + describe "complex example" do + it "works" do + record = ComplexMongoMapperExample.new + expect(record.aasm(:left).current_state).to eql :one + expect(record.left).to be_nil + expect(record.aasm(:right).current_state).to eql :alpha + expect(record.right).to be_nil + + record.save! + expect_aasm_states record, :one, :alpha + record.reload + expect_aasm_states record, :one, :alpha + + record.increment! + expect_aasm_states record, :two, :alpha + record.reload + expect_aasm_states record, :two, :alpha + + record.level_up! + expect_aasm_states record, :two, :beta + record.reload + expect_aasm_states record, :two, :beta + + record.increment! + expect { record.increment! }.to raise_error(AASM::InvalidTransition) + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :beta + + record.level_up! + expect_aasm_states record, :three, :gamma + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reset! + expect_aasm_states record, :one, :beta + end + + def expect_aasm_states(record, left_state, right_state) + expect(record.aasm(:left).current_state).to eql left_state.to_sym + expect(record.left).to eql left_state.to_s + expect(record.aasm(:right).current_state).to eql right_state.to_sym + expect(record.right).to eql right_state.to_s + end + end + + rescue LoadError + puts "--------------------------------------------------------------------------" + puts "Not running MongoMapper specs because mongo_mapper gem is not installed!!!" + puts "--------------------------------------------------------------------------" + end +end diff --git a/spec/unit/persistence/mongo_mapper_persistence_spec.rb b/spec/unit/persistence/mongo_mapper_persistence_spec.rb index e33a2d2..89f3057 100644 --- a/spec/unit/persistence/mongo_mapper_persistence_spec.rb +++ b/spec/unit/persistence/mongo_mapper_persistence_spec.rb @@ -84,6 +84,8 @@ describe 'mongo_mapper' do end rescue LoadError - puts "Not running MongoMapper specs because mongo_mapper gem is not installed!!!" + puts "--------------------------------------------------------------------------" + puts "Not running MongoMapper multiple-specs because mongo_mapper gem is not installed!!!" + puts "--------------------------------------------------------------------------" end end From 440587c806be4ba5c7612c8efddb28db577a6b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 22:10:47 +1200 Subject: [PATCH 28/45] add a complex example spec for Mongoid --- .../models/mongoid/complex_mongoid_example.rb | 37 +++++++++++++ .../mongoid_persistence_multiple_spec.rb | 54 ++++++++++++++++++- .../persistence/mongoid_persistence_spec.rb | 2 + 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 spec/models/mongoid/complex_mongoid_example.rb diff --git a/spec/models/mongoid/complex_mongoid_example.rb b/spec/models/mongoid/complex_mongoid_example.rb new file mode 100644 index 0000000..06b4809 --- /dev/null +++ b/spec/models/mongoid/complex_mongoid_example.rb @@ -0,0 +1,37 @@ +class ComplexMongoidExample + include Mongoid::Document + include AASM + + field :left, :type => String + field :right, :type => String + + aasm :left, :column => 'left' do + state :one, :initial => true + state :two + state :three + + event :increment do + transitions :from => :one, :to => :two + transitions :from => :two, :to => :three + end + event :reset do + transitions :from => :three, :to => :one + end + end + + aasm :right, :column => 'right' do + state :alpha, :initial => true + state :beta + state :gamma + + event :level_up do + transitions :from => :alpha, :to => :beta + transitions :from => :beta, :to => :gamma + end + event :level_down do + transitions :from => :gamma, :to => :beta + transitions :from => :beta, :to => :alpha + end + end + +end diff --git a/spec/unit/persistence/mongoid_persistence_multiple_spec.rb b/spec/unit/persistence/mongoid_persistence_multiple_spec.rb index 90934d6..99895e5 100644 --- a/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +++ b/spec/unit/persistence/mongoid_persistence_multiple_spec.rb @@ -69,7 +69,59 @@ describe 'mongoid' do end + describe "complex example" do + it "works" do + record = ComplexMongoidExample.new + expect_aasm_states record, :one, :alpha + + record.save! + expect_aasm_states record, :one, :alpha + record.reload + expect_aasm_states record, :one, :alpha + + record.increment! + expect_aasm_states record, :two, :alpha + record.reload + expect_aasm_states record, :two, :alpha + + record.level_up! + expect_aasm_states record, :two, :beta + record.reload + expect_aasm_states record, :two, :beta + + record.increment! + expect { record.increment! }.to raise_error(AASM::InvalidTransition) + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :beta + + record.level_up! + expect_aasm_states record, :three, :gamma + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reset! + expect_aasm_states record, :one, :beta + end + + def expect_aasm_states(record, left_state, right_state) + expect(record.aasm(:left).current_state).to eql left_state.to_sym + expect(record.left).to eql left_state.to_s + expect(record.aasm(:right).current_state).to eql right_state.to_sym + expect(record.right).to eql right_state.to_s + end + end + rescue LoadError - puts "Not running Mongoid specs because mongoid gem is not installed!!!" + puts "--------------------------------------------------------------------------" + puts "Not running Mongoid multiple-specs because mongoid gem is not installed!!!" + puts "--------------------------------------------------------------------------" end end diff --git a/spec/unit/persistence/mongoid_persistence_spec.rb b/spec/unit/persistence/mongoid_persistence_spec.rb index 7831de0..56ffd9c 100644 --- a/spec/unit/persistence/mongoid_persistence_spec.rb +++ b/spec/unit/persistence/mongoid_persistence_spec.rb @@ -72,6 +72,8 @@ describe 'mongoid' do end rescue LoadError + puts "--------------------------------------------------------------------------" puts "Not running Mongoid specs because mongoid gem is not installed!!!" + puts "--------------------------------------------------------------------------" end end From 23fe435a474c043328291f07b56fe9a078f6c808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 22:11:19 +1200 Subject: [PATCH 29/45] document changes --- PLANNED_CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index ca433e1..9e319a4 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -4,12 +4,12 @@ * add support for multiple state machines per class * persistence - * _MongoMapper_ * _Sequel_ * what happen's if someone accesses `aasm`, but has defined a state machine for `aasm(:my_name)`? * documentation * silence warnings? + * drop support for aasm_column ? # Changes so far @@ -23,4 +23,5 @@ * check all tests * _ActiveRecord_ * _Mongoid_ + * _MongoMapper_ * drop support for find_in_state, count_in_state, calculate_in_state, with_state_scope From abcad714a9829f43894d41ac07ea36098e552c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 22:20:26 +1200 Subject: [PATCH 30/45] test support for Sequel and multiple state machines per class --- .../sequel_persistence_multiple_spec.rb | 120 ++++++++++++++++++ .../persistence/sequel_persistence_spec.rb | 3 +- 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 spec/unit/persistence/sequel_persistence_multiple_spec.rb diff --git a/spec/unit/persistence/sequel_persistence_multiple_spec.rb b/spec/unit/persistence/sequel_persistence_multiple_spec.rb new file mode 100644 index 0000000..92fbaed --- /dev/null +++ b/spec/unit/persistence/sequel_persistence_multiple_spec.rb @@ -0,0 +1,120 @@ +describe 'sequel' do + begin + require 'sequel' + require 'logger' + require 'spec_helper' + + before(:all) do + db = Sequel.connect(SEQUEL_DB) + + # if you want to see the statements while running the spec enable the following line + # db.loggers << Logger.new($stderr) + db.create_table(:models) do + primary_key :id + String :status + end + + @model = Class.new(Sequel::Model(db)) do + set_dataset(:models) + include AASM + + attr_accessor :default + + aasm :left, :column => :status + aasm :left do + state :alpha, :initial => true + state :beta + state :gamma + event :release do + transitions :from => [:alpha, :beta, :gamma], :to => :beta + end + end + end + end + + describe "instance methods" do + let(:model) {@model.new} + + it "should respond to aasm persistence methods" do + expect(model).to respond_to(:aasm_read_state) + expect(model).to respond_to(:aasm_write_state) + expect(model).to respond_to(:aasm_write_state_without_persistence) + end + + it "should return the initial state when new and the aasm field is nil" do + expect(model.aasm(:left).current_state).to eq(:alpha) + end + + it "should save the initial state" do + model.save + expect(model.status).to eq("alpha") + end + + it "should return the aasm column when new and the aasm field is not nil" do + model.status = "beta" + expect(model.aasm(:left).current_state).to eq(:beta) + end + + it "should return the aasm column when not new and the aasm_column is not nil" do + allow(model).to receive(:new?).and_return(false) + model.status = "gamma" + expect(model.aasm(:left).current_state).to eq(:gamma) + end + + it "should allow a nil state" do + allow(model).to receive(:new?).and_return(false) + model.status = nil + expect(model.aasm(:left).current_state).to be_nil + end + + it "should not change the state if state is not loaded" do + model.release + model.save + model.class.select(:id).first.save + model.reload + expect(model.aasm(:left).current_state).to eq(:beta) + end + + it "should call aasm_ensure_initial_state on validation before create" do + expect(model).to receive(:aasm_ensure_initial_state).and_return(true) + model.valid? + end + + it "should call aasm_ensure_initial_state before create, even if skipping validations" do + expect(model).to receive(:aasm_ensure_initial_state).and_return(true) + model.save(:validate => false) + end + end + + describe 'subclasses' do + it "should have the same states as its parent class" do + expect(Class.new(@model).aasm(:left).states).to eq(@model.aasm(:left).states) + end + + it "should have the same events as its parent class" do + expect(Class.new(@model).aasm(:left).events).to eq(@model.aasm(:left).events) + end + + it "should have the same column as its parent even for the new dsl" do + expect(@model.aasm(:left).attribute_name).to eq(:status) + expect(Class.new(@model).aasm(:left).attribute_name).to eq(:status) + end + end + + describe 'initial states' do + it 'should support conditions' do + @model.aasm(:left) do + initial_state lambda{ |m| m.default } + end + + expect(@model.new(:default => :beta).aasm(:left).current_state).to eq(:beta) + expect(@model.new(:default => :gamma).aasm(:left).current_state).to eq(:gamma) + end + end + + rescue LoadError + puts "------------------------------------------------------------------------" + puts "Not running Sequel multiple-specs because sequel gem is not installed!!!" + puts "------------------------------------------------------------------------" + end +end diff --git a/spec/unit/persistence/sequel_persistence_spec.rb b/spec/unit/persistence/sequel_persistence_spec.rb index 8c4f01b..fd3eec0 100644 --- a/spec/unit/persistence/sequel_persistence_spec.rb +++ b/spec/unit/persistence/sequel_persistence_spec.rb @@ -1,4 +1,3 @@ - describe 'sequel' do begin require 'sequel' @@ -112,6 +111,8 @@ describe 'sequel' do end rescue LoadError + puts "------------------------------------------------------------------------" puts "Not running Sequel specs because sequel gem is not installed!!!" + puts "------------------------------------------------------------------------" end end From 5acb9a4921df2a7742f7f35d6e86d2e071a9f157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 22:27:48 +1200 Subject: [PATCH 31/45] add a complex example spec for Sequel --- spec/models/sequel/complex_sequel_example.rb | 45 +++++++++++++++ .../sequel_persistence_multiple_spec.rb | 57 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 spec/models/sequel/complex_sequel_example.rb diff --git a/spec/models/sequel/complex_sequel_example.rb b/spec/models/sequel/complex_sequel_example.rb new file mode 100644 index 0000000..9cca371 --- /dev/null +++ b/spec/models/sequel/complex_sequel_example.rb @@ -0,0 +1,45 @@ +db = Sequel.connect(SEQUEL_DB) + +# if you want to see the statements while running the spec enable the following line +# db.loggers << Logger.new($stderr) +db.create_table(:complex_sequel_examples) do + primary_key :id + String :left + String :right +end + +class ComplexSequelExample < Sequel::Model(db) + set_dataset(:complex_sequel_examples) + + include AASM + + aasm :left, :column => 'left' do + state :one, :initial => true + state :two + state :three + + event :increment do + transitions :from => :one, :to => :two + transitions :from => :two, :to => :three + end + event :reset do + transitions :from => :three, :to => :one + end + end + + aasm :right, :column => 'right' do + state :alpha, :initial => true + state :beta + state :gamma + + event :level_up do + transitions :from => :alpha, :to => :beta + transitions :from => :beta, :to => :gamma + end + event :level_down do + transitions :from => :gamma, :to => :beta + transitions :from => :beta, :to => :alpha + end + end + +end diff --git a/spec/unit/persistence/sequel_persistence_multiple_spec.rb b/spec/unit/persistence/sequel_persistence_multiple_spec.rb index 92fbaed..2142963 100644 --- a/spec/unit/persistence/sequel_persistence_multiple_spec.rb +++ b/spec/unit/persistence/sequel_persistence_multiple_spec.rb @@ -4,6 +4,10 @@ describe 'sequel' do require 'logger' require 'spec_helper' + Dir[File.dirname(__FILE__) + "/../../models/sequel/*.rb"].sort.each do |f| + require File.expand_path(f) + end + before(:all) do db = Sequel.connect(SEQUEL_DB) @@ -112,6 +116,59 @@ describe 'sequel' do end end + describe "complex example" do + it "works" do + record = ComplexSequelExample.new + expect(record.aasm(:left).current_state).to eql :one + expect(record.left).to be_nil + expect(record.aasm(:right).current_state).to eql :alpha + expect(record.right).to be_nil + + record.save + expect_aasm_states record, :one, :alpha + record.reload + expect_aasm_states record, :one, :alpha + + record.increment! + expect_aasm_states record, :two, :alpha + record.reload + expect_aasm_states record, :two, :alpha + + record.level_up! + expect_aasm_states record, :two, :beta + record.reload + expect_aasm_states record, :two, :beta + + record.increment! + expect { record.increment! }.to raise_error(AASM::InvalidTransition) + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :beta + + record.level_up! + expect_aasm_states record, :three, :gamma + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reload + expect_aasm_states record, :three, :gamma + + record.level_down # without saving + expect_aasm_states record, :three, :beta + record.reset! + expect_aasm_states record, :one, :beta + end + + def expect_aasm_states(record, left_state, right_state) + expect(record.aasm(:left).current_state).to eql left_state.to_sym + expect(record.left).to eql left_state.to_s + expect(record.aasm(:right).current_state).to eql right_state.to_sym + expect(record.right).to eql right_state.to_s + end + end + rescue LoadError puts "------------------------------------------------------------------------" puts "Not running Sequel multiple-specs because sequel gem is not installed!!!" From 01bd9758172729faf687db89045b1e382cbf1dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 22:29:55 +1200 Subject: [PATCH 32/45] clean up: test models need to be required only once --- .../persistence/mongo_mapper_persistence_multiple_spec.rb | 6 ++++-- spec/unit/persistence/mongo_mapper_persistence_spec.rb | 6 ++++-- .../unit/persistence/mongoid_persistence_multiple_spec.rb | 8 ++++---- spec/unit/persistence/mongoid_persistence_spec.rb | 8 ++++---- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb b/spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb index 90fc0a1..7e096d3 100644 --- a/spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb +++ b/spec/unit/persistence/mongo_mapper_persistence_multiple_spec.rb @@ -4,9 +4,11 @@ describe 'mongo_mapper' do require 'logger' require 'spec_helper' - before(:all) do - Dir[File.dirname(__FILE__) + "/../../models/mongo_mapper/*.rb"].sort.each { |f| require File.expand_path(f) } + Dir[File.dirname(__FILE__) + "/../../models/mongo_mapper/*.rb"].sort.each do |f| + require File.expand_path(f) + end + before(:all) do config = { 'test' => { 'database' => "mongo_mapper_#{Process.pid}" diff --git a/spec/unit/persistence/mongo_mapper_persistence_spec.rb b/spec/unit/persistence/mongo_mapper_persistence_spec.rb index 89f3057..acaf772 100644 --- a/spec/unit/persistence/mongo_mapper_persistence_spec.rb +++ b/spec/unit/persistence/mongo_mapper_persistence_spec.rb @@ -4,9 +4,11 @@ describe 'mongo_mapper' do require 'logger' require 'spec_helper' - before(:all) do - Dir[File.dirname(__FILE__) + "/../../models/mongo_mapper/*.rb"].sort.each { |f| require File.expand_path(f) } + Dir[File.dirname(__FILE__) + "/../../models/mongo_mapper/*.rb"].sort.each do |f| + require File.expand_path(f) + end + before(:all) do config = { 'test' => { 'database' => "mongo_mapper_#{Process.pid}" diff --git a/spec/unit/persistence/mongoid_persistence_multiple_spec.rb b/spec/unit/persistence/mongoid_persistence_multiple_spec.rb index 99895e5..f1c7cec 100644 --- a/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +++ b/spec/unit/persistence/mongoid_persistence_multiple_spec.rb @@ -4,11 +4,11 @@ describe 'mongoid' do require 'logger' require 'spec_helper' - before(:all) do - Dir[File.dirname(__FILE__) + "/../../models/mongoid/*.rb"].sort.each do |f| - require File.expand_path(f) - end + Dir[File.dirname(__FILE__) + "/../../models/mongoid/*.rb"].sort.each do |f| + require File.expand_path(f) + end + before(:all) do # if you want to see the statements while running the spec enable the following line # Mongoid.logger = Logger.new(STDERR) diff --git a/spec/unit/persistence/mongoid_persistence_spec.rb b/spec/unit/persistence/mongoid_persistence_spec.rb index 56ffd9c..d8ca760 100644 --- a/spec/unit/persistence/mongoid_persistence_spec.rb +++ b/spec/unit/persistence/mongoid_persistence_spec.rb @@ -4,11 +4,11 @@ describe 'mongoid' do require 'logger' require 'spec_helper' - before(:all) do - Dir[File.dirname(__FILE__) + "/../../models/mongoid/*.rb"].sort.each do |f| - require File.expand_path(f) - end + Dir[File.dirname(__FILE__) + "/../../models/mongoid/*.rb"].sort.each do |f| + require File.expand_path(f) + end + before(:all) do # if you want to see the statements while running the spec enable the following line # Mongoid.logger = Logger.new(STDERR) From 2652abd4ec76ad974e9765abb411fec9e4db9246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Sat, 11 Jul 2015 22:36:07 +1200 Subject: [PATCH 33/45] extract Sequel test models into model files --- spec/models/sequel/sequel_multiple.rb | 25 +++++++++++++++++ spec/models/sequel/sequel_simple.rb | 25 +++++++++++++++++ .../sequel_persistence_multiple_spec.rb | 26 +---------------- .../persistence/sequel_persistence_spec.rb | 28 ++++--------------- 4 files changed, 56 insertions(+), 48 deletions(-) create mode 100644 spec/models/sequel/sequel_multiple.rb create mode 100644 spec/models/sequel/sequel_simple.rb diff --git a/spec/models/sequel/sequel_multiple.rb b/spec/models/sequel/sequel_multiple.rb new file mode 100644 index 0000000..c17209e --- /dev/null +++ b/spec/models/sequel/sequel_multiple.rb @@ -0,0 +1,25 @@ +db = Sequel.connect(SEQUEL_DB) + +# if you want to see the statements while running the spec enable the following line +# db.loggers << Logger.new($stderr) +db.create_table(:multiples) do + primary_key :id + String :status +end + +class SequelMultiple < Sequel::Model(db) + set_dataset(:multiples) + include AASM + + attr_accessor :default + + aasm :left, :column => :status + aasm :left do + state :alpha, :initial => true + state :beta + state :gamma + event :release do + transitions :from => [:alpha, :beta, :gamma], :to => :beta + end + end +end diff --git a/spec/models/sequel/sequel_simple.rb b/spec/models/sequel/sequel_simple.rb new file mode 100644 index 0000000..bc84f2b --- /dev/null +++ b/spec/models/sequel/sequel_simple.rb @@ -0,0 +1,25 @@ +db = Sequel.connect(SEQUEL_DB) + +# if you want to see the statements while running the spec enable the following line +# db.loggers << Logger.new($stderr) +db.create_table(:simples) do + primary_key :id + String :status +end + +class SequelSimple < Sequel::Model(db) + set_dataset(:simples) + include AASM + + attr_accessor :default + + aasm :column => :status + aasm do + state :alpha, :initial => true + state :beta + state :gamma + event :release do + transitions :from => [:alpha, :beta, :gamma], :to => :beta + end + end +end diff --git a/spec/unit/persistence/sequel_persistence_multiple_spec.rb b/spec/unit/persistence/sequel_persistence_multiple_spec.rb index 2142963..f17244a 100644 --- a/spec/unit/persistence/sequel_persistence_multiple_spec.rb +++ b/spec/unit/persistence/sequel_persistence_multiple_spec.rb @@ -9,31 +9,7 @@ describe 'sequel' do end before(:all) do - db = Sequel.connect(SEQUEL_DB) - - # if you want to see the statements while running the spec enable the following line - # db.loggers << Logger.new($stderr) - db.create_table(:models) do - primary_key :id - String :status - end - - @model = Class.new(Sequel::Model(db)) do - set_dataset(:models) - include AASM - - attr_accessor :default - - aasm :left, :column => :status - aasm :left do - state :alpha, :initial => true - state :beta - state :gamma - event :release do - transitions :from => [:alpha, :beta, :gamma], :to => :beta - end - end - end + @model = SequelMultiple end describe "instance methods" do diff --git a/spec/unit/persistence/sequel_persistence_spec.rb b/spec/unit/persistence/sequel_persistence_spec.rb index fd3eec0..ff103f6 100644 --- a/spec/unit/persistence/sequel_persistence_spec.rb +++ b/spec/unit/persistence/sequel_persistence_spec.rb @@ -4,30 +4,12 @@ describe 'sequel' do require 'logger' require 'spec_helper' + Dir[File.dirname(__FILE__) + "/../../models/sequel/*.rb"].sort.each do |f| + require File.expand_path(f) + end + before(:all) do - db = Sequel.connect(SEQUEL_DB) - - # if you want to see the statements while running the spec enable the following line - # db.loggers << Logger.new($stderr) - db.create_table(:models) do - primary_key :id - String :status - end - - @model = Class.new(Sequel::Model(db)) do - set_dataset(:models) - attr_accessor :default - include AASM - aasm :column => :status - aasm do - state :alpha, :initial => true - state :beta - state :gamma - event :release do - transitions :from => [:alpha, :beta, :gamma], :to => :beta - end - end - end + @model = SequelSimple end describe "instance methods" do From 454f38e184d5ddcc9e6166dad6dc1211c5447252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Mon, 20 Jul 2015 20:55:47 +1200 Subject: [PATCH 34/45] add basic examples with two state machines --- spec/database.rb | 5 ++++ ...ctive_record_two_state_machines_example.rb | 25 +++++++++++++++++++ .../basic_two_state_machines_example.rb | 25 +++++++++++++++++++ .../basic_two_state_machines_example_spec.rb | 10 ++++++++ spec/unit/complex_multiple_example_spec.rb | 2 +- .../active_record_persistence_spec.rb | 9 +++++++ 6 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 spec/models/active_record/basic_active_record_two_state_machines_example.rb create mode 100644 spec/models/basic_two_state_machines_example.rb create mode 100644 spec/unit/basic_two_state_machines_example_spec.rb diff --git a/spec/database.rb b/spec/database.rb index ca82afd..7a7a19e 100644 --- a/spec/database.rb +++ b/spec/database.rb @@ -55,4 +55,9 @@ ActiveRecord::Migration.suppress_messages do t.string "aasm_state" t.string "type" end + + ActiveRecord::Migration.create_table "basic_active_record_two_state_machines_examples", :force => true do |t| + t.string "search" + t.string "sync" + end end diff --git a/spec/models/active_record/basic_active_record_two_state_machines_example.rb b/spec/models/active_record/basic_active_record_two_state_machines_example.rb new file mode 100644 index 0000000..1bb2169 --- /dev/null +++ b/spec/models/active_record/basic_active_record_two_state_machines_example.rb @@ -0,0 +1,25 @@ +class BasicActiveRecordTwoStateMachinesExample < ActiveRecord::Base + include AASM + + aasm :search, :column => :search do + state :initialised, :initial => true + state :queried + state :requested + + event :query do + transitions :from => [:initialised, :requested], :to => :queried + end + event :request do + transitions :from => :queried, :to => :requested + end + end + + aasm :sync, :column => :sync do + state :unsynced, :initial => true + state :synced + + event :synchronise do + transitions :from => :unsynced, :to => :synced + end + end +end diff --git a/spec/models/basic_two_state_machines_example.rb b/spec/models/basic_two_state_machines_example.rb new file mode 100644 index 0000000..c5af9d6 --- /dev/null +++ b/spec/models/basic_two_state_machines_example.rb @@ -0,0 +1,25 @@ +class BasicTwoStateMachinesExample + include AASM + + aasm :search do + state :initialised, :initial => true + state :queried + state :requested + + event :query do + transitions :from => [:initialised, :requested], :to => :queried + end + event :request do + transitions :from => :queried, :to => :requested + end + end + + aasm :sync do + state :unsynced, :initial => true + state :synced + + event :sync do + transitions :from => :unsynced, :to => :synced + end + end +end diff --git a/spec/unit/basic_two_state_machines_example_spec.rb b/spec/unit/basic_two_state_machines_example_spec.rb new file mode 100644 index 0000000..256e3ac --- /dev/null +++ b/spec/unit/basic_two_state_machines_example_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe 'on initialization' do + let(:example) { BasicTwoStateMachinesExample.new } + + it 'should be in the initial state' do + expect(example.aasm(:search).current_state).to eql :initialised + expect(example.aasm(:sync).current_state).to eql :unsynced + end +end diff --git a/spec/unit/complex_multiple_example_spec.rb b/spec/unit/complex_multiple_example_spec.rb index 334406e..f2a043f 100644 --- a/spec/unit/complex_multiple_example_spec.rb +++ b/spec/unit/complex_multiple_example_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'on initialization' do let(:auth) {ComplexExampleMultiple.new} - it 'should be in the pending state' do + it 'should be in the initial state' do expect(auth.aasm(:left).current_state).to eq(:pending) expect(auth.aasm(:right).current_state).to eq(:pending) end diff --git a/spec/unit/persistence/active_record_persistence_spec.rb b/spec/unit/persistence/active_record_persistence_spec.rb index 0939b3a..fec9864 100644 --- a/spec/unit/persistence/active_record_persistence_spec.rb +++ b/spec/unit/persistence/active_record_persistence_spec.rb @@ -512,3 +512,12 @@ describe "invalid states with persistence" do end end + +describe 'basic example with two state machines' do + let(:example) { BasicActiveRecordTwoStateMachinesExample.new } + + it 'should initialise properly' do + expect(example.aasm(:search).current_state).to eql :initialised + expect(example.aasm(:sync).current_state).to eql :unsynced + end +end From 3f56707017db970fe6b53a62f7f67722d333696d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Mon, 20 Jul 2015 21:11:16 +1200 Subject: [PATCH 35/45] default name for state machine with name is the state machine name (not :aasm_state) --- lib/aasm/base.rb | 6 +++++- .../basic_active_record_two_state_machines_example.rb | 4 ++-- spec/models/active_record/gate.rb | 2 +- spec/models/active_record/no_direct_assignment.rb | 2 +- spec/models/active_record/no_scope.rb | 2 +- spec/models/active_record/thief.rb | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index a9c30b3..930da8f 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -8,7 +8,7 @@ module AASM @name = name # @state_machine = @klass.aasm(@name).state_machine @state_machine = state_machine - @state_machine.config.column ||= (options[:column] || :aasm_state).to_sym # aasm4 + @state_machine.config.column ||= (options[:column] || default_column).to_sym # @state_machine.config.column = options[:column].to_sym if options[:column] # master @options = options @@ -136,6 +136,10 @@ module AASM private + def default_column + @name.to_sym == :default ? :aasm_state : @name.to_sym + end + def configure(key, default_value) if @options.key?(key) @state_machine.config.send("#{key}=", @options[key]) diff --git a/spec/models/active_record/basic_active_record_two_state_machines_example.rb b/spec/models/active_record/basic_active_record_two_state_machines_example.rb index 1bb2169..9502159 100644 --- a/spec/models/active_record/basic_active_record_two_state_machines_example.rb +++ b/spec/models/active_record/basic_active_record_two_state_machines_example.rb @@ -1,7 +1,7 @@ class BasicActiveRecordTwoStateMachinesExample < ActiveRecord::Base include AASM - aasm :search, :column => :search do + aasm :search do state :initialised, :initial => true state :queried state :requested @@ -14,7 +14,7 @@ class BasicActiveRecordTwoStateMachinesExample < ActiveRecord::Base end end - aasm :sync, :column => :sync do + aasm :sync do state :unsynced, :initial => true state :synced diff --git a/spec/models/active_record/gate.rb b/spec/models/active_record/gate.rb index 7fcc70a..5f7b688 100644 --- a/spec/models/active_record/gate.rb +++ b/spec/models/active_record/gate.rb @@ -28,7 +28,7 @@ class MultipleGate < ActiveRecord::Base 'value' end - aasm :left do + aasm :left, :column => :aasm_state do state :opened state :closed diff --git a/spec/models/active_record/no_direct_assignment.rb b/spec/models/active_record/no_direct_assignment.rb index fc942c1..978d49e 100644 --- a/spec/models/active_record/no_direct_assignment.rb +++ b/spec/models/active_record/no_direct_assignment.rb @@ -11,7 +11,7 @@ end class MultipleNoDirectAssignment < ActiveRecord::Base include AASM - aasm :left, :no_direct_assignment => true do + aasm :left, :column => :aasm_state, :no_direct_assignment => true do state :pending, :initial => true state :running event :run do diff --git a/spec/models/active_record/no_scope.rb b/spec/models/active_record/no_scope.rb index 13d7169..2d70cfc 100644 --- a/spec/models/active_record/no_scope.rb +++ b/spec/models/active_record/no_scope.rb @@ -11,7 +11,7 @@ end class MultipleNoScope < ActiveRecord::Base include AASM - aasm :left, :create_scopes => false do + aasm :left, :column => :aasm_state, :create_scopes => false do state :pending, :initial => true state :running event :run do diff --git a/spec/models/active_record/thief.rb b/spec/models/active_record/thief.rb index 9dd90d9..572b60f 100644 --- a/spec/models/active_record/thief.rb +++ b/spec/models/active_record/thief.rb @@ -20,7 +20,7 @@ class MultipleThief < ActiveRecord::Base set_table_name "multiple_thieves" end include AASM - aasm :left do + aasm :left, :column => :aasm_state do state :rich state :jailed initial_state Proc.new {|thief| thief.skilled ? :rich : :jailed } From 7ea9b93d6c0a9ca2c439ed4b85339556739d0f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Wed, 5 Aug 2015 21:30:36 +1200 Subject: [PATCH 36/45] finish sequel check-up for multiple state machines; start working on edge case --- PLANNED_CHANGES.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 9e319a4..b60c326 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -3,14 +3,18 @@ ## version 4.3 * add support for multiple state machines per class - * persistence - * _Sequel_ - * what happen's if someone accesses `aasm`, but has defined a - state machine for `aasm(:my_name)`? * documentation * silence warnings? * drop support for aasm_column ? + +# Currently working on + + * add support for multiple state machines per class + * what happen's if someone accesses `aasm`, but has defined a + state machine for `aasm(:my_name)`? + + # Changes so far ## version 4.3 @@ -24,4 +28,5 @@ * _ActiveRecord_ * _Mongoid_ * _MongoMapper_ + * _Sequel_ * drop support for find_in_state, count_in_state, calculate_in_state, with_state_scope From d540ca87610c9bd01d9647a8412d99d5c0990281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Wed, 5 Aug 2015 22:00:22 +1200 Subject: [PATCH 37/45] handle edge cases when a state machine is accessed which doesn't exist --- PLANNED_CHANGES.md | 4 ++-- lib/aasm/aasm.rb | 3 +++ lib/aasm/errors.rb | 2 ++ spec/unit/edge_cases_spec.rb | 16 ++++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 spec/unit/edge_cases_spec.rb diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index b60c326..cfa3670 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -11,8 +11,6 @@ # Currently working on * add support for multiple state machines per class - * what happen's if someone accesses `aasm`, but has defined a - state machine for `aasm(:my_name)`? # Changes so far @@ -29,4 +27,6 @@ * _Mongoid_ * _MongoMapper_ * _Sequel_ + * what happen's if someone accesses `aasm`, but has defined a + state machine for `aasm(:my_name)`? * drop support for find_in_state, count_in_state, calculate_in_state, with_state_scope diff --git a/lib/aasm/aasm.rb b/lib/aasm/aasm.rb index e9ffdc1..cf9879c 100644 --- a/lib/aasm/aasm.rb +++ b/lib/aasm/aasm.rb @@ -47,6 +47,9 @@ module AASM # this is the entry point for all instance-level access to AASM def aasm(name=:default) + unless AASM::StateMachine[self.class][name.to_sym] + raise AASM::UnknownStateMachineError.new("There is no state machine with the name '#{name}' defined in #{self.class.name}!") + end @aasm ||= {} @aasm[name.to_sym] ||= AASM::InstanceBase.new(self, name.to_sym) end diff --git a/lib/aasm/errors.rb b/lib/aasm/errors.rb index 9c8637e..72e2864 100644 --- a/lib/aasm/errors.rb +++ b/lib/aasm/errors.rb @@ -1,5 +1,7 @@ module AASM + class UnknownStateMachineError < RuntimeError; end + class InvalidTransition < RuntimeError attr_reader :object, :event_name, :state_machine_name diff --git a/spec/unit/edge_cases_spec.rb b/spec/unit/edge_cases_spec.rb new file mode 100644 index 0000000..7100820 --- /dev/null +++ b/spec/unit/edge_cases_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe "edge cases" do + describe "for classes with multiple state machines" do + it "allows accessing a multiple state machine class without state machine name" do + # it's like starting to define a new state machine within the + # requested class + expect(SimpleMultipleExample.aasm.states.map(&:name)).to be_empty + end + + it "do not know yet" do + example = ComplexExampleMultiple.new + expect { example.aasm.states.inspect }.to raise_error(AASM::UnknownStateMachineError) + end + end +end From 68431b7083fefdc8e9364c35a9492f8872271110 Mon Sep 17 00:00:00 2001 From: Evadne Wu Date: Fri, 7 Aug 2015 15:37:09 +0800 Subject: [PATCH 38/45] fixes AASM::Persistence::Base#aasm_read_state not reading with the correct name --- lib/aasm/persistence/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aasm/persistence/base.rb b/lib/aasm/persistence/base.rb index 83fd8da..3833a9c 100644 --- a/lib/aasm/persistence/base.rb +++ b/lib/aasm/persistence/base.rb @@ -35,7 +35,7 @@ module AASM def aasm_read_state(name=:default) state = send(self.class.aasm(name).attribute_name) if new_record? - state.blank? ? aasm.determine_state_name(self.class.aasm(name).initial_state) : state.to_sym + state.blank? ? aasm(name).determine_state_name(self.class.aasm(name).initial_state) : state.to_sym else state.blank? ? nil : state.to_sym end From 3ae7a44bbc03d054cab3eef9cb8865e9593d4f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 8 Sep 2015 22:40:18 +1200 Subject: [PATCH 39/45] add documentation for multiple state machines per class (README) --- PLANNED_CHANGES.md | 2 +- README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index cfa3670..ad77550 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -3,7 +3,6 @@ ## version 4.3 * add support for multiple state machines per class - * documentation * silence warnings? * drop support for aasm_column ? @@ -29,4 +28,5 @@ * _Sequel_ * what happen's if someone accesses `aasm`, but has defined a state machine for `aasm(:my_name)`? + * documentation * drop support for find_in_state, count_in_state, calculate_in_state, with_state_scope diff --git a/README.md b/README.md index 06d2ec4..ef9a883 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,76 @@ job.stage1_completed job.aasm.current_state # stage3 ``` + +### Multiple state machine per class + +Multiple state machines per class are supported. Be aware though, that _AASM_ has been +built with one state machine per class in mind. Nonetheless, here's how to do it: + +```ruby +class SimpleMultipleExample + include AASM + aasm(:move) do + state :standing, :initial => true + state :walking + state :running + + event :walk do + transitions :from => :standing, :to => :walking + end + event :run do + transitions :from => [:standing, :walking], :to => :running + end + event :hold do + transitions :from => [:walking, :running], :to => :standing + end + end + + aasm(:work) do + state :sleeping, :initial => true + state :processing + + event :start do + transitions :from => :sleeping, :to => :processing + end + event :stop do + transitions :from => :processing, :to => :sleeping + end + end +end + +simple = SimpleMultipleExample.new + +simple.aasm(:move).current_state +# => :standing +simple.aasm(:work).current +# => :sleeping + +simple.start +simple.aasm(:move).current_state +# => :standing +simple.aasm(:work).current +# => :processing + +``` + +_AASM_ doesn't prohibit to define the same event in both state machines. The +latest definition "wins" and overrides previous definitions. A warning is issued: +`SimpleMultipleExample: The event name run is already used!`. + +All _AASM_ class- and instance-level `aasm` methods accept a state machine selector. +So, for example, to use inspection on a class level, you have to use + +```ruby +SimpleMultipleExample.aasm(:work).states +# => [:standing, :walking, :running] +``` + +*Final note*: Support for multiple state machines per class is a pretty new feature +(since version `4.3`), so please bear with us in case it doesn't as expected. + + + ### ActiveRecord AASM comes with support for ActiveRecord and allows automatical persisting of the object's From a8991add6b53b13c0f78555cbd7d966a00dee976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 8 Sep 2015 22:42:04 +1200 Subject: [PATCH 40/45] adjusted planned changes --- PLANNED_CHANGES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index ad77550..4258e9d 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -1,15 +1,15 @@ # Planned changes -## version 4.3 +## later - * add support for multiple state machines per class - * silence warnings? * drop support for aasm_column ? +## version 4.3 # Currently working on * add support for multiple state machines per class + * silence warnings? # Changes so far From 886bc58992dbbf9556d6ea9eacc8fe1a40c9fd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 8 Sep 2015 22:44:21 +1200 Subject: [PATCH 41/45] adjusted planned changes --- PLANNED_CHANGES.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 4258e9d..859b28f 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -4,13 +4,8 @@ * drop support for aasm_column ? -## version 4.3 - # Currently working on - * add support for multiple state machines per class - * silence warnings? - # Changes so far From cfd9aff0ce1569ac79030def52056ef8c7911a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Tue, 8 Sep 2015 22:52:14 +1200 Subject: [PATCH 42/45] add multiple state machines per class to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28ff647..27fa59d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 4.3.0 (not yet released) + * add support for multiple state machines per class (see [issue #158](https://github.com/aasm/aasm/issues/158) and [issue #240](https://github.com/aasm/aasm/issues/240) for details) + * special thanks to [@evadne](https://github.com/evadne) for testing this feature, and providing comments and patches (see [issue #245](https://github.com/aasm/aasm/issues/245) for details) + ## 4.2.0 * support turning off and on the configuration option for `no_direct_assignment` (see [issue #223](https://github.com/aasm/aasm/issues/223) for details) From f563244d1e0861bc6ad1bf99e4645a9988febe57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Thu, 10 Sep 2015 21:59:24 +1200 Subject: [PATCH 43/45] version bump to 4.3.0 (support for multiple state machines per class) --- CHANGELOG.md | 2 +- lib/aasm/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fa59d..78305da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## 4.3.0 (not yet released) +## 4.3.0 * add support for multiple state machines per class (see [issue #158](https://github.com/aasm/aasm/issues/158) and [issue #240](https://github.com/aasm/aasm/issues/240) for details) * special thanks to [@evadne](https://github.com/evadne) for testing this feature, and providing comments and patches (see [issue #245](https://github.com/aasm/aasm/issues/245) for details) diff --git a/lib/aasm/version.rb b/lib/aasm/version.rb index f637511..7cc79bb 100644 --- a/lib/aasm/version.rb +++ b/lib/aasm/version.rb @@ -1,3 +1,3 @@ module AASM - VERSION = "4.2.0" + VERSION = "4.3.0" end From 780e30c4e0b1cf2296e01873654cdc1132cc9da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Thu, 10 Sep 2015 22:06:41 +1200 Subject: [PATCH 44/45] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f48d42..9ec5b62 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ job.aasm.current_state # stage3 ``` -### Multiple state machine per class +### Multiple state machines per class Multiple state machines per class are supported. Be aware though, that _AASM_ has been built with one state machine per class in mind. Nonetheless, here's how to do it: From 5b885c8a0d2ec9b5604e982183919b6038c4cb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20B=C3=B6ttger?= Date: Thu, 10 Sep 2015 22:07:52 +1200 Subject: [PATCH 45/45] TIL: no that with comma --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9ec5b62..b12d80f 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,7 @@ job.aasm.current_state # stage3 ### Multiple state machines per class -Multiple state machines per class are supported. Be aware though, that _AASM_ has been +Multiple state machines per class are supported. Be aware though that _AASM_ has been built with one state machine per class in mind. Nonetheless, here's how to do it: ```ruby @@ -417,7 +417,7 @@ job.run! # saved Saving includes running all validations on the `Job` class. If you want make sure the state gets saved without running validations (and thereby maybe persisting an -invalid object state), simply tell AASM to skip the validations. Be aware, that +invalid object state), simply tell AASM to skip the validations. Be aware that when skipping validations, only the state column will be updated in the database (just like ActiveRecord `change_column` is working).