transitions 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +18 -1
- data/.travis.yml +5 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +7 -6
- data/LICENSE.txt +21 -0
- data/README.md +9 -4
- data/Rakefile +9 -4
- data/bin/console +7 -0
- data/lib/active_model/transitions.rb +19 -12
- data/lib/transitions.rb +15 -15
- data/lib/transitions/event.rb +19 -15
- data/lib/transitions/machine.rb +23 -22
- data/lib/transitions/presenter.rb +2 -6
- data/lib/transitions/state.rb +10 -7
- data/lib/transitions/state_transition.rb +37 -6
- data/lib/transitions/version.rb +1 -1
- data/transitions.gemspec +23 -24
- metadata +39 -45
- data/.ruby-gemset +0 -1
- data/MIT-LICENSE.txt +0 -21
- data/test/active_record/test_active_record.rb +0 -326
- data/test/active_record/test_active_record_scopes.rb +0 -64
- data/test/active_record/test_active_record_timestamps.rb +0 -132
- data/test/active_record/test_custom_select.rb +0 -33
- data/test/event/test_event.rb +0 -72
- data/test/event/test_event_arguments.rb +0 -29
- data/test/event/test_event_being_fired.rb +0 -26
- data/test/event/test_event_checks.rb +0 -33
- data/test/helper.rb +0 -18
- data/test/machine/machine_template.rb +0 -27
- data/test/machine/test_available_states_listing.rb +0 -24
- data/test/machine/test_fire_event_machine.rb +0 -29
- data/test/machine/test_machine.rb +0 -66
- data/test/state/test_state.rb +0 -71
- data/test/state/test_state_predicate_method.rb +0 -32
- data/test/state_transition/test_state_transition.rb +0 -45
- data/test/state_transition/test_state_transition_event_failed_callback.rb +0 -36
- data/test/state_transition/test_state_transition_event_fired_callback.rb +0 -44
- data/test/state_transition/test_state_transition_guard_check.rb +0 -66
- data/test/state_transition/test_state_transition_on_transition_callback.rb +0 -48
- data/test/state_transition/test_state_transition_success_callback.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e0eecbbaa7a8c858df9799e370497da5c55691a
|
4
|
+
data.tar.gz: 2aeaec6cb95f64080a9a2a03a19ac1d9885db568
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14bdc0e63e4cad7f5cf626022738509b61878964b1fd5c86eb8c87b6550ad45e8381af5d15333ca021a5c043edf521e8b4a2360d4d3f82d6587bc2537e756818
|
7
|
+
data.tar.gz: 8e5142a6a0448e8cb29b2b4bdd08cb8ac5bd8776ff9c2f16ae7bf900775cb4768fe70e7bbdf5d82f72ed0a006a95288350c3ba9910c4436d5702b585dbfe4c0e
|
data/.rubocop.yml
CHANGED
@@ -1,4 +1,21 @@
|
|
1
1
|
Metrics/LineLength:
|
2
2
|
Max: 120
|
3
|
-
|
3
|
+
Documentation:
|
4
4
|
Enabled: false
|
5
|
+
Lint/AssignmentInCondition:
|
6
|
+
Enabled: false
|
7
|
+
# For whatever reasons disabling the cops below via source comment does **not** work.
|
8
|
+
# This happens only for event.rb, disabling those cops in other files via comment works well.
|
9
|
+
Metrics/MethodLength:
|
10
|
+
Exclude:
|
11
|
+
- 'lib/transitions/event.rb'
|
12
|
+
Metrics/AbcSize:
|
13
|
+
Exclude:
|
14
|
+
- 'lib/transitions/event.rb'
|
15
|
+
Metrics/CyclomaticComplexity:
|
16
|
+
Exclude:
|
17
|
+
- 'lib/transitions/event.rb'
|
18
|
+
Style/ConditionalAssignment:
|
19
|
+
Exclude:
|
20
|
+
- 'lib/transitions/event.rb'
|
21
|
+
- 'lib/transitions/state.rb'
|
data/.travis.yml
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
sudo: false
|
2
2
|
cache: bundler
|
3
3
|
language: ruby
|
4
|
+
bundler_args: --without local_development
|
5
|
+
script:
|
6
|
+
- bundle exec rake
|
4
7
|
rvm:
|
5
8
|
- 2.1
|
6
9
|
- 2.2
|
7
10
|
- rbx-2
|
8
11
|
- jruby
|
9
12
|
matrix:
|
13
|
+
allow_failures:
|
14
|
+
- rvm: rbx-2
|
10
15
|
fast_finish: true
|
11
16
|
notifications:
|
12
17
|
email:
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
@@ -2,11 +2,12 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
|
6
|
-
gem 'activerecord-jdbcsqlite3-adapter', platforms: :jruby
|
7
|
-
|
8
|
-
platforms :ruby do
|
9
|
-
gem 'sqlite3'
|
5
|
+
group :local_development do
|
10
6
|
gem 'pry'
|
11
|
-
gem '
|
7
|
+
gem 'sqlite3'
|
8
|
+
|
9
|
+
platforms :mri do
|
10
|
+
gem 'pry-byebug'
|
11
|
+
gem 'pry-stack_explorer'
|
12
|
+
end
|
12
13
|
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Timo Rößner
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -57,7 +57,7 @@ class Product
|
|
57
57
|
end
|
58
58
|
```
|
59
59
|
In this example we assume that you are in a rails project using Bundler, which
|
60
|
-
would automatically require `transitions`. If this is not the case
|
60
|
+
would automatically require `transitions`. If this is not the case then you
|
61
61
|
have to add
|
62
62
|
```ruby
|
63
63
|
require 'transitions'
|
@@ -69,7 +69,7 @@ wherever you load your dependencies in your application.
|
|
69
69
|
|
70
70
|
* You can only use one state machine per model. While in theory you can
|
71
71
|
define two or more, this won't work as you would expect. Not supporting
|
72
|
-
this was intentional, if you're interested in the
|
72
|
+
this was intentional, if you're interested in the rational look up version
|
73
73
|
0.1.0 in the CHANGELOG.
|
74
74
|
|
75
75
|
* Use symbols, not strings for declaring the state machine. Using strings is
|
@@ -106,13 +106,13 @@ the bang(!)-version will call `save!`. The `can_discontinue?` method will not
|
|
106
106
|
modify state but instead returns a boolean letting you know if a given
|
107
107
|
transition is possible.
|
108
108
|
|
109
|
-
In addition, a `can_transition?` method is added to the object that expects one or more event names as arguments. This semi-verbose method name is used to avoid
|
109
|
+
In addition, a `can_transition?` method is added to the object that expects one or more event names as arguments. This semi-verbose method name is used to avoid collisions with [https://github.com/ryanb/cancan](the authorization gem CanCan).
|
110
110
|
```ruby
|
111
111
|
>> Product.new.can_transition? :out_of_stock
|
112
112
|
=> true
|
113
113
|
```
|
114
114
|
|
115
|
-
If you need to get all available transitions for current state you can simply call:
|
115
|
+
If you need to get all available transitions for the current state you can simply call:
|
116
116
|
```ruby
|
117
117
|
>> Product.new.available_transitions
|
118
118
|
=> [:discontinued, :out_of_stock]
|
@@ -289,6 +289,11 @@ end
|
|
289
289
|
Any arguments passed to the event method will be passed on to the `guard`
|
290
290
|
predicate.
|
291
291
|
|
292
|
+
Note that guards will **not** raise on failure on their own. This means that if you want to
|
293
|
+
treat the failure of a guard exceptional you'll need to raise an exception yourself explicitly
|
294
|
+
in that guard (see [here](https://github.com/troessner/transitions/issues/149) for the
|
295
|
+
corresponding discussion).
|
296
|
+
|
292
297
|
|
293
298
|
#### Timestamps
|
294
299
|
|
data/Rakefile
CHANGED
@@ -1,14 +1,19 @@
|
|
1
|
-
require 'bundler'
|
2
|
-
Bundler::GemHelper.install_tasks
|
3
|
-
Bundler.setup
|
1
|
+
require 'bundler/gem_tasks'
|
4
2
|
|
5
3
|
require 'appraisal'
|
6
|
-
|
7
4
|
require 'rake/testtask'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
|
8
7
|
Rake::TestTask.new(:test) do |test|
|
9
8
|
test.libs = %w(lib test)
|
10
9
|
test.pattern = 'test/**/test_*.rb'
|
11
10
|
test.verbose = true
|
12
11
|
end
|
13
12
|
|
13
|
+
RuboCop::RakeTask.new do |task|
|
14
|
+
task.options << '--display-cop-names'
|
15
|
+
task.patterns = ['lib/**/*.rb']
|
16
|
+
end
|
17
|
+
|
14
18
|
task default: :test
|
19
|
+
task default: :rubocop
|
data/bin/console
ADDED
@@ -3,7 +3,7 @@ module ActiveModel
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
|
6
|
+
::Transitions::Machine.class_eval do
|
7
7
|
unless method_defined?(:new_transitions_initialize) || method_defined?(:new_transitions_update)
|
8
8
|
attr_reader :attribute_name
|
9
9
|
alias_method :old_transitions_initialize, :initialize
|
@@ -64,28 +64,35 @@ module ActiveModel
|
|
64
64
|
self[transitions_state_column_name] && self[transitions_state_column_name].to_sym
|
65
65
|
end
|
66
66
|
|
67
|
+
#
|
68
|
+
# rubocop:disable Metrics/AbcSize
|
69
|
+
#
|
67
70
|
def set_initial_state
|
68
|
-
# In case we use a query with a custom select that excludes our state attribute
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
71
|
+
# In case we use a query with a custom select that excludes our state attribute
|
72
|
+
# name we need to skip the initialization below.
|
73
|
+
return unless attribute_names.include?(transitions_state_column_name.to_s) && state_not_set?
|
74
|
+
self[transitions_state_column_name] = self.class.get_state_machine.initial_state.to_s
|
75
|
+
self.class.get_state_machine.state_index[self[transitions_state_column_name].to_sym].call_action(:enter, self)
|
73
76
|
end
|
74
77
|
|
75
78
|
def state_presence
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
+
return if self[transitions_state_column_name].present?
|
80
|
+
errors.add(transitions_state_column_name, :presence)
|
79
81
|
end
|
80
82
|
|
81
83
|
def state_inclusion
|
82
|
-
|
83
|
-
|
84
|
-
end
|
84
|
+
return if state_included?
|
85
|
+
errors.add(transitions_state_column_name, :inclusion, value: self[transitions_state_column_name])
|
85
86
|
end
|
86
87
|
|
87
88
|
def state_not_set?
|
88
89
|
self[transitions_state_column_name].nil?
|
89
90
|
end
|
91
|
+
|
92
|
+
def state_included?
|
93
|
+
self.class.get_state_machine.states
|
94
|
+
.map { |s| s.name.to_s }
|
95
|
+
.include?(self[transitions_state_column_name].to_s)
|
96
|
+
end
|
90
97
|
end
|
91
98
|
end
|
data/lib/transitions.rb
CHANGED
@@ -6,11 +6,13 @@ require 'transitions/state_transition'
|
|
6
6
|
require 'transitions/version'
|
7
7
|
|
8
8
|
module Transitions
|
9
|
-
class InvalidTransition
|
9
|
+
class InvalidTransition < StandardError; end
|
10
10
|
class InvalidMethodOverride < StandardError; end
|
11
11
|
include Presenter
|
12
12
|
|
13
13
|
module ClassMethods
|
14
|
+
include Presenter
|
15
|
+
|
14
16
|
def inherited(klass)
|
15
17
|
super # Make sure we call other callbacks possibly defined upstream the ancestor chain.
|
16
18
|
klass.state_machine = state_machine
|
@@ -26,37 +28,34 @@ module Transitions
|
|
26
28
|
block ? @state_machine.update(options, &block) : @state_machine
|
27
29
|
end
|
28
30
|
|
31
|
+
# rubocop:disable Style/AccessorMethodName
|
29
32
|
def get_state_machine
|
30
33
|
@state_machine
|
31
34
|
end
|
32
|
-
|
33
|
-
def available_states
|
34
|
-
@state_machine.states.map(&:name).sort_by(&:to_s)
|
35
|
-
end
|
36
|
-
|
37
|
-
def available_events
|
38
|
-
@state_machine.events.keys.sort
|
39
|
-
end
|
40
35
|
end
|
41
36
|
|
42
37
|
def self.included(base)
|
43
38
|
base.extend(ClassMethods)
|
44
39
|
end
|
45
40
|
|
41
|
+
def get_state_machine
|
42
|
+
self.class.get_state_machine
|
43
|
+
end
|
44
|
+
|
46
45
|
def update_current_state(new_state, persist = false)
|
47
|
-
|
48
|
-
ivar = sm.current_state_variable
|
46
|
+
ivar = get_state_machine.current_state_variable
|
49
47
|
|
50
48
|
if Transitions.active_model_descendant?(self.class)
|
51
49
|
write_state(new_state) if persist
|
52
|
-
|
50
|
+
# TODO: This seems like a duplicate, `write_new` already calls `write_state_without_persistence`.
|
51
|
+
write_state_without_persistence(new_state)
|
53
52
|
end
|
54
53
|
|
55
54
|
instance_variable_set(ivar, new_state)
|
56
55
|
end
|
57
56
|
|
58
57
|
def available_transitions
|
59
|
-
|
58
|
+
get_state_machine.events_for(current_state)
|
60
59
|
end
|
61
60
|
|
62
61
|
def can_transition?(*events)
|
@@ -70,7 +69,7 @@ module Transitions
|
|
70
69
|
end
|
71
70
|
|
72
71
|
def current_state
|
73
|
-
sm =
|
72
|
+
sm = get_state_machine
|
74
73
|
ivar = sm.current_state_variable
|
75
74
|
|
76
75
|
value = instance_variable_get(ivar)
|
@@ -84,6 +83,7 @@ module Transitions
|
|
84
83
|
end
|
85
84
|
|
86
85
|
def self.active_model_descendant?(klazz)
|
87
|
-
|
86
|
+
# Checking directly for "ActiveModel" wouldn't work so we use some arbitrary module close to it.
|
87
|
+
defined?(ActiveModel) && klazz.included_modules.include?(ActiveModel::Dirty)
|
88
88
|
end
|
89
89
|
end
|
data/lib/transitions/event.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
module Transitions
|
2
|
+
# rubocop:disable Metrics/ClassLength
|
2
3
|
class Event
|
3
4
|
attr_reader :name, :success, :timestamp
|
4
5
|
|
5
6
|
def initialize(machine, name, options = {}, &block)
|
6
|
-
@machine
|
7
|
+
@machine = machine
|
8
|
+
@name = name
|
9
|
+
@transitions = []
|
7
10
|
if machine
|
8
11
|
machine.klass.send(:define_method, "#{name}!") do |*args|
|
9
12
|
machine.fire_event(name, self, true, *args)
|
@@ -34,12 +37,12 @@ module Transitions
|
|
34
37
|
next_state = nil
|
35
38
|
transitions.each do |transition|
|
36
39
|
next if to_state && !Array(transition.to).include?(to_state)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
next unless transition.executable?(obj, *args)
|
41
|
+
|
42
|
+
next_state = to_state || Array(transition.to).first
|
43
|
+
transition.execute(obj, *args)
|
44
|
+
update_event_timestamp(obj, next_state) if timestamp_defined?
|
45
|
+
break
|
43
46
|
end
|
44
47
|
# Update timestamps on obj if a timestamp has been defined
|
45
48
|
next_state
|
@@ -53,11 +56,11 @@ module Transitions
|
|
53
56
|
@transitions.select { |t| t.from? state }.any? { |t| t.executable?(obj, *args) }
|
54
57
|
end
|
55
58
|
|
56
|
-
def ==(
|
57
|
-
if
|
58
|
-
name ==
|
59
|
+
def ==(other)
|
60
|
+
if other.is_a? Symbol
|
61
|
+
name == other
|
59
62
|
else
|
60
|
-
name ==
|
63
|
+
name == other.name
|
61
64
|
end
|
62
65
|
end
|
63
66
|
|
@@ -100,8 +103,8 @@ module Transitions
|
|
100
103
|
|
101
104
|
# If @timestamp is true, try a default timestamp name
|
102
105
|
def default_timestamp_name(obj, next_state)
|
103
|
-
at_name =
|
104
|
-
on_name =
|
106
|
+
at_name = "#{next_state}_at"
|
107
|
+
on_name = "#{next_state}_on"
|
105
108
|
case
|
106
109
|
when obj.respond_to?(at_name) then at_name
|
107
110
|
when obj.respond_to?(on_name) then on_name
|
@@ -128,12 +131,13 @@ module Transitions
|
|
128
131
|
when Proc
|
129
132
|
callback_names
|
130
133
|
when Symbol
|
131
|
-
|
134
|
+
->(record) { record.send(callback_names) }
|
132
135
|
end
|
133
136
|
end
|
134
137
|
|
135
138
|
def error_message_for_invalid_transitions(obj)
|
136
|
-
"Can't fire event `#{name}` in current state `#{obj.current_state}` for `#{obj.class.name}`
|
139
|
+
"Can't fire event `#{name}` in current state `#{obj.current_state}` for `#{obj.class.name}`"\
|
140
|
+
" #{obj.class < ActiveRecord::Base && obj.persisted? ? "with ID #{obj.id} " : nil}"
|
137
141
|
end
|
138
142
|
end
|
139
143
|
end
|
data/lib/transitions/machine.rb
CHANGED
@@ -5,7 +5,10 @@ module Transitions
|
|
5
5
|
attr_reader :klass, :auto_scopes
|
6
6
|
|
7
7
|
def initialize(klass, options = {}, &block)
|
8
|
-
@klass
|
8
|
+
@klass = klass
|
9
|
+
@states = []
|
10
|
+
@state_index = {}
|
11
|
+
@events = {}
|
9
12
|
update(options, &block)
|
10
13
|
end
|
11
14
|
|
@@ -21,7 +24,9 @@ module Transitions
|
|
21
24
|
self
|
22
25
|
end
|
23
26
|
|
24
|
-
#
|
27
|
+
#
|
28
|
+
# rubocop:disable Metrics/MethodLength
|
29
|
+
#
|
25
30
|
def fire_event(event, record, persist, *args)
|
26
31
|
handle_state_exit_callback record
|
27
32
|
if new_state = transition_to_new_state(record, event, *args)
|
@@ -35,12 +40,9 @@ module Transitions
|
|
35
40
|
return false
|
36
41
|
end
|
37
42
|
rescue => e
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
else
|
42
|
-
raise e
|
43
|
-
end
|
43
|
+
raise e unless record.respond_to?(:event_failed)
|
44
|
+
record.send(:event_failed, event)
|
45
|
+
return false
|
44
46
|
end
|
45
47
|
|
46
48
|
def events_for(state)
|
@@ -49,7 +51,7 @@ module Transitions
|
|
49
51
|
end
|
50
52
|
|
51
53
|
def current_state_variable
|
52
|
-
# TODO Refactor me away.
|
54
|
+
# TODO: Refactor me away.
|
53
55
|
:@current_state
|
54
56
|
end
|
55
57
|
|
@@ -68,9 +70,8 @@ module Transitions
|
|
68
70
|
end
|
69
71
|
|
70
72
|
def handle_event_fired_callback(record, new_state, event)
|
71
|
-
|
72
|
-
|
73
|
-
end
|
73
|
+
return unless record.respond_to?(:event_fired, true)
|
74
|
+
record.send(:event_fired, record.current_state, new_state, event)
|
74
75
|
end
|
75
76
|
|
76
77
|
def handle_event_success_callback(record, event)
|
@@ -78,18 +79,16 @@ module Transitions
|
|
78
79
|
end
|
79
80
|
|
80
81
|
def handle_event_failed_callback(record, event)
|
81
|
-
|
82
|
-
|
83
|
-
end
|
82
|
+
return unless record.respond_to?(:event_failed, true)
|
83
|
+
record.send(:event_failed, event)
|
84
84
|
end
|
85
85
|
|
86
86
|
def state(name, options = {})
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
87
|
+
return if @state_index.key?(name) # Just ignore duplicates
|
88
|
+
state = State.new(name, machine: self)
|
89
|
+
state.update options
|
90
|
+
@state_index[name] = state
|
91
|
+
@states << state
|
93
92
|
end
|
94
93
|
|
95
94
|
def event(name, options = {}, &block)
|
@@ -100,7 +99,9 @@ module Transitions
|
|
100
99
|
@states.each do |state|
|
101
100
|
state_name = state.name.to_s
|
102
101
|
if @klass.respond_to?(state_name)
|
103
|
-
fail InvalidMethodOverride,
|
102
|
+
fail InvalidMethodOverride,
|
103
|
+
"Transitions: Can not define scope `#{state_name}` because there is already"\
|
104
|
+
'an equally named method defined - either rename the existing method or the state.'
|
104
105
|
end
|
105
106
|
@klass.scope state_name, -> { @klass.where(@klass.state_machine.attribute_name => state_name) }
|
106
107
|
end
|