activeinteractor 2.0.0.alpha.3.0.1 → 2.0.0.alpha.4.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58a8598be9a7d0407430fcda338c3ad1b7a37c922561701abc928398a72d9b2b
4
- data.tar.gz: f206517d7852204973b1a0155a542ae9b37b2b361df65a00a5af846a42512d21
3
+ metadata.gz: fc451865dfb531e53701af2784f24db10856e07edff20046bd034c389e959121
4
+ data.tar.gz: cfc832189a947dc7be86b9ae84e7cc933ae5220e8d2ce83bc0ce3a3e8a45f529
5
5
  SHA512:
6
- metadata.gz: 456111cd323e168b19e73aa509a90cd39eca61500828ab3f3a8083a41386b82ed7082fedc4741fbdc702755454a048c20f89f92e8d38a2d557311d58ad89c4db
7
- data.tar.gz: 2741a8657cbaff1139be07b09624668d220dd246a23edff71dcdf65ef26ea461c1d7a30cd252d84cf50121e4196f941dc7f391786de6ee30996918f29f7786a7
6
+ metadata.gz: 59502fe574c00069b29558599baaf545201e7a48a4d7439bbafa5ee7f2a3eb3ba797346f086d18956fd28369bb712b6c137dfc977fdce2d6e1eac043a7eae0de
7
+ data.tar.gz: 79bc149020a1cbf36090a1937de49689312e2b62a1001e4d06e4e0d6cf83557876009416ab9fe5d839520610dfbd3989cbc4c4cdcb7758ed18942a2f0e8ef23d
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Allen
8
- autorequire:
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,13 +89,13 @@ 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.1/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.1
95
+ documentation_uri: https://activeinteractor.org/api/activeinteractor/v2.0.0-alpha.4.0.1
99
96
  wiki_uri: https://github.com/activeinteractor/activeinteractor/wiki
100
97
  rubygems_mfa_required: 'true'
101
- post_install_message:
98
+ post_install_message:
102
99
  rdoc_options: []
103
100
  require_paths:
104
101
  - lib
@@ -113,8 +110,8 @@ 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
117
- signing_key:
113
+ rubygems_version: 3.2.33
114
+ signing_key:
118
115
  specification_version: 4
119
116
  summary: Ruby interactors with ActiveModel::Validations
120
117
  test_files: []
@@ -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