aasm 3.0.16 → 3.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +2 -1
  3. data/API +34 -0
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +1 -1
  6. data/HOWTO +12 -0
  7. data/README.md +57 -4
  8. data/aasm.gemspec +2 -0
  9. data/lib/aasm.rb +5 -4
  10. data/lib/aasm/aasm.rb +50 -75
  11. data/lib/aasm/base.rb +22 -18
  12. data/lib/aasm/event.rb +130 -0
  13. data/lib/aasm/instance_base.rb +87 -0
  14. data/lib/aasm/localizer.rb +54 -0
  15. data/lib/aasm/persistence.rb +22 -14
  16. data/lib/aasm/persistence/active_record_persistence.rb +38 -69
  17. data/lib/aasm/persistence/base.rb +42 -2
  18. data/lib/aasm/persistence/mongoid_persistence.rb +33 -64
  19. data/lib/aasm/state.rb +78 -0
  20. data/lib/aasm/state_machine.rb +2 -2
  21. data/lib/aasm/transition.rb +49 -0
  22. data/lib/aasm/version.rb +1 -1
  23. data/spec/models/active_record/api.rb +75 -0
  24. data/spec/models/auth_machine.rb +1 -1
  25. data/spec/models/bar.rb +15 -0
  26. data/spec/models/foo.rb +34 -0
  27. data/spec/models/mongoid/simple_mongoid.rb +10 -0
  28. data/spec/models/mongoid/{mongoid_models.rb → simple_new_dsl_mongoid.rb} +1 -12
  29. data/spec/models/persistence.rb +2 -1
  30. data/spec/models/this_name_better_not_be_in_use.rb +11 -0
  31. data/spec/schema.rb +1 -1
  32. data/spec/spec_helper.rb +8 -1
  33. data/spec/unit/api_spec.rb +72 -0
  34. data/spec/unit/callbacks_spec.rb +2 -2
  35. data/spec/unit/event_spec.rb +269 -0
  36. data/spec/unit/inspection_spec.rb +43 -5
  37. data/spec/unit/{supporting_classes/localizer_spec.rb → localizer_spec.rb} +2 -2
  38. data/spec/unit/memory_leak_spec.rb +12 -12
  39. data/spec/unit/persistence/active_record_persistence_spec.rb +0 -40
  40. data/spec/unit/persistence/mongoid_persistance_spec.rb +3 -2
  41. data/spec/unit/simple_example_spec.rb +6 -0
  42. data/spec/unit/{supporting_classes/state_spec.rb → state_spec.rb} +2 -2
  43. data/spec/unit/{supporting_classes/state_transition_spec.rb → transition_spec.rb} +18 -18
  44. metadata +127 -38
  45. data/lib/aasm/persistence/read_state.rb +0 -40
  46. data/lib/aasm/supporting_classes/event.rb +0 -146
  47. data/lib/aasm/supporting_classes/localizer.rb +0 -56
  48. data/lib/aasm/supporting_classes/state.rb +0 -80
  49. data/lib/aasm/supporting_classes/state_transition.rb +0 -51
  50. data/spec/spec_helpers/models_spec_helper.rb +0 -64
  51. data/spec/unit/supporting_classes/event_spec.rb +0 -203
