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
@@ -2,6 +2,44 @@ module AASM
2
2
  module Persistence
3
3
  module Base
4
4
 
5
+ def self.included(base) #:nodoc:
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ # Returns the value of the aasm_column - called from <tt>aasm.current_state</tt>
10
+ #
11
+ # If it's a new record, and the aasm state column is blank it returns the initial state
12
+ # (example provided here for ActiveRecord, but it's true for Mongoid as well):
13
+ #
14
+ # class Foo < ActiveRecord::Base
15
+ # include AASM
16
+ # aasm :column => :status do
17
+ # state :opened
18
+ # state :closed
19
+ # end
20
+ # end
21
+ #
22
+ # foo = Foo.new
23
+ # foo.current_state # => :opened
24
+ # foo.close
25
+ # foo.current_state # => :closed
26
+ #
27
+ # foo = Foo.find(1)
28
+ # foo.current_state # => :opened
29
+ # foo.aasm_state = nil
30
+ # foo.current_state # => nil
31
+ #
32
+ # NOTE: intended to be called from an event
33
+ #
34
+ # This allows for nil aasm states - be sure to add validation to your model
35
+ def aasm_read_state
36
+ if new_record?
37
+ send(self.class.aasm_column).blank? ? aasm.determine_state_name(self.class.aasm_initial_state) : send(self.class.aasm_column).to_sym
38
+ else
39
+ send(self.class.aasm_column).nil? ? nil : send(self.class.aasm_column).to_sym
40
+ end
41
+ end
42
+
5
43
  module ClassMethods
6
44
  # Maps to the aasm_column in the database. Defaults to "aasm_state". You can write
7
45
  # (example provided here for ActiveRecord, but it's true for Mongoid as well):
@@ -39,18 +77,20 @@ module AASM
39
77
  # @aasm_column
40
78
  AASM::StateMachine[self].config.column
41
79
  end
42
- end
80
+ end # ClassMethods
43
81
 
44
82
  end # Base
45
83
  end # Persistence
46
84
 
47
85
  class Base
86
+ # make sure to create a (named) scope for each state
48
87
  def state_with_scope(name, *args)
49
88
  state_without_scope(name, *args)
50
89
  unless @clazz.respond_to?(name)
51
90
  if @clazz.ancestors.map {|klass| klass.to_s}.include?("ActiveRecord::Base")
52
- scope_options = {:conditions => { "#{@clazz.table_name}.#{@clazz.aasm_column}" => name.to_s}}
91
+ scope_options_hash = {:conditions => { "#{@clazz.table_name}.#{@clazz.aasm_column}" => name.to_s}}
53
92
  scope_method = ActiveRecord::VERSION::MAJOR >= 3 ? :scope : :named_scope
93
+ scope_options = ActiveRecord::VERSION::MAJOR >= 4 ? lambda { where(scope_options_hash[:conditions])} : scope_options_hash
54
94
  @clazz.send(scope_method, name, scope_options)
55
95
  elsif @clazz.ancestors.map {|klass| klass.to_s}.include?("Mongoid::Document")
56
96
  scope_options = lambda { @clazz.send(:where, {@clazz.aasm_column.to_sym => name.to_s}) }
@@ -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
@@ -34,12 +29,9 @@ module AASM
34
29
  # end
35
30
  #
36
31
  def self.included(base)
37
- base.extend AASM::Persistence::Base::ClassMethods
32
+ base.send(:include, AASM::Persistence::Base)
38
33
  base.extend AASM::Persistence::MongoidPersistence::ClassMethods
39
34
  base.send(:include, AASM::Persistence::MongoidPersistence::InstanceMethods)
40
- base.send(:include, AASM::Persistence::ReadState) unless base.method_defined?(:aasm_read_state)
41
- base.send(:include, AASM::Persistence::MongoidPersistence::WriteState) unless base.method_defined?(:aasm_write_state)
42
- base.send(:include, AASM::Persistence::MongoidPersistence::WriteStateWithoutPersistence) unless base.method_defined?(:aasm_write_state_without_persistence)
43
35
 
