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.
- data/.gitignore +1 -0
- data/.travis.yml +2 -1
- data/API +34 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +1 -1
- data/HOWTO +12 -0
- data/README.md +57 -4
- data/aasm.gemspec +2 -0
- data/lib/aasm.rb +5 -4
- data/lib/aasm/aasm.rb +50 -75
- data/lib/aasm/base.rb +22 -18
- data/lib/aasm/event.rb +130 -0
- data/lib/aasm/instance_base.rb +87 -0
- data/lib/aasm/localizer.rb +54 -0
- data/lib/aasm/persistence.rb +22 -14
- data/lib/aasm/persistence/active_record_persistence.rb +38 -69
- data/lib/aasm/persistence/base.rb +42 -2
- data/lib/aasm/persistence/mongoid_persistence.rb +33 -64
- data/lib/aasm/state.rb +78 -0
- data/lib/aasm/state_machine.rb +2 -2
- data/lib/aasm/transition.rb +49 -0
- data/lib/aasm/version.rb +1 -1
- data/spec/models/active_record/api.rb +75 -0
- data/spec/models/auth_machine.rb +1 -1
- data/spec/models/bar.rb +15 -0
- data/spec/models/foo.rb +34 -0
- data/spec/models/mongoid/simple_mongoid.rb +10 -0
- data/spec/models/mongoid/{mongoid_models.rb → simple_new_dsl_mongoid.rb} +1 -12
- data/spec/models/persistence.rb +2 -1
- data/spec/models/this_name_better_not_be_in_use.rb +11 -0
- data/spec/schema.rb +1 -1
- data/spec/spec_helper.rb +8 -1
- data/spec/unit/api_spec.rb +72 -0
- data/spec/unit/callbacks_spec.rb +2 -2
- data/spec/unit/event_spec.rb +269 -0
- data/spec/unit/inspection_spec.rb +43 -5
- data/spec/unit/{supporting_classes/localizer_spec.rb → localizer_spec.rb} +2 -2
- data/spec/unit/memory_leak_spec.rb +12 -12
- data/spec/unit/persistence/active_record_persistence_spec.rb +0 -40
- data/spec/unit/persistence/mongoid_persistance_spec.rb +3 -2
- data/spec/unit/simple_example_spec.rb +6 -0
- data/spec/unit/{supporting_classes/state_spec.rb → state_spec.rb} +2 -2
- data/spec/unit/{supporting_classes/state_transition_spec.rb → transition_spec.rb} +18 -18
- metadata +127 -38
- data/lib/aasm/persistence/read_state.rb +0 -40
- data/lib/aasm/supporting_classes/event.rb +0 -146
- data/lib/aasm/supporting_classes/localizer.rb +0 -56
- data/lib/aasm/supporting_classes/state.rb +0 -80
- data/lib/aasm/supporting_classes/state_transition.rb +0 -51
- data/spec/spec_helpers/models_spec_helper.rb +0 -64
- data/spec/unit/supporting_classes/event_spec.rb +0 -203
data/lib/aasm/event.rb
ADDED
@@ -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
|
data/lib/aasm/persistence.rb
CHANGED
@@ -1,20 +1,28 @@
|
|
1
1
|
module AASM
|
2
2
|
module Persistence
|
3
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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.
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
#
|
91
|
-
# foo.aasm_current_state # => :pending
|
106
|
+
# Foo.find(1).aasm_current_state # => :closed
|
92
107
|
#
|
93
|
-
|
94
|
-
|
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
|
-
|
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
|
-
|
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
|