aasm 4.12.3 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +14 -12
  3. data/Appraisals +39 -16
  4. data/CHANGELOG.md +16 -3
  5. data/Dockerfile +44 -0
  6. data/Gemfile +1 -2
  7. data/README.md +41 -11
  8. data/docker-compose.yml +40 -0
  9. data/gemfiles/rails_3.2.gemfile +6 -6
  10. data/gemfiles/rails_4.0.gemfile +7 -7
  11. data/gemfiles/rails_4.2.gemfile +8 -8
  12. data/gemfiles/rails_4.2_mongoid_5.gemfile +5 -5
  13. data/gemfiles/rails_4.2_nobrainer.gemfile +9 -0
  14. data/gemfiles/rails_5.0.gemfile +5 -6
  15. data/gemfiles/rails_5.0_nobrainer.gemfile +9 -0
  16. data/gemfiles/rails_5.1.gemfile +13 -0
  17. data/lib/aasm.rb +5 -0
  18. data/lib/aasm/aasm.rb +1 -0
  19. data/lib/aasm/core/event.rb +6 -21
  20. data/lib/aasm/core/invoker.rb +129 -0
  21. data/lib/aasm/core/invokers/base_invoker.rb +75 -0
  22. data/lib/aasm/core/invokers/class_invoker.rb +52 -0
  23. data/lib/aasm/core/invokers/literal_invoker.rb +47 -0
  24. data/lib/aasm/core/invokers/proc_invoker.rb +59 -0
  25. data/lib/aasm/core/state.rb +10 -9
  26. data/lib/aasm/core/transition.rb +7 -68
  27. data/lib/aasm/errors.rb +2 -2
  28. data/lib/aasm/persistence.rb +3 -0
  29. data/lib/aasm/persistence/active_record_persistence.rb +5 -4
  30. data/lib/aasm/persistence/no_brainer_persistence.rb +105 -0
  31. data/lib/aasm/persistence/plain_persistence.rb +2 -1
  32. data/lib/aasm/persistence/sequel_persistence.rb +0 -1
  33. data/lib/aasm/rspec/allow_event.rb +5 -1
  34. data/lib/aasm/rspec/allow_transition_to.rb +5 -1
  35. data/lib/aasm/version.rb +1 -1
  36. data/lib/generators/nobrainer/aasm_generator.rb +28 -0
  37. data/lib/motion-aasm.rb +1 -0
  38. data/spec/database.rb +3 -0
  39. data/spec/generators/no_brainer_generator_spec.rb +29 -0
  40. data/spec/models/active_record/simple_new_dsl.rb +15 -0
  41. data/spec/models/nobrainer/complex_no_brainer_example.rb +36 -0
  42. data/spec/models/nobrainer/invalid_persistor_no_brainer.rb +39 -0
  43. data/spec/models/nobrainer/no_scope_no_brainer.rb +21 -0
  44. data/spec/models/nobrainer/nobrainer_relationships.rb +25 -0
  45. data/spec/models/nobrainer/silent_persistor_no_brainer.rb +39 -0
  46. data/spec/models/nobrainer/simple_new_dsl_nobrainer.rb +25 -0
  47. data/spec/models/nobrainer/simple_no_brainer.rb +23 -0
  48. data/spec/models/nobrainer/validator_no_brainer.rb +98 -0
  49. data/spec/models/simple_example.rb +2 -0
  50. data/spec/models/simple_example_with_guard_args.rb +17 -0
  51. data/spec/spec_helpers/active_record.rb +2 -1
  52. data/spec/spec_helpers/dynamoid.rb +7 -5
  53. data/spec/spec_helpers/mongoid.rb +20 -1
  54. data/spec/spec_helpers/nobrainer.rb +15 -0
  55. data/spec/spec_helpers/redis.rb +5 -2
  56. data/spec/spec_helpers/sequel.rb +1 -1
  57. data/spec/unit/callback_multiple_spec.rb +3 -3
  58. data/spec/unit/callbacks_spec.rb +2 -2
  59. data/spec/unit/exception_spec.rb +1 -1
  60. data/spec/unit/invoker_spec.rb +189 -0
  61. data/spec/unit/invokers/base_invoker_spec.rb +72 -0
  62. data/spec/unit/invokers/class_invoker_spec.rb +95 -0
  63. data/spec/unit/invokers/literal_invoker_spec.rb +86 -0
  64. data/spec/unit/invokers/proc_invoker_spec.rb +86 -0
  65. data/spec/unit/persistence/active_record_persistence_spec.rb +16 -0
  66. data/spec/unit/persistence/mongoid_persistence_multiple_spec.rb +0 -4
  67. data/spec/unit/persistence/mongoid_persistence_spec.rb +0 -4
  68. data/spec/unit/persistence/no_brainer_persistence_multiple_spec.rb +198 -0
  69. data/spec/unit/persistence/no_brainer_persistence_spec.rb +158 -0
  70. data/spec/unit/rspec_matcher_spec.rb +6 -0
  71. data/spec/unit/state_spec.rb +2 -2
  72. data/spec/unit/transition_spec.rb +1 -1
  73. data/test/minitest_helper.rb +2 -2
  74. data/test/unit/minitest_matcher_test.rb +1 -1
  75. metadata +51 -3
@@ -2,15 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => :ruby
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", :platforms => [:ruby_19]
9
- gem "mime-types", "~> 2", :platforms => [:ruby_19, :jruby]
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", :platforms => :ruby
13
- gem "aws-sdk", "~>2", :platforms => :ruby
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 :path => "../"
16
+ gemspec path: "../"
@@ -2,10 +2,10 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => :ruby
6
- gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby
5
+ gem "sqlite3", platforms: :ruby
7
6
  gem "rails", "4.2.5"
8
- gem "mime-types", "~> 2", :platforms => [:ruby_19, :jruby]
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 :path => "../"
11
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", platforms: :ruby
6
+ gem "rails", "4.2.5"
7
+ gem "nobrainer", "~> 0.33.0"
8
+
9
+ gemspec path: "../"
@@ -2,13 +2,12 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => :ruby
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", :platforms => :ruby
11
- gem "aws-sdk", "~>2", :platforms => :ruby
9
+ gem "dynamoid", "~> 1", platforms: :ruby
10
+ gem "aws-sdk", "~> 2", platforms: :ruby
12
11
  gem "redis-objects"
13
12
 
14
- gemspec :path => "../"
13
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", platforms: :ruby
6
+ gem "rails", "5.0.0"
7
+ gem "nobrainer", "~> 0.33.0"
8
+
9
+ 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))
@@ -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
- case code
160
- when Symbol, String
161
- unless record.respond_to?(code, true)
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