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
@@ -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