interaktor 0.5.1 → 0.6.0.pre

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: c53ee54c903489b487c825865dd7e8c7d6ef7aaca737e62b656b48922484a972
4
- data.tar.gz: f375cb18e249b2503ebadd167ffcd41e830e646a677944721c2b004d42c50deb
3
+ metadata.gz: d3821ddb2f6707bec7ea45c0a7043b193cd7623be4c51bd83268806d76c652db
4
+ data.tar.gz: 0bc4cfb698d304703ad26e9aeb3130485a7fa77fa0d29e309224e191755d2c1f
5
5
  SHA512:
6
- metadata.gz: 3f1fc6e18e8057443f2b38346051180970fc5b9d9ce98c9420e3943702c985ae2f2f2575139ce8fe3138063d975e23f29202c6d20a9722036b89b84fbff9647d
7
- data.tar.gz: d25819e237ea94598f607aaa9d9f3365ee0ceea48a3ae75685c950191cbeb791858186a3f684b11ab4aa7f10e54f6a7419688b0a9250e0fc6267814fb4907e29
6
+ metadata.gz: a3af0ae98c859a81df86bd02ca3eb085d053e9a436f27755cec325ec5a83ed782bb5ceffae3ce2e37cdecb780eefe72c217eecaac3ef0759ea13c0a127c9a1d8
7
+ data.tar.gz: 34223391e17c7c74a575b66e3ab70aca05e4effedd511f5b458d4e61fecbfd790776bce72c2cd53d85712fa29a73ef3e11377a622cf262a1362ebc866b750aca
data/README.md CHANGED
@@ -8,10 +8,10 @@
8
8
 
9
9
  Fundamentally, Interaktor is the same as Interactor, but with the following changes:
10
10
 
