activeinteractor 0.1.7 → 1.0.0.beta.1

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -1
  3. data/README.md +397 -395
  4. data/lib/active_interactor.rb +12 -30
  5. data/lib/active_interactor/base.rb +18 -8
  6. data/lib/active_interactor/context.rb +4 -181
  7. data/lib/active_interactor/context/attributes.rb +37 -149
  8. data/lib/active_interactor/context/base.rb +141 -0
  9. data/lib/active_interactor/context/loader.rb +45 -0
  10. data/lib/active_interactor/error.rb +22 -15
  11. data/lib/active_interactor/interactor.rb +24 -57
  12. data/lib/active_interactor/interactor/callbacks.rb +64 -76
  13. data/lib/active_interactor/interactor/context.rb +97 -63
  14. data/lib/active_interactor/interactor/worker.rb +22 -65
  15. data/lib/active_interactor/organizer.rb +180 -164
  16. data/lib/active_interactor/version.rb +2 -3
  17. data/lib/rails/generators/active_interactor.rb +2 -37
  18. data/lib/rails/generators/active_interactor/application_interactor_generator.rb +23 -0
  19. data/lib/rails/generators/active_interactor/install_generator.rb +8 -12
  20. data/lib/rails/generators/active_interactor/templates/application_context.rb +4 -0
  21. data/lib/rails/generators/{templates/application_interactor.erb → active_interactor/templates/application_interactor.rb} +0 -0
  22. data/lib/rails/generators/active_interactor/templates/application_organizer.rb +4 -0
  23. data/lib/rails/generators/active_interactor/templates/initializer.erb +5 -0
  24. data/lib/rails/generators/interactor/context/rspec_generator.rb +19 -0
  25. data/lib/rails/generators/interactor/context/templates/rspec.erb +7 -0
  26. data/lib/rails/generators/interactor/context/templates/test_unit.erb +9 -0
  27. data/lib/rails/generators/interactor/context/test_unit_generator.rb +19 -0
  28. data/lib/rails/generators/interactor/context_generator.rb +19 -0
  29. data/lib/rails/generators/interactor/interactor_generator.rb +8 -3
  30. data/lib/rails/generators/interactor/organizer_generator.rb +8 -3
  31. data/lib/rails/generators/interactor/rspec_generator.rb +4 -3
  32. data/lib/rails/generators/interactor/templates/context.erb +4 -0
  33. data/lib/rails/generators/{templates → interactor/templates}/interactor.erb +0 -0
  34. data/lib/rails/generators/{templates → interactor/templates}/organizer.erb +1 -1
  35. data/lib/rails/generators/{templates → interactor/templates}/rspec.erb +0 -0
  36. data/lib/rails/generators/{templates → interactor/templates}/test_unit.erb +0 -0
  37. data/lib/rails/generators/interactor/test_unit_generator.rb +4 -3
  38. data/spec/active_interactor/base_spec.rb +51 -0
  39. data/spec/active_interactor/context/base_spec.rb +229 -0
  40. data/spec/active_interactor/error_spec.rb +43 -0
  41. data/spec/active_interactor/interactor/worker_spec.rb +89 -0
  42. data/spec/active_interactor/organizer_spec.rb +178 -0
  43. data/spec/active_interactor_spec.rb +26 -0
  44. data/spec/integration/basic_callback_integration_spec.rb +355 -0
  45. data/spec/integration/basic_context_integration_spec.rb +73 -0
  46. data/spec/integration/basic_integration_spec.rb +220 -0
  47. data/spec/integration/basic_validations_integration_spec.rb +204 -0
  48. data/spec/spec_helper.rb +44 -0
  49. data/spec/support/helpers/factories.rb +41 -0
  50. data/spec/support/shared_examples/a_class_with_interactor_callback_methods_example.rb +99 -0
  51. data/spec/support/shared_examples/a_class_with_interactor_context_methods_example.rb +58 -0
  52. data/spec/support/shared_examples/a_class_with_interactor_methods_example.rb +21 -0
  53. data/spec/support/shared_examples/a_class_with_organizer_callback_methods_example.rb +39 -0
  54. data/spec/support/spec_helpers.rb +7 -0
  55. metadata +68 -138
  56. data/lib/active_interactor/configuration.rb +0 -38
  57. data/lib/active_interactor/interactor/execution.rb +0 -24
  58. data/lib/rails/generators/templates/initializer.erb +0 -15
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_model'
3
+ require 'active_support/dependencies/autoload'
4
4
 
5
5
  require 'active_interactor/version'
6
6
 
@@ -23,46 +23,28 @@ require 'active_interactor/version'
23
23
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
24
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
25
  # THE SOFTWARE.
