interaktor 0.2.0 → 0.4.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 +5 -4
- data/.github/workflows/tests.yml +5 -5
- data/.gitignore +3 -0
- data/.ruby-version +1 -1
- data/Gemfile +1 -1
- data/Gemfile.ci +6 -0
- data/README.md +65 -29
- data/interaktor.gemspec +3 -2
- 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/organizer_missing_passed_attribute_error.rb +21 -0
- data/lib/interaktor/error/organizer_success_attribute_missing_error.rb +20 -0
- data/lib/interaktor/hooks.rb +16 -0
- data/lib/interaktor/organizer.rb +33 -7
- data/lib/interaktor.rb +11 -16
- data/spec/integration_spec.rb +142 -71
- data/spec/{interactor → interaktor}/context_spec.rb +1 -1
- data/spec/{interactor → interaktor}/hooks_spec.rb +100 -2
- 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 -17
- data/.travis.yml +0 -14
- data/lib/interaktor/error/disallowed_attribute_assignment_error.rb +0 -9
- data/lib/interaktor/error/missing_attribute_error.rb +0 -5
- data/lib/interaktor/error/option_error.rb +0 -16
- data/lib/interaktor/error/unknown_attribute_error.rb +0 -5
- data/lib/interaktor/error/unknown_option_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: d187b41c956972596b434b05aa5e4b3f8558457953327e305c5d76165f7c195b
|
4
|
+
data.tar.gz: e123fbeb3e8c1db843cb41b54023ca71d077a6947f0a3a50e0ec5bd78dfd4ddb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5549218310ebd0e3d0505eac4d43302f83c9508a0bee463f3a8c0f2042de5bfd25954aec67beae050a140606b14e75ce1e7ff8f0866726e5a24a09dd8071b4e6
|
7
|
+
data.tar.gz: a7eabf0b930c9efd51ec3407e119234d6a1d29aa3d88494bacde287cc0e3d67ae9882707d8636ddba7289c6889f536bcdbad22eebc47a8344b659828a0718f4b
|
@@ -1,8 +1,9 @@
|
|
1
1
|
name: Publish
|
2
2
|
|
3
3
|
on:
|
4
|
+
# Manually publish
|
5
|
+
workflow_dispatch:
|
4
6
|
push:
|
5
|
-
branches: [master]
|
6
7
|
tags:
|
7
8
|
- v*
|
8
9
|
|
@@ -12,11 +13,11 @@ jobs:
|
|
12
13
|
runs-on: ubuntu-latest
|
13
14
|
|
14
15
|
steps:
|
15
|
-
- uses: actions/checkout@
|
16
|
+
- uses: actions/checkout@v3
|
16
17
|
- name: Set up Ruby
|
17
|
-
uses:
|
18
|
+
uses: ruby/setup-ruby@v1
|
18
19
|
with:
|
19
|
-
ruby-version: 2
|
20
|
+
ruby-version: 3.2
|
20
21
|
|
21
22
|
- name: Publish to RubyGems
|
22
23
|
run: |
|
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/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.1.3
|
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.4.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"
|