interaktor 0.5.0 → 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: 6937b87e9844c597a62670ffe90e7d2abba7a7559bc42254d195973e685b109f
4
- data.tar.gz: 2d6f4b42e049de1d271b42118118a1a847f7fb71f21ce03c87394549af710419
3
+ metadata.gz: d3821ddb2f6707bec7ea45c0a7043b193cd7623be4c51bd83268806d76c652db
4
+ data.tar.gz: 0bc4cfb698d304703ad26e9aeb3130485a7fa77fa0d29e309224e191755d2c1f
5
5
  SHA512:
6
- metadata.gz: 3c68b887ff7cd0888d165ef5468029225b20f046f339bbea9184cd072ee221d7a1bb7bf37dbfff8b24c06ffc17c11839333172000d277d17a07307d788ad2530
7
- data.tar.gz: 3b6546acf884222f52472887ddd2af0439a891486aa542a7410ef787ed7b8ac2e662c728068d7f2852ee8cc3bf83610fa4c3f97410c19ac764d04a6a81457575
6
+ metadata.gz: a3af0ae98c859a81df86bd02ca3eb085d053e9a436f27755cec325ec5a83ed782bb5ceffae3ce2e37cdecb780eefe72c217eecaac3ef0759ea13c0a127c9a1d8
7
+ data.tar.gz: 34223391e17c7c74a575b66e3ab70aca05e4effedd511f5b458d4e61fecbfd790776bce72c2cd53d85712fa29a73ef3e11377a622cf262a1362ebc866b750aca
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Interaktor
2
2
 
