aasm 4.12.3 → 5.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +14 -12
- data/Appraisals +39 -16
- data/CHANGELOG.md +16 -3
- data/Dockerfile +44 -0
- data/Gemfile +1 -2
- data/README.md +41 -11
- data/docker-compose.yml +40 -0
- data/gemfiles/rails_3.2.gemfile +6 -6
- data/gemfiles/rails_4.0.gemfile +7 -7
- data/gemfiles/rails_4.2.gemfile +8 -8
- data/gemfiles/rails_4.2_mongoid_5.gemfile +5 -5
- data/gemfiles/rails_4.2_nobrainer.gemfile +9 -0
- data/gemfiles/rails_5.0.gemfile +5 -6
- data/gemfiles/rails_5.0_nobrainer.gemfile +9 -0
- data/gemfiles/rails_5.1.gemfile +13 -0
- data/lib/aasm.rb +5 -0
- data/lib/aasm/aasm.rb +1 -0
- data/lib/aasm/core/event.rb +6 -21
- data/lib/aasm/core/invoker.rb +129 -0
- data/lib/aasm/core/invokers/base_invoker.rb +75 -0
- data/lib/aasm/core/invokers/class_invoker.rb +52 -0
- data/lib/aasm/core/invokers/literal_invoker.rb +47 -0
- data/lib/aasm/core/invokers/proc_invoker.rb +59 -0
- data/lib/aasm/core/state.rb +10 -9
- data/lib/aasm/core/transition.rb +7 -68
- data/lib/aasm/errors.rb +2 -2
- data/lib/aasm/persistence.rb +3 -0
- data/lib/aasm/persistence/active_record_persistence.rb +5 -4
- data/lib/aasm/persistence/no_brainer_persistence.rb +105 -0
- data/lib/aasm/persistence/plain_persistence.rb +2 -1
- data/lib/aasm/persistence/sequel_persistence.rb +0 -1
- data/lib/aasm/rspec/allow_event.rb +5 -1
- data/lib/aasm/rspec/allow_transition_to.rb +5 -1
- data/lib/aasm/version.rb +1 -1
- data/lib/generators/nobrainer/aasm_generator.rb +28 -0
- data/lib/motion-aasm.rb +1 -0
- data/spec/database.rb +3 -0
- data/spec/generators/no_brainer_generator_spec.rb +29 -0
- data/spec/models/active_record/simple_new_dsl.rb +15 -0
- data/spec/models/nobrainer/complex_no_brainer_example.rb +36 -0
- data/spec/models/nobrainer/invalid_persistor_no_brainer.rb +39 -0
- data/spec/models/nobrainer/no_scope_no_brainer.rb +21 -0
- data/spec/models/nobrainer/nobrainer_relationships.rb +25 -0
- data/spec/models/nobrainer/silent_persistor_no_brainer.rb +39 -0
- data/spec/models/nobrainer/simple_new_dsl_nobrainer.rb +25 -0
- data/spec/models/nobrainer/simple_no_brainer.rb +23 -0
- data/spec/models/nobrainer/validator_no_brainer.rb +98 -0
- data/spec/models/simple_example.rb +2 -0
- data/spec/models/simple_example_with_guard_args.rb +17 -0
- data/spec/spec_helpers/active_record.rb +2 -1
- data/spec/spec_helpers/dynamoid.rb +7 -5
- data/spec/spec_helpers/mongoid.rb +20 -1
- data/spec/spec_helpers/nobrainer.rb +15 -0
- data/spec/spec_helpers/redis.rb +5 -2
- data/spec/spec_helpers/sequel.rb +1 -1
- data/spec/unit/callback_multiple_spec.rb +3 -3
- data/spec/unit/callbacks_spec.rb +2 -2
- data/spec/unit/exception_spec.rb +1 -1
- data/spec/unit/invoker_spec.rb +189 -0
- data/spec/unit/invokers/base_invoker_spec.rb +72 -0
- data/spec/unit/invokers/class_invoker_spec.rb +95 -0
- data/spec/unit/invokers/literal_invoker_spec.rb +86 -0
- data/spec/unit/invokers/proc_invoker_spec.rb +86 -0
- data/spec/unit/persistence/active_record_persistence_spec.rb +16 -0
- data/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +0 -4
- data/spec/unit/persistence/mongoid_persistence_spec.rb +0 -4
- data/spec/unit/persistence/no_brainer_persistence_multiple_spec.rb +198 -0
- data/spec/unit/persistence/no_brainer_persistence_spec.rb +158 -0
- data/spec/unit/rspec_matcher_spec.rb +6 -0
- data/spec/unit/state_spec.rb +2 -2
- data/spec/unit/transition_spec.rb +1 -1
- data/test/minitest_helper.rb +2 -2
- data/test/unit/minitest_matcher_test.rb +1 -1
- metadata +51 -3
data/gemfiles/rails_4.2.gemfile
CHANGED
@@ -2,15 +2,15 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "sqlite3", :
|
6
|
-
gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
|
5
|
+
gem "sqlite3", platforms: :ruby
|
7
6
|
gem "rails", "4.2.5"
|
8
|
-
gem "nokogiri", "1.6.8.1", :
|
9
|
-
gem "mime-types", "~> 2", :
|
10
|
-
gem "mongoid", "~>4.0"
|
7
|
+
gem "nokogiri", "1.6.8.1", platforms: [:ruby_19]
|
8
|
+
gem "mime-types", "~> 2", platforms: [:ruby_19, :jruby]
|
9
|
+
gem "mongoid", "~> 4.0"
|
11
10
|
gem "sequel"
|
12
|
-
gem "dynamoid", "~> 1", :
|
13
|
-
gem "aws-sdk", "~>2", :
|
11
|
+
gem "dynamoid", "~> 1", platforms: :ruby
|
12
|
+
gem "aws-sdk", "~> 2", platforms: :ruby
|
14
13
|
gem "redis-objects"
|
14
|
+
gem "activerecord-jdbcsqlite3-adapter", "1.3.24", platforms: :jruby
|
15
15
|
|
16
|
-
gemspec :
|
16
|
+
gemspec path: "../"
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "sqlite3", :
|
6
|
-
gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
|
5
|
+
gem "sqlite3", platforms: :ruby
|
7
6
|
gem "rails", "4.2.5"
|
8
|
-
gem "mime-types", "~> 2", :
|
9
|
-
gem "mongoid", "~>5.0"
|
7
|
+
gem "mime-types", "~> 2", platforms: [:ruby_19, :jruby]
|
8
|
+
gem "mongoid", "~> 5.0"
|
9
|
+
gem "activerecord-jdbcsqlite3-adapter", "1.3.24", platforms: :jruby
|
10
10
|
|
11
|
-
gemspec :
|
11
|
+
gemspec path: "../"
|
data/gemfiles/rails_5.0.gemfile
CHANGED
@@ -2,13 +2,12 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "sqlite3", :
|
6
|
-
gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
|
5
|
+
gem "sqlite3", platforms: :ruby
|
7
6
|
gem "rails", "5.0.0"
|
8
|
-
gem "mongoid", "~>6.0"
|
7
|
+
gem "mongoid", "~> 6.0"
|
9
8
|
gem "sequel"
|
10
|
-
gem "dynamoid", "~> 1", :
|
11
|
-
gem "aws-sdk", "~>2", :
|
9
|
+
gem "dynamoid", "~> 1", platforms: :ruby
|
10
|
+
gem "aws-sdk", "~> 2", platforms: :ruby
|
12
11
|
gem "redis-objects"
|
13
12
|
|
14
|
-
gemspec :
|
13
|
+
gemspec path: "../"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "sqlite3", platforms: :ruby
|
6
|
+
gem "rails", "5.1"
|
7
|
+
gem "mongoid", "~>6.0"
|
8
|
+
gem "sequel"
|
9
|
+
gem "dynamoid", "~> 1", platforms: :ruby
|
10
|
+
gem "aws-sdk", "~>2", platforms: :ruby
|
11
|
+
gem "redis-objects"
|
12
|
+
|
13
|
+
gemspec path: "../"
|
data/lib/aasm.rb
CHANGED
@@ -9,6 +9,11 @@ require 'aasm/instance_base'
|
|
9
9
|
require 'aasm/core/transition'
|
10
10
|
require 'aasm/core/event'
|
11
11
|
require 'aasm/core/state'
|
12
|
+
require 'aasm/core/invoker'
|
13
|
+
require 'aasm/core/invokers/base_invoker'
|
14
|
+
require 'aasm/core/invokers/class_invoker'
|
15
|
+
require 'aasm/core/invokers/literal_invoker'
|
16
|
+
require 'aasm/core/invokers/proc_invoker'
|
12
17
|
require 'aasm/localizer'
|
13
18
|
require 'aasm/state_machine_store'
|
14
19
|
require 'aasm/state_machine'
|
data/lib/aasm/aasm.rb
CHANGED
@@ -130,6 +130,7 @@ private
|
|
130
130
|
event.fire_callbacks(:error, self, e, *process_args(event, aasm(state_machine_name).current_state, *args)) ||
|
131
131
|
event.fire_global_callbacks(:error_on_all_events, self, e, *process_args(event, aasm(state_machine_name).current_state, *args)) ||
|
132
132
|
raise(e)
|
133
|
+
false
|
133
134
|
ensure
|
134
135
|
event.fire_callbacks(:ensure, self, *process_args(event, aasm(state_machine_name).current_state, *args))
|
135
136
|
event.fire_global_callbacks(:ensure_on_all_events, self, *process_args(event, aasm(state_machine_name).current_state, *args))
|
data/lib/aasm/core/event.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AASM::Core
|
2
4
|
class Event
|
3
5
|
include DslHelper
|
@@ -137,6 +139,7 @@ module AASM::Core
|
|
137
139
|
end
|
138
140
|
|
139
141
|
transitions.each do |transition|
|
142
|
+
transition.failures.clear #https://github.com/aasm/aasm/issues/383
|
140
143
|
next if to_state and !Array(transition.to).include?(to_state)
|
141
144
|
if (options.key?(:may_fire) && transition.eql?(options[:may_fire])) ||
|
142
145
|
(!options.key?(:may_fire) && transition.allowed?(obj, *args))
|
@@ -156,27 +159,9 @@ module AASM::Core
|
|
156
159
|
end
|
157
160
|
|
158
161
|
def invoke_callbacks(code, record, args)
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
raise NoMethodError.new("NoMethodError: undefined method `#{code}' for #{record.inspect}:#{record.class}")
|
163
|
-
end
|
164
|
-
arity = record.__send__(:method, code.to_sym).arity
|
165
|
-
record.__send__(code, *(arity < 0 ? args : args[0...arity]))
|
166
|
-
true
|
167
|
-
|
168
|
-
when Proc
|
169
|
-
arity = code.arity
|
170
|
-
record.instance_exec(*(arity < 0 ? args : args[0...arity]), &code)
|
171
|
-
true
|
172
|
-
|
173
|
-
when Array
|
174
|
-
code.each {|a| invoke_callbacks(a, record, args)}
|
175
|
-
true
|
176
|
-
|
177
|
-
else
|
178
|
-
false
|
179
|
-
end
|
162
|
+
Invoker.new(code, record, args)
|
163
|
+
.with_default_return_value(false)
|
164
|
+
.invoke
|
180
165
|
end
|
181
166
|
end
|
182
167
|
end # AASM
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AASM
|
4
|
+
module Core
|
5
|
+
##
|
6
|
+
# main invoker class which encapsulates the logic
|
7
|
+
# for invoking literal-based, proc-based, class-based
|
8
|
+
# and array-based callbacks for different entities.
|
9
|
+
class Invoker
|
10
|
+
DEFAULT_RETURN_VALUE = true
|
11
|
+
|
12
|
+
##
|
13
|
+
# Initialize a new invoker instance.
|
14
|
+
# NOTE that invoker must be used per-subject/record
|
15
|
+
# (one instance per subject/record)
|
16
|
+
#
|
17
|
+
# ==Options:
|
18
|
+
#
|
19
|
+
# +subject+ - invoking subject, may be Proc,
|
20
|
+
# Class, String, Symbol or Array
|
21
|
+
# +record+ - invoking record
|
22
|
+
# +args+ - arguments which will be passed to the callback
|
23
|
+
|
24
|
+
def initialize(subject, record, args)
|
25
|
+
@subject = subject
|
26
|
+
@record = record
|
27
|
+
@args = args
|
28
|
+
@options = {}
|
29
|
+
@failures = []
|
30
|
+
@default_return_value = DEFAULT_RETURN_VALUE
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Pass additional options to concrete invoker
|
35
|
+
#
|
36
|
+
# ==Options:
|
37
|
+
#
|
38
|
+
# +options+ - hash of options which will be passed to
|
39
|
+
# concrete invokers
|
40
|
+
#
|
41
|
+
# ==Example:
|
42
|
+
#
|
43
|
+
# with_options(guard: proc {...})
|
44
|
+
|
45
|
+
def with_options(options)
|
46
|
+
@options = options
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Collect failures to a specified buffer
|
52
|
+
#
|
53
|
+
# ==Options:
|
54
|
+
#
|
55
|
+
# +failures+ - failures buffer to collect failures
|
56
|
+
|
57
|
+
def with_failures(failures)
|
58
|
+
@failures = failures
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Change default return value of #invoke method
|
64
|
+
# if none of invokers processed the request.
|
65
|
+
#
|
66
|
+
# The default return value is #DEFAULT_RETURN_VALUE
|
67
|
+
#
|
68
|
+
# ==Options:
|
69
|
+
#
|
70
|
+
# +value+ - default return value for #invoke method
|
71
|
+
|
72
|
+
def with_default_return_value(value)
|
73
|
+
@default_return_value = value
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Find concrete invoker for specified subject and invoker it,
|
79
|
+
# or return default value set by #DEFAULT_RETURN_VALUE or
|
80
|
+
# overridden by #with_default_return_value
|
81
|
+
|
82
|
+
# rubocop:disable Metrics/AbcSize
|
83
|
+
def invoke
|
84
|
+
return invoke_array if subject.is_a?(Array)
|
85
|
+
return literal_invoker.invoke if literal_invoker.may_invoke?
|
86
|
+
return proc_invoker.invoke if proc_invoker.may_invoke?
|
87
|
+
return class_invoker.invoke if class_invoker.may_invoke?
|
88
|
+
default_return_value
|
89
|
+
end
|
90
|
+
# rubocop:enable Metrics/AbcSize
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
attr_reader :subject, :record, :args, :options, :failures,
|
95
|
+
:default_return_value
|
96
|
+
|
97
|
+
def invoke_array
|
98
|
+
return subject.all? { |item| sub_invoke(item) } if options[:guard]
|
99
|
+
return subject.all? { |item| !sub_invoke(item) } if options[:unless]
|
100
|
+
subject.map { |item| sub_invoke(item) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def sub_invoke(new_subject)
|
104
|
+
self.class.new(new_subject, record, args)
|
105
|
+
.with_failures(failures)
|
106
|
+
.with_options(options)
|
107
|
+
.invoke
|
108
|
+
end
|
109
|
+
|
110
|
+
def proc_invoker
|
111
|
+
@proc_invoker ||= Invokers::ProcInvoker
|
112
|
+
.new(subject, record, args)
|
113
|
+
.with_failures(failures)
|
114
|
+
end
|
115
|
+
|
116
|
+
def class_invoker
|
117
|
+
@class_invoker ||= Invokers::ClassInvoker
|
118
|
+
.new(subject, record, args)
|
119
|
+
.with_failures(failures)
|
120
|
+
end
|
121
|
+
|
122
|
+
def literal_invoker
|
123
|
+
@literal_invoker ||= Invokers::LiteralInvoker
|
124
|
+
.new(subject, record, args)
|
125
|
+
.with_failures(failures)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AASM
|
4
|
+
module Core
|
5
|
+
module Invokers
|
6
|
+
##
|
7
|
+
# Base concrete invoker class which contain basic
|
8
|
+
# invoking and logging definitions
|
9
|
+
class BaseInvoker
|
10
|
+
attr_reader :failures, :subject, :record, :args, :result
|
11
|
+
|
12
|
+
##
|
13
|
+
# Initialize a new concrete invoker instance.
|
14
|
+
# NOTE that concrete invoker must be used per-subject/record
|
15
|
+
# (one instance per subject/record)
|
16
|
+
#
|
17
|
+
# ==Options:
|
18
|
+
#
|
19
|
+
# +subject+ - invoking subject comparable with this invoker
|
20
|
+
# +record+ - invoking record
|
21
|
+
# +args+ - arguments which will be passed to the callback
|
22
|
+
|
23
|
+
def initialize(subject, record, args)
|
24
|
+
@subject = subject
|
25
|
+
@record = record
|
26
|
+
@args = args
|
27
|
+
@result = false
|
28
|
+
@failures = []
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Collect failures to a specified buffer
|
33
|
+
#
|
34
|
+
# ==Options:
|
35
|
+
#
|
36
|
+
# +failures+ - failures buffer to collect failures
|
37
|
+
|
38
|
+
def with_failures(failures_buffer)
|
39
|
+
@failures = failures_buffer
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Execute concrete invoker, log the error and return result
|
45
|
+
|
46
|
+
def invoke
|
47
|
+
return unless may_invoke?
|
48
|
+
log_failure unless invoke_subject
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Check if concrete invoker may be invoked for a specified subject
|
54
|
+
|
55
|
+
def may_invoke?
|
56
|
+
raise NoMethodError, '"#may_invoke?" is not implemented'
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Log failed invoking
|
61
|
+
|
62
|
+
def log_failure
|
63
|
+
raise NoMethodError, '"#log_failure" is not implemented'
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Execute concrete invoker
|
68
|
+
|
69
|
+
def invoke_subject
|
70
|
+
raise NoMethodError, '"#invoke_subject" is not implemented'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AASM
|
4
|
+
module Core
|
5
|
+
module Invokers
|
6
|
+
##
|
7
|
+
# Class invoker which allows to use classes which respond to #call
|
8
|
+
# to be used as state/event/transition callbacks.
|
9
|
+
class ClassInvoker < BaseInvoker
|
10
|
+
def may_invoke?
|
11
|
+
subject.is_a?(Class) && subject.instance_methods.include?(:call)
|
12
|
+
end
|
13
|
+
|
14
|
+
def log_failure
|
15
|
+
return log_source_location if Method.method_defined?(:source_location)
|
16
|
+
log_method_info
|
17
|
+
end
|
18
|
+
|
19
|
+
def invoke_subject
|
20
|
+
@result = retrieve_instance.call
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def log_source_location
|
26
|
+
failures << instance.method(:call).source_location.join('#')
|
27
|
+
end
|
28
|
+
|
29
|
+
def log_method_info
|
30
|
+
failures << instance.method(:call)
|
31
|
+
end
|
32
|
+
|
33
|
+
def instance
|
34
|
+
@instance ||= retrieve_instance
|
35
|
+
end
|
36
|
+
|
37
|
+
# rubocop:disable Metrics/AbcSize
|
38
|
+
def retrieve_instance
|
39
|
+
return subject.new if subject_arity.zero?
|
40
|
+
return subject.new(record) if subject_arity == 1
|
41
|
+
return subject.new(record, *args) if subject_arity < 0
|
42
|
+
subject.new(record, *args[0..(subject_arity - 2)])
|
43
|
+
end
|
44
|
+
# rubocop:enable Metrics/AbcSize
|
45
|
+
|
46
|
+
def subject_arity
|
47
|
+
@arity ||= subject.instance_method(:initialize).arity
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AASM
|
4
|
+
module Core
|
5
|
+
module Invokers
|
6
|
+
##
|
7
|
+
# Literal invoker which allows to use strings or symbols to call
|
8
|
+
# record methods as state/event/transition callbacks.
|
9
|
+
class LiteralInvoker < BaseInvoker
|
10
|
+
def may_invoke?
|
11
|
+
subject.is_a?(String) || subject.is_a?(Symbol)
|
12
|
+
end
|
13
|
+
|
14
|
+
def log_failure
|
15
|
+
failures << subject
|
16
|
+
end
|
17
|
+
|
18
|
+
def invoke_subject
|
19
|
+
@result = exec_subject
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def subject_arity
|
25
|
+
@arity ||= record.__send__(:method, subject.to_sym).arity
|
26
|
+
end
|
27
|
+
|
28
|
+
# rubocop:disable Metrics/AbcSize
|
29
|
+
def exec_subject
|
30
|
+
raise(*record_error) unless record.respond_to?(subject, true)
|
31
|
+
return record.__send__(subject) if subject_arity.zero?
|
32
|
+
return record.__send__(subject, *args) if subject_arity < 0
|
33
|
+
record.__send__(subject, *args[0..(subject_arity - 1)])
|
34
|
+
end
|
35
|
+
# rubocop:enable Metrics/AbcSize
|
36
|
+
|
37
|
+
def record_error
|
38
|
+
[
|
39
|
+
NoMethodError,
|
40
|
+
'NoMethodError: undefined method ' \
|
41
|
+
"`#{subject}' for #{record.inspect}:#{record.class}"
|
42
|
+
]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|