@@ -0,0 +1,130 @@
1
+ module AASM
2
+ class Event
3
+
4
+ attr_reader :name, :options
5
+
6
+ def initialize(name, options = {}, &block)
7
+ @name = name
8
+ @transitions = []
9
+ update(options, &block)
10
+ end
11
+
12
+ # a neutered version of fire - it doesn't actually fire the event, it just
13
+ # executes the transition guards to determine if a transition is even
14
+ # an option given current conditions.
15
+ def may_fire?(obj, to_state=nil, *args)
16
+ _fire(obj, true, to_state, *args) # true indicates test firing
17
+ end
18
+
19
+ def fire(obj, to_state=nil, *args)
20
+ _fire(obj, false, to_state, *args) # false indicates this is not a test (fire!)
21
+ end
22
+
23
+ def transitions_from_state?(state)
24
+ transitions_from_state(state).any?
25
+ end
26
+
27
+ def transitions_from_state(state)
28
+ @transitions.select { |t| t.from == state }
29
+ end
30
+
31
+ def transitions_to_state?(state)
32
+ transitions_to_state(state).any?
33
+ end
34
+
35
+ def transitions_to_state(state)
36
+ @transitions.select { |t| t.to == state }
37
+ end
38
+
39
+ # deprecated
40
+ def all_transitions
41
+ # warn "Event#all_transitions is deprecated and will be removed in version 3.2.0; please use Event#transitions instead!"
42
+ transitions
43
+ end
44
+
45
+ def fire_callbacks(callback_name, record, *args)
46
+ invoke_callbacks(@options[callback_name], record, args)
47
+ end
48
+
49
+ def ==(event)
50
+ if event.is_a? Symbol
51
+ name == event
52
+ else
53
+ name == event.name
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def update(options = {}, &block)
60
+ @options = options
61
+ if block then
62
+ instance_eval(&block)
63
+ end
64
+ self
65
+ end
66
+
67
+ # Execute if test? == false, otherwise return true/false depending on whether it would fire
68
+ def _fire(obj, test, to_state=nil, *args)
69
+ if @transitions.map(&:from).any?
70
+ transitions = @transitions.select { |t| t.from == obj.aasm_current_state }
71
+ return nil if transitions.size == 0
72
+ else
73
+ transitions = @transitions
74
+ end
75
+
76
+ result = test ? false : nil
77
+ transitions.each do |transition|
78
+ next if to_state and !Array(transition.to).include?(to_state)
79
+ if transition.perform(obj, *args)
80
+ if test
81
+ result = true
82
+ else
83
+ result = to_state || Array(transition.to).first
84
+ transition.execute(obj, *args)
85
+ end
86
+
87
+ break
88
+ end
89
+ end
90
+ result
91
+ end
92
+
93
+ def invoke_callbacks(code, record, args)
94
+ case code
95
+ when Symbol, String
96
+ record.send(code, *args)
97
+ true
98
+ when Proc
99
+ record.instance_exec(*args, &code)
100
+ true
101
+ when Array
102
+ code.each {|a| invoke_callbacks(a, record, args)}
103
+ true
104
+ else
105
+ false
106
+ end
107
+ end
108
+
109
+ ## DSL interface
110
+ def transitions(trans_opts=nil)
111
+ if trans_opts # define new transitions
112
+ # Create a separate transition for each from state to the given state
113
+ Array(trans_opts[:from]).each do |s|
114
+ @transitions << AASM::Transition.new(trans_opts.merge({:from => s.to_sym}))
115
+ end
116
+ # Create a transition if to is specified without from (transitions from ANY state)
117
+ @transitions << AASM::Transition.new(trans_opts) if @transitions.empty? && trans_opts[:to]
118
+ end
119
+ @transitions
120
+ end
121
+
122
+ [:after, :before, :error, :success].each do |callback_name|
123
+ define_method callback_name do |*args, &block|
124
+ options[callback_name] = Array(options[callback_name])
125
+ options[callback_name] << block if block
126
+ options[callback_name] += Array(args)
127
+ end
128
+ end
129
+ end
130
+ end # AASM
@@ -0,0 +1,87 @@
1
+ module AASM
2
+ class InstanceBase
3
+
4
+ def initialize(instance)
5
+ @instance = instance
6
+ end
7
+
8
+ def current_state
9
+ @current_state ||= @instance.aasm_read_state
10
+ end
11
+
12
+ def current_state=(state)
13
+ @instance.aasm_write_state_without_persistence(state)
14
+ @current_state = state
15
+ end
16
+
17
+ def enter_initial_state
18
+ state_name = determine_state_name(@instance.class.aasm_initial_state)
19
+ state_object = state_object_for_name(state_name)
20
+
21
+ state_object.fire_callbacks(:before_enter, @instance)
22
+ state_object.fire_callbacks(:enter, @instance)
23
+ self.current_state = state_name
24
+ state_object.fire_callbacks(:after_enter, @instance)
25
+
26
+ state_name
27
+ end
28
+
29
+ def human_state
30
+ AASM::Localizer.new.human_state_name(@instance.class, current_state)
31
+ end
32
+
33
+ def states(options={})
34
+ if options[:permissible]
35
+ # ugliness level 1000
36
+ transitions = @instance.class.aasm.events.values.map {|e| e.transitions_from_state(current_state) }
37
+ tos = transitions.map {|t| t[0] ? t[0].to : nil}.flatten.compact.map(&:to_sym).uniq
38
+ @instance.class.aasm.states.select {|s| tos.include?(s.name.to_sym)}
39
+ else
40
+ @instance.class.aasm.states
41
+ end
42
+ end
43
+
44
+ # QUESTION: shouldn't events and permissible_events be the same thing?
45
+ # QUESTION: shouldn't events return objects instead of strings?
46
+ def events(state=current_state)
47
+ events = @instance.class.aasm.events.values.select {|e| e.transitions_from_state?(state) }
48
+ events.map {|e| e.name}
49
+ end
50
+
51
+ # filters the results of events_for_current_state so that only those that
52
+ # are really currently possible (given transition guards) are shown.
53
+ # QUESTION: what about events.permissible ?
54
+ def permissible_events
55
+ events.select{ |e| @instance.send(("may_" + e.to_s + "?").to_sym) }
56
+ end
57
+
58
+ def state_object_for_name(name)
59
+ obj = @instance.class.aasm.states.find {|s| s == name}
60
+ raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil?
61
+ obj
62
+ end
63
+
64
+ def determine_state_name(state)
65
+ case state
66
+ when Symbol, String
67
+ state
68
+ when Proc
69
+ state.call(@instance)
70
+ else
71
+ raise NotImplementedError, "Unrecognized state-type given. Expected Symbol, String, or Proc."
72
+ end
73
+ end
74
+
75
+ def may_fire_event?(name, *args)
76
+ event = @instance.class.aasm.events[name]
77
+ event.may_fire?(@instance, *args)
78
+ end
79
+
80
+ def set_current_state_with_persistence(state)
81
+ save_success = @instance.aasm_write_state(state)
82
+ self.current_state = state if save_success
83
+ save_success
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,54 @@
1
+ module AASM
2
+ class Localizer
3
+ def human_event_name(klass, event)
4
+ checklist = ancestors_list(klass).inject([]) do |list, ancestor|
5
+ list << :"#{i18n_scope(klass)}.events.#{i18n_klass(ancestor)}.#{event}"
6
+ list
7
+ end
8
+ translate_queue(checklist) || I18n.translate(checklist.shift, :default => event.to_s.humanize)
9
+ end
10
+
11
+ def human_state_name(klass, state)
12
+ checklist = ancestors_list(klass).inject([]) do |list, ancestor|
13
+ list << item_for(klass, state, ancestor)
14
+ list << item_for(klass, state, ancestor, :old_style => true)
15
+ list
16
+ end
17
+ translate_queue(checklist) || I18n.translate(checklist.shift, :default => state.to_s.humanize)
18
+ end
19
+
20
+ private
21
+
22
+ def item_for(klass, state, ancestor, options={})
23
+ separator = options[:old_style] ? '.' : '/'
24
+ :"#{i18n_scope(klass)}.attributes.#{i18n_klass(ancestor)}.#{klass.aasm_column}#{separator}#{state}"
25
+ end
26
+
27
+ def translate_queue(checklist)
28
+ (0...(checklist.size-1)).each do |i|
29
+ begin
30
+ return I18n.translate(checklist.shift, :raise => true)
31
+ rescue I18n::MissingTranslationData
32
+ # that's okay
33
+ end
34
+ end
35
+ nil
36
+ end
37
+
38
+ # added for rails 2.x compatibility
39
+ def i18n_scope(klass)
40
+ klass.respond_to?(:i18n_scope) ? klass.i18n_scope : :activerecord
41
+ end
42
+
43
+ # added for rails < 3.0.3 compatibility
44
+ def i18n_klass(klass)
45
+ klass.model_name.respond_to?(:i18n_key) ? klass.model_name.i18n_key : klass.name.underscore
46
+ end
47
+
48
+ def ancestors_list(klass)
49
+ klass.ancestors.select do |ancestor|
50
+ ancestor.respond_to?(:model_name) unless ancestor == ActiveRecord::Base
51
+ end
52
+ end
53
+ end
54
+ end # AASM
@@ -1,20 +1,28 @@
1
1
  module AASM