26
- #
27
- # @author Aaron Allen <hello@aaronmallen.me>
28
- # @since 0.0.1
29
- # @version 0.2
30
26
  module ActiveInteractor
31
27
  extend ActiveSupport::Autoload
32
28
 
33
29
  autoload :Base
34
- autoload :Configuration
35
30
  autoload :Context
36
- autoload :Interactor
37
31
  autoload :Organizer
38
32
 
39
33
  eager_autoload do
40
34
  autoload :Error
41
35
  end
42
36
 
43
- class << self
44
- # The ActiveInteractor configuration
45
- # @return [ActiveInteractor::Configuration] the configuration instance
46
- def configuration
47
- @configuration ||= Configuration.new
48
- end
49
-
50
- # Configures the ActiveInteractor gem
51
- #
52
- # @example Configure ActiveInteractor
53
- # ActiveInteractor.configure do |config|
54
- # config.logger = Rails.logger
55
- # end
56
- #
57
- # @yield [ActiveInteractor#configuration]
58
- def configure
59
- yield(configuration)
60
- end
37
+ # The ActiveInteractor logger object
38
+ # @return [Logger] an instance of Logger
39
+ def self.logger
40
+ @logger ||= Logger.new(STDOUT)
41
+ end
61
42
 
62
- # The ActiveInteractor logger object
63
- # @return [Logger] the configured logger instance
64
- def logger
65
- configuration.logger
66
- end
43
+ # Set the ActiveInteractor logger object
44
+ # @example
45
+ # ActiveInteractor.logger = ::Rails.logger
46
+ # @return [Logger] an instance of Logger
47
+ def self.logger=(logger)
48
+ @logger = logger
67
49
  end
68
50
  end
@@ -1,19 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_interactor/interactor'
4
+
3
5
  module ActiveInteractor
4
- # The Base Interactor class inherited by all interactors
5
- #
6
+ # The base interactor class. All interactors should inherit from
7
+ # {Base}.
6
8
  # @author Aaron Allen <hello@aaronmallen.me>
7
9
  # @since 0.0.1
8
- # @version 0.1
10
+ # @example a basic interactor
11
+ # class MyInteractor < ActiveInteractor::Base
12
+ # def perform
13
+ # context.called = true
14
+ # end
15
+ # end
16
+ #
17
+ # MyInteractor.perform
18
+ # #=> <MyInteractor::Context called=true>
9
19
  class Base
10
20
  include Interactor
11
21
 
12
- # A new instance of {Base}
13
- # @param context [Hash, nil] the properties of the context
14
- # @return [ActiveInteractor::Base] a new instance of {Base}
15
- def initialize(context = {})
16
- @context = self.class.context_class.new(self, context)
22
+ # Duplicates an instance of {Base}
23
+ # @since 1.0.0-alpha.1
24
+ # @return [Base] a new instance of {Base}
25
+ def dup
26
+ self.class.new(context.dup)
17
27
  end
18
28
  end
19
29
  end
@@ -1,190 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
4
-
5
- Dir[File.expand_path('context/*.rb', __dir__)].each { |file| require file }
6
-
7
3
  module ActiveInteractor
8
- # ActiveInteractor::Context module
9
- #
4
+ # ActiveInteractor::Context classes
10
5
  # @author Aaron Allen <hello@aaronmallen.me>
11
6
  # @since 0.0.1
12
- # @version 0.1
13
7
  module Context
