interaktor 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/publish.yml +0 -1
- data/.github/workflows/tests.yml +5 -5
- data/.gitignore +3 -0
- data/Gemfile +1 -1
- data/Gemfile.ci +6 -0
- data/README.md +65 -29
- data/interaktor.gemspec +3 -2
- data/lib/interaktor.rb +11 -16
- data/lib/interaktor/callable.rb +235 -108
- data/lib/interaktor/error/attribute_error.rb +3 -1
- data/lib/interaktor/error/attribute_schema_validation_error.rb +54 -0
- data/lib/interaktor/error/missing_explicit_success_error.rb +5 -0
- data/lib/interaktor/error/option_error.rb +3 -1
- data/lib/interaktor/error/organizer_missing_passed_attribute_error.rb +21 -0
- data/lib/interaktor/error/organizer_success_attribute_missing_error.rb +20 -0
- data/lib/interaktor/organizer.rb +33 -7
- data/spec/integration_spec.rb +142 -71
- data/spec/{interactor → interaktor}/context_spec.rb +1 -1
- data/spec/{interactor → interaktor}/hooks_spec.rb +1 -1
- data/spec/interaktor/organizer_spec.rb +249 -0
- data/spec/interaktor_spec.rb +2 -2
- data/spec/spec_helper.rb +20 -0
- data/spec/support/helpers.rb +14 -0
- data/spec/support/lint.rb +403 -166
- metadata +31 -12
- data/.travis.yml +0 -14
- data/lib/interaktor/error/missing_attribute_error.rb +0 -5
- data/spec/interactor/organizer_spec.rb +0 -128
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 750eeac422a483d0c47ed00a3e457e97c43a134e611962f3a2316335d5c1a942
|
4
|
+
data.tar.gz: 6fb7f28d30d78c6dcca2ea25383bdb85576e83600d7de3cad58a13ffa5e14b94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 239222c9b5d7633b6c011d9e14ff1ec65f9612a3e1f13709ce8326c35bef28455b84f749a063927bb97e5387b8fd561019b3ead8ce681c7efc23c523736c777c
|
7
|
+
data.tar.gz: aaf8d1f62d5e86fbfa079ad2c3222d920eb3119999c7a4dc3809cb32799abe7bc8102db7bb431d3cf07f27cc06f9ef38abce9e22955396464440ccb61b9d6634
|
data/.github/workflows/tests.yml
CHANGED
@@ -7,10 +7,12 @@ jobs:
|
|
7
7
|
strategy:
|
8
8
|
fail-fast: false
|
9
9
|
matrix:
|
10
|
-
os: [ubuntu, macos]
|
11
|
-
ruby: [2.5, 2.6, 2.7, head, debug]
|
12
|
-
runs-on: ${{ matrix.os }}
|
10
|
+
os: [ubuntu-latest, macos-latest]
|
11
|
+
ruby: [2.5, 2.6, 2.7, 3.0, head, debug]
|
12
|
+
runs-on: ${{ matrix.os }}
|
13
13
|
continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
|
14
|
+
env:
|
15
|
+
BUNDLE_GEMFILE: "Gemfile.ci"
|
14
16
|
steps:
|
15
17
|
- uses: actions/checkout@v2
|
16
18
|
- name: Set up Ruby
|
@@ -18,7 +20,5 @@ jobs:
|
|
18
20
|
with:
|
19
21
|
ruby-version: ${{ matrix.ruby }}
|
20
22
|
bundler-cache: true
|
21
|
-
- name: Install dependencies
|
22
|
-
run: bundle install
|
23
23
|
- name: Run tests
|
24
24
|
run: bundle exec rspec
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -2,12 +2,12 @@ source "https://rubygems.org"
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
+
gem "guard-rspec", require: false
|
5
6
|
gem "rubocop"
|
6
7
|
gem "rubocop-performance"
|
7
8
|
gem "rubocop-rspec"
|
8
9
|
gem "rufo", "~> 0.12.0"
|
9
10
|
gem "solargraph"
|
10
|
-
gem "guard-rspec", require: false
|
11
11
|
|
12
12
|
group :test do
|
13
13
|
gem "pry-byebug", platforms: [:mri]
|
data/Gemfile.ci
ADDED
data/README.md
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
**DISCLAIMER: Interaktor is under active development. Feel free to use it, but until 1.0 is released, any update could break compatibility with an older version.**
|
7
7
|
|
8
|
-
**Interaktor** is a fork of [
|
8
|
+
**Interaktor** is a fork of [Interactor by collectiveidea](https://github.com/collectiveidea/interactor). While Interactor is still used by collectiveidea internally, communication and progress has been slow in adapting to pull requests and issues. This inactivity combined with my desire to dial back on the Interactor's inherent permissivity led me to fork it and create Interaktor.
|
9
9
|
|
10
10
|
Fundamentally, Interaktor is the same as Interactor, but with the following changes:
|
11
11
|
|
12
|
-
- Required explicit definition of interaktor "attributes" which replaces the concept of the interaktor context. Attributes
|
12
|
+
- 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.
|
13
13
|
- The interaktor "context" is no longer a public-facing concept, all data/attribute accessors/setters are defined as attributes
|
14
14
|
- Attributes passed to `#fail!` must be defined in advance
|
15
15
|
- 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
|
@@ -34,15 +34,18 @@ Interaktors are used to encapsulate your application's [business logic](http://e
|
|
34
34
|
|
35
35
|
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.
|
36
36
|
|
37
|
-
|
37
|
+
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.
|
38
|
+
|
39
|
+
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.
|
38
40
|
|
39
41
|
```ruby
|
40
42
|
class CreateUser
|
41
43
|
include Interaktor
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
|
45
|
+
input do
|
46
|
+
required(:name)
|
47
|
+
optional(:email)
|
48
|
+
end
|
46
49
|
|
47
50
|
def call
|
48
51
|
User.create!(
|
@@ -53,9 +56,12 @@ class CreateUser
|
|
53
56
|
end
|
54
57
|
|
55
58
|
CreateUser.call(name: "Foo Bar")
|
56
|
-
|
57
59
|
```
|
58
60
|
|
61
|
+
`input` will also accept a `Dry::Schema::Params` object directly, if for some reason the schema needs to be constructed elsewhere.
|
62
|
+
|
63
|
+
**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.
|
64
|
+
|
59
65
|
#### Output attributes
|
60
66
|
|
61
67
|
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.
|
@@ -66,11 +72,17 @@ The use of `#success!` allows you to early-return from an interaktor's work. If
|
|
66
72
|
class CreateUser
|
67
73
|
include Interaktor
|
68
74
|
|
69
|
-
|
75
|
+
input do
|
76
|
+
required(:name).filled(:string)
|
77
|
+
end
|
70
78
|
|
71
|
-
success
|
79
|
+
success do
|
80
|
+
required(:user_id).value(:integer)
|
81
|
+
end
|
72
82
|
|
73
|
-
failure
|
83
|
+
failure do
|
84
|
+
required(:error_messages).value(array[:string])
|
85
|
+
end
|
74
86
|
|
75
87
|
def call
|
76
88
|
user = User.new(name: name)
|
@@ -98,7 +110,7 @@ end
|
|
98
110
|
|
99
111
|
Normally, however, these exceptions are not seen. In the recommended usage, the caller invokes the interaktor using the class method `.call`, then checks the `#success?` method of the returned object. This works because the `call` class method swallows exceptions. When unit testing an interaktor, if calling custom business logic methods directly and bypassing `call`, be aware that `fail!` will generate such exceptions.
|
100
112
|
|
101
|
-
See _Interaktors in the controller_, below, for the recommended usage of
|
113
|
+
See _Interaktors in the controller_, below, for the recommended usage of `.call` and `#success?`.
|
102
114
|
|
103
115
|
### Hooks
|
104
116
|
|
@@ -235,13 +247,19 @@ A basic interaktor is a class that includes `Interaktor` and defines `call`.
|
|
235
247
|
class AuthenticateUser
|
236
248
|
include Interaktor
|
237
249
|
|
238
|
-
|
239
|
-
|
250
|
+
input do
|
251
|
+
required(:email).filled(:string)
|
252
|
+
required(:password).filled(:string)
|
253
|
+
end
|
240
254
|
|
241
|
-
success
|
242
|
-
|
255
|
+
success do
|
256
|
+
required(:user)
|
257
|
+
required(:token).filled(:string)
|
258
|
+
end
|
243
259
|
|
244
|
-
failure
|
260
|
+
failure do
|
261
|
+
required(:message).filled(:string)
|
262
|
+
end
|
245
263
|
|
246
264
|
def call
|
247
265
|
if user = User.authenticate(email, password)
|
@@ -263,7 +281,9 @@ An organizer is an important variation on the basic interaktor. Its single purpo
|
|
263
281
|
class PlaceOrder
|
264
282
|
include Interaktor::Organizer
|
265
283
|
|
266
|
-
|
284
|
+
input do
|
285
|
+
required(:order_params).filled(:hash)
|
286
|
+
end
|
267
287
|
|
268
288
|
organize CreateOrder, ChargeCard, SendThankYou
|
269
289
|
end
|
@@ -304,9 +324,13 @@ In addition, any interaktors that had already run are given the chance to undo t
|
|
304
324
|
class CreateOrder
|
305
325
|
include Interaktor
|
306
326
|
|
307
|
-
|
327
|
+
input do
|
328
|
+
required(:order_params).filled(:hash)
|
329
|
+
end
|
308
330
|
|
309
|
-
success
|
331
|
+
success do
|
332
|
+
required(:order)
|
333
|
+
end
|
310
334
|
|
311
335
|
def call
|
312
336
|
order = Order.create(order_params)
|
@@ -334,13 +358,19 @@ When written correctly, an interaktor is easy to test because it only _does_ one
|
|
334
358
|
class AuthenticateUser
|
335
359
|
include Interaktor
|
336
360
|
|
337
|
-
|
338
|
-
|
361
|
+
input do
|
362
|
+
required(:email).filled(:string)
|
363
|
+
required(:password).filled(:string)
|
364
|
+
end
|
339
365
|
|
340
|
-
success
|
341
|
-
|
366
|
+
success do
|
367
|
+
required(:user)
|
368
|
+
required(:token).filled(:string)
|
369
|
+
end
|
342
370
|
|
343
|
-
failure
|
371
|
+
failure do
|
372
|
+
required(:message).filled(:string)
|
373
|
+
end
|
344
374
|
|
345
375
|
def call
|
346
376
|
if user = User.authenticate(email, password)
|
@@ -406,12 +436,18 @@ It's a good idea to define your own interfaces to your models. Doing so makes it
|
|
406
436
|
class AuthenticateUser
|
407
437
|
include Interaktor
|
408
438
|
|
409
|
-
|
410
|
-
|
439
|
+
input do
|
440
|
+
required(:email).filled(:string)
|
441
|
+
required(:password).filled(:string)
|
442
|
+
end
|
411
443
|
|
412
|
-
success
|
444
|
+
success do
|
445
|
+
required(:user)
|
446
|
+
end
|
413
447
|
|
414
|
-
failure
|
448
|
+
failure do
|
449
|
+
required(:message).filled(:string)
|
450
|
+
end
|
415
451
|
|
416
452
|
def call
|
417
453
|
user = User.find_by(email: email)
|
@@ -515,4 +551,4 @@ This controller test will have to change very little during the life of the appl
|
|
515
551
|
|
516
552
|
### Rails
|
517
553
|
|
518
|
-
Interactor provided [interactor-rails](https://github.com/collectiveidea/interactor-rails), which ensures `app/interactors` is included in your autoload paths, and provides generators for new interactors. I have no intention of maintaining generators but if someone feels strongly enough to submit a pull request to include the functionality in _this_ gem (not a separate Rails one) then I will be happy to take a look. Making sure `app/
|
554
|
+
Interactor provided [interactor-rails](https://github.com/collectiveidea/interactor-rails), which ensures `app/interactors` is included in your autoload paths, and provides generators for new interactors. I have no intention of maintaining generators but if someone feels strongly enough to submit a pull request to include the functionality in _this_ gem (not a separate Rails one) then I will be happy to take a look. Making sure `app/interaktors` is included in your autoload paths is something I would like to do soon.
|
data/interaktor.gemspec
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
Gem::Specification.new do |spec|
|
2
2
|
spec.name = "interaktor"
|
3
|
-
spec.version = "0.
|
3
|
+
spec.version = "0.3.0"
|
4
4
|
|
5
5
|
spec.author = "Taylor Thurlow"
|
6
|
-
spec.email = "
|
6
|
+
spec.email = "thurlow@hey.com"
|
7
7
|
spec.description = "A common interface for building service objects."
|
8
8
|
spec.summary = "Simple service object implementation"
|
9
9
|
spec.homepage = "https://github.com/taylorthurlow/interaktor"
|
@@ -13,6 +13,7 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.required_ruby_version = ">= 2.5"
|
14
14
|
spec.require_path = "lib"
|
15
15
|
|
16
|
+
spec.add_runtime_dependency "dry-schema", "~> 1.0"
|
16
17
|
spec.add_runtime_dependency "zeitwerk", "~> 2.0"
|
17
18
|
|
18
19
|
spec.add_development_dependency "rake", "~> 13.0"
|
data/lib/interaktor.rb
CHANGED
@@ -32,11 +32,11 @@ module Interaktor
|
|
32
32
|
#
|
33
33
|
# @return [void]
|
34
34
|
def fail!(failure_attributes = {})
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
# Silently remove any attributes that are not included in the schema
|
36
|
+
allowed_keys = self.class.failure_schema.key_map.keys.map { |k| k.name.to_sym }
|
37
|
+
failure_attributes.select! { |k, _| allowed_keys.include?(k.to_sym) }
|
38
|
+
|
39
|
+
self.class.validate_failure_schema(failure_attributes)
|
40
40
|
|
41
41
|
@context.fail!(failure_attributes)
|
42
42
|
end
|
@@ -48,16 +48,11 @@ module Interaktor
|
|
48
48
|
#
|
49
49
|
# @return [void]
|
50
50
|
def success!(success_attributes = {})
|
51
|
-
#
|
52
|
-
|
53
|
-
|
54
|
-
.reject { |success_attr| success_attributes.key?(success_attr) }
|
55
|
-
raise Interaktor::Error::MissingAttributeError.new(self.class.to_s, missing_attrs) if missing_attrs.any?
|
51
|
+
# Silently remove any attributes that are not included in the schema
|
52
|
+
allowed_keys = self.class.success_schema.key_map.keys.map { |k| k.name.to_sym }
|
53
|
+
success_attributes.select! { |k, _| allowed_keys.include?(k.to_sym) }
|
56
54
|
|
57
|
-
|
58
|
-
unknown_attrs = success_attributes.keys
|
59
|
-
.reject { |success_attr| self.class.success_attributes.include?(success_attr) }
|
60
|
-
raise Interaktor::Error::UnknownAttributeError.new(self.class.to_s, unknown_attrs) if unknown_attrs.any?
|
55
|
+
self.class.validate_success_schema(success_attributes)
|
61
56
|
|
62
57
|
@context.success!(success_attributes)
|
63
58
|
end
|
@@ -104,8 +99,8 @@ module Interaktor
|
|
104
99
|
call
|
105
100
|
end
|
106
101
|
|
107
|
-
if !@context.early_return? && self.class.
|
108
|
-
raise Interaktor::Error::
|
102
|
+
if !@context.early_return? && self.class.required_success_attributes.any?
|
103
|
+
raise Interaktor::Error::MissingExplicitSuccessError.new(self, self.class.required_success_attributes)
|
109
104
|
end
|
110
105
|
|
111
106
|
@context.called!(self)
|
data/lib/interaktor/callable.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
require "dry-schema"
|
2
|
+
|
3
|
+
Dry::Schema.load_extensions(:info)
|
4
|
+
|
1
5
|
module Interaktor::Callable
|
2
6
|
# When the module is included in a class, add the relevant class methods to
|
3
7
|
# that class.
|
@@ -8,131 +12,277 @@ module Interaktor::Callable
|
|
8
12
|
end
|
9
13
|
|
10
14
|
module ClassMethods
|
15
|
+
####################
|
16
|
+
# INPUT ATTRIBUTES #
|
17
|
+
####################
|
18
|
+
|
11
19
|
# The list of attributes which are required to be passed in when calling
|
12
20
|
# the interaktor.
|
13
21
|
#
|
14
22
|
# @return [Array<Symbol>]
|
15
|
-
def
|
16
|
-
@
|
23
|
+
def required_input_attributes
|
24
|
+
@required_input_attributes ||= input_schema.info[:keys]
|
25
|
+
.select { |_, info| info[:required] }
|
26
|
+
.keys
|
17
27
|
end
|
18
28
|
|
19
|
-
# The list of attributes which are
|
29
|
+
# The list of attributes which are not required to be passed in when
|
20
30
|
# calling the interaktor.
|
21
31
|
#
|
22
32
|
# @return [Array<Symbol>]
|
23
|
-
def
|
24
|
-
|
33
|
+
def optional_input_attributes
|
34
|
+
# Adding an optional attribute with NO predicates with Dry::Schema is
|
35
|
+
# sort of a "nothing statement" - the schema can sort of ignore it. The
|
36
|
+
# problem is that the optional-with-no-predicate key is not included in
|
37
|
+
# the #info results, so we need to find an list of keys elsewhere, find
|
38
|
+
# the ones that are listed there but not in the #info results, and find
|
39
|
+
# the difference. The result are the keys that are omitted from the #info
|
40
|
+
# result because they are optional and have no predicates.
|
41
|
+
#
|
42
|
+
# See https://github.com/dry-rb/dry-schema/issues/347
|
43
|
+
@optional_input_attributes ||= begin
|
44
|
+
attributes_in_info = input_schema.info[:keys].keys
|
45
|
+
all_attributes = input_schema.key_map.keys.map(&:id)
|
46
|
+
optional_attributes_by_exclusion = all_attributes - attributes_in_info
|
47
|
+
|
48
|
+
explicitly_optional_attributes = input_schema.info[:keys].reject { |_, info| info[:required] }.keys
|
49
|
+
|
50
|
+
explicitly_optional_attributes + optional_attributes_by_exclusion
|
51
|
+
end
|
25
52
|
end
|
26
53
|
|
27
|
-
#
|
54
|
+
# The complete list of input attributes.
|
28
55
|
#
|
29
56
|
# @return [Array<Symbol>]
|
30
|
-
def
|
31
|
-
|
57
|
+
def input_attributes
|
58
|
+
required_input_attributes + optional_input_attributes
|
32
59
|
end
|
33
60
|
|
34
|
-
#
|
61
|
+
# Get the input attribute schema. Fall back to an empty schema with a
|
62
|
+
# configuration that will deny ALL provided attributes - not defining an
|
63
|
+
# input schema should mean the interaktor has no input attributes.
|
35
64
|
#
|
36
|
-
# @return [
|
37
|
-
def
|
38
|
-
|
65
|
+
# @return [Dry::Schema::Params]
|
66
|
+
def input_schema
|
67
|
+
@input_schema || Dry::Schema.Params
|
39
68
|
end
|
40
69
|
|
41
|
-
#
|
42
|
-
# `#fail!` from within the interaktor.
|
70
|
+
# @param context [Hash]
|
43
71
|
#
|
44
|
-
# @return [
|
45
|
-
def
|
46
|
-
|
72
|
+
# @return [void]
|
73
|
+
def validate_input_schema(context)
|
74
|
+
return unless input_schema
|
75
|
+
|
76
|
+
result = input_schema.call(context)
|
77
|
+
|
78
|
+
if result.errors.any?
|
79
|
+
raise Interaktor::Error::AttributeSchemaValidationError.new(
|
80
|
+
self,
|
81
|
+
result.errors.to_h,
|
82
|
+
)
|
83
|
+
end
|
47
84
|
end
|
48
85
|
|
49
|
-
#
|
50
|
-
#
|
86
|
+
# @param schema [Dry::Schema::Params, nil] a predefined schema object
|
87
|
+
# @yield a new Dry::Schema::Params definition block
|
88
|
+
def input(schema = nil, &block)
|
89
|
+
raise "No schema or schema definition block provided to interaktor input." if schema.nil? && !block
|
90
|
+
|
91
|
+
raise "Provided both a schema and a schema definition block for interaktor input." if schema && block
|
92
|
+
|
93
|
+
if schema
|
94
|
+
raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
|
95
|
+
|
96
|
+
@input_schema = schema
|
97
|
+
elsif block
|
98
|
+
@input_schema = Dry::Schema.Params { instance_eval(&block) }
|
99
|
+
end
|
100
|
+
|
101
|
+
# define the getters and setters for the input attributes
|
102
|
+
@input_schema.key_map.keys.each do |key| # rubocop:disable Style/HashEachMethods
|
103
|
+
attribute_name = key.id
|
104
|
+
|
105
|
+
# Define getter
|
106
|
+
define_method(attribute_name) { @context.send(attribute_name) }
|
107
|
+
|
108
|
+
# Define setter
|
109
|
+
define_method("#{attribute_name}=".to_sym) do |value|
|
110
|
+
@context.send("#{attribute_name}=".to_sym, value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
######################
|
116
|
+
# FAILURE ATTRIBUTES #
|
117
|
+
######################
|
118
|
+
|
119
|
+
# The list of attributes which are required to be provided when failing the
|
120
|
+
# interaktor.
|
51
121
|
#
|
52
122
|
# @return [Array<Symbol>]
|
53
|
-
def
|
54
|
-
@
|
123
|
+
def required_failure_attributes
|
124
|
+
@required_failure_attributes ||= failure_schema.info[:keys]
|
125
|
+
.select { |_, info| info[:required] }
|
126
|
+
.keys
|
55
127
|
end
|
56
128
|
|
57
|
-
#
|
58
|
-
#
|
59
|
-
# @param attributes [Symbol, Array<Symbol>] the list of attribute names
|
60
|
-
# @param options [Hash]
|
129
|
+
# The list of attributes which are not required to be provided when failing
|
130
|
+
# the interaktor.
|
61
131
|
#
|
62
|
-
# @return [
|
63
|
-
def
|
64
|
-
|
132
|
+
# @return [Array<Symbol>]
|
133
|
+
def optional_failure_attributes
|
134
|
+
# Adding an optional attribute with NO predicates with Dry::Schema is
|
135
|
+
# sort of a "nothing statement" - the schema can sort of ignore it. The
|
136
|
+
# problem is that the optional-with-no-predicate key is not included in
|
137
|
+
# the #info results, so we need to find an list of keys elsewhere, find
|
138
|
+
# the ones that are listed there but not in the #info results, and find
|
139
|
+
# the difference. The result are the keys that are omitted from the #info
|
140
|
+
# result because they are optional and have no predicates.
|
141
|
+
#
|
142
|
+
# See https://github.com/dry-rb/dry-schema/issues/347
|
143
|
+
@optional_failure_attributes ||= begin
|
144
|
+
attributes_in_info = failure_schema.info[:keys].keys
|
145
|
+
all_attributes = failure_schema.key_map.keys.map(&:id)
|
146
|
+
optional_attributes_by_exclusion = all_attributes - attributes_in_info
|
65
147
|
|
66
|
-
|
67
|
-
# Define getter
|
68
|
-
define_method(attribute) { @context.send(attribute) }
|
148
|
+
explicitly_optional_attributes = failure_schema.info[:keys].reject { |_, info| info[:required] }.keys
|
69
149
|
|
70
|
-
|
71
|
-
define_method("#{attribute}=".to_sym) do |value|
|
72
|
-
@context.send("#{attribute}=".to_sym, value)
|
150
|
+
explicitly_optional_attributes + optional_attributes_by_exclusion
|
73
151
|
end
|
152
|
+
end
|
74
153
|
|
75
|
-
|
76
|
-
|
154
|
+
# The complete list of failure attributes.
|
155
|
+
#
|
156
|
+
# @return [Array<Symbol>]
|
157
|
+
def failure_attributes
|
158
|
+
required_failure_attributes + optional_failure_attributes
|
77
159
|
end
|
78
160
|
|
79
|
-
#
|
161
|
+
# Get the failure attribute schema. Fall back to an empty schema with a
|
162
|
+
# configuration that will deny ALL provided attributes - not defining an
|
163
|
+
# failure schema should mean the interaktor has no failure attributes.
|
80
164
|
#
|
81
|
-
# @
|
82
|
-
|
165
|
+
# @return [Dry::Schema::Params]
|
166
|
+
def failure_schema
|
167
|
+
@failure_schema || Dry::Schema.Params
|
168
|
+
end
|
169
|
+
|
170
|
+
# @param context [Hash]
|
83
171
|
#
|
84
172
|
# @return [void]
|
85
|
-
def
|
86
|
-
|
173
|
+
def validate_failure_schema(context)
|
174
|
+
return unless failure_schema
|
87
175
|
|
88
|
-
|
89
|
-
# Define getter
|
90
|
-
define_method(attribute) { @context.send(attribute) }
|
176
|
+
result = failure_schema.call(context)
|
91
177
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
178
|
+
if result.errors.any?
|
179
|
+
raise Interaktor::Error::AttributeSchemaValidationError.new(
|
180
|
+
self,
|
181
|
+
result.errors.to_h,
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end
|
97
185
|
|
98
|
-
|
99
|
-
|
186
|
+
# @param schema [Dry::Schema::Params, nil] a predefined schema object
|
187
|
+
# @yield a new Dry::Schema::Params definition block
|
188
|
+
def failure(schema = nil, &block)
|
189
|
+
raise "No schema or schema definition block provided to interaktor failure method." if schema.nil? && !block
|
100
190
|
|
101
|
-
|
102
|
-
optional_defaults[attribute] = options[:default] if options[:default]
|
103
|
-
options.delete(:default)
|
191
|
+
raise "Provided both a schema and a schema definition block for interaktor failure method." if schema && block
|
104
192
|
|
105
|
-
|
193
|
+
if schema
|
194
|
+
raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
|
195
|
+
|
196
|
+
@failure_schema = schema
|
197
|
+
elsif block
|
198
|
+
@failure_schema = Dry::Schema.Params { instance_eval(&block) }
|
106
199
|
end
|
107
200
|
end
|
108
201
|
|
109
|
-
|
202
|
+
######################
|
203
|
+
# SUCCESS ATTRIBUTES #
|
204
|
+
######################
|
205
|
+
|
206
|
+
# The list of attributes which are required to be provided when the
|
207
|
+
# interaktor succeeds.
|
110
208
|
#
|
111
|
-
# @
|
112
|
-
|
209
|
+
# @return [Array<Symbol>]
|
210
|
+
def required_success_attributes
|
211
|
+
@required_success_attributes ||= success_schema.info[:keys]
|
212
|
+
.select { |_, info| info[:required] }
|
213
|
+
.keys
|
214
|
+
end
|
215
|
+
|
216
|
+
# The list of attributes which are not required to be provided when failing
|
217
|
+
# the interaktor.
|
113
218
|
#
|
114
|
-
# @return [
|
115
|
-
def
|
116
|
-
|
219
|
+
# @return [Array<Symbol>]
|
220
|
+
def optional_success_attributes
|
221
|
+
# Adding an optional attribute with NO predicates with Dry::Schema is
|
222
|
+
# sort of a "nothing statement" - the schema can sort of ignore it. The
|
223
|
+
# problem is that the optional-with-no-predicate key is not included in
|
224
|
+
# the #info results, so we need to find an list of keys elsewhere, find
|
225
|
+
# the ones that are listed there but not in the #info results, and find
|
226
|
+
# the difference. The result are the keys that are omitted from the #info
|
227
|
+
# result because they are optional and have no predicates.
|
228
|
+
#
|
229
|
+
# See https://github.com/dry-rb/dry-schema/issues/347
|
230
|
+
@optional_success_attributes ||= begin
|
231
|
+
attributes_in_info = success_schema.info[:keys].keys
|
232
|
+
all_attributes = success_schema.key_map.keys.map(&:id)
|
233
|
+
optional_attributes_by_exclusion = all_attributes - attributes_in_info
|
117
234
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
235
|
+
explicitly_optional_attributes = success_schema.info[:keys].reject { |_, info| info[:required] }.keys
|
236
|
+
|
237
|
+
explicitly_optional_attributes + optional_attributes_by_exclusion
|
238
|
+
end
|
122
239
|
end
|
123
240
|
|
124
|
-
#
|
241
|
+
# The complete list of success attributes.
|
125
242
|
#
|
126
|
-
# @
|
127
|
-
|
243
|
+
# @return [Array<Symbol>]
|
244
|
+
def success_attributes
|
245
|
+
required_success_attributes + optional_success_attributes
|
246
|
+
end
|
247
|
+
|
248
|
+
# Get the success attribute schema. Fall back to an empty schema with a
|
249
|
+
# configuration that will deny ALL provided attributes - not defining an
|
250
|
+
# success schema should mean the interaktor has no success attributes.
|
251
|
+
#
|
252
|
+
# @return [Dry::Schema::Params]
|
253
|
+
def success_schema
|
254
|
+
@success_schema || Dry::Schema.Params
|
255
|
+
end
|
256
|
+
|
257
|
+
# @param context [Hash]
|
128
258
|
#
|
129
259
|
# @return [void]
|
130
|
-
def
|
131
|
-
|
260
|
+
def validate_success_schema(context)
|
261
|
+
return unless success_schema
|
262
|
+
|
263
|
+
result = success_schema.call(context)
|
132
264
|
|
133
|
-
|
134
|
-
|
135
|
-
|
265
|
+
if result.errors.any?
|
266
|
+
raise Interaktor::Error::AttributeSchemaValidationError.new(
|
267
|
+
self,
|
268
|
+
result.errors.to_h,
|
269
|
+
)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# @param schema [Dry::Schema::Params, nil] a predefined schema object
|
274
|
+
# @yield a new Dry::Schema::Params definition block
|
275
|
+
def success(schema = nil, &block)
|
276
|
+
raise "No schema or schema definition block provided to interaktor success method." if schema.nil? && !block
|
277
|
+
|
278
|
+
raise "Provided both a schema and a schema definition block for interaktor success method." if schema && block
|
279
|
+
|
280
|
+
if schema
|
281
|
+
raise "Provided argument is not a Dry::Schema::Params object." unless schema.is_a?(Dry::Schema::Params)
|
282
|
+
|
283
|
+
@success_schema = schema
|
284
|
+
elsif block
|
285
|
+
@success_schema = Dry::Schema.Params { instance_eval(&block) }
|
136
286
|
end
|
137
287
|
end
|
138
288
|
|
@@ -174,45 +324,22 @@ module Interaktor::Callable
|
|
174
324
|
#
|
175
325
|
# @return [Interaktor::Context] the context, following interaktor execution
|
176
326
|
def execute(context, raise_exception)
|
177
|
-
unless context.is_a?(Hash) || context.is_a?(Interaktor::Context)
|
178
|
-
raise ArgumentError, "Expected a hash argument when calling the interaktor, got a #{context.class} instead."
|
179
|
-
end
|
180
|
-
|
181
|
-
apply_default_optional_attributes(context)
|
182
|
-
verify_attribute_presence(context)
|
183
|
-
|
184
327
|
run_method = raise_exception ? :run! : :run
|
185
328
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
# have not been provided, or if there are any attributes which have been
|
192
|
-
# provided but are not listed as either required or optional.
|
193
|
-
#
|
194
|
-
# @param context [Interaktor::Context] the context to check
|
195
|
-
#
|
196
|
-
# @return [void]
|
197
|
-
def verify_attribute_presence(context)
|
198
|
-
# TODO: Add "allow_nil?" option to required attributes
|
199
|
-
missing_attrs = required_attributes.reject { |required_attr| context.to_h.key?(required_attr) }
|
200
|
-
raise Interaktor::Error::MissingAttributeError.new(self, missing_attrs) if missing_attrs.any?
|
329
|
+
case context
|
330
|
+
when Hash
|
331
|
+
# Silently remove any attributes that are not included in the schema
|
332
|
+
allowed_keys = input_schema.key_map.keys.map { |k| k.name.to_sym }
|
333
|
+
context.select! { |k, _| allowed_keys.include?(k.to_sym) }
|
201
334
|
|
202
|
-
|
203
|
-
extra_attrs = context.to_h.keys.reject { |attr| allowed_attrs.include?(attr) }
|
204
|
-
raise Interaktor::Error::UnknownAttributeError.new(self, extra_attrs) if extra_attrs.any?
|
205
|
-
end
|
335
|
+
validate_input_schema(context)
|
206
336
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
def apply_default_optional_attributes(context)
|
214
|
-
optional_defaults.each do |attribute, default|
|
215
|
-
context[attribute] ||= default
|
337
|
+
new(context).tap(&run_method).instance_variable_get(:@context)
|
338
|
+
when Interaktor::Context
|
339
|
+
new(context).tap(&run_method).instance_variable_get(:@context)
|
340
|
+
else
|
341
|
+
raise ArgumentError,
|
342
|
+
"Expected a hash argument when calling the interaktor, got a #{context.class} instead."
|
216
343
|
end
|
217
344
|
end
|
218
345
|
end
|