2
2
  module Persistence
3
- # Checks to see this class or any of it's superclasses inherit from
4
- # ActiveRecord::Base and if so includes ActiveRecordPersistence
5
- def self.set_persistence(base)
6
- # Use a fancier auto-loading thingy, perhaps. When there are more persistence engines.
7
- hierarchy = base.ancestors.map {|klass| klass.to_s}
3
+ class << self
8
4
 
9
- require File.join(File.dirname(__FILE__), 'persistence', 'base')
10
- require File.join(File.dirname(__FILE__), 'persistence', 'read_state')
11
- if hierarchy.include?("ActiveRecord::Base")
12
- require File.join(File.dirname(__FILE__), 'persistence', 'active_record_persistence')
13
- base.send(:include, AASM::Persistence::ActiveRecordPersistence)
14
- elsif hierarchy.include?("Mongoid::Document")
15
- require File.join(File.dirname(__FILE__), 'persistence', 'mongoid_persistence')
16
- base.send(:include, AASM::Persistence::MongoidPersistence)
5
+ def load_persistence(base)
6
+ # Use a fancier auto-loading thingy, perhaps. When there are more persistence engines.
7
+ hierarchy = base.ancestors.map {|klass| klass.to_s}
8
+
9
+ if hierarchy.include?("ActiveRecord::Base")
10
+ require_files_for(:active_record)
11
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence)
12
+ elsif hierarchy.include?("Mongoid::Document")
13
+ require_files_for(:mongoid)
14
+ base.send(:include, AASM::Persistence::MongoidPersistence)
15
+ end
17
16
  end
