aasm 4.8.0 → 4.9.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +128 -1
- data/lib/aasm/aasm.rb +7 -1
- data/lib/aasm/base.rb +24 -15
- data/lib/aasm/configuration.rb +7 -1
- data/lib/aasm/core/transition.rb +30 -4
- data/lib/aasm/persistence/active_record_persistence.rb +13 -1
- data/lib/aasm/persistence/base.rb +1 -1
- data/lib/aasm/version.rb +1 -1
- data/lib/generators/aasm/aasm_generator.rb +1 -2
- data/lib/generators/active_record/aasm_generator.rb +3 -3
- data/lib/generators/mongoid/aasm_generator.rb +4 -4
- data/spec/database.rb +11 -16
- data/spec/models/simple_custom_example.rb +53 -0
- data/spec/models/transactor.rb +48 -0
- data/spec/unit/persistence/active_record_persistence_spec.rb +33 -0
- data/spec/unit/simple_custom_example_spec.rb +39 -0
- data/spec/unit/transition_spec.rb +63 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4a393ed849111b8beee789a849433679c36f940
|
4
|
+
data.tar.gz: 905f16c76e62d5481bcfe55f4bb935039b0c7726
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e64e9fffd9c560a166ab728105d3891ca9a6a8784c27b70f83d9fd91ed7ccde1c839662f8e7e099ef4128db6bc91a690a30c8fbb3c0f83e4bfeba7d2c0ad67f
|
7
|
+
data.tar.gz: 574cafabbe8b20e1bf9db7ef87ff10c358dd814fd3bc86fc2f843824c9be8deb4ad645c9a0273a7ecf861bfb6136b519476f1b749a0d3706e798b24a3dd92da8
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## 4.9.0
|
4
|
+
|
5
|
+
* add support for callback classes (`after` only) (see [issue #316](https://github.com/aasm/aasm/pull/316) for details, thanks to [@mlr](https://github.com/mlr))
|
6
|
+
* allow easier extension of _AASM_ (utilising the idea of _ApplicationRecords_ from _Rails 5_) (see [issue #296](https://github.com/aasm/aasm/pull/296) for details, thanks to [@mlr](https://github.com/mlr))
|
7
|
+
* support pessimistic locking for _ActiveRecord_ (see [issue #283](https://github.com/aasm/aasm/pull/283) for details, thanks to [@HoyaBoya](https://github.com/HoyaBoya))
|
8
|
+
* fix: support database sharding for _ActiveRecord_ (see [issue #289](https://github.com/aasm/aasm/pull/289) for details, thanks to [@scambra](https://github.com/scambra))
|
9
|
+
* fix: some issues with RubyMotion (see [issue #318](https://github.com/aasm/aasm/pull/318) for details, thanks to [@Infotaku](https://github.com/Infotaku))
|
10
|
+
* fix: Rails generator now features the correct namespace (see [issue #328](https://github.com/aasm/aasm/pull/328) and [issue #329](https://github.com/aasm/aasm/pull/329) for details, thanks to [@anilmaurya](https://github.com/anilmaurya))
|
11
|
+
|
12
|
+
|
3
13
|
## 4.8.0
|
4
14
|
|
5
15
|
* add support for [dynamoid](http://joshsymonds.com/Dynamoid/) (see [issue #300](https://github.com/aasm/aasm/pull/300) for details, thanks to [@LeeChSien](https://github.com/LeeChSien))
|
data/README.md
CHANGED
@@ -106,6 +106,7 @@ class Job
|
|
106
106
|
end
|
107
107
|
|
108
108
|
transitions :from => :sleeping, :to => :running, :after => Proc.new {|*args| set_process(*args) }
|
109
|
+
transitions :from => :running, :to => :finished, :after => LogRunTime
|
109
110
|
end
|
110
111
|
|
111
112
|
event :sleep do
|
@@ -136,12 +137,34 @@ class Job
|
|
136
137
|
end
|
137
138
|
|
138
139
|
end
|
140
|
+
|
141
|
+
class LogRunTime
|
142
|
+
def call
|
143
|
+
log "Job was running for X seconds"
|
144
|
+
end
|
145
|
+
end
|
139
146
|
```
|
140
147
|
|
141
148
|
In this case `do_something` is called before actually entering the state `sleeping`,
|
142
149
|
while `notify_somebody` is called after the transition `run` (from `sleeping` to `running`)
|
143
150
|
is finished.
|
144
151
|
|
152
|
+
AASM will also initialize `LogRunTime` and run the `call` method for you after the transition from `runnung` to `finished` in the example above. You can pass arguments to the class by defining an initialize method on it, like this:
|
153
|
+
|
154
|
+
```
|
155
|
+
class LogRunTime
|
156
|
+
# optional args parameter can be omitted, but if you define initialize
|
157
|
+
# you must accept the model instance as the first parameter to it.
|
158
|
+
def initialize(job, args = {})
|
159
|
+
@job = job
|
160
|
+
end
|
161
|
+
|
162
|
+
def call
|
163
|
+
log "Job was running for #{@job.run_time} seconds"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
145
168
|
Here you can see a list of all possible callbacks, together with their order of calling:
|
146
169
|
|
147
170
|
```ruby
|
@@ -394,6 +417,71 @@ SimpleMultipleExample.aasm(:work).states
|
|
394
417
|
*Final note*: Support for multiple state machines per class is a pretty new feature
|
395
418
|
(since version `4.3`), so please bear with us in case it doesn't work as expected.
|
396
419
|
|
420
|
+
### Extending AASM
|
421
|
+
|
422
|
+
AASM allows you to easily extend `AASM::Base` for your own application purposes.
|
423
|
+
|
424
|
+
Let's suppose we have common logic across many AASM models. We can embody this logic in a sub-class of `AASM::Base`.
|
425
|
+
|
426
|
+
```
|
427
|
+
class CustomAASMBase < AASM::Base
|
428
|
+
# A custom transiton that we want available across many AASM models.
|
429
|
+
def count_transitions!
|
430
|
+
klass.class_eval do
|
431
|
+
aasm :with_klass => CustomAASMBase do
|
432
|
+
after_all_transitions :increment_transition_count
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
# A custom annotation that we want available across many AASM models.
|
438
|
+
def requires_guards!
|
439
|
+
klass.class_eval do
|
440
|
+
attr_reader :authorizable_called,
|
441
|
+
:transition_count,
|
442
|
+
:fillable_called
|
443
|
+
|
444
|
+
def authorizable?
|
445
|
+
@authorizable_called = true
|
446
|
+
end
|
447
|
+
|
448
|
+
def fillable?
|
449
|
+
@fillable_called = true
|
450
|
+
end
|
451
|
+
|
452
|
+
def increment_transition_count
|
453
|
+
@transition_count ||= 0
|
454
|
+
@transition_count += 1
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
```
|
460
|
+
|
461
|
+
When we declare our model that has an AASM state machine, we simply declare the AASM block with a `:with` key to our own class.
|
462
|
+
|
463
|
+
```
|
464
|
+
class SimpleCustomExample
|
465
|
+
include AASM
|
466
|
+
|
467
|
+
# Let's build an AASM state machine with our custom class.
|
468
|
+
aasm :with_klass => CustomAASMBase do
|
469
|
+
requires_guards!
|
470
|
+
count_transitions!
|
471
|
+
|
472
|
+
state :initialised, :initial => true
|
473
|
+
state :filled_out
|
474
|
+
state :authorised
|
475
|
+
|
476
|
+
event :fill_out do
|
477
|
+
transitions :from => :initialised, :to => :filled_out, :guard => :fillable?
|
478
|
+
end
|
479
|
+
event :authorise do
|
480
|
+
transitions :from => :filled_out, :to => :authorised, :guard => :authorizable?
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
```
|
397
485
|
|
398
486
|
|
399
487
|
### ActiveRecord
|
@@ -693,6 +781,41 @@ end
|
|
693
781
|
|
694
782
|
which then leads to `transaction(:requires_new => false)`, the Rails default.
|
695
783
|
|
784
|
+
### Pessimistic Locking
|
785
|
+
|
786
|
+
AASM supports [Active Record pessimistic locking via `with_lock`](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html#method-i-with_lock) for database persistence layers.
|
787
|
+
|
788
|
+
| Option | Purpose |
|
789
|
+
| ------ | ------- |
|
790
|
+
| `false` (default) | No lock is obtained | |
|
791
|
+
| `true` | Obtain a blocking pessimistic lock e.g. `FOR UPDATE` |
|
792
|
+
| String | Obtain a lock based on the SQL string e.g. `FOR UPDATE NOWAIT` |
|
793
|
+
|
794
|
+
|
795
|
+
```ruby
|
796
|
+
class Job < ActiveRecord::Base
|
797
|
+
include AASM
|
798
|
+
|
799
|
+
aasm :requires_lock => true do
|
800
|
+
...
|
801
|
+
end
|
802
|
+
|
803
|
+
...
|
804
|
+
end
|
805
|
+
```
|
806
|
+
|
807
|
+
```ruby
|
808
|
+
class Job < ActiveRecord::Base
|
809
|
+
include AASM
|
810
|
+
|
811
|
+
aasm :requires_lock => 'FOR UPDATE NOWAIT' do
|
812
|
+
...
|
813
|
+
end
|
814
|
+
|
815
|
+
...
|
816
|
+
end
|
817
|
+
```
|
818
|
+
|
696
819
|
|
697
820
|
### Column name & migration
|
698
821
|
|
@@ -761,8 +884,12 @@ Job.aasm.states_for_select
|
|
761
884
|
|
762
885
|
To use AASM with a RubyMotion project, use it with the [motion-bundler](https://github.com/archan937/motion-bundler) gem.
|
763
886
|
|
764
|
-
|
887
|
+
Warnings:
|
888
|
+
- Due to the way key-value observation (KVO) works in iOS,
|
765
889
|
you currently CANNOT use AASM with an object you are observing. (Yes.. that's pretty sad).
|
890
|
+
- Due to RubyMotion Proc's lack of 'source_location' method, it may be harder
|
891
|
+
to find out the origin of a "cannot transition from" error. I would recommend using
|
892
|
+
the 'instance method symbol / string' way whenever possible when defining guardians and callbacks.
|
766
893
|
|
767
894
|
|
768
895
|
### Testing
|
data/lib/aasm/aasm.rb
CHANGED
@@ -38,6 +38,12 @@ module AASM
|
|
38
38
|
|
39
39
|
AASM::StateMachine[self][state_machine_name] ||= AASM::StateMachine.new(state_machine_name)
|
40
40
|
|
41
|
+
# use a default despite the DSL configuration default.
|
42
|
+
# this is because configuration hasn't been setup for the AASM class but we are accessing a DSL option already for the class.
|
43
|
+
aasm_klass = options[:with_klass] || AASM::Base
|
44
|
+
|
45
|
+
raise ArgumentError, "The class #{aasm_klass} must inherit from AASM::Base!" unless aasm_klass.ancestors.include?(AASM::Base)
|
46
|
+
|
41
47
|
@aasm ||= {}
|
42
48
|
if @aasm[state_machine_name]
|
43
49
|
# make sure to use provided options
|
@@ -46,7 +52,7 @@ module AASM
|
|
46
52
|
end
|
47
53
|
else
|
48
54
|
# create a new base
|
49
|
-
@aasm[state_machine_name] =
|
55
|
+
@aasm[state_machine_name] = aasm_klass.new(
|
50
56
|
self,
|
51
57
|
state_machine_name,
|
52
58
|
AASM::StateMachine[self][state_machine_name],
|
data/lib/aasm/base.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
module AASM
|
2
2
|
class Base
|
3
3
|
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :klass,
|
5
|
+
:state_machine
|
5
6
|
|
6
7
|
def initialize(klass, name, state_machine, options={}, &block)
|
7
8
|
@klass = klass
|
8
9
|
@name = name
|
9
|
-
# @state_machine =
|
10
|
+
# @state_machine = klass.aasm(@name).state_machine
|
10
11
|
@state_machine = state_machine
|
11
12
|
@state_machine.config.column ||= (options[:column] || default_column).to_sym
|
12
13
|
# @state_machine.config.column = options[:column].to_sym if options[:column] # master
|
@@ -24,15 +25,23 @@ module AASM
|
|
24
25
|
# use requires_new for nested transactions (in ActiveRecord)
|
25
26
|
configure :requires_new_transaction, true
|
26
27
|
|
28
|
+
# use pessimistic locking (in ActiveRecord)
|
29
|
+
# true for FOR UPDATE lock
|
30
|
+
# string for a specific lock type i.e. FOR UPDATE NOWAIT
|
31
|
+
configure :requires_lock, false
|
32
|
+
|
27
33
|
# set to true to forbid direct assignment of aasm_state column (in ActiveRecord)
|
28
34
|
configure :no_direct_assignment, false
|
29
35
|
|
36
|
+
# allow a AASM::Base sub-class to be used for state machine
|
37
|
+
configure :with_klass, AASM::Base
|
38
|
+
|
30
39
|
configure :enum, nil
|
31
40
|
|
32
41
|
# make sure to raise an error if no_direct_assignment is enabled
|
33
42
|
# and attribute is directly assigned though
|
34
43
|
aasm_name = @name
|
35
|
-
|
44
|
+
klass.send :define_method, "#{@state_machine.config.column}=", ->(state_name) do
|
36
45
|
if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
|
37
46
|
raise AASM::NoDirectAssignmentError.new(
|
38
47
|
'direct assignment of AASM column has been disabled (see AASM configuration for this class)'
|
@@ -63,19 +72,19 @@ module AASM
|
|
63
72
|
|
64
73
|
# define a state
|
65
74
|
def state(name, options={})
|
66
|
-
@state_machine.add_state(name,
|
75
|
+
@state_machine.add_state(name, klass, options)
|
67
76
|
|
68
|
-
if
|
69
|
-
warn "#{
|
77
|
+
if klass.instance_methods.include?("#{name}?")
|
78
|
+
warn "#{klass.name}: The aasm state name #{name} is already used!"
|
70
79
|
end
|
71
80
|
|
72
81
|
aasm_name = @name
|
73
|
-
|
82
|
+
klass.send :define_method, "#{name}?", ->() do
|
74
83
|
aasm(:"#{aasm_name}").current_state == :"#{name}"
|
75
84
|
end
|
76
85
|
|
77
|
-
unless
|
78
|
-
|
86
|
+
unless klass.const_defined?("STATE_#{name.upcase}")
|
87
|
+
klass.const_set("STATE_#{name.upcase}", name)
|
79
88
|
end
|
80
89
|
end
|
81
90
|
|
@@ -83,8 +92,8 @@ module AASM
|
|
83
92
|
def event(name, options={}, &block)
|
84
93
|
@state_machine.add_event(name, options, &block)
|
85
94
|
|
86
|
-
if
|
87
|
-
warn "#{
|
95
|
+
if klass.instance_methods.include?("may_#{name}?".to_sym)
|
96
|
+
warn "#{klass.name}: The aasm event name #{name} is already used!"
|
88
97
|
end
|
89
98
|
|
90
99
|
# an addition over standard aasm so that, before firing an event, you can ask
|
@@ -92,16 +101,16 @@ module AASM
|
|
92
101
|
# on the transition will let this happen.
|
93
102
|
aasm_name = @name
|
94
103
|
|
95
|
-
|
104
|
+
klass.send :define_method, "may_#{name}?", ->(*args) do
|
96
105
|
aasm(:"#{aasm_name}").may_fire_event?(:"#{name}", *args)
|
97
106
|
end
|
98
107
|
|
99
|
-
|
108
|
+
klass.send :define_method, "#{name}!", ->(*args, &block) do
|
100
109
|
aasm(:"#{aasm_name}").current_event = :"#{name}!"
|
101
110
|
aasm_fire_event(:"#{aasm_name}", :"#{name}", {:persist => true}, *args, &block)
|
102
111
|
end
|
103
112
|
|
104
|
-
|
113
|
+
klass.send :define_method, "#{name}", ->(*args, &block) do
|
105
114
|
aasm(:"#{aasm_name}").current_event = :"#{name}"
|
106
115
|
aasm_fire_event(:"#{aasm_name}", :"#{name}", {:persist => false}, *args, &block)
|
107
116
|
end
|
@@ -145,7 +154,7 @@ module AASM
|
|
145
154
|
|
146
155
|
# aasm.event(:event_name).human?
|
147
156
|
def human_event_name(event) # event_name?
|
148
|
-
AASM::Localizer.new.human_event_name(
|
157
|
+
AASM::Localizer.new.human_event_name(klass, event)
|
149
158
|
end
|
150
159
|
|
151
160
|
def states_for_select
|
data/lib/aasm/configuration.rb
CHANGED
@@ -15,9 +15,15 @@ module AASM
|
|
15
15
|
# for ActiveRecord: use requires_new for nested transactions?
|
16
16
|
attr_accessor :requires_new_transaction
|
17
17
|
|
18
|
+
# for ActiveRecord: use pessimistic locking
|
19
|
+
attr_accessor :requires_lock
|
20
|
+
|
18
21
|
# forbid direct assignment in aasm_state column (in ActiveRecord)
|
19
22
|
attr_accessor :no_direct_assignment
|
20
23
|
|
24
|
+
# allow a AASM::Base sub-class to be used for state machine
|
25
|
+
attr_accessor :with_klass
|
26
|
+
|
21
27
|
attr_accessor :enum
|
22
28
|
end
|
23
|
-
end
|
29
|
+
end
|
data/lib/aasm/core/transition.rb
CHANGED
@@ -53,13 +53,39 @@ module AASM::Core
|
|
53
53
|
|
54
54
|
case code
|
55
55
|
when Symbol, String
|
56
|
-
|
57
|
-
result = (arity == 0 ? record.__send__(code) : result = record.__send__(code, *args))
|
56
|
+
result = (record.__send__(:method, code.to_sym).arity == 0 ? record.__send__(code) : result = record.__send__(code, *args))
|
58
57
|
failures << code unless result
|
59
58
|
result
|
60
59
|
when Proc
|
61
|
-
|
62
|
-
|
60
|
+
if code.respond_to?(:parameters)
|
61
|
+
# In Ruby's Proc, the 'arity' method is not a good condidate to know if
|
62
|
+
# we should pass the arguments or not, since its does return 0 even in
|
63
|
+
# presence of optional parameters.
|
64
|
+
result = (code.parameters.size == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code))
|
65
|
+
|
66
|
+
failures << code.source_location.join('#') unless result
|
67
|
+
else
|
68
|
+
# In RubyMotion's Proc, the 'parameter' method does not exists, however its
|
69
|
+
# 'arity' method works just like the one from Method, only returning 0 when
|
70
|
+
# there is no parameters whatsoever, optional or not.
|
71
|
+
result = (code.arity == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code))
|
72
|
+
|
73
|
+
# Sadly, RubyMotion's Proc does not define the method 'source_location' either.
|
74
|
+
failures << code unless result
|
75
|
+
end
|
76
|
+
|
77
|
+
result
|
78
|
+
when Class
|
79
|
+
arity = code.instance_method(:initialize).arity
|
80
|
+
if arity == 0
|
81
|
+
instance = code.new
|
82
|
+
elsif arity == 1
|
83
|
+
instance = code.new(record)
|
84
|
+
else
|
85
|
+
instance = code.new(record, *args)
|
86
|
+
end
|
87
|
+
result = instance.call
|
88
|
+
failures << instance.method(:call).source_location.join('#') unless result
|
63
89
|
result
|
64
90
|
when Array
|
65
91
|
if options[:guard]
|
@@ -165,7 +165,15 @@ module AASM
|
|
165
165
|
end
|
166
166
|
|
167
167
|
begin
|
168
|
-
success = options[:persist]
|
168
|
+
success = if options[:persist]
|
169
|
+
self.class.transaction(:requires_new => requires_new?(state_machine_name)) do
|
170
|
+
lock!(requires_lock?(state_machine_name)) if requires_lock?(state_machine_name)
|
171
|
+
super
|
172
|
+
end
|
173
|
+
else
|
174
|
+
super
|
175
|
+
end
|
176
|
+
|
169
177
|
if options[:persist] && success
|
170
178
|
event.fire_callbacks(:after_commit, self, *args)
|
171
179
|
event.fire_global_callbacks(:after_all_commits, self, *args)
|
@@ -184,6 +192,10 @@ module AASM
|
|
184
192
|
AASM::StateMachine[self.class][state_machine_name].config.requires_new_transaction
|
185
193
|
end
|
186
194
|
|
195
|
+
def requires_lock?(state_machine_name)
|
196
|
+
AASM::StateMachine[self.class][state_machine_name].config.requires_lock
|
197
|
+
end
|
198
|
+
|
187
199
|
def aasm_validate_states
|
188
200
|
AASM::StateMachine[self.class].keys.each do |state_machine_name|
|
189
201
|
unless aasm_skipping_validations(state_machine_name)
|
@@ -82,7 +82,7 @@ module AASM
|
|
82
82
|
|
83
83
|
def create_for_active_record(name)
|
84
84
|
conditions = {
|
85
|
-
|
85
|
+
@klass.table_name => { @klass.aasm(@name).attribute_name => name.to_s }
|
86
86
|
}
|
87
87
|
if ActiveRecord::VERSION::MAJOR >= 3
|
88
88
|
@klass.class_eval do
|
data/lib/aasm/version.rb
CHANGED
@@ -3,8 +3,7 @@ require 'rails/generators/named_base'
|
|
3
3
|
module AASM
|
4
4
|
module Generators
|
5
5
|
class AASMGenerator < Rails::Generators::NamedBase
|
6
|
-
|
7
|
-
source_root File.expand_path("../templates", __FILE__)
|
6
|
+
namespace "aasm"
|
8
7
|
argument :column_name, type: :string, default: 'aasm_state'
|
9
8
|
|
10
9
|
desc "Generates a model with the given NAME (if one does not exist) with aasm " <<
|
@@ -5,7 +5,7 @@ module ActiveRecord
|
|
5
5
|
module Generators
|
6
6
|
class AASMGenerator < ActiveRecord::Generators::Base
|
7
7
|
include AASM::Generators::OrmHelpers
|
8
|
-
|
8
|
+
namespace "active_record:aasm"
|
9
9
|
argument :column_name, type: :string, default: 'aasm_state'
|
10
10
|
|
11
11
|
source_root File.expand_path("../templates", __FILE__)
|
@@ -16,11 +16,11 @@ module ActiveRecord
|
|
16
16
|
else
|
17
17
|
migration_template "migration.rb", "db/migrate/aasm_create_#{table_name}.rb"
|
18
18
|
end
|
19
|
-
end
|
19
|
+
end
|
20
20
|
|
21
21
|
def generate_model
|
22
22
|
invoke "active_record:model", [name], migration: false unless model_exists?
|
23
|
-
end
|
23
|
+
end
|
24
24
|
|
25
25
|
def inject_aasm_content
|
26
26
|
content = model_contents
|
@@ -5,20 +5,20 @@ module Mongoid
|
|
5
5
|
module Generators
|
6
6
|
class AASMGenerator < Rails::Generators::NamedBase
|
7
7
|
include AASM::Generators::OrmHelpers
|
8
|
-
|
8
|
+
namespace "mongoid:aasm"
|
9
9
|
argument :column_name, type: :string, default: 'aasm_state'
|
10
10
|
|
11
11
|
def generate_model
|
12
12
|
invoke "mongoid:model", [name] unless model_exists?
|
13
|
-
end
|
13
|
+
end
|
14
14
|
|
15
15
|
def inject_aasm_content
|
16
16
|
inject_into_file model_path, model_contents, after: "include Mongoid::Document\n" if model_exists?
|
17
|
-
end
|
17
|
+
end
|
18
18
|
|
19
19
|
def inject_field_types
|
20
20
|
inject_into_file model_path, migration_data, after: "include Mongoid::Document\n" if model_exists?
|
21
|
-
end
|
21
|
+
end
|
22
22
|
|
23
23
|
def migration_data
|
24
24
|
" field :#{column_name}"
|
data/spec/database.rb
CHANGED
@@ -17,24 +17,19 @@ ActiveRecord::Migration.suppress_messages do
|
|
17
17
|
t.string "right"
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
t.string "name"
|
26
|
-
t.string "status"
|
20
|
+
%w(validators multiple_validators).each do |table_name|
|
21
|
+
ActiveRecord::Migration.create_table table_name, :force => true do |t|
|
22
|
+
t.string "name"
|
23
|
+
t.string "status"
|
24
|
+
end
|
27
25
|
end
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
t.string "name"
|
36
|
-
t.string "status"
|
37
|
-
t.integer "worker_id"
|
27
|
+
%w(transactors no_lock_transactors lock_transactors lock_no_wait_transactors multiple_transactors).each do |table_name|
|
28
|
+
ActiveRecord::Migration.create_table table_name, :force => true do |t|
|
29
|
+
t.string "name"
|
30
|
+
t.string "status"
|
31
|
+
t.integer "worker_id"
|
32
|
+
end
|
38
33
|
end
|
39
34
|
|
40
35
|
ActiveRecord::Migration.create_table "workers", :force => true do |t|
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class CustomAASMBase < AASM::Base
|
2
|
+
# A custom transiton that we want available across many AASM models.
|
3
|
+
def count_transitions!
|
4
|
+
klass.class_eval do
|
5
|
+
aasm :with_klass => CustomAASMBase do
|
6
|
+
after_all_transitions :increment_transition_count
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# A custom annotation that we want available across many AASM models.
|
12
|
+
def requires_guards!
|
13
|
+
klass.class_eval do
|
14
|
+
attr_reader :authorizable_called,
|
15
|
+
:transition_count,
|
16
|
+
:fillable_called
|
17
|
+
|
18
|
+
def authorizable?
|
19
|
+
@authorizable_called = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def fillable?
|
23
|
+
@fillable_called = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def increment_transition_count
|
27
|
+
@transition_count ||= 0
|
28
|
+
@transition_count += 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class SimpleCustomExample
|
35
|
+
include AASM
|
36
|
+
|
37
|
+
# Let's build an AASM state machine with our custom class.
|
38
|
+
aasm :with_klass => CustomAASMBase do
|
39
|
+
requires_guards!
|
40
|
+
count_transitions!
|
41
|
+
|
42
|
+
state :initialised, :initial => true
|
43
|
+
state :filled_out
|
44
|
+
state :authorised
|
45
|
+
|
46
|
+
event :fill_out do
|
47
|
+
transitions :from => :initialised, :to => :filled_out, :guard => :fillable?
|
48
|
+
end
|
49
|
+
event :authorise do
|
50
|
+
transitions :from => :filled_out, :to => :authorised, :guard => :authorizable?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/spec/models/transactor.rb
CHANGED
@@ -26,6 +26,54 @@ private
|
|
26
26
|
|
27
27
|
end
|
28
28
|
|
29
|
+
class NoLockTransactor < ActiveRecord::Base
|
30
|
+
|
31
|
+
belongs_to :worker
|
32
|
+
|
33
|
+
include AASM
|
34
|
+
|
35
|
+
aasm :column => :status do
|
36
|
+
state :sleeping, :initial => true
|
37
|
+
state :running
|
38
|
+
|
39
|
+
event :run do
|
40
|
+
transitions :to => :running, :from => :sleeping
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class LockTransactor < ActiveRecord::Base
|
46
|
+
|
47
|
+
belongs_to :worker
|
48
|
+
|
49
|
+
include AASM
|
50
|
+
|
51
|
+
aasm :column => :status, requires_lock: true do
|
52
|
+
state :sleeping, :initial => true
|
53
|
+
state :running
|
54
|
+
|
55
|
+
event :run do
|
56
|
+
transitions :to => :running, :from => :sleeping
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class LockNoWaitTransactor < ActiveRecord::Base
|
62
|
+
|
63
|
+
belongs_to :worker
|
64
|
+
|
65
|
+
include AASM
|
66
|
+
|
67
|
+
aasm :column => :status, requires_lock: 'FOR UPDATE NOWAIT' do
|
68
|
+
state :sleeping, :initial => true
|
69
|
+
state :running
|
70
|
+
|
71
|
+
event :run do
|
72
|
+
transitions :to => :running, :from => :sleeping
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
29
77
|
class MultipleTransactor < ActiveRecord::Base
|
30
78
|
|
31
79
|
belongs_to :worker
|
@@ -425,6 +425,39 @@ describe 'transitions with persistence' do
|
|
425
425
|
expect(persistor).not_to be_sleeping
|
426
426
|
end
|
427
427
|
|
428
|
+
describe 'pessimistic locking' do
|
429
|
+
let(:worker) { Worker.create!(:name => 'worker', :status => 'sleeping') }
|
430
|
+
|
431
|
+
subject { transactor.run! }
|
432
|
+
|
433
|
+
context 'no lock' do
|
434
|
+
let(:transactor) { NoLockTransactor.create!(:name => 'no_lock_transactor', :worker => worker) }
|
435
|
+
|
436
|
+
it 'should not invoke lock!' do
|
437
|
+
expect(transactor).to_not receive(:lock!)
|
438
|
+
subject
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
context 'a default lock' do
|
443
|
+
let(:transactor) { LockTransactor.create!(:name => 'lock_transactor', :worker => worker) }
|
444
|
+
|
445
|
+
it 'should invoke lock! with true' do
|
446
|
+
expect(transactor).to receive(:lock!).with(true).and_call_original
|
447
|
+
subject
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
context 'a FOR UPDATE NOWAIT lock' do
|
452
|
+
let(:transactor) { LockNoWaitTransactor.create!(:name => 'lock_no_wait_transactor', :worker => worker) }
|
453
|
+
|
454
|
+
it 'should invoke lock! with FOR UPDATE NOWAIT' do
|
455
|
+
expect(transactor).to receive(:lock!).with('FOR UPDATE NOWAIT').and_call_original
|
456
|
+
subject
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
428
461
|
describe 'transactions' do
|
429
462
|
let(:worker) { Worker.create!(:name => 'worker', :status => 'sleeping') }
|
430
463
|
let(:transactor) { Transactor.create!(:name => 'transactor', :worker => worker) }
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Custom AASM::Base' do
|
4
|
+
context 'when aasm_with invoked with SimpleCustomExample' do
|
5
|
+
let(:simple_custom) { SimpleCustomExample.new }
|
6
|
+
|
7
|
+
subject do
|
8
|
+
simple_custom.fill_out!
|
9
|
+
simple_custom.authorise
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'has invoked authorizable?' do
|
13
|
+
expect { subject }.to change { simple_custom.authorizable_called }.from(nil).to(true)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'has invoked fillable?' do
|
17
|
+
expect { subject }.to change { simple_custom.fillable_called }.from(nil).to(true)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'has two transition counts' do
|
21
|
+
expect { subject }.to change { simple_custom.transition_count }.from(nil).to(2)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when aasm_with invoked with non AASM::Base' do
|
26
|
+
subject do
|
27
|
+
Class.new do
|
28
|
+
include AASM
|
29
|
+
|
30
|
+
aasm :with_klass => String do
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should raise an ArgumentError' do
|
36
|
+
expect { subject }.to raise_error(ArgumentError, 'The class String must inherit from AASM::Base!')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -300,3 +300,66 @@ describe AASM::Core::Transition, '- when executing the transition with an :after
|
|
300
300
|
end
|
301
301
|
|
302
302
|
end
|
303
|
+
|
304
|
+
describe AASM::Core::Transition, '- when executing the transition with a Class' do
|
305
|
+
let(:state_machine) { AASM::StateMachine.new(:name) }
|
306
|
+
let(:event) { AASM::Core::Event.new(:event, state_machine) }
|
307
|
+
|
308
|
+
class AfterTransitionClass
|
309
|
+
def initialize(record)
|
310
|
+
@record = record
|
311
|
+
end
|
312
|
+
|
313
|
+
def call
|
314
|
+
"from: #{@record.aasm.from_state} to: #{@record.aasm.to_state}"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
class AfterTransitionClassWithArgs
|
319
|
+
def initialize(record, args)
|
320
|
+
@record = record
|
321
|
+
@args = args
|
322
|
+
end
|
323
|
+
|
324
|
+
def call
|
325
|
+
"arg1: #{@args[:arg1]}, arg2: #{@args[:arg2]}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
class AfterTransitionClassWithoutArgs
|
330
|
+
def call
|
331
|
+
'success'
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
it 'passes the record to the initialize method on the class to give access to the from_state and to_state' do
|
336
|
+
opts = {:from => 'foo', :to => 'bar', :after => AfterTransitionClass}
|
337
|
+
transition = AASM::Core::Transition.new(event, opts)
|
338
|
+
obj = double('object', :aasm => AASM::InstanceBase.new('object'))
|
339
|
+
|
340
|
+
return_value = transition.execute(obj)
|
341
|
+
|
342
|
+
expect(return_value).to eq('from: foo to: bar')
|
343
|
+
end
|
344
|
+
|
345
|
+
it 'should pass args to the initialize method on the class if it accepts them' do
|
346
|
+
opts = {:from => 'foo', :to => 'bar', :after => AfterTransitionClassWithArgs}
|
347
|
+
st = AASM::Core::Transition.new(event, opts)
|
348
|
+
args = {:arg1 => '1', :arg2 => '2'}
|
349
|
+
obj = double('object', :aasm => 'aasm')
|
350
|
+
|
351
|
+
return_value = st.execute(obj, args)
|
352
|
+
|
353
|
+
expect(return_value).to eq('arg1: 1, arg2: 2')
|
354
|
+
end
|
355
|
+
|
356
|
+
it 'should NOT pass args if the call method of the class if it does NOT accept them' do
|
357
|
+
opts = {:from => 'foo', :to => 'bar', :after => AfterTransitionClassWithoutArgs}
|
358
|
+
st = AASM::Core::Transition.new(event, opts)
|
359
|
+
obj = double('object', :aasm => 'aasm')
|
360
|
+
|
361
|
+
return_value = st.execute(obj)
|
362
|
+
|
363
|
+
expect(return_value).to eq('success')
|
364
|
+
end
|
365
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aasm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Barron
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2016-02-
|
13
|
+
date: 2016-02-22 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rake
|
@@ -215,6 +215,7 @@ files:
|
|
215
215
|
- spec/models/sequel/sequel_multiple.rb
|
216
216
|
- spec/models/sequel/sequel_simple.rb
|
217
217
|
- spec/models/silencer.rb
|
218
|
+
- spec/models/simple_custom_example.rb
|
218
219
|
- spec/models/simple_example.rb
|
219
220
|
- spec/models/simple_multiple_example.rb
|
220
221
|
- spec/models/state_machine_with_failed_event.rb
|
@@ -260,6 +261,7 @@ files:
|
|
260
261
|
- spec/unit/readme_spec.rb
|
261
262
|
- spec/unit/reloading_spec.rb
|
262
263
|
- spec/unit/rspec_matcher_spec.rb
|
264
|
+
- spec/unit/simple_custom_example_spec.rb
|
263
265
|
- spec/unit/simple_example_spec.rb
|
264
266
|
- spec/unit/simple_multiple_example_spec.rb
|
265
267
|
- spec/unit/state_spec.rb
|
@@ -286,7 +288,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
286
288
|
version: '0'
|
287
289
|
requirements: []
|
288
290
|
rubyforge_project:
|
289
|
-
rubygems_version: 2.
|
291
|
+
rubygems_version: 2.2.2
|
290
292
|
signing_key:
|
291
293
|
specification_version: 4
|
292
294
|
summary: State machine mixin for Ruby objects
|
@@ -362,6 +364,7 @@ test_files:
|
|
362
364
|
- spec/models/sequel/sequel_multiple.rb
|
363
365
|
- spec/models/sequel/sequel_simple.rb
|
364
366
|
- spec/models/silencer.rb
|
367
|
+
- spec/models/simple_custom_example.rb
|
365
368
|
- spec/models/simple_example.rb
|
366
369
|
- spec/models/simple_multiple_example.rb
|
367
370
|
- spec/models/state_machine_with_failed_event.rb
|
@@ -407,6 +410,7 @@ test_files:
|
|
407
410
|
- spec/unit/readme_spec.rb
|
408
411
|
- spec/unit/reloading_spec.rb
|
409
412
|
- spec/unit/rspec_matcher_spec.rb
|
413
|
+
- spec/unit/simple_custom_example_spec.rb
|
410
414
|
- spec/unit/simple_example_spec.rb
|
411
415
|
- spec/unit/simple_multiple_example_spec.rb
|
412
416
|
- spec/unit/state_spec.rb
|