14
- # The base context class inherited by all {Interactor::Context} classes
15
- #
16
- # @author Aaron Allen <hello@aaronmallen.me>
17
- # @since 0.0.1
18
- # @version 0.2
19
- class Base < OpenStruct
20
- include ActiveModel::Validations
21
- include Attributes
22
-
23
- # A new instance of {Base}
24
- # @param interactor [ActiveInteractor::Base] an interactor instance
25
- # @param attributes [Hash, nil] the attributes of the context
26
- # @return [ActiveInteractor::Context::Base] a new instance of {Base}
27
- def initialize(interactor, attributes = {})
28
- copy_flags!(attributes)
29
- @interactor = interactor
30
- super(attributes)
31
- end
32
-
33
- # Track that an Interactor has been called. The {#called!} method
34
- # is used by the interactor being invoked with this context. After an
35
- # interactor is successfully called, the interactor instance is tracked in
36
- # the context for the purpose of potential future rollback
37
- #
38
- # @return [Array<ActiveInteractor::Base>] all called interactors
39
- def called!
40
- _called << interactor
41
- end
42
-
43
- # Fail the context instance. Failing a context raises an error
44
- # that may be rescued by the calling interactor. The context is also flagged
45
- # as having failed
46
- #
47
- # @example Fail an interactor context
48
- # interactor = MyInteractor.new(name: 'Aaron')
49
- # #=> <#MyInteractor name='Aaron'>
50
- #
51
- # interactor.context.fail!
52
- # #=> ActiveInteractor::Error::ContextFailure: <#MyInteractor::Context name='Aaron'>
53
- #
54
- # @param errors [ActiveModel::Errors, Hash] errors to add to the context on failure
55
- # @see https://api.rubyonrails.org/classes/ActiveModel/Errors.html ActiveModel::Errors
56
- # @raise [Error::ContextFailure]
57
- def fail!(errors = {})
58
- merge_errors(errors)
59
- mark_failed!
60
- raise_context_failure!
61
- end
62
-
63
- # Whether the context instance has failed. By default, a new
64
- # context is successful and only changes when explicitly failed
65
- #
66
- # @note The {#failure?} method is the inverse of the {#success?} method
67
- #
68
- # @example Check if a context has failed
69
- # context = MyInteractor::Context.new
70
- # #=> <#MyInteractor::Context>
71
- #
72
- # context.failure?
73
- # false
74
- #
75
- # context.fail!
76
- # #=> ActiveInteractor::Error::ContextFailure: <#MyInteractor::Context>
77
- #
78
- # context.failure?
79
- # #=> true
80
- #
81
- # @return [Boolean] `false` by default or `true` if failed
82
- def failure?
83
- @_failed || false
84
- end
85
- alias fail? failure?
86
-
87
- # Attempt to call the interactor for missing validation callback methods
88
- # @raise [NameError] if the method is not a validation callback or method
89
- # does not exist on the interactor instance
90
- def method_missing(name, *args, &block)
91
- interactor.send(name, *args, &block) if validation_callback?(name)
92
- super
93
- end
94
-
95
- # Attempt to call the interactor for missing validation callback methods
96
- # @return [Boolean] `true` if method is a validation callback and exists
97
- # on the interactor instance
98
- def respond_to_missing?(name, include_private)
99
- return false unless validation_callback?(name)
100
-
101
- interactor.respond_to?(name, include_private)
102
- end
103
-
104
- # Roll back an interactor context. Any interactors to which this
105
- # context has been passed and which have been successfully called are asked
106
- # to roll themselves back by invoking their
107
- # {ActiveInteractor::Interactor#rollback #rollback} instance methods.
108
- #
109
- # @example Rollback an interactor's context
110
- # context = MyInteractor.perform(name: 'Aaron')
111
- # #=> <#MyInteractor::Context name='Aaron'>
112
- #
113
- # context.rollback!
114
- # #=> true
115
- #
116
- # context
117
- # #=> <#MyInteractor::Context name='Aaron'>
118
- #
119
- # @return [Boolean] `true` if rolled back successfully or `false` if already
120
- # rolled back
121
- def rollback!
122
- return false if @_rolled_back
123
-
124
- rollback_called
125
- mark_rolledback!
126
- end
127
-
128
- # Whether the context instance is successful. By default, a new
129
- # context is successful and only changes when explicitly failed
130
- #
131
- # @note the {#success?} method is the inverse of the {#failure?} method
132
- #
133
- # @example Check if a context has failed
134
- # context = MyInteractor::Context.new
135
- # #=> <#MyInteractor::Context>
136
- #
137
- # context.success?
138
- # true
139
- #
140
- # context.fail!
141
- # #=> ActiveInteractor::Error::ContextFailure: <#MyInteractor::Context>
142
- #
143
- # context.success?
144
- # #=> false
145
- #
146
- # @return [Boolean] `true` by default or `false` if failed
147
- def success?
148
- !failure?
149
- end
150
- alias successful? success?
151
-
152
- private
153
-
154
- attr_reader :interactor
155
-
156
- def copy_flags!(context)
157
- @_called = context.send(:_called) if context.respond_to?(:_called, true)
158
- @_failed = context.failure? if context.respond_to?(:failure?)
159
- end
160
-
161
- def _called
162
- @_called ||= []
163
- end
164
-
165
- def mark_failed!
166
- @_failed = true
167
- end
168
-
169
- def mark_rolledback!
170
- @_rolled_back = true
171
- end
172
-
173
- def merge_errors(errors)
174
- self.errors.merge!(errors) unless errors.empty?
175
- end
176
-
177
- def raise_context_failure!
178
- raise Error::ContextFailure, self
179
- end
180
-
181
- def rollback_called
182
- _called.reverse_each(&:execute_rollback)
183
- end
8
+ extend ActiveSupport::Autoload
184
9
 