11
- - Required explicit definition of interaktor "attributes" which replaces the concept of the interaktor context. Attributes are defined using a schema DSL provided by [dry-schema](https://github.com/dry-rb/dry-schema), which allows for complex validation, if desired.
11
+ - Explicit definition of interaktor "attributes" which replaces the concept of the interaktor context. Attributes are defined using the DSL provided by [ActiveModel](https://rubygems.org/gems/activemodel), which allows for complex validation, if desired. This is the same DSL used internally by ActiveRecord, so you will be familiar with validations if you have experience with ActiveRecord.
12
12
  - The interaktor "context" is no longer a public-facing concept, all data/attribute accessors/setters are defined as attributes
13
13
  - Attributes passed to `#fail!` must be defined in advance
14
- - Interaktors support early-exit functionality through the use of `#success!`, which functions the same as `#fail!` in that you must define the required success attributes on the interaktor
14
+ - Interaktors support early-exit functionality through the use of `#success!`, which functions the same as `#fail!` in that you must define the success attributes on the interaktor
15
15
 
16
16
  ## Getting started
17
17
 
@@ -29,21 +29,44 @@ Interaktors are used to encapsulate your application's [business logic](http://e
29
29
 
30
30
  ### Attributes
31
31
 
32
- #### Input attributes
32
+ Attributes are defined using the DSL provided by ActiveModel, whose documentation can be found [here](https://api.rubyonrails.org/classes/ActiveModel/Attributes.html). The same DSL is used to define _input_ attributes, _success_ attributes, and _failure_ attributes.
33
+
34
+ The aforementioned documentation provides a reasonably comprehensive overview of the DSL is used. But generally a definition block looks like this:
35
+
36
+ ```ruby
37
+ attribute :name, :string
38
+ attribute :email, :string
39
+ attribute :date_of_birth
40
+
41
+ validates :name, presence: true
42
+ validates :email, presence: true, allow_nil: true
43
+ validates :date_of_birth, presence: true
44
+ ```
33
45
 
34
- Depending on its definition, an interaktor may require attributes to be passed in when it is invoked. These attributes contain everything the interaktor needs to do its work.
46
+ Defining an attribute requires a name, and optionally a type. The available types are not currently well documented, but can be found clearly in the source code of ActiveModel. At the time of writing (activemodel `8.1.1`), the following types are available: `big_integer`, `binary`, `boolean`, `date`, `datetime`, `decimal`, `float`, `immutable_string`, `integer`, `string`, and `time`.
35
47
 
36
- Attributes are defined using a schema DSL provided by the [dry-schema](https://github.com/dry-rb/dry-schema) gem. It allows the construction of schemas for validating attributes. The schema is typically provided as a block argument to the `input` class method as seen below.
48
+ Defining a type will allow ActiveModel to perform type-specific validation and coercion. This means that when a value is assigned to an attribute, ActiveModel will attempt to convert it to the specified type, and raise an error if the conversion is not possible.
37
49
 
38
- This example is an extremely simple case, and dry-schema supports highly complex schema validation, like type checking, nested hash data validation, and more. For more information on defining an attribute schema, please see the [dry-schema documentation website](https://dry-rb.org/gems/dry-schema). This link should take you to the latest version of dry-schema, but be sure to check that the version of dry-schema in your application bundle matches the documentation you are viewing.
50
+ The type argument can also be a class constant, but that class will need to define certain methods that ActiveModel relies on in order to work properly - the errors raised by ActiveModel should illustrate which methods are required.
51
+
52
+ Validations should look familiar to Rails developers. They are defined using the `validates` method, which takes a hash of options. The options are the same as those used in Rails, so you can refer to the Rails documentation for more information.
53
+
54
+ In general, it is recommended to be careful with validations. These are not database records - they are simply a way to ensure that the data passed into an interaktor is valid and consistent before it is processed. Consider thinking twice before adding validations more complicated than typical `nil`/`blank?` checks.
55
+
56
+ #### Input attributes
57
+
58
+ Input attributes are attributes that are passed into an interaktor when it is invoked. The following interaktor defines a required `name` attribute and an optional `email` attribute.
39
59
 
40
60
  ```ruby
41
61
  class CreateUser
42
62
  include Interaktor
43
63
 
44
64
  input do
45
- required(:name)
46
- optional(:email)
65
+ attribute :name, :string
66
+ attribute :email, :string
67
+
68
+ validates :name, presence: true
69
+ validates :email, presence: true, allow_nil: true
47
70
  end
48
71
 
49
72
  def call
@@ -54,33 +77,37 @@ class CreateUser
54
77
  end
55
78
  end
56
79
 
57
- CreateUser.call(name: "Foo Bar")
80
+ CreateUser.call!(name: "Foo Bar")
58
81
  ```
59
82
 
60
- `input` will also accept a `Dry::Schema::Params` object directly, if for some reason the schema needs to be constructed elsewhere.
83
+ #### Success and failure attributes
61
84
 
62
- **A note about type checking**: Type checking is cool, but Ruby is a dynamic language, and Ruby developers tend to utilize the idea of [duck typing](https://en.wikipedia.org/wiki/Duck_typing). Forcing the attributes of an interaktor to be of a certain type in order to validate might sound like a good idea, but it can often cause problems in situations where you might like to use duck typing, for example, when using stubs in tests.
85
+ Based on the outcome of the interaktor's work, we can define attributes to be provided.
63
86
 
64
- #### Output attributes
87
+ The use of `#success!` allows you to early return from an interaktor's work. If no `success` attributes are defined, and the `call` method finishes execution normally, then the interaktor is considered to to have completed successfully. Conversely, if `success` attributes are defined, and the `call` method finishes execution normally (i.e. without a raised error, and without calling `success!`), then an exception will be raised.
65
88
 
66
- Based on the outcome of the interaktor's work, we can require certain attributes. In the example below, we must succeed with a `user_id` attribute, and if we fail, we must provide an `error_messages` attribute.
67
-
68
- The use of `#success!` allows you to early-return from an interaktor's work. If no `success` attribute is provided, and the `call` method finishes execution normally, then the interaktor is considered to to have completed successfully.
89
+ In the example below, we must succeed with a `user_id` attribute, and if we fail, we must provide an `error_messages` attribute.
69
90
 
70
91
  ```ruby
71
92
  class CreateUser
72
93
  include Interaktor
73
94
 
74
95
  input do
75
- required(:name).filled(:string)
96
+ attribute :name, :string
97
+
98
+ validates :name, presence: true
76
99
  end
77
100
 
78
101
  success do
79
- required(:user_id).value(:integer)
102
+ attribute :user_id, :integer
103
+
104
+ validates :user_id, presence: true
80
105
  end
81
106
 
82
107
  failure do
83
- required(:error_messages).value(array[:string])
108
+ attribute :error_messages # string array
109
+
110
+ validates :error_messages, presence: true
84
111
  end
85
112
 
86
113
  def call
@@ -103,6 +130,8 @@ else
103
130
  end
104
131
  ```
105
132
 
133
+ The returned object is an instance of `Interaktor::Interaction`. Depending on whether the interaction was successful or not, the object will have different attributes and methods available. These methods are determined by the success and failure attributes defined on the interaktor. It is not possible to access input attributes on the Interaction object.
134
+
106
135
  #### Dealing with failure
107
136
 
108
137
  `#fail!` always throws an exception of type `Interaktor::Failure`.
@@ -253,29 +282,13 @@ There are two kinds of interaktors built into the Interaktor library: basic inte
253
282
  A basic interaktor is a class that includes `Interaktor` and defines `call`.
254
283
 
255
284
  ```ruby
256
- class AuthenticateUser
285
+ class PrintAThing
257
286
  include Interaktor
258
287
 
259
- input do
260
- required(:email).filled(:string)
261
- required(:password).filled(:string)
262
- end
263
-
264
- success do
265
- required(:user)
266
- required(:token).filled(:string)
267
- end
268
-
269
- failure do
270
- required(:message).filled(:string)
271
- end
288
+ input { attribute :name }
272
289
 
273
290
  def call
274
- if user = User.authenticate(email, password)
275
- success!(user: user, token: user.secret_token)
276
- else
277
- fail!(message: "authenticate_user.failure")
278
- end
291
+ puts name
279
292
  end
280
293
  end
281
294
  ```
@@ -284,18 +297,16 @@ Basic interaktors are the building blocks. They are your application's single-pu
284
297
 
285
298
  ### Organizers
286
299
 
287
- An organizer is an important variation on the basic interaktor. Its single purpose is to run _other_ interaktors.
300
+ An organizer is a variation on the basic interaktor. Its single purpose is to run _other_ interaktors.
288
301
 
289
302
  ```ruby
290
- class PlaceOrder
303
+ class DoSomeThingsInOrder
291
304
  include Interaktor::Organizer
292
305
 
293
306
  input do
294
- required(:order_params).filled(:hash)
295
- end
307
+ attribute :name, :string
296
308
 
297
- success do
298
- required(:order)
309
+ validates :name, presence: true
299
310
  end
300
311
 
301
312
  organize CreateOrder, ChargeCard, SendThankYou
@@ -325,14 +336,12 @@ class OrdersController < ApplicationController
325
336
  end
326
337
  ```
327
338
 
328
- The organizer passes its own input arguments (if present) into first interaktor that it organizes, which is called and executed using those arguments. For the following interaktors in the organize list, each interaktor receives its input arguments from the previous interaktor (both input arguments and success arguments, with success arguments taking priority in the case of a name collision).
339
+ The organizer passes its own input arguments (if present) into first interaktor that it organizes (in the above example, `CreateOrder`), which is called and executed using those arguments. For the following interaktors in the organize list, each interaktor receives its input arguments from the previous interaktor (both input arguments and success arguments, with success arguments taking priority in the case of a name collision).
329
340
 
330
- Any arguments which are _not_ accepted by the next interaktor (listed as required or optional input attributes) are dropped in the transition.
341
+ Any arguments which are _not_ accepted by the next interaktor (listed as an input attribute) are dropped in the transition.
331
342
 
332
343
  If the organizer specifies any success attributes, the final interaktor in the
333
- organized list must also specify those success attributes. In general, it is
334
- recommended to avoid using success attributes on an organizer in the first
335
- place, to avoid coupling between the organizer and the interaktors it organizes.
344
+ organized list must also specify those success attributes.
336
345
 
337
346
  #### Rollback
338
347
 
@@ -345,11 +354,15 @@ class CreateOrder
345
354
  include Interaktor
346
355
 
347
356
  input do
348
- required(:order_params).filled(:hash)
357
+ attribute :order_params
358
+
359
+ validates :order_params, presence: true
349
360
  end
350
361
 
351
362
  success do
352
- required(:order)
363
+ attribute :order
364
+
365
+ validates :order, presence: true
353
366
  end
354
367
 
355
368
  def call
@@ -379,17 +392,25 @@ class AuthenticateUser
379
392
  include Interaktor
380
393
 
381
394
  input do
382
- required(:email).filled(:string)
383
- required(:password).filled(:string)
395
+ attribute :email, :string
396
+ attribute :password, :string
397
+
398
+ validates :email, presence: true
399
+ validates :password, presence: true
384
400
  end
385
401
 
386
402
  success do
387
- required(:user)
388
- required(:token).filled(:string)
403
+ attribute :user
404
+ attribute :token, :string
405
+
406
+ validates :user, presence: true
407
+ validates :token, presence: true
389
408
  end
390
409
 
391
410
  failure do
392
- required(:message).filled(:string)
411
+ attribute :message
412
+
413
+ validates :message, presence: true
393
414
  end
394
415
 
395
416
  def call
@@ -421,11 +442,11 @@ describe AuthenticateUser do
421
442
  end
422
443
 
423
444
  it "provides the user" do
424
- expect(result.user).to eq(user)
445
+ expect(result.user).to eq user
425
446
  end
426
447
 
427
448
  it "provides the user's secret token" do
428
- expect(result.token).to eq("token")
449
+ expect(result.token).to eq "token"
429
450
  end
430
451
  end
431
452
 
@@ -456,18 +477,7 @@ It's a good idea to define your own interfaces to your models. Doing so makes it
456
477
  class AuthenticateUser
457
478
  include Interaktor
458
479
 
459
- input do
460
- required(:email).filled(:string)
461
- required(:password).filled(:string)
462
- end
463
-
464
- success do
465
- required(:user)
466
- end
467
-
468
- failure do
469
- required(:message).filled(:string)
470
- end
480
+ # ...
471
481
 
472
482
  def call
473
483
  user = User.find_by(email: email)
data/interaktor.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "interaktor"
3
- spec.version = "0.5.1"
3
+ spec.version = "0.6.0.pre"
4
4
 
5
5
  spec.author = "Taylor Thurlow"
6
6
  spec.email = "thurlow@hey.com"
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.required_ruby_version = ">= 3.0"
13
13
  spec.require_path = "lib"
14
14
 
15
- spec.add_runtime_dependency "dry-schema", "~> 1.0"
15
+ spec.add_runtime_dependency "activemodel", ">= 7.0.9"
16
16
  spec.add_runtime_dependency "zeitwerk", ">= 2"
17
17
 
18
18
  spec.add_development_dependency "rake", "~> 13.0"
@@ -0,0 +1,24 @@
1
+ require "active_model"
2
+
3
+ module Interaktor
4
+ class Attributes
5
+ include ::ActiveModel::Attributes
6
+ include ::ActiveModel::Serialization
7
+ include ::ActiveModel::Validations
8
+
9
+ if defined?(::ActiveModel::Attributes::Normalization)
10
+ include ::ActiveModel::Attributes::Normalization
11
+ end
12
+
13
+ DISALLOWED_ATTRIBUTE_NAMES = instance_methods
14
+ .map { |m| m.to_s.freeze }
15
+ .freeze
16
+
17
+ def self.check_for_disallowed_attribute_names!
18
+ attribute_names
19
+ .select { |name| DISALLOWED_ATTRIBUTE_NAMES.include?(name) }
20
+ .join(", ")
21
+ .tap { |names| raise ArgumentError, "Disallowed attribute name(s): #{names}" if names.present? }
22
+ end
23
+ end
24
+ end
@@ -1,279 +1,58 @@
1
- require "dry-schema"
2
-
3
- Dry::Schema.load_extensions(:info)
4
-
5
1
  module Interaktor::Callable
6
2
  # When the module is included in a class, add the relevant class methods to
7
3
  # that class.
8
4
  #
9
5
  # @param base [Class] the class which is including the module
10
6
  def self.included(base)
11
- base.class_eval { extend ClassMethods }
7
+ base.class_eval do
8
+ extend ClassMethods
9
+ end
12
10
  end
13
11
 
14
12
  module ClassMethods
15
- ####################
16
- # INPUT ATTRIBUTES #
17
- ####################
18
-
19
- # The list of attributes which are required to be passed in when calling
20
- # the interaktor.
21
- #
22
- # @return [Array<Symbol>]
23
- def required_input_attributes
24
- @required_input_attributes ||= input_schema
25
- .info[:keys]
26
- .select { |_, info| info[:required] }
27
- .keys
28
- end
29
-
30
- # The list of attributes which are not required to be passed in when
31
- # calling the interaktor.
32
- #
33
- # @return [Array<Symbol>]
34
- def optional_input_attributes
35
- # Adding an optional attribute with NO predicates with Dry::Schema is
36
- # sort of a "nothing statement" - the schema can sort of ignore it. The
37
- # problem is that the optional-with-no-predicate key is not included in
38
- # the #info results, so we need to find an list of keys elsewhere, find
39
- # the ones that are listed there but not in the #info results, and find
40
- # the difference. The result are the keys that are omitted from the #info
41
- # result because they are optional and have no predicates.
42
- #
43
- # See https://github.com/dry-rb/dry-schema/issues/347
44
- @optional_input_attributes ||= begin
45
- attributes_in_info = input_schema.info[:keys].keys
46
- all_attributes = input_schema.key_map.keys.map(&:id)
47
- optional_attributes_by_exclusion = all_attributes - attributes_in_info
13
+ def input(&block)
14
+ raise "Input block already defined" if defined?(self::InputAttributesModel)
48
15
 
49
- explicitly_optional_attributes = input_schema.info[:keys].reject { |_, info| info[:required] }.keys
16
+ # Define self::InputAttributesModel
17
+ Class.new(Interaktor::Attributes, &block).tap do |klass|
18
+ klass.define_singleton_method(:inspect) { name.to_s }
19
+ klass.define_singleton_method(:to_s) { inspect }
50
20
 
51
- explicitly_optional_attributes + optional_attributes_by_exclusion
52
- end
53
- end
21
+ const_set(:InputAttributesModel, klass)
54
22
 
55
- # The complete list of input attributes.
56
- #
57
- # @return [Array<Symbol>]
58
- def input_attributes
59
- required_input_attributes + optional_input_attributes
60
- end
61
-
62
- # Get the input attribute schema. Fall back to an empty schema with a
63
- # configuration that will deny ALL provided attributes - not defining an
64
- # input schema should mean the interaktor has no input attributes.
65
- #
66
- # @return [Dry::Schema::Params]
67
- def input_schema
68
- @input_schema || Dry::Schema.Params
69
- end
70
-
71
- # @param args [Hash]
72
- def validate_input_schema(args)
73
- return if !input_schema
74
-
75
- if (errors = input_schema.call(args).errors).any?
76
- raise Interaktor::Error::AttributeSchemaValidationError.new(
77
- self,
78
- errors.to_h
79
- )
80
- end
81
- end
23
+ klass.check_for_disallowed_attribute_names!
82
24
 
83
- # @param schema [Dry::Schema::Params, nil] a predefined schema object
84
- # @yield a new Dry::Schema::Params definition block
85
- def input(schema = nil, &block)
86
- raise "No schema or schema definition block provided to interaktor input." if schema.nil? && !block
87
-
88
- raise "Provided both a schema and a schema definition block for interaktor input." if schema && block
89
-
90
- if schema
91
- raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
92
-
93
- @input_schema = schema
94
- elsif block
95
- @input_schema = Dry::Schema.Params { instance_eval(&block) }
96
- end
97
-
98
- # define the getters and setters for the input attributes
99
- @input_schema.key_map.keys.each do |key| # rubocop:disable Style/HashEachMethods
100
- attribute_name = key.id
101
-
102
- # Define getter
103
- define_method(attribute_name) { @interaction.send(attribute_name) }
104
-
105
- # Define setter
106
- define_method(:"#{attribute_name}=") do |value|
107
- @interaction.send(:"#{attribute_name}=", value)
25
+ klass.attribute_names.each do |name|
26
+ define_method(name) { @interaction.send(name) }
108
27
  end
109
28
  end
110
29
  end
111
30
 
112
- ######################
113
- # FAILURE ATTRIBUTES #
114
- ######################
115
-
116
- # The list of attributes which are required to be provided when failing the
117
- # interaktor.
118
- #
119
- # @return [Array<Symbol>]
120
- def required_failure_attributes
121
- @required_failure_attributes ||= failure_schema.info[:keys]
122
- .select { |_, info| info[:required] }
123
- .keys
124
- end
31
+ def failure(&block)
32
+ raise "Failure block already defined" if defined?(self::FailureAttributesModel)
125
33
 
126
- # The list of attributes which are not required to be provided when failing
127
- # the interaktor.
128
- #
129
- # @return [Array<Symbol>]
130
- def optional_failure_attributes
131
- # Adding an optional attribute with NO predicates with Dry::Schema is
132
- # sort of a "nothing statement" - the schema can sort of ignore it. The
133
- # problem is that the optional-with-no-predicate key is not included in
134
- # the #info results, so we need to find an list of keys elsewhere, find
135
- # the ones that are listed there but not in the #info results, and find
136
- # the difference. The result are the keys that are omitted from the #info
137
- # result because they are optional and have no predicates.
138
- #
139
- # See https://github.com/dry-rb/dry-schema/issues/347
140
- @optional_failure_attributes ||= begin
141
- attributes_in_info = failure_schema.info[:keys].keys
142
- all_attributes = failure_schema.key_map.keys.map(&:id)
143
- optional_attributes_by_exclusion = all_attributes - attributes_in_info
34
+ # Define self::FailureAttributesModel
35
+ Class.new(Interaktor::Attributes, &block).tap do |klass|
36
+ klass.define_singleton_method(:inspect) { name.to_s }
37
+ klass.define_singleton_method(:to_s) { inspect }
144
38
 
145
- explicitly_optional_attributes = failure_schema.info[:keys].reject { |_, info| info[:required] }.keys
39
+ const_set(:FailureAttributesModel, klass)
146
40
 
147
- explicitly_optional_attributes + optional_attributes_by_exclusion
41
+ klass.check_for_disallowed_attribute_names!
148
42
  end
149
43
  end
150
44
 
151
- # The complete list of failure attributes.
152
- #
153
- # @return [Array<Symbol>]
154
- def failure_attributes
155
- required_failure_attributes + optional_failure_attributes
156
- end
45
+ def success(&block)
46
+ raise "Success block already defined" if defined?(self::SuccessAttributesModel)
157
47
 
158
- # Get the failure attribute schema. Fall back to an empty schema with a
159
- # configuration that will deny ALL provided attributes - not defining an
160
- # failure schema should mean the interaktor has no failure attributes.
161
- #
162
- # @return [Dry::Schema::Params]
163
- def failure_schema
164
- @failure_schema || Dry::Schema.Params
165
- end
48
+ # Define self::SuccessAttributesModel
49
+ Class.new(Interaktor::Attributes, &block).tap do |klass|
50
+ klass.define_singleton_method(:inspect) { name.to_s }
51
+ klass.define_singleton_method(:to_s) { inspect }
166
52
 
167
- # @param args [Hash]
168
- #
169
- # @return [void]
170
- def validate_failure_schema(args)
171
- return if !failure_schema
53
+ const_set(:SuccessAttributesModel, klass)
172
54
 
173
- if (errors = failure_schema.call(args).errors).any?
174
- raise Interaktor::Error::AttributeSchemaValidationError.new(
175
- self,
176
- errors.to_h
177
- )
178
- end
179
- end
180
-
181
- # @param schema [Dry::Schema::Params, nil] a predefined schema object
182
- # @yield a new Dry::Schema::Params definition block
183
- def failure(schema = nil, &block)
184
- raise "No schema or schema definition block provided to interaktor failure method." if schema.nil? && !block
185
-
186
- raise "Provided both a schema and a schema definition block for interaktor failure method." if schema && block
187
-
188
- if schema
189
- raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
190
-
191
- @failure_schema = schema
192
- elsif block
193
- @failure_schema = Dry::Schema.Params { instance_eval(&block) }
194
- end
195
- end
196
-
197
- ######################
198
- # SUCCESS ATTRIBUTES #
199
- ######################
200
-
201
- # The list of attributes which are required to be provided when the
202
- # interaktor succeeds.
203
- #
204
- # @return [Array<Symbol>]
205
- def required_success_attributes
206
- @required_success_attributes ||= success_schema.info[:keys]
207
- .select { |_, info| info[:required] }
208
- .keys
209
- end
210
-
211
- # The list of attributes which are not required to be provided when failing
212
- # the interaktor.
213
- #
214
- # @return [Array<Symbol>]
215
- def optional_success_attributes
216
- # Adding an optional attribute with NO predicates with Dry::Schema is
217
- # sort of a "nothing statement" - the schema can sort of ignore it. The
218
- # problem is that the optional-with-no-predicate key is not included in
219
- # the #info results, so we need to find an list of keys elsewhere, find
220
- # the ones that are listed there but not in the #info results, and find
221
- # the difference. The result are the keys that are omitted from the #info
222
- # result because they are optional and have no predicates.
223
- #
224
- # See https://github.com/dry-rb/dry-schema/issues/347
225
- @optional_success_attributes ||= begin
226
- attributes_in_info = success_schema.info[:keys].keys
227
- all_attributes = success_schema.key_map.keys.map(&:id)
228
- optional_attributes_by_exclusion = all_attributes - attributes_in_info
229
-
230
- explicitly_optional_attributes = success_schema.info[:keys].reject { |_, info| info[:required] }.keys
231
-
232
- explicitly_optional_attributes + optional_attributes_by_exclusion
233
- end
234
- end
235
-
236
- # The complete list of success attributes.
237
- #
238
- # @return [Array<Symbol>]
239
- def success_attributes
240
- required_success_attributes + optional_success_attributes
241
- end
242
-
243
- # Get the success attribute schema. Fall back to an empty schema with a
244
- # configuration that will deny ALL provided attributes - not defining an
245
- # success schema should mean the interaktor has no success attributes.
246
- #
247
- # @return [Dry::Schema::Params]
248
- def success_schema
249
- @success_schema || Dry::Schema.Params
250
- end
251
-
252
- # @param args [Hash]
253
- def validate_success_schema(args)
254
- return if !success_schema
255
-
256
- if (errors = success_schema.call(args).errors).any?
257
- raise Interaktor::Error::AttributeSchemaValidationError.new(
258
- self,
259
- errors.to_h
260
- )
261
- end
262
- end
263
-
264
- # @param schema [Dry::Schema::Params, nil] a predefined schema object
265
- # @yield a new Dry::Schema::Params definition block
266
- def success(schema = nil, &block)
267
- raise "No schema or schema definition block provided to interaktor success method." if schema.nil? && !block
268
-
269
- raise "Provided both a schema and a schema definition block for interaktor success method." if schema && block
270
-
271
- if schema
272
- raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
273
-
274
- @success_schema = schema
275
- elsif block
276
- @success_schema = Dry::Schema.Params { instance_eval(&block) }
55
+ klass.check_for_disallowed_attribute_names!
277
56
  end
278
57
  end
279
58
 
@@ -312,22 +91,23 @@ module Interaktor::Callable
312
91
  #
313
92
  # @return [Interaktor::Interaction]
314
93
  def execute(args, raise_exception:)
315
- run_method = raise_exception ? :run! : :run
316
-
317
- case args
318
- when Hash
319
- if (disallowed_key = args.keys.find { |k| !input_attributes.include?(k.to_sym) })
320
- raise Interaktor::Error::UnknownAttributeError.new(self, disallowed_key)
321
- end
322
-
323
- validate_input_schema(args)
324
- new(args).tap(&run_method).instance_variable_get(:@interaction)
325
- when Interaktor::Interaction
326
- new(args).tap(&run_method).instance_variable_get(:@interaction)
94
+ interaction = case args
95
+ when Hash, Interaktor::Interaction
96
+ new(args)
97
+ .tap(&(raise_exception ? :run! : :run))
98
+ .instance_variable_get(:@interaction)
327
99
  else
328
100
  raise ArgumentError,
329
101
  "Expected a hash argument when calling the interaktor, got a #{args.class} instead."
330
102
  end
103
+
104
+ if interaction.success? &&
105
+ !interaction.early_return? &&
106
+ defined?(self::SuccessAttributesModel)
107
+ raise Interaktor::Error::MissingExplicitSuccessError.new(self)
108
+ end
109
+
110
+ interaction
331
111
  end
332
112
  end
333
113
  end