activeinteractor 2.0.0.alpha.3.0.1 → 2.0.0.alpha.4.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58a8598be9a7d0407430fcda338c3ad1b7a37c922561701abc928398a72d9b2b
4
- data.tar.gz: f206517d7852204973b1a0155a542ae9b37b2b361df65a00a5af846a42512d21
3
+ metadata.gz: 1ec1233daeb01ec3145d13cef57caf6c7f69a8128c311e5a0414c594c9b36f5b
4
+ data.tar.gz: e76d9a078f540d5e8eaa953cb1f88726bde6f507222f5f6f8c807a4f771dd6e6
5
5
  SHA512:
6
- metadata.gz: 456111cd323e168b19e73aa509a90cd39eca61500828ab3f3a8083a41386b82ed7082fedc4741fbdc702755454a048c20f89f92e8d38a2d557311d58ad89c4db
7
- data.tar.gz: 2741a8657cbaff1139be07b09624668d220dd246a23edff71dcdf65ef26ea461c1d7a30cd252d84cf50121e4196f941dc7f391786de6ee30996918f29f7786a7
6
+ metadata.gz: 79f453fbfafecdf6cc59c1eebcb26229132818c18c5109977cf70efd40e94dbfb8ce7094b3e24cb9c39238a787bcf92b12f1c39716992402e7627e6c454defe1
7
+ data.tar.gz: a58f48730d1494e0a52608a8b76f07a11c53158d1f67966a4f5538965fc1e0495abd847613660f1be72cfbd9b21e720d4631a8d562c37bdd28567f1acc3e73fa
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Version](https://img.shields.io/gem/v/activeinteractor.svg?logo=ruby)](https://rubygems.org/gems/activeinteractor)
4
4
  [![Build](https://github.com/activeinteractor/activeinteractor/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/activeinteractor/activeinteractor/actions/workflows/build.yml)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/9a44100fb77f785dac8a/test_coverage)](https://codeclimate.com/github/activeinteractor/activeinteractor/test_coverage)
5
6
  [![Docs](https://img.shields.io/badge/docs-blue)](https://activeinteractor.org/)
6
7
  [![License](https://img.shields.io/github/license/activeinteractor/activeinteractor.svg?maxAge=300)](https://github.com/activeinteractor/activeinteractor/blob/main/LICENSE)
7
8
 
@@ -34,7 +34,8 @@ module ActiveInteractor
34
34
  end
35
35
 
36
36
  def validate!
37
- validate_presence! && validate_type!
37
+ validate_presence!
38
+ validate_type!
38
39
  end
39
40
 
40
41
  def value
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Context
5
+ module AttributeValidation
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ validate :validate_attributes!
10
+ end
11
+
12
+ protected
13
+
14
+ def validate_attributes!
15
+ attribute_set.attributes.each do |attribute|
16
+ attribute.validate!
17
+ attribute.error_messages.each { |message| errors.add(attribute.name, message) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,27 +2,81 @@
2
2
 
3
3
  module ActiveInteractor
4
4
  module Context
5
+ # The base class for all context objects
5
6
  class Base
7
+ extend Type::DeclerationMethods::ClassMethods
6
8
  include ActiveModel::Validations
7
- include ActiveModelErrorMethods
8
- include AttributeRegistration
9
- include AttributeAssignment
10
9
  include Type::DeclerationMethods
11
10
 
12
- validate :validate_attributes!
11
+ class << self
12
+ def method_defined?(method_name)
13
+ attribute_set.attribute_names.include?(method_name.to_s.delete('=').to_sym) || super
14
+ end
15
+
16
+ protected
17
+
18
+ def attribute_set
19
+ @attribute_set ||= AttributeSet.new(self)
20
+ end
21
+ end
13
22
 
14
23
  def initialize(attributes = {})
15
- super
16
- @errors = ActiveModel::Errors.new(self)
24
+ attribute_set.attributes.each do |attribute|
25
+ next unless attributes.with_indifferent_access.key?(attribute.name)
26
+
27
+ assign_attribute_value(attribute.name, attributes.with_indifferent_access[attribute.name])
28
+ end
29
+ end
30
+
31
+ def [](attribute_name)
32
+ read_attribute_value(attribute_name)
33
+ end
34
+
35
+ def []=(attribute_name, value)
36
+ assign_attribute_value(attribute_name, value)
17
37
  end
18
38
 
19
39
  protected
20
40
 
21
- def validate_attributes!
22
- attribute_set.attributes.each do |attribute|
23
- attribute.validate!
24
- attribute.error_messages.each { |message| errors.add(attribute.name, message) }
41
+ def assign_attribute_value(attribute_name, value)
42
+ attribute = attribute_set.find(attribute_name)
43
+ raise NoMethodError, "unknown attribute '#{attribute_name}' for #{self.class.name}" unless attribute
44
+
45
+ attribute.assign_value(value)
46
+ end
47
+
48
+ def assignment_method_missing(method_name, *arguments)
49
+ if arguments.length != 1
50
+ raise ArgumentError,
51
+ "wrong number of arguments (given #{arguments.length}, expected 1)"
25
52
  end
53
+
54
+ assign_attribute_value(method_name.to_s.delete('=').to_sym, arguments.first)
55
+ end
56
+
57
+ def attribute_set
58
+ @attribute_set ||= AttributeSet.new(self, *self.class.send(:attribute_set).attributes.map(&:dup))
59
+ end
60
+
61
+ def method_missing(method_name, *arguments)
62
+ return super unless respond_to_missing?(method_name)
63
+ return assignment_method_missing(method_name, *arguments) if method_name.to_s.end_with?('=')
64
+
65
+ read_attribute_value(method_name)
66
+ end
67
+
68
+ def read_attribute_value(attribute_name)
69
+ attribute = attribute_set.find(attribute_name)
70
+ raise NoMethodError, "unknown attribute '#{attribute_name}' for #{self.class.name}" unless attribute
71
+
72
+ attribute.value
73
+ end
74
+
75
+ def respond_to_missing?(method_name, _include_private = false)
76
+ return true if attribute_set.attribute_names.include?(method_name.to_sym)
77
+ return true if attribute_set.attribute_names.include?(method_name.to_s.delete('=').to_sym)
78
+
79
+ super
26
80
  end
27
81
  end
28
82
  end
@@ -3,6 +3,11 @@
3
3
  module ActiveInteractor
4
4
  module Context
5
5
  class Input < Base
6
+ delegate :as_json, to: :arguments
7
+ delegate :to_h, to: :arguments
8
+ delegate :to_hash, to: :arguments
9
+ delegate :to_json, to: :arguments
10
+
6
11
  def self.argument(*attribute_args)
7
12
  attribute_set.add(*attribute_args)
8
13
  end
@@ -14,6 +19,12 @@ module ActiveInteractor
14
19
  def self.arguments
15
20
  attribute_set.attributes
16
21
  end
22
+
23
+ def arguments
24
+ attribute_set.attributes.each_with_object({}) do |attribute, result|
25
+ result[attribute.name] = attribute.value
26
+ end
27
+ end
17
28
  end
18
29
  end
19
30
  end
@@ -2,8 +2,12 @@
2
2
 
3
3
  module ActiveInteractor
4
4
  module Context
5
+ # The base class for all output context objects
5
6
  class Output < Base
6
- delegate :to_h, :to_hash, :to_json, to: :fields
7
+ delegate :as_json, to: :fields
8
+ delegate :to_h, to: :fields
9
+ delegate :to_hash, to: :fields
10
+ delegate :to_json, to: :fields
7
11
 
8
12
  def self.returns(*attribute_args)
9
13
  attribute_set.add(*attribute_args)
@@ -3,7 +3,9 @@
3
3
  module ActiveInteractor
4
4
  module Context
5
5
  class Result
6
- delegate :[], :as_json, :to_json, to: :to_hash
6
+ delegate :[], to: :to_hash
7
+ delegate :as_json, to: :to_hash
8
+ delegate :to_json, to: :to_hash
7
9
 
8
10
  def self.register_owner(owner)
9
11
  owner.const_set(:ResultContext, Class.new(self))
@@ -11,7 +13,7 @@ module ActiveInteractor
11
13
 
12
14
  def self.for_output_context(owner, context)
13
15
  context.fields.each_key { |field| owner::ResultContext.send(:attr_reader, field) }
14
- owner::ResultContext.new(context.fields)
16
+ owner::ResultContext.new(context.fields.deep_dup)
15
17
  end
16
18
 
17
19
  def initialize(attributes = {})
@@ -2,6 +2,7 @@
2
2
 
3
3
  module ActiveInteractor
4
4
  module Context
5
+ # The base class for all runtime context objects
5
6
  class Runtime < Base
6
7
  def initialize(attributes = {})
7
8
  super
@@ -6,9 +6,7 @@ module ActiveInteractor
6
6
  extend ActiveSupport::Autoload
7
7
 
8
8
  autoload :Attribute
9
- autoload :AttributeRegistration
10
9
  autoload :AttributeSet
11
- autoload :AttributeAssignment
12
10
  autoload :Base
13
11
  autoload :Input
14
12
  autoload :Output
@@ -1,22 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveInteractor
4
- # Raised when an interactor fails
4
+ # Raised on interactor failure
5
5
  #
6
6
  # @!attribute [r] result
7
- # @return [ActiveInteractor::Result] An instance of {ActiveInteractor::Result} for the
8
- # {ActiveInteractor::Interactor::Base interactor} that failed
7
+ # @return [ActiveInteractor::Result] the result of the interactor that failed
9
8
  class Error < StandardError
10
9
  attr_reader :result
11
10
 
12
- # Create a new instance of {ActiveInteractor::Error}
13
- #
14
- # @param result [ActiveInteractor::Result] An instance of {ActiveInteractor::Result} for the
15
- # {ActiveInteractor::Interactor::Base interactor} that failed
16
- # @param message [String] The error message
17
- #
18
- # @private
19
- # @return [ActiveInteractor::Error]
20
11
  def initialize(result, message = nil)
21
12
  @result = result
22
13
  super(message)
@@ -2,37 +2,295 @@
2
2
 
3
3
  module ActiveInteractor
4
4
  module Interactor
5
+ # The Base Class inherited by all Interactors
5
6
  class Base
6
- include ContextMethods
7
- include InteractionMethods
7
+ include ActiveSupport::Callbacks
8
+ extend Type::DeclerationMethods::ClassMethods
8
9
  include Type::DeclerationMethods
9
10
 
10
- def initialize(input = {})
11
- @raw_input = input.dup
12
- validate_input_and_generate_runtime_context!
11
+ define_callbacks :fail, :input_context_create, :input_context_validation, :output_context_create,
12
+ :output_context_validation, :perform, :rollback, :runtime_context_create
13
+
14
+ class << self
15
+ delegate :argument, :argument_names, :arguments, to: :input_context_class
16
+ delegate :returns, :field_names, :fields, to: :output_context_class
17
+ delegate(*ActiveModel::Validations::ClassMethods.instance_methods, to: :input_context_class, prefix: :input)
18
+ delegate(*ActiveModel::Validations::HelperMethods.instance_methods, to: :input_context_class, prefix: :input)
19
+ delegate(*ActiveModel::Validations::ClassMethods.instance_methods, to: :output_context_class, prefix: :output)
20
+ delegate(*ActiveModel::Validations::HelperMethods.instance_methods, to: :output_context_class, prefix: :output)
21
+
22
+ def accepts_arguments_matching(set_input_context_class)
23
+ @input_context_class = set_input_context_class
24
+ end
25
+ alias input_context accepts_arguments_matching
26
+ alias input_type accepts_arguments_matching
27
+
28
+ def after_fail(...)
29
+ set_callback(:fail, :after, ...)
30
+ end
31
+
32
+ def after_input_context_create(...)
33
+ set_callback(:input_context_create, :after, ...)
34
+ end
35
+
36
+ def after_input_context_validation(...)
37
+ set_callback(:input_context_validation, :after, ...)
38
+ end
39
+
40
+ def after_output_context_create(...)
41
+ set_callback(:output_context_create, :after, ...)
42
+ end
43
+
44
+ def after_output_context_validation(...)
45
+ set_callback(:output_context_validation, :after, ...)
46
+ end
47
+
48
+ def after_perform(...)
49
+ set_callback(:perform, :after, ...)
50
+ end
51
+
52
+ def after_rollback(...)
53
+ set_callback(:rollback, :after, ...)
54
+ end
55
+
56
+ def after_runtime_context_create(...)
57
+ set_callback(:runtime_context_create, :after, ...)
58
+ end
59
+
60
+ def around_fail(...)
61
+ set_callback(:fail, :around, ...)
62
+ end
63
+
64
+ def around_input_context_create(...)
65
+ set_callback(:input_context_create, :around, ...)
66
+ end
67
+
68
+ def around_input_context_validation(...)
69
+ set_callback(:input_context_validation, :around, ...)
70
+ end
71
+
72
+ def around_output_context_create(...)
73
+ set_callback(:output_context_create, :around, ...)
74
+ end
75
+
76
+ def around_output_context_validation(...)
77
+ set_callback(:output_context_validation, :around, ...)
78
+ end
79
+
80
+ def around_perform(...)
81
+ set_callback(:perform, :around, ...)
82
+ end
83
+
84
+ def around_rollback(...)
85
+ set_callback(:rollback, :around, ...)
86
+ end
87
+
88
+ def around_runtime_context_create(...)
89
+ set_callback(:runtime_context_create, :around, ...)
90
+ end
91
+
92
+ def before_fail(...)
93
+ set_callback(:fail, :before, ...)
94
+ end
95
+
96
+ def before_input_context_create(...)
97
+ set_callback(:input_context_create, :before, ...)
98
+ end
99
+
100
+ def before_input_context_validation(...)
101
+ set_callback(:input_context_validation, :before, ...)
102
+ end
103
+
104
+ def before_output_context_create(...)
105
+ set_callback(:output_context_create, :before, ...)
106
+ end
107
+
108
+ def before_output_context_validation(...)
109
+ set_callback(:output_context_validation, :before, ...)
110
+ end
111
+
112
+ def before_perform(...)
113
+ set_callback(:perform, :before, ...)
114
+ end
115
+
116
+ def before_rollback(...)
117
+ set_callback(:rollback, :before, ...)
118
+ end
119
+
120
+ def before_runtime_context_create(...)
121
+ set_callback(:runtime_context_create, :before, ...)
122
+ end
123
+
124
+ def input_context_class
125
+ @input_context_class ||= const_set(:InputContext, Class.new(Context::Input))
126
+ end
127
+
128
+ def output_context_class
129
+ @output_context_class ||= const_set(:OutputContext, Class.new(Context::Output))
130
+ end
131
+
132
+ def perform!(input_context = {}, options = {})
133
+ new(options).perform!(input_context)
134
+ end
135
+
136
+ def perform(input_context = {}, options = {})
137
+ perform!(input_context, options)
138
+ rescue Error => e
139
+ e.result
140
+ rescue StandardError => e
141
+ Result.failure(errors: e.message)
142
+ end
143
+
144
+ def returns_data_matching(set_output_context_class)
145
+ @output_context_class = set_output_context_class
146
+ end
147
+ alias output_context returns_data_matching
148
+ alias output_type returns_data_matching
149
+
150
+ def runtime_context_class
151
+ @runtime_context_class ||= begin
152
+ context_class = const_set(:RuntimeContext, Class.new(Context::Runtime))
153
+ context_class.send(:attribute_set).merge(input_context_class.send(:attribute_set).attributes)
154
+ context_class.send(:attribute_set).merge(output_context_class.send(:attribute_set).attributes)
155
+ context_class
156
+ end
157
+ end
158
+
159
+ def with_options(options)
160
+ new(options)
161
+ end
162
+
163
+ protected
164
+
165
+ def result_context
166
+ @result_context ||= Context::Result.register_owner(self)
167
+ end
168
+ end
169
+
170
+ def initialize(options = {})
171
+ @options = Options.new(options.deep_dup)
13
172
  end
14
173
 
15
- def perform!
174
+ def interact; end
175
+
176
+ def perform!(input_context = {})
177
+ @raw_input = input_context.deep_dup
178
+ create_input_context
179
+ validate_input_context!
180
+ create_runtime_context
181
+ return execute_perform_with_callbacks unless @options.skip_perform_callbacks
182
+
183
+ execute_perform
184
+ end
185
+
186
+ def perform(input_context = {})
187
+ perform!(input_context)
188
+ rescue Error => e
189
+ e.result
190
+ rescue StandardError => e
191
+ Result.failure(errors: e.message)
192
+ end
193
+
194
+ def rollback; end
195
+
196
+ def with_options(options)
197
+ @options = Options.new(options.deep_dup)
198
+ self
199
+ end
200
+
201
+ protected
202
+
203
+ attr_reader :context
204
+
205
+ def create_input_context
206
+ run_callbacks :input_context_create do
207
+ @input_context = self.class.input_context_class.new(@raw_input.deep_dup)
208
+ end
209
+ end
210
+
211
+ def create_output_context
212
+ run_callbacks :output_context_create do
213
+ @output_context = self.class.output_context_class.new(@context.attributes.deep_dup)
214
+ end
215
+ end
216
+
217
+ def create_runtime_context
218
+ run_callbacks :runtime_context_create do
219
+ @context = self.class.runtime_context_class.new(@input_context.to_h.deep_dup)
220
+ end
221
+ end
222
+
223
+ def execute_perform
16
224
  with_notification(:perform) do |payload|
17
225
  interact
18
- generate_and_validate_output_context!
226
+ create_output_context
227
+ validate_output_context!
19
228
  payload[:result] = Result.success(data: output_to_result_context!)
20
229
  end
21
230
  end
22
231
 
23
- protected
232
+ def execute_perform_with_callbacks
233
+ run_callbacks :perform do
234
+ execute_perform
235
+ end
236
+ end
24
237
 
25
- def fail!(errors = {})
26
- result = nil
27
- with_notification(:rollback) do |payload|
238
+ def execute_rollback_with_callbacks
239
+ run_callbacks :rollback do
28
240
  rollback
29
- result = Result.failure(data: @output, errors: errors)
30
- payload[:result] = result
241
+ end
242
+ end
243
+
244
+ def fail!(errors = {})
245
+ run_callbacks :fail do
246
+ result = nil
247
+ with_notification(:rollback) do |payload|
248
+ perform_rollback unless @options.skip_rollback
249
+ result = Result.failure(data: @output, errors: errors)
250
+ payload[:result] = result
251
+ end
31
252
  end
32
253
 
33
254
  raise Error, result
34
255
  end
35
256
 
257
+ def output_to_result_context!
258
+ self.class.send(:result_context).for_output_context(self.class, @output_context)
259
+ end
260
+
261
+ def perform_rollback
262
+ return execute_rollback_with_callbacks unless @options.skip_rollback_callbacks
263
+
264
+ rollback
265
+ end
266
+
267
+ def validate_input_context!
268
+ return unless @options.validate && @options.validate_input_context
269
+
270
+ run_callbacks :input_context_validation do
271
+ @input_context.valid?
272
+ end
273
+ return if @input_context.errors.empty?
274
+
275
+ raise Error,
276
+ Result.failure(errors: @input_context.errors,
277
+ status: Result::STATUS[:failed_at_input])
278
+ end
279
+
280
+ def validate_output_context!
281
+ return unless @options.validate && @options.validate_output_context
282
+
283
+ run_callbacks :output_context_validation do
284
+ @output_context.valid?
285
+ end
286
+
287
+ return if @output_context.errors.empty?
288
+
289
+ raise Error,
290
+ Result.failure(errors: @output_context.errors,
291
+ status: Result::STATUS[:failed_at_output])
292
+ end
293
+
36
294
  def with_notification(action)
37
295
  ActiveSupport::Notifications.instrument("#{self.class.name}::#{action.to_s.classify}") do |payload|
38
296
  yield payload if block_given?
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveInteractor
4
+ module Interactor
5
+ class Options
6
+ DEFAULTS = {
7
+ skip_perform_callbacks: false,
8
+ skip_rollback: false,
9
+ skip_rollback_callbacks: false,
10
+ validate: true,
11
+ validate_input_context: true,
12
+ validate_output_context: true
13
+ }.freeze
14
+
15
+ attr_reader(*DEFAULTS.keys)
16
+
17
+ def initialize(options = {})
18
+ prepared_options = DEFAULTS.merge(options.deep_dup)
19
+ prepared_options.each_pair { |key, value| instance_variable_set(:"@#{key}", value) }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -6,7 +6,6 @@ module ActiveInteractor
6
6
  extend ActiveSupport::Autoload
7
7
 
8
8
  autoload :Base
9
- autoload :ContextMethods
10
- autoload :InteractionMethods
9
+ autoload :Options
11
10
  end
12
11
  end
@@ -1,64 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveInteractor
4
- # The object returned by an {ActiveInteractor::Interactor::Base interactor}
4
+ # An interactor result
5
5
  #
6
6
  # @!attribute [r] data
7
+ # @return [ActiveInteractor::Context::Result, Object, nil] the data returned by the interactor
7
8
  #
8
- # @example Given an {ActiveInteractor::Interactor::Base interactor} that creates an ActiveRecord User
9
- # class CreateUser < ActiveInteractor::Interactor::Base
10
- # argument :login, String, 'The login for the User', required: true
11
- # argument :password, String, 'The password for the User', required: true
12
- # argument :password_confirmation, String, 'The password confirmation for the user', required: true
13
- #
14
- # returns :user, User, 'The created User', required: true
15
- #
16
- # def interact
17
- # context.user = User.new(context)
18
- # fail!(context.user.errors) unless context.user.save
19
- # end
20
- # end
21
- #
22
- # result = CreateUser.perform(login: 'johndoe', password: 'password', password_confirmation: 'password')
9
+ # @example
10
+ # result = CreateUser.perform(email: 'hello@aaronmallen.me', password: 'password')
23
11
  # result.data.user
24
- #
25
- # #=> <# User @login='johndoe'>
26
- #
27
- # @return [ActiveInteractor::Context::Result] the data returned by the
28
- # {ActiveInteractor::Interactor::Base interactor}
12
+ # #=> <#User @email="hello@aaronmallen.me">
29
13
  #
30
14
  # @!attribute [r] errors
31
- # @return [ActiveModel::Errors] the errors returned by the
32
- # {ActiveInteractor::Interactor::Base interactor}
33
- # @see https://github.com/rails/rails/blob/main/activemodel/lib/active_model/errors.rb ActiveModel::Errors
15
+ # @return [ActiveModel::Errors] the errors from the interactor
16
+ #
17
+ # @example
18
+ # result = CreateUser.perform(
19
+ # email: 'hello@aaronmallen',
20
+ # password: 'password',
21
+ # password_confirmation: 'not password'
22
+ # )
23
+ # result.errors.full_messages
24
+ # #=> ["Password confirmation doesn't match Password"]
34
25
  class Result
35
- include ActiveModelErrorMethods
26
+ extend ActiveModel::Naming
36
27
 
37
- # @!method to_json
38
- # The {ActiveInteractor::Interactor::Base result} as a Hash
39
- #
40
- # @example When an {ActiveInteractor::Interactor::Base interactor} succeeds
41
- # result = CreateUser.perform(login: 'johndoe', password: 'password', password_confirmation: 'password')
42
- # result.to_hash
43
- #
44
- # #=> { :success => true, :errors => {}, :data => { :login => 'johndoe' } }
45
- #
46
- # @example When an {ActiveInteractor::Interactor::Base interactor} fails
47
- # result = CreateUser.perform(login: 'johndoe', password: 'password', password_confirmation: 'notpassword')
48
- # result.to_hash
49
- #
50
- # #=> {
51
- # #=> :success => false,
52
- # #=> :errors => { :password_confirmation => ["doesn't match Password"] },
53
- # #=> :data => { :login => 'johndoe' }
54
- # #=> }
55
- #
56
- # @deprecated will be removed in version 2.0.0-alpha.3.0.0
57
- # use {#to_hash} instead
58
- # @return [Hash {Symbol => Boolean, Hash}]
28
+ attr_reader :data, :errors
29
+
30
+ # Return a result as a JSON string
59
31
  delegate :to_json, to: :to_hash
60
32
 
61
- # @private
33
+ # Status codes for interactor results
34
+ #
35
+ # @visibility private
62
36
  STATUS = {
63
37
  success: 0,
64
38
  failed_at_input: 1,
@@ -66,15 +40,16 @@ module ActiveInteractor
66
40
  failed_at_output: 3
67
41
  }.freeze
68
42
 
69
- attr_reader :data
70
-
71
43
  class << self
72
- # @private
73
- def success(data: {})
74
- new(status: STATUS[:success], data: data)
75
- end
76
-
77
- # @private
44
+ # Return a failed result
45
+ #
46
+ # @visibility private
47
+ #
48
+ # @param data [ActiveInteractor::Context::Result, Object, nil] the data returned by the interactor
49
+ # @param errors [ActiveModel::Errors, Hash, String] the errors from the interactor
50
+ # @param status [Integer] the status code for the result
51
+ #
52
+ # @return [ActiveInteractor::Result] the failed result
78
53
  def failure(data: {}, errors: {}, status: STATUS[:failed_at_runtime])
79
54
  result = new(status: status, data: data)
80
55
  parse_errors(errors).each_pair do |attribute, messages|
@@ -83,6 +58,31 @@ module ActiveInteractor
83
58
  result
84
59
  end
85
60
 
61
+ # Needed for ActiveModel::Errors
62
+ #
63
+ # @visibility private
64
+ def human_attribute_name(attribute, _options = {})
65
+ attribute.respond_to?(:to_s) ? attribute.to_s.humanize : attribute
66
+ end
67
+
68
+ # Needed for ActiveModel::Errors
69
+ #
70
+ # @visibility private
71
+ def lookup_ancestors
72
+ [self]
73
+ end
74
+
75
+ # Return a successful result
76
+ #
77
+ # @visibility private
78
+ #
79
+ # @param data [ActiveInteractor::Context::Result, Object, nil] the data returned by the interactor
80
+ #
81
+ # @return [ActiveInteractor::Result] the successful result
82
+ def success(data: {})
83
+ new(status: STATUS[:success], data: data)
84
+ end
85
+
86
86
  private
87
87
 
88
88
  def parse_errors(errors)
@@ -98,68 +98,90 @@ module ActiveInteractor
98
98
  end
99
99
 
100
100
  private_class_method :new
101
- # @private
101
+
102
+ # Create a new instance of {ActiveInteractor::Result}
103
+ #
104
+ # @visibility private
105
+ #
106
+ # @param status [Integer] the status code for the result
107
+ # @param data [ActiveInteractor::Context::Result, Object, nil] the data returned by the interactor
108
+ #
109
+ # @return [ActiveInteractor::Result] the result
102
110
  def initialize(status:, data: {})
103
111
  @status = status
104
112
  @data = data
105
113
  @errors = ActiveModel::Errors.new(self)
106
114
  end
107
115
 
108
- # Whether or not the {ActiveInteractor::Interactor::Base result} is a failure
109
- #
110
- # @example When an {ActiveInteractor::Interactor::Base interactor} fails
111
- # result = CreateUser.perform(login: 'johndoe', password: 'password', password_confirmation: 'notpassword')
116
+ # Whether or not the result is a failure
117
+ #
118
+ # @example When the result is a success
119
+ # result = CreateUser.perform(
120
+ # email: 'hello@aaronmallen',
121
+ # password: 'password',
122
+ # password_confirmation: 'password'
123
+ # )
112
124
  # result.failure?
125
+ # #=> false
113
126
  #
114
- # #=> true
115
- #
116
- # @example When an {ActiveInteractor::Interactor::Base interactor} succeeds
117
- # result = CreateUser.perform(login: 'johndoe', password: 'password', password_confirmation: 'password')
127
+ # @example When the result is a failure
128
+ # result = CreateUser.perform(
129
+ # email: 'hello@aaronmallen',
130
+ # password: 'password',
131
+ # password_confirmation: 'not password'
132
+ # )
118
133
  # result.failure?
134
+ # #=> true
119
135
  #
120
- # #=> false
121
- #
122
- # @return [Boolean]
136
+ # @return [Boolean] whether or not the result is a failure
123
137
  def failure?
124
138
  !success?
125
139
  end
126
140
  alias failed? failure?
127
141
 
128
- # @private
142
+ # Needed for ActiveModel::Errors
143
+ #
144
+ # @visibility private
129
145
  def read_attribute_for_validation(attribute_name)
130
146
  data&.send(attribute_name.to_sym)
131
147
  end
132
148
 
133
- # Whether or not the {ActiveInteractor::Interactor::Base result} is a success
149
+ # Whether or not the result is a success
150
+ #
151
+ # @example When the result is a success
152
+ # result = CreateUser.perform(
153
+ # email: 'hello@aaronmallen',
154
+ # password: 'password',
155
+ # password_confirmation: 'password'
156
+ # )
157
+ # result.success?
158
+ # #=> true
159
+ #
160
+ # @example When the result is a failure
161
+ # result = CreateUser.perform(
162
+ # email: 'hello@aaronmallen',
163
+ # password: 'password',
164
+ # password_confirmation: 'not password'
165
+ # )
166
+ # result.success?
167
+ # #=> false
134
168
  #
135
- # @return [Boolean]
169
+ # @return [Boolean] whether or not the result is a failure
136
170
  def success?
137
171
  @status == STATUS[:success]
138
172
  end
139
173
  alias successful? success?
140
174
 
141
- # The {ActiveInteractor::Interactor::Base result} as a Hash
142
- #
143
- # @example When an {ActiveInteractor::Interactor::Base interactor} succeeds
144
- # result = CreateUser.perform(login: 'johndoe', password: 'password', password_confirmation: 'password')
145
- # result.to_hash
175
+ # Return the result as a hash
146
176
  #
147
- # #=> { :success => true, :errors => {}, :data => { :login => 'johndoe' } }
148
- #
149
- # @example When an {ActiveInteractor::Interactor::Base interactor} fails
150
- # result = CreateUser.perform(login: 'johndoe', password: 'password', password_confirmation: 'notpassword')
177
+ # @example
178
+ # result = CreateUser.perform(email: 'hello@aaronmallen', password: 'password')
151
179
  # result.to_hash
152
- #
153
- # #=> {
154
- # #=> :success => false,
155
- # #=> :errors => { :password_confirmation => ["doesn't match Password"] },
156
- # #=> :data => { :login => 'johndoe' }
157
- # #=> }
158
- #
159
- # @return [Hash {Symbol => Boolean, Hash}]
180
+ # #=> { success: true, status: 0, errors: {}, data: { user: <#User> } }
160
181
  def to_hash
161
182
  {
162
183
  success: success?,
184
+ status: @status,
163
185
  errors: errors.to_hash,
164
186
  data: data.to_json
165
187
  }
@@ -25,10 +25,6 @@ module ActiveInteractor
25
25
  :untyped
26
26
  end
27
27
  end
28
-
29
- included do
30
- extend ClassMethods
31
- end
32
28
  end
33
29
  end
34
30
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveInteractor
4
- # @private
4
+ # The Type namespace
5
5
  module Type
6
6
  extend ActiveSupport::Autoload
7
7
 
@@ -33,6 +33,8 @@ require_relative 'active_interactor/errors'
33
33
  # It has features like rich support for attributes, callbacks, and validations, and
34
34
  # thread safe performance methods.
35
35
  #
36
+ # {https://activeinteractor.org/docs/getting-started Getting Started}
37
+ #
36
38
  # {file:CHANGELOG.md Changelog}
37
39
  #
38
40
  # {file:HUMANS.md Acknowledgements}
@@ -44,7 +46,6 @@ require_relative 'active_interactor/errors'
44
46
  module ActiveInteractor
45
47
  extend ActiveSupport::Autoload
46
48
 
47
- autoload :ActiveModelErrorMethods
48
49
  autoload :Context
49
50
  autoload :Interactor
50
51
  autoload :Result
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeinteractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.alpha.3.0.1
4
+ version: 2.0.0.alpha.4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Allen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-03 00:00:00.000000000 Z
11
+ date: 2024-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -63,12 +63,10 @@ files:
63
63
  - LICENSE
64
64
  - README.md
65
65
  - lib/active_interactor.rb
66
- - lib/active_interactor/active_model_error_methods.rb
67
66
  - lib/active_interactor/context.rb
68
67
  - lib/active_interactor/context/attribute.rb
69
- - lib/active_interactor/context/attribute_assignment.rb
70
- - lib/active_interactor/context/attribute_registration.rb
71
68
  - lib/active_interactor/context/attribute_set.rb
69
+ - lib/active_interactor/context/attribute_validation.rb
72
70
  - lib/active_interactor/context/base.rb
73
71
  - lib/active_interactor/context/input.rb
74
72
  - lib/active_interactor/context/output.rb
@@ -77,8 +75,7 @@ files:
77
75
  - lib/active_interactor/errors.rb
78
76
  - lib/active_interactor/interactor.rb
79
77
  - lib/active_interactor/interactor/base.rb
80
- - lib/active_interactor/interactor/context_methods.rb
81
- - lib/active_interactor/interactor/interaction_methods.rb
78
+ - lib/active_interactor/interactor/options.rb
82
79
  - lib/active_interactor/result.rb
83
80
  - lib/active_interactor/type.rb
84
81
  - lib/active_interactor/type/base.rb
@@ -92,10 +89,10 @@ licenses:
92
89
  - MIT
93
90
  metadata:
94
91
  bug_tracker_uri: https://github.com/activeinteractor/activeinteractor/issues
95
- changelog_uri: https://github.com/activeinteractor/activeinteractor/blob/v2.0.0-alpha.3.0.1/CHANGELOG.md
92
+ changelog_uri: https://github.com/activeinteractor/activeinteractor/blob/v2.0.0-alpha.4.0.0/CHANGELOG.md
96
93
  homepage_uri: https://activeinteractor.org
97
- source_code_uri: https://github.com/activeinteractor/activeinteractor/tree/v2.0.0-alpha.3.0.1
98
- documentation_uri: https://activeinteractor.org/api/activeinteractor/v2.0.0-alpha.3.0.1
94
+ source_code_uri: https://github.com/activeinteractor/activeinteractor/tree/v2.0.0-alpha.4.0.0
95
+ documentation_uri: https://activeinteractor.org/api/activeinteractor/v2.0.0-alpha.4.0.0
99
96
  wiki_uri: https://github.com/activeinteractor/activeinteractor/wiki
100
97
  rubygems_mfa_required: 'true'
101
98
  post_install_message:
@@ -113,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
110
  - !ruby/object:Gem::Version
114
111
  version: 1.3.1
115
112
  requirements: []
116
- rubygems_version: 3.4.10
113
+ rubygems_version: 3.4.22
117
114
  signing_key:
118
115
  specification_version: 4
119
116
  summary: Ruby interactors with ActiveModel::Validations
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveInteractor
4
- # @private
5
- module ActiveModelErrorMethods
6
- extend ActiveSupport::Concern
7
-
8
- attr_reader :errors
9
-
10
- # @private
11
- module ClassMethods
12
- def human_attribute_name(attribute, _options = {})
13
- attribute.respond_to?(:to_s) ? attribute.to_s.humanize : attribute
14
- end
15
-
16
- def lookup_ancestors
17
- [self]
18
- end
19
- end
20
-
21
- included do
22
- extend ActiveModel::Naming
23
- extend ClassMethods
24
- end
25
-
26
- def read_attribute_for_validation(attribute_name)
27
- send(attribute_name.to_sym)
28
- end
29
- end
30
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveInteractor
4
- module Context
5
- module AttributeAssignment
6
- extend ActiveSupport::Concern
7
-
8
- def initialize(attributes = {})
9
- attribute_set.attributes.each do |attribute|
10
- next unless attributes.with_indifferent_access.key?(attribute.name)
11
-
12
- assign_attribute_value(attribute.name, attributes.with_indifferent_access[attribute.name])
13
- end
14
- end
15
-
16
- def [](attribute_name)
17
- read_attribute_value(attribute_name)
18
- end
19
-
20
- def []=(attribute_name, value)
21
- assign_attribute_value(attribute_name, value)
22
- end
23
-
24
- protected
25
-
26
- def assign_attribute_value(attribute_name, value)
27
- attribute = attribute_set.find(attribute_name)
28
- raise NoMethodError, "unknown attribute '#{attribute_name}' for #{self.class.name}" unless attribute
29
-
30
- attribute.assign_value(value)
31
- end
32
-
33
- def assignment_method_missing(method_name, *arguments)
34
- if arguments.length != 1
35
- raise ArgumentError,
36
- "wrong number of arguments (given #{arguments.length}, expected 1)"
37
- end
38
-
39
- assign_attribute_value(method_name.to_s.delete('=').to_sym, arguments.first)
40
- end
41
-
42
- def method_missing(method_name, *arguments)
43
- return super unless respond_to_missing?(method_name)
44
- return assignment_method_missing(method_name, *arguments) if method_name.to_s.end_with?('=')
45
-
46
- read_attribute_value(method_name)
47
- end
48
-
49
- def read_attribute_value(attribute_name)
50
- attribute = attribute_set.find(attribute_name)
51
- raise NoMethodError, "unknown attribute '#{attribute_name}' for #{self.class.name}" unless attribute
52
-
53
- attribute.value
54
- end
55
-
56
- def respond_to_missing?(method_name, _include_private = false)
57
- return true if attribute_set.attribute_names.include?(method_name.to_sym)
58
- return true if attribute_set.attribute_names.include?(method_name.to_s.delete('=').to_sym)
59
-
60
- super
61
- end
62
- end
63
- end
64
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveInteractor
4
- module Context
5
- module AttributeRegistration
6
- extend ActiveSupport::Concern
7
-
8
- module ClassMethods
9
- protected
10
-
11
- def attribute_set
12
- @attribute_set ||= AttributeSet.new(self)
13
- end
14
- end
15
-
16
- included do
17
- extend ClassMethods
18
- end
19
-
20
- protected
21
-
22
- def attribute_set
23
- @attribute_set ||= AttributeSet.new(self, *self.class.send(:attribute_set).attributes.map(&:dup))
24
- end
25
- end
26
- end
27
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveInteractor
4
- module Interactor
5
- module ContextMethods
6
- extend ActiveSupport::Concern
7
-
8
- module ClassMethods
9
- delegate :argument, :argument_names, :arguments, to: :input_context_class
10
- delegate :returns, :field_names, :fields, to: :output_context_class
11
- delegate(*ActiveModel::Validations::ClassMethods.instance_methods, to: :input_context_class, prefix: :input)
12
- delegate(*ActiveModel::Validations::HelperMethods.instance_methods, to: :input_context_class, prefix: :input)
13
- delegate(*ActiveModel::Validations::ClassMethods.instance_methods, to: :output_context_class, prefix: :output)
14
- delegate(*ActiveModel::Validations::HelperMethods.instance_methods, to: :output_context_class, prefix: :output)
15
-
16
- def input_context_class
17
- @input_context_class ||= const_set(:InputContext, Class.new(Context::Input))
18
- end
19
-
20
- def accepts_arguments_matching(set_input_context_class)
21
- @input_context_class = set_input_context_class
22
- end
23
- alias input_context accepts_arguments_matching
24
- alias input_type accepts_arguments_matching
25
-
26
- def output_context_class
27
- @output_context_class ||= const_set(:OutputContext, Class.new(Context::Output))
28
- end
29
-
30
- def returns_data_matching(set_output_context_class)
31
- @output_context_class = set_output_context_class
32
- end
33
- alias output_context returns_data_matching
34
- alias output_type returns_data_matching
35
-
36
- def runtime_context_class
37
- @runtime_context_class ||= begin
38
- context_class = const_set(:RuntimeContext, Class.new(Context::Runtime))
39
- context_class.send(:attribute_set).merge(input_context_class.send(:attribute_set).attributes)
40
- context_class.send(:attribute_set).merge(output_context_class.send(:attribute_set).attributes)
41
- context_class
42
- end
43
- end
44
-
45
- protected
46
-
47
- def result_context
48
- @result_context ||= Context::Result.register_owner(self)
49
- end
50
- end
51
-
52
- included do
53
- extend ClassMethods
54
-
55
- protected
56
-
57
- attr_reader :context
58
- end
59
-
60
- protected # rubocop:disable Lint/UselessAccessModifier
61
-
62
- def generate_and_validate_output_context!
63
- @output = self.class.output_context_class.new(context.attributes)
64
- @output.valid?
65
- return if @output.errors.empty?
66
-
67
- raise Error, Result.failure(errors: @output.errors, status: Result::STATUS[:failed_at_output])
68
- end
69
-
70
- def output_to_result_context!
71
- self.class.send(:result_context).for_output_context(self.class, @output)
72
- end
73
-
74
- def validate_input_and_generate_runtime_context!
75
- @input = self.class.input_context_class.new(@raw_input)
76
- @input.valid?
77
- return (@context = self.class.runtime_context_class.new(@raw_input)) if @input.errors.empty?
78
-
79
- raise Error, Result.failure(errors: @input.errors, status: Result::STATUS[:failed_at_input])
80
- end
81
- end
82
- end
83
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveInteractor
4
- module Interactor
5
- module InteractionMethods
6
- extend ActiveSupport::Concern
7
-
8
- module ClassMethods
9
- def perform!(input_context = {})
10
- new(input_context).perform!
11
- end
12
-
13
- def perform(input_context = {})
14
- perform!(input_context)
15
- rescue Error => e
16
- e.result
17
- rescue StandardError => e
18
- Result.failure(errors: e.message)
19
- end
20
- end
21
-
22
- included do
23
- extend ClassMethods
24
- end
25
-
26
- def perform
27
- perform!
28
- rescue Error => e
29
- e.result
30
- rescue StandardError => e
31
- Result.failure(errors: e.message)
32
- end
33
-
34
- def interact; end
35
- def rollback; end
36
- end
37
- end
38
- end