185
- def validation_callback?(method_name)
186
- _validate_callbacks.map(&:filter).include?(method_name)
187
- end
188
- end
10
+ autoload :Base
11
+ autoload :Loader
189
12
  end
190
13
  end
@@ -1,190 +1,78 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext/array/wrap'
4
- require 'active_support/core_ext/class/attribute'
5
-
6
3
  module ActiveInteractor
7
4
  module Context
8
- # Provides Context Attribute methods to included classes
9
- #
5
+ # Context attribute methods included by all {Context::Base}
10
6
  # @author Aaron Allen <hello@aaronmallen.me>
11
7
  # @since 0.1.4
12
- # @version 0.1
13
8
  module Attributes
14
- extend ActiveSupport::Concern
15
-
16
- included do
17
- extend ClassMethods
18
- class_attribute :__default_attributes, instance_writer: false, default: []
9
+ def self.included(base)
10
+ base.class_eval do
11
+ extend ClassMethods
12
+ end
19
13
  end
20
14
 
15
+ # Context attribute class methods extended by all {Context::Base}
21
16
  module ClassMethods
22
- # Attributes defined on the context class
23
- #
17
+ # Set or get attributes defined on the context class
18
+ # @example Set attributes on a context class
19
+ # class MyInteractor::Context < ActiveInteractor::Context::Base
20
+ # attributes :first_name, :last_name
21
+ # end
24
22
  # @example Get attributes defined on a context class
25
23
  # MyInteractor::Context.attributes
26
24
  # #=> [:first_name, :last_name]
27
- #
28
- # @return [Array<Symbol>] the defined attributes
29
- def attributes
30
- __default_attributes
31
- .concat(_validate_callbacks.map(&:filter).map(&:attributes).flatten)
32
- .flatten
33
- .uniq
34
- end
35
-
36
- # Set attributes on a context class
37
- #
38
- # @param attributes [Array<Symbol, String>] the attributes of the context
39
- #
40
- # @example Define attributes on a context class
41
- # MyInteractor::Context.attributes = :first_name, :last_name
42
- # #=> [:first_name, :last_name]
43
- #
44
25
  # @return [Array<Symbol>] the defined attributes
45
- def attributes=(*attributes)
46
- self.__default_attributes = self.attributes.concat(attributes.flatten.map(&:to_sym)).uniq
47
- end
48
-
49
- # Attribute aliases defined on the context class
50
- #
51
- # @example Get attribute aliases defined on a context class
52
- # MyInteractor::Context.attribute_aliases
53
- # #=> { last_name: [:sir_name] }
54
- #
55
- # @return [Hash{Symbol => Array<Symbol>}]
56
- def attribute_aliases
57
- @attribute_aliases ||= {}
58
- end
26
+ def attributes(*attributes)
27
+ return __attributes if attributes.empty?
59
28
 
60
- # Set attribute aliases on the context class
61
- #
62
- # @param aliases [Hash{Symbol => Symbol, Array<Symbol>}] the attribute aliases of
63
- # the context
64
- #
65
- # @return [Hash{Symbol => Array<Symbol>}]
66
- def alias_attributes(aliases = {})
67
- map_attribute_aliases(aliases)
68
- attribute_aliases
29
+ @__attributes = __attributes.concat(attributes).compact.uniq.sort
69
30
  end
70
31
 
71
32
  private
72
33
 
73
- def map_attribute_aliases(aliases)
74
- aliases.each_key do |attribute|
75
- key = attribute.to_sym
76
- attribute_aliases[key] ||= []
77
- attribute_aliases[key].concat(Array.wrap(aliases[attribute]).map(&:to_sym))
78
- end
34
+ def __attributes
35
+ @__attributes ||= []
79
36
  end
80
37
  end
81
38
 
82
- def initialize(attributes = {})
83
- super(map_attributes(attributes))
39
+ # @api private
40
+ # @param context [Hash|Context::Base] attributes to assign to the context
41
+ # @return [Context::Base] a new instance of {Context::Base}
42
+ def initialize(context = {})
43
+ copy_flags!(context)
44
+ super
84
45
  end
85
46
 
86
47
  # Attributes defined on the instance
87
- #
88
48
  # @example Get attributes defined on an instance