18
- end
17
+
18
+ private
19
+
20
+ def require_files_for(persistence)
21
+ ['base', "#{persistence}_persistence"].each do |file_name|
22
+ require File.join(File.dirname(__FILE__), 'persistence', file_name)
23
+ end
24
+ end
25
+
26
+ end # class << self
19
27
  end
20
28
  end # AASM
@@ -6,11 +6,6 @@ module AASM
6
6
  # * extends the model with ClassMethods
7
7
  # * includes InstanceMethods
8
8
  #
9
- # Unless the corresponding methods are already defined, it includes
10
- # * ReadState
11
- # * WriteState
12
- # * WriteStateWithoutPersistence
13
- #
14
9
  # Adds
15
10
  #
16
11
  # before_validation :aasm_ensure_initial_state, :on => :create
@@ -32,12 +27,9 @@ module AASM
32
27
  # end
33
28
  #
34
29
  def self.included(base)
35
- base.extend AASM::Persistence::Base::ClassMethods
30
+ base.send(:include, AASM::Persistence::Base)
36
31
  base.extend AASM::Persistence::ActiveRecordPersistence::ClassMethods
37
32
  base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
38
- base.send(:include, AASM::Persistence::ReadState) unless base.method_defined?(:aasm_read_state)
39
- base.send(:include, AASM::Persistence::ActiveRecordPersistence::WriteState) unless base.method_defined?(:aasm_write_state)
40
- base.send(:include, AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence) unless base.method_defined?(:aasm_write_state_without_persistence)
41
33
 
42
34
  if ActiveRecord::VERSION::MAJOR >= 3
43
35
  base.before_validation(:aasm_ensure_initial_state, :on => :create)
@@ -76,25 +68,49 @@ module AASM
76
68
 
77
69
  module InstanceMethods
78
70
 
79
- # Returns the current aasm_state of the object. Respects reload and
80
- # any changes made to the aasm_state field directly
71
+ # Writes <tt>state</tt> to the state column and persists it to the database
81
72
  #