44
36
  # Mongoid's Validatable gem dependency goes not have a before_validation_on_xxx hook yet.
45
37
  # base.before_validation_on_create :aasm_ensure_initial_state
@@ -70,48 +62,28 @@ module AASM
70
62
 
71
63
  module InstanceMethods
72
64
 
73
- # Returns the current aasm_state of the object. Respects reload and
74
- # any changes made to the aasm_state field directly
75
- #
76
- # Internally just calls <tt>aasm_read_state</tt>
65
+ # Writes <tt>state</tt> to the state column and persists it to the database
66
+ # using update_attribute (which bypasses validation)
77
67
  #
78
68
  # foo = Foo.find(1)
79
- # foo.aasm_current_state # => :pending
80
- # foo.aasm_state = "opened"
81
69
  # foo.aasm_current_state # => :opened
82
- # foo.close # => calls aasm_write_state_without_persistence
70
+ # foo.close!
83
71
  # foo.aasm_current_state # => :closed
84
- # foo.reload
85
- # foo.aasm_current_state # => :pending
72
+ # Foo.find(1).aasm_current_state # => :closed
86
73
  #
87
- def aasm_current_state
88
- @current_state = aasm_read_state
89
- end
74
+ # NOTE: intended to be called from an event
75
+ def aasm_write_state(state)
76
+ old_value = read_attribute(self.class.aasm_column)
77
+ write_attribute(self.class.aasm_column, state.to_s)
90
78
 
91
- private
79
+ unless self.save(:validate => false)
80
+ write_attribute(self.class.aasm_column, old_value)
81
+ return false
82
+ end
92
83
 
93
- # Ensures that if the aasm_state column is nil and the record is new
94
- # that the initial state gets populated before validation on create
95
- #
96
- # foo = Foo.new
97
- # foo.aasm_state # => nil
98
- # foo.valid?
99
- # foo.aasm_state # => "open" (where :open is the initial state)
100
- #
101
- #
102
- # foo = Foo.find(:first)
103
- # foo.aasm_state # => 1
104
- # foo.aasm_state = nil
105
- # foo.valid?
106
- # foo.aasm_state # => nil
107
- #
108
- def aasm_ensure_initial_state
109
- send("#{self.class.aasm_column}=", self.aasm_enter_initial_state.to_s) if send(self.class.aasm_column).blank?
84
+ true
110
85
  end
111
86
 
112
- end
113
-
114
- module WriteStateWithoutPersistence
115
87
  # Writes <tt>state</tt> to the state column, but does not persist it to the database
116
88
  #
117
89
  # foo = Foo.find(1)
@@ -127,31 +99,28 @@ module AASM
127
99
  def aasm_write_state_without_persistence(state)
128
100
  write_attribute(self.class.aasm_column, state.to_s)
129
101
  end
130
- end
131
102
 
132
- module WriteState
133
- # Writes <tt>state</tt> to the state column and persists it to the database
134
- # using update_attribute (which bypasses validation)
103
+ private
104
+
105
+ # Ensures that if the aasm_state column is nil and the record is new
106
+ # that the initial state gets populated before validation on create
135
107
  #
136
- # foo = Foo.find(1)
137
- # foo.aasm_current_state # => :opened
138
- # foo.close!
139
- # foo.aasm_current_state # => :closed
140
- # Foo.find(1).aasm_current_state # => :closed
108
+ # foo = Foo.new
109
+ # foo.aasm_state # => nil
110
+ # foo.valid?
111
+ # foo.aasm_state # => "open" (where :open is the initial state)
141
112
  #