89
- # MyInteractor::Context.attributes = :first_name, :last_name
90
- # #=> [:first_name, :last_name]
91
- #
92
- # context = MyInteractor::Context.new(first_name: 'Aaron', last_name: 'Allen')
93
- # #=> <#MyInteractor::Context first_name='Aaron', last_name='Allen'>
94
- #
95
- # context.attributes
96
- # #=> { first_name: 'Aaron', last_name: 'Allen' }
97
- #
98
- # @example Get attributes defined on an instance with unknown attribute
99
- # MyInteractor::Context.attributes = :first_name, :last_name
100
- # #=> [:first_name, :last_name]
101
- #
102
- # context = MyInteractor::Context.new(first_name: 'Aaron', last_name: 'Allen', unknown: 'unknown')
103
- # #=> <#MyInteractor::Context first_name='Aaron', last_name='Allen', unknown='unknown'>
49
+ # class MyInteractor::Context < ActiveInteractor::Context::Base
50
+ # attributes :first_name, :last_name
51
+ # end
104
52
  #
105
- # context.attributes
106
- # #=> { first_name: 'Aaron', last_name: 'Allen' }
53
+ # class MyInteractor < ActiveInteractor::Base
54
+ # def perform; end
55
+ # end
107
56
  #
108
- # context.unknown
109
- # #=> 'unknown'
57
+ # result = MyInteractor.perform(first_name: 'Aaron', last_name: 'Allen', occupation: 'Software Nerd')
58
+ # #=> <#MyInteractor::Context first_name='Aaron' last_name='Allen' occupation='Software Nerd'>
110
59
  #
111
- # @return [Hash{Symbol => *}] the defined attributes and values
60
+ # result.attributes
61
+ # #=> { first_name: 'Aaron', last_name: 'Allen' }
62
+ # @return [Hash{Symbol=>*}] the defined attributes and values
112
63
  def attributes
113
64
  self.class.attributes.each_with_object({}) do |attribute, hash|
114
65
  hash[attribute] = self[attribute] if self[attribute]
115
66
  end
116
67
  end
117
68
 
118
- # Removes properties from the instance that are not
119
- # explicitly defined in the context instance {#attributes}
120
- #
121
- # @example Clean an instance of Context with unknown attribute
122
- # MyInteractor::Context.attributes = :first_name, :last_name
123
- # #=> [:first_name, :last_name]
124
- #
125
- # context = MyInteractor::Context.new(first_name: 'Aaron', last_name: 'Allen', unknown: 'unknown')
126
- # #=> <#MyInteractor::Context first_name='Aaron', last_name='Allen', unknown='unknown'>
127
- #
128
- # context.unknown
129
- # #=> 'unknown'
130
- #
131
- # context.clean!
132
- # #=> { unknown: 'unknown' }
133
- #
134
- # context.unknown
135
- # #=> nil
136
- #
137
- # @return [Hash{Symbol => *}] the deleted attributes
138
- def clean!
139
- return {} if keys.empty?
140
-
141
- clean_keys!
142
- end
143
-
144
- # All keys of properties currently defined on the instance
145
- #
146
- # @example An instance of Context with unknown attribute
147
- # MyInteractor::Context.attributes = :first_name, :last_name
148
- # #=> [:first_name, :last_name]
149
- #
150
- # context = MyInteractor::Context.new(first_name: 'Aaron', last_name: 'Allen', unknown: 'unknown')
151
- # #=> <#MyInteractor::Context first_name='Aaron', last_name='Allen', unknown='unknown'>
152
- #
153
- # context.keys
154
- # #=> [:first_name, :last_name, :unknown]
155
- #
156
- # @return [Array<Symbol>] keys defined on the instance
157
- def keys
158
- each_pair.map { |pair| pair[0].to_sym }
159
- end
160
-
161
69
  private
162
70
 
163
- def aliased_key(key)
164
- self.class.attribute_aliases.each_pair do |attribute, aliases|
165
- key = aliases.any? { |aliased| aliased == key.to_sym } ? attribute : key.to_sym
71
+ def copy_flags!(context)
72
+ %w[_called _failed _rolled_back].each do |flag|
73
+ value = context.instance_variable_get("@#{flag}")
74
+ instance_variable_set("@#{flag}", value)
166
75
  end
167
- key
168
- end
169
-
170
- def clean_keys!
171
- keys.reject { |key| self.class.attributes.include?(key) }.each_with_object({}) do |attribute, deleted|
172
- deleted[attribute] = self[attribute] if self[attribute]
173
- delete_field(key.to_s)
174
- end
175
- end
176
-
177
- def map_attributes(attributes)
178
- return {} unless attributes
179
-
180
- attributes.keys.each_with_object({}) do |attribute, hash|
181
- key = aliased_key(attribute)
182
- hash[key] = attributes[attribute]
183
- end
184
- end
185
-
186
- def new_ostruct_member!(name)
187
- super(aliased_key(name))
188
76
  end
189
77
  end
190
78
  end