82
- # Internally just calls <tt>aasm_read_state</tt>
73
+ # foo = Foo.find(1)
74
+ # foo.aasm_current_state # => :opened
75
+ # foo.close!
76
+ # foo.aasm_current_state # => :closed
77
+ # Foo.find(1).aasm_current_state # => :closed
78
+ #
79
+ # NOTE: intended to be called from an event
80
+ def aasm_write_state(state)
81
+ old_value = read_attribute(self.class.aasm_column)
82
+ write_attribute(self.class.aasm_column, state.to_s)
83
+
84
+ success = if AASM::StateMachine[self.class].config.skip_validation_on_save
85
+ self.class.update_all({ self.class.aasm_column => state.to_s }, self.class.primary_key => self.id) == 1
86
+ else
87
+ self.save
88
+ end
89
+ unless success
90
+ write_attribute(self.class.aasm_column, old_value)
91
+ return false
92
+ end
93
+
94
+ true
95
+ end
96
+
97
+ # Writes <tt>state</tt> to the state column, but does not persist it to the database
83
98
  #
84
99
  # foo = Foo.find(1)
85
- # foo.aasm_current_state # => :pending
86
- # foo.aasm_state = "opened"
87
100
  # foo.aasm_current_state # => :opened
88
- # foo.close # => calls aasm_write_state_without_persistence
101
+ # foo.close
102
+ # foo.aasm_current_state # => :closed
103
+ # Foo.find(1).aasm_current_state # => :opened
104
+ # foo.save
89
105
  # foo.aasm_current_state # => :closed
90
- # foo.reload
91
- # foo.aasm_current_state # => :pending
106
+ # Foo.find(1).aasm_current_state # => :closed
92
107
  #
93
- def aasm_current_state
94
- @current_state = aasm_read_state
108
+ # NOTE: intended to be called from an event
109
+ def aasm_write_state_without_persistence(state)
110
+ write_attribute(self.class.aasm_column, state.to_s)
95
111
  end
96
112
 
97
- private
113
+ private
98
114
 
99
115
  # Ensures that if the aasm_state column is nil and the record is new
100
116
  # that the initial state gets populated before validation on create
@@ -112,7 +128,7 @@ module AASM
112
128
  # foo.aasm_state # => nil
113
129
  #
114
130
  def aasm_ensure_initial_state
115
- aasm_enter_initial_state if send(self.class.aasm_column).blank?
131
+ aasm.enter_initial_state if send(self.class.aasm_column).blank?
116
132
  end
117
133
 
118
134
  def aasm_fire_event(name, options, *args)
@@ -120,54 +136,7 @@ module AASM
120
136
  super
121
137
  end
122
138
  end
123
-
124
- end
125
-
126
- module WriteStateWithoutPersistence
127
- # Writes <tt>state</tt> to the state column, but does not persist it to the database
128
- #
129
- # foo = Foo.find(1)
130
- # foo.aasm_current_state # => :opened
131
- # foo.close
132
- # foo.aasm_current_state # => :closed
133
- # Foo.find(1).aasm_current_state # => :opened
134
- # foo.save
135
- # foo.aasm_current_state # => :closed
136
- # Foo.find(1).aasm_current_state # => :closed
137
- #
138
- # NOTE: intended to be called from an event
139
- def aasm_write_state_without_persistence(state)
140
- write_attribute(self.class.aasm_column, state.to_s)
141
- end
142
- end
143
-
144
- module WriteState
145
- # Writes <tt>state</tt> to the state column and persists it to the database
146
- #
147
- # foo = Foo.find(1)
148
- # foo.aasm_current_state # => :opened
149
- # foo.close!
150
- # foo.aasm_current_state # => :closed
151
- # Foo.find(1).aasm_current_state # => :closed
152
- #
153
- # NOTE: intended to be called from an event
154
- def aasm_write_state(state)
155
- old_value = read_attribute(self.class.aasm_column)
156
- write_attribute(self.class.aasm_column, state.to_s)
157
-
158
- success = if AASM::StateMachine[self.class].config.skip_validation_on_save
159
- self.class.update_all({ self.class.aasm_column => state.to_s }, self.class.primary_key => self.id) == 1
160
- else
161
- self.save
162
- end
163
- unless success
164
- write_attribute(self.class.aasm_column, old_value)
165
- return false
166
- end
167
-
168
- true
169
- end
170
- end
139
+ end # InstanceMethods
171
140
 
172
141
  end
173
142
  end