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
@@ -1,40 +0,0 @@
1
- module AASM
2
- module Persistence
3
- module ReadState
4
-
5
- # Returns the value of the aasm_column - called from <tt>aasm_current_state</tt>
6
- #
7
- # If it's a new record, and the aasm state column is blank it returns the initial state
8
- # (example provided here for ActiveRecord, but it's true for Mongoid as well):
9
- #
10
- # class Foo < ActiveRecord::Base
11
- # include AASM
12
- # aasm_column :status
13
- # aasm_state :opened
14
- # aasm_state :closed
15
- # end
16
- #
17
- # foo = Foo.new
18
- # foo.current_state # => :opened
19
- # foo.close
20
- # foo.current_state # => :closed
21
- #
22
- # foo = Foo.find(1)
23
- # foo.current_state # => :opened
24
- # foo.aasm_state = nil
25
- # foo.current_state # => nil
26
- #
27
- # NOTE: intended to be called from an event
28
- #
29
- # This allows for nil aasm states - be sure to add validation to your model
30
- def aasm_read_state
31
- if new_record?
32
- send(self.class.aasm_column).blank? ? aasm_determine_state_name(self.class.aasm_initial_state) : send(self.class.aasm_column).to_sym
33
- else
34
- send(self.class.aasm_column).nil? ? nil : send(self.class.aasm_column).to_sym
35
- end
36
- end
37
-
38
- end # ReadState
39
- end # Persistence
40
- end # AASM
@@ -1,146 +0,0 @@
1
- module AASM
2
- module SupportingClasses
3
- class Event
4
- attr_reader :name, :success, :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
- def all_transitions
40
- @transitions
41
- end
42
-
43
- def fire_callbacks(action, record)
44
- action = @options[action]
45
- action.is_a?(Array) ?
46
- action.each {|a| _fire_callbacks(a, record)} :
47
- _fire_callbacks(action, record)
48
- end
49
-
50
- def ==(event)
51
- if event.is_a? Symbol
52
- name == event
53
- else
54
- name == event.name
55
- end
56
- end
57
-
58
- def execute_success_callback(obj, success = nil)
59
- callback = success || @success
60
- case(callback)
61
- when String, Symbol
62
- obj.send(callback)
63
- when Proc
64
- callback.call(obj)
65
- when Array
66
- callback.each{|meth|self.execute_success_callback(obj, meth)}
67
- end
68
- end
69
-
70
- def execute_error_callback(obj, error, error_callback=nil)
71
- callback = error_callback || @error
72
- raise error unless callback
73
- case(callback)
74
- when String, Symbol
75
- raise NoMethodError unless obj.respond_to?(callback.to_sym)
76
- obj.send(callback, error)
77
- when Proc
78
- callback.call(obj, error)
79
- when Array
80
- callback.each{|meth|self.execute_error_callback(obj, error, meth)}
81
- end
82
- end
83
-
84
- private
85
-
86
- def update(options = {}, &block)
87
- if options.key?(:success) then
88
- @success = options[:success]
89
- end
90
- if options.key?(:error) then
91
- @error = options[:error]
92
- end
93
- if block then
94
- instance_eval(&block)
95
- end
96
- @options = options
97
- self
98
- end
99
-
100
- # Execute if test? == false, otherwise return true/false depending on whether it would fire
101
- def _fire(obj, test, to_state=nil, *args)
102
- if @transitions.map(&:from).any?
103
- transitions = @transitions.select { |t| t.from == obj.aasm_current_state }
104
- return nil if transitions.size == 0
105
- else
106
- transitions = @transitions
107
- end
108
-
109
- result = test ? false : nil
110
- transitions.each do |transition|
111
- next if to_state and !Array(transition.to).include?(to_state)
112
- if transition.perform(obj, *args)
113
- if test
114
- result = true
115
- else
116
- result = to_state || Array(transition.to).first
117
- transition.execute(obj, *args)
118
- end
119
-
120
- break
121
- end
122
- end
123
- result
124
- end
125
-
126
- def _fire_callbacks(action, record)
127
- case action
128
- when Symbol, String
129
- record.send(action)
130
- when Proc
131
- action.call(record)
132
- end
133
- end
134
-
135
- def transitions(trans_opts)
136
- # Create a separate transition for each from state to the given state
137
- Array(trans_opts[:from]).each do |s|
138
- @transitions << AASM::SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
139
- end
140
- # Create a transition if to is specified without from (transitions from ANY state)
141
- @transitions << AASM::SupportingClasses::StateTransition.new(trans_opts) if @transitions.empty? && trans_opts[:to]
142
- end
143
-
144
- end
145
- end # SupportingClasses
146
- end # AASM
@@ -1,56 +0,0 @@
1
- module AASM
2
- module SupportingClasses
3
- class Localizer
4
- def human_event_name(klass, event)
5
- checklist = ancestors_list(klass).inject([]) do |list, ancestor|
6
- list << :"#{i18n_scope(klass)}.events.#{i18n_klass(ancestor)}.#{event}"
7
- list
8
- end
9
- translate_queue(checklist) || I18n.translate(checklist.shift, :default => event.to_s.humanize)
10
- end
11
-
12
- def human_state_name(klass, state)
13
- checklist = ancestors_list(klass).inject([]) do |list, ancestor|
14
- list << item_for(klass, state, ancestor)
15
- list << item_for(klass, state, ancestor, :old_style => true)
16
- list
17
- end
18
- translate_queue(checklist) || I18n.translate(checklist.shift, :default => state.to_s.humanize)
19
- end
20
-
21
- private
22
-
23
- def item_for(klass, state, ancestor, options={})
24
- separator = options[:old_style] ? '.' : '/'
25
- :"#{i18n_scope(klass)}.attributes.#{i18n_klass(ancestor)}.#{klass.aasm_column}#{separator}#{state}"
26
- end
27
-
28
- def translate_queue(checklist)
29
- (0...(checklist.size-1)).each do |i|
30
- begin
31
- return I18n.translate(checklist.shift, :raise => true)
32
- rescue I18n::MissingTranslationData
33
- # that's okay
34
- end
35
- end
36
- nil
37
- end
38
-
39
- # added for rails 2.x compatibility
40
- def i18n_scope(klass)
41
- klass.respond_to?(:i18n_scope) ? klass.i18n_scope : :activerecord
42
- end
43
-
44
- # added for rails < 3.0.3 compatibility
45
- def i18n_klass(klass)
46
- klass.model_name.respond_to?(:i18n_key) ? klass.model_name.i18n_key : klass.name.underscore
47
- end
48
-
49
- def ancestors_list(klass)
50
- klass.ancestors.select do |ancestor|
51
- ancestor.respond_to?(:model_name) unless ancestor == ActiveRecord::Base
52
- end
53
- end
54
- end
55
- end # SupportingClasses
56
- end # AASM
@@ -1,80 +0,0 @@
1
- module AASM
2
- module SupportingClasses
3
- class State
4
- attr_reader :name, :options
5
-
6
- def initialize(name, clazz, options={})
7
- @name = name
8
- @clazz = clazz
9
- update(options)
10
- end
11
-
12
- def ==(state)
13
- if state.is_a? Symbol
14
- name == state
15
- else
16
- name == state.name
17
- end
18
- end
19
-
20
- def <=>(state)
21
- if state.is_a? Symbol
22
- name <=> state
23
- else
24
- name <=> state.name
25
- end
26
- end
27
-
28
- def to_s
29
- name.to_s
30
- end
31
-
32
- def fire_callbacks(action, record)
33
- action = @options[action]
34
- catch :halt_aasm_chain do
35
- action.is_a?(Array) ?
36
- action.each {|a| _fire_callbacks(a, record)} :
37
- _fire_callbacks(action, record)
38
- end
39
- end
40
-
41
- def display_name
42
- @display_name ||= begin
43
- if Module.const_defined?(:I18n)
44
- localized_name
45
- else
46
- name.to_s.gsub(/_/, ' ').capitalize
47
- end
48
- end
49
- end
50
-
51
- def localized_name
52
- AASM::SupportingClasses::Localizer.new.human_state_name(@clazz, self)
53
- end
54
-
55
- def for_select
56
- [display_name, name.to_s]
57
- end
58
-
59
- private
60
-
61
- def update(options = {})
62
- if options.key?(:display) then
63
- @display_name = options.delete(:display)
64
- end
65
- @options = options
66
- self
67
- end
68
-
69
- def _fire_callbacks(action, record)
70
- case action
71
- when Symbol, String
72
- record.send(action)
73
- when Proc
74
- action.call(record)
75
- end
76
- end
77
-
78
- end
79
- end # SupportingClasses
80
- end # AASM
@@ -1,51 +0,0 @@
1
- module AASM
2
- module SupportingClasses
3
- class StateTransition
4
- attr_reader :from, :to, :opts
5
- alias_method :options, :opts
6
-
7
- def initialize(opts)
8
- @from, @to, @guard, @on_transition = opts[:from], opts[:to], opts[:guard], opts[:on_transition]
9
- @opts = opts
10
- end
11
-
12
- # TODO: should be named allowed? or similar
13
- def perform(obj, *args)
14
- case @guard
15
- when Symbol, String
16
- obj.send(@guard, *args)
17
- when Proc
18
- @guard.call(obj, *args)
19
- else
20
- true
21
- end
22
- end
23
-
24
- def execute(obj, *args)
25
- @on_transition.is_a?(Array) ?
26
- @on_transition.each {|ot| _execute(obj, ot, *args)} :
27
- _execute(obj, @on_transition, *args)
28
- end
29
-
30
- def ==(obj)
31
- @from == obj.from && @to == obj.to
32
- end
33
-
34
- def from?(value)
35
- @from == value
36
- end
37
-
38
- private
39
-
40
- def _execute(obj, on_transition, *args)
41
- case on_transition
42
- when Proc
43
- on_transition.arity == 0 ? on_transition.call : on_transition.call(obj, *args)
44
- when Symbol, String
45
- obj.send(:method, on_transition.to_sym).arity == 0 ? obj.send(on_transition) : obj.send(on_transition, *args)
46
- end
47
- end
48
-
49
- end
50
- end # SupportingClasses
51
- end # AASM
@@ -1,64 +0,0 @@
1
- Dir[File.dirname(__FILE__) + "/../models/*.rb"].sort.each { |f| require File.expand_path(f) }
2
-
3
- class Foo
4
- include AASM
5
- aasm do
6
- state :open, :initial => true, :exit => :exit
7
- state :closed, :enter => :enter
8
-
9
- event :close, :success => :success_callback do
10
- transitions :to => :closed, :from => [:open]
11
- end
12
-
13
- event :null do
14
- transitions :to => :closed, :from => [:open], :guard => :always_false
15
- end
16
- end
17
-
18
- def always_false
19
- false
20
- end
21
-
22
- def success_callback
23
- end
24
-
25
- def enter
26
- end
27
- def exit
28
- end
29
- end
30
-
31
- class FooTwo < Foo
32
- include AASM
33
- aasm do
34
- state :foo
35
- end
36
- end
37
-
38
- class Bar
39
- include AASM
40
-
41
- aasm do
42
- state :read
43
- state :ended
44
-
45
- event :foo do
46
- transitions :to => :ended, :from => [:read]
47
- end
48
- end
49
- end
50
-
51
- class Baz < Bar
52
- end
53
-
54
- class ThisNameBetterNotBeInUse
55
- include AASM
56
-
57
- aasm do
58
- state :initial
59
- state :symbol
60
- state :string
61
- state :array
62
- state :proc
63
- end
64
- end
@@ -1,203 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe 'adding an event' do
4
- let(:event) do
5
- AASM::SupportingClasses::Event.new(:close_order, {:success => :success_callback}) do
6
- transitions :to => :closed, :from => [:open, :received]
7
- end
8
- end
9
-
10
- it 'should set the name' do
11
- event.name.should == :close_order
12
- end
13
-
14
- it 'should set the success callback' do
15
- event.success.should == :success_callback
16
- end
17
-
18
- it 'should create transitions' do
19
- transitions = event.all_transitions
20
- transitions[0].from.should == :open
21
- transitions[0].to.should == :closed
22
- transitions[1].from.should == :received
23
- transitions[1].to.should == :closed
24
- end
25
- end
26
-
27
- describe 'transition inspection' do
28
- let(:event) do
29
- AASM::SupportingClasses::Event.new(:run) do
30
- transitions :to => :running, :from => :sleeping
31
- end
32
- end
33
-
34
- it 'should support inspecting transitions from other states' do
35
- event.transitions_from_state(:sleeping).map(&:to).should == [:running]
36
- event.transitions_from_state?(:sleeping).should be_true
37
-
38
- event.transitions_from_state(:cleaning).map(&:to).should == []
39
- event.transitions_from_state?(:cleaning).should be_false
40
- end
41
-
42
- it 'should support inspecting transitions to other states' do
43
- event.transitions_to_state(:running).map(&:from).should == [:sleeping]
44
- event.transitions_to_state?(:running).should be_true
45
-
46
- event.transitions_to_state(:cleaning).map(&:to).should == []
47
- event.transitions_to_state?(:cleaning).should be_false
48
- end
49
- end
50
-
51
- describe 'firing an event' do
52
- it 'should return nil if the transitions are empty' do
53
- obj = mock('object')
54
- obj.stub!(:aasm_current_state)
55
-
56
- event = AASM::SupportingClasses::Event.new(:event)
57
- event.fire(obj).should be_nil
58
- end
59
-
60
- it 'should return the state of the first matching transition it finds' do
61
- event = AASM::SupportingClasses::Event.new(:event) do
62
- transitions :to => :closed, :from => [:open, :received]
63
- end
64
-
65
- obj = mock('object')
66
- obj.stub!(:aasm_current_state).and_return(:open)
67
-
68
- event.fire(obj).should == :closed
69
- end
70
-
71
- it 'should call the guard with the params passed in' do
72
- event = AASM::SupportingClasses::Event.new(:event) do
73
- transitions :to => :closed, :from => [:open, :received], :guard => :guard_fn
74
- end
75
-
76
- obj = mock('object')
77
- obj.stub!(:aasm_current_state).and_return(:open)
78
- obj.should_receive(:guard_fn).with('arg1', 'arg2').and_return(true)
79
-
80
- event.fire(obj, nil, 'arg1', 'arg2').should == :closed
81
- end
82
-
83
- end
84
-
85
- describe 'executing the success callback' do
86
-
87
- it "should send the success callback if it's a symbol" do
88
- ThisNameBetterNotBeInUse.instance_eval {
89
- aasm_event :with_symbol, :success => :symbol_success_callback do
90
- transitions :to => :symbol, :from => [:initial]
91
- end
92
- }
93
-
94
- model = ThisNameBetterNotBeInUse.new
95
- model.should_receive(:symbol_success_callback)
96
- model.with_symbol!
97
- end
98
-
99
- it "should send the success callback if it's a string" do
100
- ThisNameBetterNotBeInUse.instance_eval {
101
- aasm_event :with_string, :success => 'string_success_callback' do
102
- transitions :to => :string, :from => [:initial]
103
- end
104
- }
105
-
106
- model = ThisNameBetterNotBeInUse.new
107
- model.should_receive(:string_success_callback)
108
- model.with_string!
109
- end
110
-
111
- it "should call each success callback if passed an array of strings and/or symbols" do
112
- ThisNameBetterNotBeInUse.instance_eval {
113
- aasm_event :with_array, :success => [:success_callback1, 'success_callback2'] do
114
- transitions :to => :array, :from => [:initial]
115
- end
116
- }
117
-
118
- model = ThisNameBetterNotBeInUse.new
119
- model.should_receive(:success_callback1)
120
- model.should_receive(:success_callback2)
121
- model.with_array!
122
- end
123
-
124
- it "should call each success callback if passed an array of strings and/or symbols and/or procs" do
125
- ThisNameBetterNotBeInUse.instance_eval {
126
- aasm_event :with_array_including_procs, :success => [:success_callback1, 'success_callback2', lambda { |obj| obj.proc_success_callback }] do
127
- transitions :to => :array, :from => [:initial]
128
- end
129
- }
130
-
131
- model = ThisNameBetterNotBeInUse.new
132
- model.should_receive(:success_callback1)
133
- model.should_receive(:success_callback2)
134
- model.should_receive(:proc_success_callback)
135
- model.with_array_including_procs!
136
- end
137
-
138
- it "should call the success callback if it's a proc" do
139
- ThisNameBetterNotBeInUse.instance_eval {
140
- aasm_event :with_proc, :success => lambda { |obj| obj.proc_success_callback } do
141
- transitions :to => :proc, :from => [:initial]
142
- end
143
- }
144
-
145
- model = ThisNameBetterNotBeInUse.new
146
- model.should_receive(:proc_success_callback)
147
- model.with_proc!
148
- end
149
- end
150
-
151
- describe 'parametrised events' do
152
- let(:pe) {ParametrisedEvent.new}
153
-
154
- it 'should transition to specified next state (sleeping to showering)' do
155
- pe.wakeup!(:showering)
156
- pe.aasm_current_state.should == :showering
157
- end
158
-
159
- it 'should transition to specified next state (sleeping to working)' do
160
- pe.wakeup!(:working)
161
- pe.aasm_current_state.should == :working
162
- end
163
-
164
- it 'should transition to default (first or showering) state' do
165
- pe.wakeup!
166
- pe.aasm_current_state.should == :showering
167
- end
168
-
169
- it 'should transition to default state when on_transition invoked' do
170
- pe.dress!(nil, 'purple', 'dressy')
171
- pe.aasm_current_state.should == :working
172
- end
173
-
174
- it 'should call on_transition method with args' do
175
- pe.wakeup!(:showering)
176
- pe.should_receive(:wear_clothes).with('blue', 'jeans')
177
- pe.dress!(:working, 'blue', 'jeans')
178
- end
179
-
180
- it 'should call on_transition proc' do
181
- pe.wakeup!(:showering)
182
- pe.should_receive(:wear_clothes).with('purple', 'slacks')
183
- pe.dress!(:dating, 'purple', 'slacks')
184
- end
185
-
186
- it 'should call on_transition with an array of methods' do
187
- pe.wakeup!(:showering)
188
- pe.should_receive(:condition_hair)
189
- pe.should_receive(:fix_hair)
190
- pe.dress!(:prettying_up)
191
- end
192
- end
193
-
194
- describe 'event firing without persistence' do
195
- it 'should attempt to persist if aasm_write_state is defined' do
196
- foo = Foo.new
197
- def foo.aasm_write_state; end
198
- foo.should be_open
199
-
200
- foo.should_receive(:aasm_write_state_without_persistence)
201
- foo.close
202
- end
203
- end