142
- # NOTE: intended to be called from an event
143
- def aasm_write_state(state)
144
- old_value = read_attribute(self.class.aasm_column)
145
- write_attribute(self.class.aasm_column, state.to_s)
146
-
147
- unless self.save(:validate => false)
148
- write_attribute(self.class.aasm_column, old_value)
149
- return false
150
- end
151
-
152
- true
113
+ #
114
+ # foo = Foo.find(:first)
115
+ # foo.aasm_state # => 1
116
+ # foo.aasm_state = nil
117
+ # foo.valid?
118
+ # foo.aasm_state # => nil
119
+ #
120
+ def aasm_ensure_initial_state
121
+ send("#{self.class.aasm_column}=", aasm.enter_initial_state.to_s) if send(self.class.aasm_column).blank?
153
122
  end
154
- end
123
+ end # InstanceMethods
155
124
 
156
125
  module NamedScopeMethods
157
126
  def aasm_state_with_named_scope name, options = {}
@@ -161,4 +130,4 @@ module AASM
161
130
  end
162
131
  end
163
132
  end
164
- end
133
+ end
@@ -0,0 +1,78 @@
1
+ module AASM
2
+ class State
3
+ attr_reader :name, :options
4
+
5
+ def initialize(name, clazz, options={})
6
+ @name = name
7
+ @clazz = clazz
8
+ update(options)
9
+ end
10
+
11
+ def ==(state)
12
+ if state.is_a? Symbol
13
+ name == state
14
+ else
15
+ name == state.name
16
+ end
17
+ end
18
+
19
+ def <=>(state)
20
+ if state.is_a? Symbol
21
+ name <=> state
22
+ else
23
+ name <=> state.name
24
+ end
25
+ end
26
+
27
+ def to_s
28
+ name.to_s
29
+ end
30
+
31
+ def fire_callbacks(action, record)
32
+ action = @options[action]
33
+ catch :halt_aasm_chain do
34
+ action.is_a?(Array) ?
35
+ action.each {|a| _fire_callbacks(a, record)} :
36
+ _fire_callbacks(action, record)
37
+ end
38
+ end
39
+
40
+ def display_name
41
+ @display_name ||= begin
42
+ if Module.const_defined?(:I18n)
43
+ localized_name
44
+ else
45
+ name.to_s.gsub(/_/, ' ').capitalize
46
+ end
47
+ end
48
+ end
49
+
50
+ def localized_name
51
+ AASM::Localizer.new.human_state_name(@clazz, self)
52
+ end
53
+
54
+ def for_select
55
+ [display_name, name.to_s]
56
+ end
57
+
58
+ private
59
+
60
+ def update(options = {})
61
+ if options.key?(:display) then
62
+ @display_name = options.delete(:display)
63
+ end
64
+ @options = options
65
+ self
66
+ end
67
+
68
+ def _fire_callbacks(action, record)
69
+ case action
70
+ when Symbol, String
71
+ record.send(action)
72
+ when Proc
73
+ action.call(record)
74
+ end
75
+ end
76
+
77
+ end
78
+ end # AASM
@@ -29,8 +29,8 @@ module AASM
29
29
  @events = @events.dup
30
30
  end
31
31
 
32
- def create_state(name, clazz, options)
33
- @states << AASM::SupportingClasses::State.new(name, clazz, options) unless @states.include?(name)
32
+ def add_state(name, clazz, options)
33
+ @states << AASM::State.new(name, clazz, options) unless @states.include?(name)
34
34
  end
35
35
 
36
36
  end # StateMachine