3
- [![Gem Version](https://img.shields.io/gem/v/interaktor.svg)](http://rubygems.org/gems/interaktor)
4
- [![Build Status](https://img.shields.io/travis/collectiveidea/interaktor/master.svg)](https://travis-ci.org/taylorthurlow/interaktor)
3
+ [![gem version](https://badge.fury.io/rb/interaktor.svg)](http://rubygems.org/gems/interaktor)
5
4
 
6
5
  **DISCLAIMER: Interaktor is considered to be stable, but has not yet reached version 1.0. Following semantic versioning, minor version updates can introduce breaking changes. Please review the changelog when updating.**
7
6
 
@@ -9,10 +8,10 @@
9
8
 
10
9
  Fundamentally, Interaktor is the same as Interactor, but with the following changes:
11
10
 
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.
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.
13
12
  - The interaktor "context" is no longer a public-facing concept, all data/attribute accessors/setters are defined as attributes
14
13
  - Attributes passed to `#fail!` must be defined in advance
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
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
16
15
 
17
16
  ## Getting started
18
17
 
@@ -30,21 +29,44 @@ Interaktors are used to encapsulate your application's [business logic](http://e
30
29
 
31
30
  ### Attributes
32
31
 
33
- #### 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
+ ```
34
45
 
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.
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`.
36
47
 
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.
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.
38
49
 
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.
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.
40
59
 
41
60
  ```ruby
42
61
  class CreateUser
43
62
  include Interaktor
44
63
 
45
64
  input do
46
- required(:name)
47
- optional(:email)
65
+ attribute :name, :string
66
+ attribute :email, :string
67
+
68
+ validates :name, presence: true
69
+ validates :email, presence: true, allow_nil: true
48
70
  end
49
71
 
50
72
  def call
@@ -55,33 +77,37 @@ class CreateUser
55
77
  end
56
78
  end
57
79
 
58
- CreateUser.call(name: "Foo Bar")
80
+ CreateUser.call!(name: "Foo Bar")
59
81
  ```
60
82
 
61
- `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
62
84
 
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.
85
+ Based on the outcome of the interaktor's work, we can define attributes to be provided.
64
86
 
65
- #### 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.
66
88
 
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.
68
-
69
- 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.
70
90
 
71
91
  ```ruby
72
92
  class CreateUser
73
93
  include Interaktor
74
94
 
75
95
  input do
76
- required(:name).filled(:string)
96
+ attribute :name, :string
97
+
98
+ validates :name, presence: true
77
99
  end
78
100
 
79
101
  success do
80
- required(:user_id).value(:integer)
102
+ attribute :user_id, :integer
103
+
104
+ validates :user_id, presence: true
81
105
  end
82
106
 
83
107
  failure do
84
- required(:error_messages).value(array[:string])
108
+ attribute :error_messages # string array
109
+
110
+ validates :error_messages, presence: true
85
111
  end
86
112
 
87
113
  def call
@@ -104,6 +130,8 @@ else
104
130
  end
105
131
  ```
106
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
+
107
135
  #### Dealing with failure
108
136
 
109
137
  `#fail!` always throws an exception of type `Interaktor::Failure`.
@@ -254,29 +282,13 @@ There are two kinds of interaktors built into the Interaktor library: basic inte
254
282
  A basic interaktor is a class that includes `Interaktor` and defines `call`.
255
283
 
256
284
  ```ruby
257
- class AuthenticateUser
285
+ class PrintAThing
258
286
  include Interaktor
259
287
 
260
- input do
261
- required(:email).filled(:string)
262
- required(:password).filled(:string)
263
- end
264
-
265
- success do
266
- required(:user)
267
- required(:token).filled(:string)
268
- end
269
-
270
- failure do
271
- required(:message).filled(:string)
272
- end
288
+ input { attribute :name }
273
289
 
274
290
  def call
275
- if user = User.authenticate(email, password)
276
- success!(user: user, token: user.secret_token)
277
- else
278
- fail!(message: "authenticate_user.failure")
279
- end
291
+ puts name
280
292
  end
281
293
  end
282
294
  ```
@@ -285,18 +297,16 @@ Basic interaktors are the building blocks. They are your application's single-pu
285
297
 
286
298
  ### Organizers
287
299
 
288
- 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.
289
301
 
290
302
  ```ruby
291
- class PlaceOrder
303
+ class DoSomeThingsInOrder
292
304
  include Interaktor::Organizer
293
305
 
294
306
  input do
295
- required(:order_params).filled(:hash)
296
- end
307
+ attribute :name, :string
297
308
 
298
- success do
299
- required(:order)
309
+ validates :name, presence: true
300
310
  end
301
311
 
302
312
  organize CreateOrder, ChargeCard, SendThankYou
@@ -326,14 +336,12 @@ class OrdersController < ApplicationController
326
336
  end
327
337
  ```
328
338
 
329
- 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).
330
340
 
331
- 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.
332
342
 
333
343
  If the organizer specifies any success attributes, the final interaktor in the
334
- organized list must also specify those success attributes. In general, it is
335
- recommended to avoid using success attributes on an organizer in the first
336
- place, to avoid coupling between the organizer and the interaktors it organizes.
344
+ organized list must also specify those success attributes.
337
345
 
338
346
  #### Rollback
339
347
 
@@ -346,11 +354,15 @@ class CreateOrder
346
354
  include Interaktor
347
355
 
348
356
  input do
349
- required(:order_params).filled(:hash)
357
+ attribute :order_params
358
+
359
+ validates :order_params, presence: true
350
360
  end
351
361
 
352
362
  success do
353
- required(:order)
363
+ attribute :order
364
+
365
+ validates :order, presence: true
354
366
  end
355
367
 
356
368
  def call
@@ -380,17 +392,25 @@ class AuthenticateUser
380
392
  include Interaktor
381
393
 
382
394
  input do
383
- required(:email).filled(:string)
384
- required(:password).filled(:string)
395
+ attribute :email, :string
396
+ attribute :password, :string
397
+
398
+ validates :email, presence: true
399
+ validates :password, presence: true
385
400
  end
386
401
 
387
402
  success do
388
- required(:user)
389
- required(:token).filled(:string)
403
+ attribute :user
404
+ attribute :token, :string
405
+
406
+ validates :user, presence: true
407
+ validates :token, presence: true
390
408
  end
391
409
 
392
410
  failure do
393
- required(:message).filled(:string)
411
+ attribute :message
412
+
413
+ validates :message, presence: true
394
414
  end
395
415
 
396
416
  def call
@@ -422,11 +442,11 @@ describe AuthenticateUser do
422
442
  end
423
443
 
424
444
  it "provides the user" do
425
- expect(result.user).to eq(user)
445
+ expect(result.user).to eq user
426
446
  end
427
447
 
428
448
  it "provides the user's secret token" do
429
- expect(result.token).to eq("token")
449
+ expect(result.token).to eq "token"
430
450
  end
431
451
  end
432
452
 
@@ -457,18 +477,7 @@ It's a good idea to define your own interfaces to your models. Doing so makes it
457
477
  class AuthenticateUser
458
478
  include Interaktor
459
479
 
460
- input do
461
- required(:email).filled(:string)
462
- required(:password).filled(:string)
463
- end
464
-
465
- success do
466
- required(:user)
467
- end
468
-
469
- failure do
470
- required(:message).filled(:string)
471
- end
480
+ # ...
472
481
 
473
482
  def call
474
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.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 "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