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 +4 -4
- data/README.md +79 -69
- data/interaktor.gemspec +2 -2
- data/lib/interaktor/attributes.rb +24 -0
- data/lib/interaktor/callable.rb +42 -262
- data/lib/interaktor/error/attribute_error.rb +2 -2
- data/lib/interaktor/error/attribute_validation_error.rb +20 -0
- data/lib/interaktor/error/missing_explicit_success_error.rb +7 -2
- data/lib/interaktor/error/organizer_missing_passed_attribute_error.rb +7 -7
- data/lib/interaktor/error/organizer_success_attribute_missing_error.rb +4 -4
- data/lib/interaktor/error/unknown_attribute_error.rb +4 -4
- data/lib/interaktor/interaction.rb +83 -29
- data/lib/interaktor/organizer.rb +7 -35
- data/lib/interaktor.rb +2 -18
- data/spec/interaktor/hooks_spec.rb +357 -363
- data/spec/interaktor/organizer_spec.rb +57 -48
- data/spec/support/helpers.rb +13 -2
- data/spec/support/lint.rb +219 -271
- metadata +11 -11
- data/lib/interaktor/error/attribute_schema_validation_error.rb +0 -54
- data/spec/interaktor/context_spec.rb +0 -187
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3821ddb2f6707bec7ea45c0a7043b193cd7623be4c51bd83268806d76c652db
|
|
4
|
+
data.tar.gz: 0bc4cfb698d304703ad26e9aeb3130485a7fa77fa0d29e309224e191755d2c1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
83
|
+
#### Success and failure attributes
|
|
61
84
|
|
|
62
|
-
|
|
85
|
+
Based on the outcome of the interaktor's work, we can define attributes to be provided.
|
|
63
86
|
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
attribute :name, :string
|
|
97
|
+
|
|
98
|
+
validates :name, presence: true
|
|
76
99
|
end
|
|
77
100
|
|
|
78
101
|
success do
|
|
79
|
-
|
|
102
|
+
attribute :user_id, :integer
|
|
103
|
+
|
|
104
|
+
validates :user_id, presence: true
|
|
80
105
|
end
|
|
81
106
|
|
|
82
107
|
failure do
|
|
83
|
-
|
|
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
|
|
285
|
+
class PrintAThing
|
|
257
286
|
include Interaktor
|
|
258
287
|
|
|
259
|
-
input
|
|
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
|
-
|
|
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
|
|
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
|
|
303
|
+
class DoSomeThingsInOrder
|
|
291
304
|
include Interaktor::Organizer
|
|
292
305
|
|
|
293
306
|
input do
|
|
294
|
-
|
|
295
|
-
end
|
|
307
|
+
attribute :name, :string
|
|
296
308
|
|
|
297
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
357
|
+
attribute :order_params
|
|
358
|
+
|
|
359
|
+
validates :order_params, presence: true
|
|
349
360
|
end
|
|
350
361
|
|
|
351
362
|
success do
|
|
352
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 "
|
|
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
|
data/lib/interaktor/callable.rb
CHANGED
|
@@ -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
|
|
7
|
+
base.class_eval do
|
|
8
|
+
extend ClassMethods
|
|
9
|
+
end
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
module ClassMethods
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
end
|
|
53
|
-
end
|
|
21
|
+
const_set(:InputAttributesModel, klass)
|
|
54
22
|
|
|
55
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
39
|
+
const_set(:FailureAttributesModel, klass)
|
|
146
40
|
|
|
147
|
-
|
|
41
|
+
klass.check_for_disallowed_attribute_names!
|
|
148
42
|
end
|
|
149
43
|
end
|
|
150
44
|
|
|
151
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
#
|
|
169
|
-
# @return [void]
|
|
170
|
-
def validate_failure_schema(args)
|
|
171
|
-
return if !failure_schema
|
|
53
|
+
const_set(:SuccessAttributesModel, klass)
|
|
172
54
|
|
|
173
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|