@@ -0,0 +1,49 @@
1
+ module AASM
2
+ class Transition
3
+ attr_reader :from, :to, :opts
4
+ alias_method :options, :opts
5
+
6
+ def initialize(opts)
7
+ @from, @to, @guard, @on_transition = opts[:from], opts[:to], opts[:guard], opts[:on_transition]
8
+ @opts = opts
9
+ end
10
+
11
+ # TODO: should be named allowed? or similar
12
+ def perform(obj, *args)
13
+ case @guard
14
+ when Symbol, String
15
+ obj.send(@guard, *args)
16
+ when Proc
17
+ @guard.call(obj, *args)
18
+ else
19
+ true
20
+ end
21
+ end
22
+
23
+ def execute(obj, *args)
24
+ @on_transition.is_a?(Array) ?
25
+ @on_transition.each {|ot| _execute(obj, ot, *args)} :
26
+ _execute(obj, @on_transition, *args)
27
+ end
28
+
29
+ def ==(obj)
30
+ @from == obj.from && @to == obj.to
31
+ end
32
+
33
+ def from?(value)
34
+ @from == value
35
+ end
36
+
37
+ private
38
+
39
+ def _execute(obj, on_transition, *args)
40
+ case on_transition
41
+ when Proc
42
+ on_transition.arity == 0 ? on_transition.call : on_transition.call(obj, *args)
43
+ when Symbol, String
44
+ obj.send(:method, on_transition.to_sym).arity == 0 ? obj.send(on_transition) : obj.send(on_transition, *args)
45
+ end
46
+ end
47
+
48
+ end
49
+ end # AASM
@@ -1,3 +1,3 @@
1
1
  module AASM
2
- VERSION = "3.0.16"
2
+ VERSION = "3.0.17"
3
3
  end
@@ -0,0 +1,75 @@
1
+ class DefaultState
2
+ attr_accessor :transient_store, :persisted_store
3
+ include AASM
4
+ aasm do
5
+ state :alpha, :initial => true
6
+ state :beta
7
+ state :gamma
8
+ event :release do
9
+ transitions :from => [:alpha, :beta, :gamma], :to => :beta
10
+ end
11
+ end
12
+ end
13
+
14
+ class ProvidedState
15
+ attr_accessor :transient_store, :persisted_store
16
+ include AASM
17
+ aasm do
18
+ state :alpha, :initial => true
19
+ state :beta
20
+ state :gamma
21
+ event :release do
22
+ transitions :from => [:alpha, :beta, :gamma], :to => :beta
23
+ end
24
+ end
25
+
26
+ def aasm_read_state
27
+ :beta
28
+ end
29
+
30
+ def aasm_write_state(new_state)
31
+ @persisted_store = new_state
32
+ end
33
+
34
+ def aasm_write_state_without_persistence(new_state)
35
+ @transient_store = new_state
36
+ end
37
+ end
38
+
39
+ class PersistedState < ActiveRecord::Base
40
+ attr_accessor :transient_store, :persisted_store
41
+ include AASM
42
+ aasm do
43
+ state :alpha, :initial => true
44
+ state :beta
45
+ state :gamma
46
+ event :release do
47
+ transitions :from => [:alpha, :beta, :gamma], :to => :beta
48
+ end
49
+ end
50
+ end
51
+
52
+ class ProvidedAndPersistedState < ActiveRecord::Base
53
+ attr_accessor :transient_store, :persisted_store
54
+ include AASM
55
+ aasm do
56
+ state :alpha, :initial => true
57
+ state :beta
58
+ state :gamma
59
+ event :release do
60
+ transitions :from => [:alpha, :beta, :gamma], :to => :beta
61
+ end
62
+ end
63
+
64
+ def aasm_read_state
65
+ :gamma
66
+ end
67
+
68
+ def aasm_write_state(new_state)
69
+ @persisted_store = new_state
70
+ end
71
+
72
+ def aasm_write_state_without_persistence(new_state)
73
+ @transient_store = new_state
74
+ end
75
+ end
@@ -46,7 +46,7 @@ class AuthMachine
46
46
  def initialize
47
47
  # the AR backend uses a before_validate_on_create :aasm_ensure_initial_state
48
48
  # lets do something similar here for testing purposes.
49
- aasm_enter_initial_state
49
+ aasm.enter_initial_state
50
50
  end
51
51
 
52
52
  def make_activation_code
@@ -0,0 +1,15 @@
1
+ class Bar
2
+ include AASM
3
+
4
+ aasm do
5
+ state :read
6
+ state :ended
7
+
8
+ event :foo do
9
+ transitions :to => :ended, :from => [:read]
10
+ end
11
+ end
12
+ end
13
+
14
+ class Baz < Bar
15
+ end