jeanmartin-aasm 2.1.1

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.
data/lib/aasm.rb ADDED
@@ -0,0 +1,186 @@
1
+ require File.join(File.dirname(__FILE__), 'event')
2
+ require File.join(File.dirname(__FILE__), 'state')
3
+ require File.join(File.dirname(__FILE__), 'state_machine')
4
+ require File.join(File.dirname(__FILE__), 'persistence')
5
+
6
+ module AASM
7
+ def self.Version
8
+ '2.1.0'
9
+ end
10
+
11
+ class InvalidTransition < RuntimeError
12
+ end
13
+
14
+ class UndefinedState < RuntimeError
15
+ end
16
+
17
+ def self.included(base) #:nodoc:
18
+ # TODO - need to ensure that a machine is being created because
19
+ # AASM was either included or arrived at via inheritance. It
20
+ # cannot be both.
21
+ base.extend AASM::ClassMethods
22
+ AASM::Persistence.set_persistence(base)
23
+ AASM::StateMachine[base] = AASM::StateMachine.new('')
24
+ end
25
+
26
+ module ClassMethods
27
+ def inherited(klass)
28
+ AASM::StateMachine[klass] = AASM::StateMachine[self].clone
29
+ super
30
+ end
31
+
32
+ def aasm_initial_state(set_state=nil)
33
+ if set_state
34
+ AASM::StateMachine[self].initial_state = set_state
35
+ else
36
+ AASM::StateMachine[self].initial_state
37
+ end
38
+ end
39
+
40
+ def aasm_initial_state=(state)
41
+ AASM::StateMachine[self].initial_state = state
42
+ end
43
+
44
+ def aasm_state(name, options={})
45
+ sm = AASM::StateMachine[self]
46
+ sm.create_state(name, options)
47
+ sm.initial_state = name unless sm.initial_state
48
+
49
+ define_method("#{name.to_s}?") do
50
+ aasm_current_state == name
51
+ end
52
+ end
53
+
54
+ def aasm_event(name, options = {}, &block)
55
+ sm = AASM::StateMachine[self]
56
+
57
+ unless sm.events.has_key?(name)
58
+ sm.events[name] = AASM::SupportingClasses::Event.new(name, options, &block)
59
+ end
60
+
61
+ define_method("#{name.to_s}!") do |*args|
62
+ aasm_fire_event(name, true, *args)
63
+ end
64
+
65
+ define_method("#{name.to_s}") do |*args|
66
+ aasm_fire_event(name, false, *args)
67
+ end
68
+ end
69
+
70
+ def aasm_states
71
+ AASM::StateMachine[self].states
72
+ end
73
+
74
+ def aasm_events
75
+ AASM::StateMachine[self].events
76
+ end
77
+
78
+ def aasm_states_for_select
79
+ AASM::StateMachine[self].states.map { |state| state.for_select }
80
+ end
81
+
82
+ end
83
+
84
+ # Instance methods
85
+ def aasm_current_state
86
+ return @aasm_current_state if @aasm_current_state
87
+
88
+ if self.respond_to?(:aasm_read_state) || self.private_methods.include?('aasm_read_state')
89
+ @aasm_current_state = aasm_read_state
90
+ end
91
+ return @aasm_current_state if @aasm_current_state
92
+ aasm_determine_state_name(self.class.aasm_initial_state)
93
+ end
94
+
95
+ def aasm_events_for_current_state
96
+ aasm_events_for_state(aasm_current_state)
97
+ end
98
+
99
+ def aasm_events_for_state(state)
100
+ events = self.class.aasm_events.values.select {|event| event.transitions_from_state?(state) }
101
+ events.map {|event| event.name}
102
+ end
103
+
104
+ private
105
+ def set_aasm_current_state_with_persistence(state)
106
+ save_success = true
107
+ if self.respond_to?(:aasm_write_state) || self.private_methods.include?('aasm_write_state')
108
+ save_success = aasm_write_state(state)
109
+ end
110
+ self.aasm_current_state = state if save_success
111
+
112
+ save_success
113
+ end
114
+
115
+ def aasm_current_state=(state)
116
+ if self.respond_to?(:aasm_write_state_without_persistence) || self.private_methods.include?('aasm_write_state_without_persistence')
117
+ aasm_write_state_without_persistence(state)
118
+ end
119
+ @aasm_current_state = state
120
+ end
121
+
122
+ def aasm_determine_state_name(state)
123
+ case state
124
+ when Symbol, String
125
+ state
126
+ when Proc
127
+ state.call(self)
128
+ else
129
+ raise NotImplementedError, "Unrecognized state-type given. Expected Symbol, String, or Proc."
130
+ end
131
+ end
132
+
133
+ def aasm_state_object_for_state(name)
134
+ obj = self.class.aasm_states.find {|s| s == name}
135
+ raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil?
136
+ obj
137
+ end
138
+
139
+ def aasm_fire_event(name, persist, *args)
140
+ old_state = aasm_state_object_for_state(aasm_current_state)
141
+ event = self.class.aasm_events[name]
142
+
143
+ old_state.call_action(:exit, self)
144
+
145
+ # new event before callback
146
+ event.call_action(:before, self)
147
+
148
+ new_state_name = event.fire(self, *args)
149
+
150
+ unless new_state_name.nil?
151
+ new_state = aasm_state_object_for_state(new_state_name)
152
+
153
+ # new before_ callbacks
154
+ old_state.call_action(:before_exit, self)
155
+ new_state.call_action(:before_enter, self)
156
+
157
+ new_state.call_action(:enter, self)
158
+
159
+ persist_successful = true
160
+ if persist
161
+ persist_successful = set_aasm_current_state_with_persistence(new_state_name)
162
+ event.execute_success_callback(self) if persist_successful
163
+ else
164
+ self.aasm_current_state = new_state_name
165
+ end
166
+
167
+ if persist_successful
168
+ old_state.call_action(:after_exit, self)
169
+ new_state.call_action(:after_enter, self)
170
+ event.call_action(:after, self)
171
+
172
+ self.aasm_event_fired(name, old_state.name, self.aasm_current_state) if self.respond_to?(:aasm_event_fired)
173
+ else
174
+ self.aasm_event_failed(name, old_state.name) if self.respond_to?(:aasm_event_failed)
175
+ end
176
+
177
+ persist_successful
178
+ else
179
+ if self.respond_to?(:aasm_event_failed)
180
+ self.aasm_event_failed(name, old_state.name)
181
+ end
182
+
183
+ false
184
+ end
185
+ end
186
+ end
data/lib/event.rb ADDED
@@ -0,0 +1,76 @@
1
+ require File.join(File.dirname(__FILE__), 'state_transition')
2
+
3
+ module AASM
4
+ module SupportingClasses
5
+ class Event
6
+ attr_reader :name, :success, :options
7
+
8
+ def initialize(name, options = {}, &block)
9
+ @name = name
10
+ @success = options[:success]
11
+ @transitions = []
12
+ @options = options
13
+ instance_eval(&block) if block
14
+ end
15
+
16
+ def fire(obj, to_state=nil, *args)
17
+ transitions = @transitions.select { |t| t.from == obj.aasm_current_state }
18
+ raise AASM::InvalidTransition, "Event '#{name}' cannot transition from '#{obj.aasm_current_state}'" if transitions.size == 0
19
+
20
+ next_state = nil
21
+ transitions.each do |transition|
22
+ next if to_state and !Array(transition.to).include?(to_state)
23
+ if transition.perform(obj)
24
+ next_state = to_state || Array(transition.to).first
25
+ transition.execute(obj, *args)
26
+ break
27
+ end
28
+ end
29
+ next_state
30
+ end
31
+
32
+ def transitions_from_state?(state)
33
+ @transitions.any? { |t| t.from == state }
34
+ end
35
+
36
+ def transitions_from_state(state)
37
+ @transitions.select { |t| t.from == state }
38
+ end
39
+
40
+ def execute_success_callback(obj, success = nil)
41
+ callback = success || @success
42
+ case(callback)
43
+ when String, Symbol
44
+ obj.send(callback)
45
+ when Proc
46
+ callback.call(obj)
47
+ when Array
48
+ callback.each{|meth|self.execute_success_callback(obj, meth)}
49
+ end
50
+ end
51
+
52
+ def call_action(action, record)
53
+ action = @options[action]
54
+ case action
55
+ when Symbol, String
56
+ record.send(action)
57
+ when Proc
58
+ action.call(record)
59
+ when Array
60
+ action.each { |a| record.send(a) }
61
+ end
62
+ end
63
+
64
+ def all_transitions
65
+ @transitions
66
+ end
67
+
68
+ private
69
+ def transitions(trans_opts)
70
+ Array(trans_opts[:from]).each do |s|
71
+ @transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,248 @@
1
+ module AASM
2
+ module Persistence
3
+ module ActiveRecordPersistence
4
+ # This method:
5
+ #
6
+ # * extends the model with ClassMethods
7
+ # * includes InstanceMethods
8
+ #
9
+ # Unless the corresponding methods are already defined, it includes
10
+ # * ReadState
11
+ # * WriteState
12
+ # * WriteStateWithoutPersistence
13
+ #
14
+ # Adds
15
+ #
16
+ # before_validation_on_create :aasm_ensure_initial_state
17
+ #
18
+ # As a result, it doesn't matter when you define your methods - the following 2 are equivalent
19
+ #
20
+ # class Foo < ActiveRecord::Base
21
+ # def aasm_write_state(state)
22
+ # "bar"
23
+ # end
24
+ # include AASM
25
+ # end
26
+ #
27
+ # class Foo < ActiveRecord::Base
28
+ # include AASM
29
+ # def aasm_write_state(state)
30
+ # "bar"
31
+ # end
32
+ # end
33
+ #
34
+ def self.included(base)
35
+ base.extend AASM::Persistence::ActiveRecordPersistence::ClassMethods
36
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
37
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence::ReadState) unless base.method_defined?(:aasm_read_state)
38
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence::WriteState) unless base.method_defined?(:aasm_write_state)
39
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence) unless base.method_defined?(:aasm_write_state_without_persistence)
40
+
41
+ if base.respond_to?(:named_scope)
42
+ base.extend(AASM::Persistence::ActiveRecordPersistence::NamedScopeMethods)
43
+
44
+ base.class_eval do
45
+ class << self
46
+ unless method_defined?(:aasm_state_without_named_scope)
47
+ alias_method :aasm_state_without_named_scope, :aasm_state
48
+ alias_method :aasm_state, :aasm_state_with_named_scope
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ base.before_validation_on_create :aasm_ensure_initial_state
55
+ end
56
+
57
+ module ClassMethods
58
+ # Maps to the aasm_column in the database. Deafults to "aasm_state". You can write:
59
+ #
60
+ # create_table :foos do |t|
61
+ # t.string :name
62
+ # t.string :aasm_state
63
+ # end
64
+ #
65
+ # class Foo < ActiveRecord::Base
66
+ # include AASM
67
+ # end
68
+ #
69
+ # OR:
70
+ #
71
+ # create_table :foos do |t|
72
+ # t.string :name
73
+ # t.string :status
74
+ # end
75
+ #
76
+ # class Foo < ActiveRecord::Base
77
+ # include AASM
78
+ # aasm_column :status
79
+ # end
80
+ #
81
+ # This method is both a getter and a setter
82
+ def aasm_column(column_name=nil)
83
+ if column_name
84
+ AASM::StateMachine[self].config.column = column_name.to_sym
85
+ # @aasm_column = column_name.to_sym
86
+ else
87
+ AASM::StateMachine[self].config.column ||= :aasm_state
88
+ # @aasm_column ||= :aasm_state
89
+ end
90
+ # @aasm_column
91
+ AASM::StateMachine[self].config.column
92
+ end
93
+
94
+ def find_in_state(number, state, *args)
95
+ with_state_scope state do
96
+ find(number, *args)
97
+ end
98
+ end
99
+
100
+ def count_in_state(state, *args)
101
+ with_state_scope state do
102
+ count(*args)
103
+ end
104
+ end
105
+
106
+ def calculate_in_state(state, *args)
107
+ with_state_scope state do
108
+ calculate(*args)
109
+ end
110
+ end
111
+
112
+ protected
113
+ def with_state_scope(state)
114
+ with_scope :find => {:conditions => ["#{table_name}.#{aasm_column} = ?", state.to_s]} do
115
+ yield if block_given?
116
+ end
117
+ end
118
+ end
119
+
120
+ module InstanceMethods
121
+
122
+ # Returns the current aasm_state of the object. Respects reload and
123
+ # any changes made to the aasm_state field directly
124
+ #
125
+ # Internally just calls <tt>aasm_read_state</tt>
126
+ #
127
+ # foo = Foo.find(1)
128
+ # foo.aasm_current_state # => :pending
129
+ # foo.aasm_state = "opened"
130
+ # foo.aasm_current_state # => :opened
131
+ # foo.close # => calls aasm_write_state_without_persistence
132
+ # foo.aasm_current_state # => :closed
133
+ # foo.reload
134
+ # foo.aasm_current_state # => :pending
135
+ #
136
+ def aasm_current_state
137
+ @current_state = aasm_read_state
138
+ end
139
+
140
+ private
141
+
142
+ # Ensures that if the aasm_state column is nil and the record is new
143
+ # that the initial state gets populated before validation on create
144
+ #
145
+ # foo = Foo.new
146
+ # foo.aasm_state # => nil
147
+ # foo.valid?
148
+ # foo.aasm_state # => "open" (where :open is the initial state)
149
+ #
150
+ #
151
+ # foo = Foo.find(:first)
152
+ # foo.aasm_state # => 1
153
+ # foo.aasm_state = nil
154
+ # foo.valid?
155
+ # foo.aasm_state # => nil
156
+ #
157
+ def aasm_ensure_initial_state
158
+ send("#{self.class.aasm_column}=", self.aasm_current_state.to_s)
159
+ end
160
+
161
+ end
162
+
163
+ module WriteStateWithoutPersistence
164
+ # Writes <tt>state</tt> to the state column, but does not persist it to the database
165
+ #
166
+ # foo = Foo.find(1)
167
+ # foo.aasm_current_state # => :opened
168
+ # foo.close
169
+ # foo.aasm_current_state # => :closed
170
+ # Foo.find(1).aasm_current_state # => :opened
171
+ # foo.save
172
+ # foo.aasm_current_state # => :closed
173
+ # Foo.find(1).aasm_current_state # => :closed
174
+ #
175
+ # NOTE: intended to be called from an event
176
+ def aasm_write_state_without_persistence(state)
177
+ write_attribute(self.class.aasm_column, state.to_s)
178
+ end
179
+ end
180
+
181
+ module WriteState
182
+ # Writes <tt>state</tt> to the state column and persists it to the database
183
+ # using update_attribute (which bypasses validation)
184
+ #
185
+ # foo = Foo.find(1)
186
+ # foo.aasm_current_state # => :opened
187
+ # foo.close!
188
+ # foo.aasm_current_state # => :closed
189
+ # Foo.find(1).aasm_current_state # => :closed
190
+ #
191
+ # NOTE: intended to be called from an event
192
+ def aasm_write_state(state)
193
+ old_value = read_attribute(self.class.aasm_column)
194
+ write_attribute(self.class.aasm_column, state.to_s)
195
+
196
+ unless self.save(false)
197
+ write_attribute(self.class.aasm_column, old_value)
198
+ return false
199
+ end
200
+
201
+ true
202
+ end
203
+ end
204
+
205
+ module ReadState
206
+
207
+ # Returns the value of the aasm_column - called from <tt>aasm_current_state</tt>
208
+ #
209
+ # If it's a new record, and the aasm state column is blank it returns the initial state:
210
+ #
211
+ # class Foo < ActiveRecord::Base
212
+ # include AASM
213
+ # aasm_column :status
214
+ # aasm_state :opened
215
+ # aasm_state :closed
216
+ # end
217
+ #
218
+ # foo = Foo.new
219
+ # foo.current_state # => :opened
220
+ # foo.close
221
+ # foo.current_state # => :closed
222
+ #
223
+ # foo = Foo.find(1)
224
+ # foo.current_state # => :opened
225
+ # foo.aasm_state = nil
226
+ # foo.current_state # => nil
227
+ #
228
+ # NOTE: intended to be called from an event
229
+ #
230
+ # This allows for nil aasm states - be sure to add validation to your model
231
+ def aasm_read_state
232
+ if new_record?
233
+ send(self.class.aasm_column).blank? ? aasm_determine_state_name(self.class.aasm_initial_state) : send(self.class.aasm_column).to_sym
234
+ else
235
+ send(self.class.aasm_column).nil? ? nil : send(self.class.aasm_column).to_sym
236
+ end
237
+ end
238
+ end
239
+
240
+ module NamedScopeMethods
241
+ def aasm_state_with_named_scope name, options = {}
242
+ aasm_state_without_named_scope name, options
243
+ self.named_scope name, :conditions => { "#{table_name}.#{self.aasm_column}" => name.to_s} unless self.respond_to?(name)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,16 @@
1
+ module AASM
2
+ module Persistence
3
+
4
+ # Checks to see this class or any of it's superclasses inherit from
5
+ # ActiveRecord::Base and if so includes ActiveRecordPersistence
6
+ def self.set_persistence(base)
7
+ # Use a fancier auto-loading thingy, perhaps. When there are more persistence engines.
8
+ hierarchy = base.ancestors.map {|klass| klass.to_s}
9
+
10
+ if hierarchy.include?("ActiveRecord::Base")
11
+ require File.join(File.dirname(__FILE__), 'persistence', 'active_record_persistence')
12
+ base.send(:include, AASM::Persistence::ActiveRecordPersistence)
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/state.rb ADDED
@@ -0,0 +1,35 @@
1
+ module AASM
2
+ module SupportingClasses
3
+ class State
4
+ attr_reader :name, :options
5
+
6
+ def initialize(name, options={})
7
+ @name, @options = name, options
8
+ end
9
+
10
+ def ==(state)
11
+ if state.is_a? Symbol
12
+ name == state
13
+ else
14
+ name == state.name
15
+ end
16
+ end
17
+
18
+ def call_action(action, record)
19
+ action = @options[action]
20
+ case action
21
+ when Symbol, String
22
+ record.send(action)
23
+ when Proc
24
+ action.call(record)
25
+ when Array
26
+ action.each { |a| record.send(a) }
27
+ end
28
+ end
29
+
30
+ def for_select
31
+ [name.to_s.gsub(/_/, ' ').capitalize, name.to_s]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ require 'ostruct'
2
+
3
+ module AASM
4
+ class StateMachine
5
+ def self.[](*args)
6
+ (@machines ||= {})[args]
7
+ end
8
+
9
+ def self.[]=(*args)
10
+ val = args.pop
11
+ (@machines ||= {})[args] = val
12
+ end
13
+
14
+ attr_accessor :states, :events, :initial_state, :config
15
+ attr_reader :name
16
+
17
+ def initialize(name)
18
+ @name = name
19
+ @initial_state = nil
20
+ @states = []
21
+ @events = {}
22
+ @config = OpenStruct.new
23
+ end
24
+
25
+ def clone
26
+ klone = super
27
+ klone.states = states.clone
28
+ klone
29
+ end
30
+
31
+ def create_state(name, options)
32
+ @states << AASM::SupportingClasses::State.new(name, options) unless @states.include?(name)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ module AASM
2
+
3
+ # When raising a guard failure, simply give an explanation i.e. "the value is not in the allowed range" similar to
4
+ # the :message specification for AR validations.
5
+ class GuardFailure < RuntimeError; end
6
+
7
+ module SupportingClasses
8
+ class StateTransition
9
+ attr_reader :from, :to, :opts
10
+
11
+ def initialize(opts)
12
+ @from, @to, @guard, @on_transition, @failure_message = opts[:from], opts[:to], opts[:guard], opts[:on_transition], opts[:failure_message]
13
+ @opts = opts
14
+ end
15
+
16
+ def perform(obj)
17
+ result = begin
18
+ guard_result = case @guard
19
+ when Symbol, String
20
+ obj.send(@guard)
21
+ when Proc
22
+ @guard.call(obj)
23
+ else
24
+ true
25
+ end
26
+
27
+ # Catch generic false return with no exception raised
28
+ raise(GuardFailure, I18n.t('activerecord.errors.messages.guard_fail', :default => 'guard condition(s) failed')) unless result
29
+
30
+ rescue GuardFailure => e
31
+ obj.errors.send(:add, obj.class.aasm_column.to_sym, e.message)
32
+ false
33
+ else
34
+ true
35
+ end
36
+ end
37
+
38
+ def execute(obj, *args)
39
+ case @on_transition
40
+ when Symbol, String
41
+ obj.send(@on_transition, *args)
42
+ when Proc
43
+ @on_transition.call(obj, *args)
44
+ end
45
+ end
46
+
47
+ def ==(obj)
48
+ @from == obj.from && @to == obj.to
49
+ end
50
+ end
51
+ end
52
+ end