aasm 3.0.16 → 3.0.17

Sign up to get free protection for your applications and to get access to all the features.
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