activeinteractor 0.1.7 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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