makwa 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +12 -3
- data/README.md +160 -8
- data/doc/about_interactions.md +42 -0
- data/doc/features_and_design_considerations.md +3 -0
- data/doc/how_to_release_new_version.md +5 -6
- data/doc/usage_examples.md +216 -0
- data/lib/makwa/interaction.rb +5 -5
- data/lib/makwa/returning_interaction.rb +2 -1
- data/lib/makwa/version.rb +1 -1
- data/makwa.gemspec +2 -2
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5b57daebaedd9cdfe6e14503bc559f2b3cc9dc40beeb3461c2070b1f2c659a1
|
4
|
+
data.tar.gz: 1f57dc2aa9f5376a14ae4bde76d88be11ba6ad897892f8adeb69272962426116
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41aa06f6e10421a0c1fe9af084b36838670c1300f00bb359975482539537f0bf852903a28cb058a0ea8ee996bcb953f850ccc915a8ac99861aa2a74e11860182
|
7
|
+
data.tar.gz: 76f38892c52dbeec20840416e10196c702e34ad351c2fd7f366c1d43aad4b2eb21221fc8ff113a793a4f83716c8703bfe18e9be751e36774003f655f0c2e0b8a
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,19 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.0.0] - 2022-11-26
|
4
|
+
|
5
|
+
- Upgrades ActiveInteraction from 4.x to 5.2.x. Please see the [ActiveInteraction CHANGELOG](https://github.com/AaronLasseigne/active_interaction/blob/main/CHANGELOG.md) for what has changed between v. 4.x and 5. There are some breaking changes.
|
6
|
+
- Adds documentation.
|
7
|
+
|
8
|
+
## [0.2.1] - 2021-11-03
|
9
|
+
|
10
|
+
- Fixes bug where the `returning` attribute was not ignored correctly when assigning attrs under certain error conditions.
|
11
|
+
|
3
12
|
## [0.2.0] - 2021-10-27
|
4
13
|
|
5
|
-
- Adds implementation
|
6
|
-
- Updates ReturningInteraction to copy errors and values to returned object on invalid inputs
|
14
|
+
- Adds implementation.
|
15
|
+
- Updates ReturningInteraction to copy errors and values to returned object on invalid inputs.
|
7
16
|
|
8
17
|
## [0.1.0] - 2021-09-23
|
9
18
|
|
10
|
-
- Initial release
|
19
|
+
- Initial release.
|
data/README.md
CHANGED
@@ -1,36 +1,188 @@
|
|
1
1
|
# Makwa
|
2
2
|
|
3
|
-
|
3
|
+
Makwa is an extension of the [ActiveInteraction](https://github.com/AaronLasseigne/active_interaction) gem, bringing interactions to Ruby on Rails apps.
|
4
|
+
|
5
|
+
> ActiveInteraction manages application-specific business logic. It's an implementation of service objects designed to blend seamlessly into Rails. It also helps you write safer code by validating that your inputs conform to your expectations. If ActiveModel deals with your nouns, then ActiveInteraction handles your verbs.
|
6
|
+
|
7
|
+
<p align="right">Readme for ActiveInteraction.</p>
|
8
|
+
|
9
|
+
Makwa improves the ergonomics around mutating ActiveRecord instances.
|
4
10
|
|
5
11
|
## Installation
|
6
12
|
|
7
13
|
Add this line to your application's Gemfile:
|
8
14
|
|
9
15
|
```ruby
|
10
|
-
gem
|
16
|
+
gem "makwa"
|
11
17
|
```
|
12
18
|
|
13
19
|
And then execute:
|
14
20
|
|
15
|
-
|
21
|
+
```shell
|
22
|
+
$ bundle install
|
23
|
+
```
|
16
24
|
|
17
25
|
Or install it yourself as:
|
18
26
|
|
19
|
-
|
27
|
+
```shell
|
28
|
+
$ gem install makwa
|
29
|
+
```
|
30
|
+
|
31
|
+
Makwa extends the ActiveInteraction gem and will install it automatically as a dependency.
|
32
|
+
|
33
|
+
Makwa is compatible with Ruby 2.7 or greater and Rails 5 or greater.
|
34
|
+
|
35
|
+
## What does Makwa add to ActiveInteraction?
|
36
|
+
|
37
|
+
Please read through the [ActiveInteraction Readme](https://github.com/AaronLasseigne/active_interaction) first and then come back here to see what Makwa adds to ActiveInteraction:
|
38
|
+
|
39
|
+
### ReturningInteraction
|
40
|
+
|
41
|
+
ReturningInteractions are a special kind of interaction, optimized for usage with Rails forms:
|
42
|
+
|
43
|
+
The basic approach of ActiveInteraction (AI) when rendering ActiveRecord model forms is to pass an AI instance to the form. That approach works great for simple mutations of ActiveRecord instances. However, for this to work, the AI class has to implement all methods required for rendering your forms. That can get tricky when you need to traverse associations, or call complex decorators on your models. This approach also fails if the interaction's `#execute` method is never run because the input validations fail.
|
44
|
+
|
45
|
+
ReturningInteraction (RI) chooses a different approach: It accepts the to-be-mutated ActiveRecord instance as an input argument and is guaranteed to return that instance, no matter if the interaction outcome is successful or not. The RI will merge all errors that occurred during execution to the returned ActiveRecord instance. This allows you to pass the actual ActiveRecord instance to your form, and you don't have to implement all methods required for the form to be rendered.
|
46
|
+
|
47
|
+
Let's look at some example code to see the difference:
|
48
|
+
|
49
|
+
The ActiveInteraction way
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# app/controllers/users_controller.rb
|
53
|
+
class UsersController < ApplicationController
|
54
|
+
def new
|
55
|
+
@user = User.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def create
|
59
|
+
outcome = Users::Create.run(
|
60
|
+
params.fetch(:user, {})
|
61
|
+
)
|
62
|
+
|
63
|
+
if outcome.valid?
|
64
|
+
redirect_to(outcome.result)
|
65
|
+
else
|
66
|
+
@user = outcome
|
67
|
+
render(:new)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# app/interactions/users/create.rb
|
73
|
+
module Users
|
74
|
+
class Create < ApplicationInteraction
|
75
|
+
string :first_name
|
76
|
+
string :last_name
|
77
|
+
array :role_ids, default: []
|
78
|
+
|
79
|
+
def execute
|
80
|
+
user = User.new(inputs)
|
81
|
+
errors.merge!(user.errors) unless user.save
|
82
|
+
user
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_model
|
86
|
+
User.new
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
The Makwa way
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
# app/controllers/users_controller.rb
|
96
|
+
class UsersController < ApplicationController
|
97
|
+
def new
|
98
|
+
@user = User.new
|
99
|
+
end
|
100
|
+
|
101
|
+
def create
|
102
|
+
# Differences: Pass in the `:user` input and call `#run_returning!` instead of `#run`.
|
103
|
+
@user = Users::Create.run_returning!(
|
104
|
+
{user: User.new}.merge(params.fetch(:user, {}))
|
105
|
+
)
|
106
|
+
|
107
|
+
if @user.errors_empty?
|
108
|
+
redirect_to(@user)
|
109
|
+
else
|
110
|
+
render(:new)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# app/interactions/users/create.rb
|
116
|
+
module Users
|
117
|
+
class Create < ApplicationReturningInteraction
|
118
|
+
returning :user # This is different from AI: Specifies which input will be returned.
|
119
|
+
|
120
|
+
string :first_name
|
121
|
+
string :last_name
|
122
|
+
string :email
|
123
|
+
record :user
|
124
|
+
|
125
|
+
def execute_returning # Notice: Method is called `execute_returning`, not `execute`!
|
126
|
+
user.update(inputs.except(:user))
|
127
|
+
# No need to merge any errors. This will be done automatically by Makwa
|
128
|
+
return_if_errors!
|
129
|
+
|
130
|
+
compose(
|
131
|
+
Infrastructure::SendEmail,
|
132
|
+
recipient_email: user.email,
|
133
|
+
subject: "Welcome to Makwa",
|
134
|
+
body: "Lorem ipsum..."
|
135
|
+
)
|
136
|
+
# No need for an explicit return of user, also done by Makwa
|
137
|
+
# (via `returning` input filter).
|
138
|
+
end
|
139
|
+
|
140
|
+
# No need to implement the `#to_model` method and any other methods required to
|
141
|
+
# render your forms.
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
### Other improvements
|
147
|
+
|
148
|
+
Makwa offers **safe ways to check for errors**. Instead of `#valid?` or `#invalid?` use `#errors_empty?` or `#errors_any?`. Rails' `#valid?` method is a destructive method that will clear all errors and re-run validations. This will eradicate any errors you added in the body of the `#execute` or `#execute_returning` methods.
|
149
|
+
|
150
|
+
Makwa offers a simple way to **exit early** from the interaction. Use `#return_if_errors!` at any point in the `#execute` method if errors make it impossible to continue execution of the interaction.
|
151
|
+
|
152
|
+
Makwa offers **detailed logging** around interaction invocations (with inputs) and outcomes (with errors):
|
153
|
+
|
154
|
+
```
|
155
|
+
Executing interaction Users::SendWelcomeEmail (id#1234567)
|
156
|
+
↳ called from Users::Create (id#7654321)
|
157
|
+
↳ inputs: {first_name: "Giselher", last_name: "Wulla"} (id#7654321)
|
158
|
+
# ... execute interaction
|
159
|
+
↳ outcome: failed (id#7654321)
|
160
|
+
↳ errors: "Email is missing" (id#7654321)
|
161
|
+
```
|
162
|
+
|
163
|
+
To enable debug logging just define this method in your `ApplicationInteraction`:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
def debug(txt)
|
167
|
+
puts indent + txt
|
168
|
+
end
|
169
|
+
```
|
20
170
|
|
21
|
-
|
171
|
+
### Further reading
|
22
172
|
|
23
|
-
|
173
|
+
* [Usage](doc/usage_examples.md): More complex examples and conventions.
|
174
|
+
* [About interactions](doc/about_interactions.md): Motivation, when to use them.
|
175
|
+
* [Features and design considerations](doc/features_and_design_considerations.md)
|
24
176
|
|
25
177
|
## Development
|
26
178
|
|
27
179
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
28
180
|
|
29
|
-
|
181
|
+
We have [instructions for releasing a new version](doc/how_to_release_new_version.md).
|
30
182
|
|
31
183
|
## Contributing
|
32
184
|
|
33
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
185
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/animikii/makwa.
|
34
186
|
|
35
187
|
## License
|
36
188
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# About Interactions
|
2
|
+
|
3
|
+
## When to use interactions
|
4
|
+
|
5
|
+
When to use an interaction:
|
6
|
+
|
7
|
+
* Use interactions for all but the most trivial ActiveRecord mutating CRUD operations.
|
8
|
+
* Replace **ALL** ActiveRecord `:after_...` callbacks with interactions. This refers to all after callbacks, not just save.
|
9
|
+
* Replace **MOST** ActiveRecord `:before_...` callbacks with interactions. This refers to all `:before_…` callbacks, not just save. An exception that can remain as a callback could be a `:before_validation` callback to sanitize an email address (strip surrounding whitespace, lower case), however, if there is already an interaction to create/update a user, you may as well do it in the interaction.
|
10
|
+
* Replace Model instance and class methods that implement complex behaviours with interactions. Note that you can still use the Model methods as interface, however, the implementation should live in an interaction.
|
11
|
+
|
12
|
+
## ReturningInteractions
|
13
|
+
|
14
|
+
ReturningInteractions are a special kind of interaction, optimized for usage with Rails forms.
|
15
|
+
|
16
|
+
### Motivation
|
17
|
+
|
18
|
+
When processing a Rails form submission, e.g., via :create or :update, if the inputs are not valid (and the interaction’s input validation fails), we still need a form object that we can use to re-render the form. When an ActiveInteraction’s input validation fails, we don’t get that. We just get the errors and the interaction itself. And the interaction may not be a suitable stand-in for the model object if it does not implement all the methods found on the model object (e.g., associations or decorators).
|
19
|
+
|
20
|
+
This is where ReturningInteractions come into play. Instead of returning themselves when errors exist, they will always return the specified input value. And any errors are merged into the returned value. The convention is to use the ActiveRecord instance as returned input value. Then, if errors exist, the returned instance can be used as form object to re-render the form.
|
21
|
+
|
22
|
+
### How they are different from regular interactions
|
23
|
+
|
24
|
+
ReturningInteractions inherit from ActiveInteraction, however, they are different in the following ways:
|
25
|
+
|
26
|
+
* They will always return the specified input, no matter what happens after you invoke them:
|
27
|
+
* If input validations fail, and the `#execute_returning` method is never reached.
|
28
|
+
* If associated ActiveModel validations fail.
|
29
|
+
* If exceptions are rescued.
|
30
|
+
* If the `#execute_returning` method returns early.
|
31
|
+
* If it follows the happy path, everything goes as expected, independently of what the `#execute_returning` method returns.
|
32
|
+
* They merge any errors added to the returned input argument before returning it.
|
33
|
+
* They inherit from ReturningInteraction.
|
34
|
+
* They have an additional returning macro to specify which one of the inputs is guaranteed to be returned.
|
35
|
+
* The `#execute` method is replaced with `#execute_returning`.
|
36
|
+
|
37
|
+
### Additional notes
|
38
|
+
|
39
|
+
* They require the returned input argument to implement the ActiveModel::Errors interface so that any errors can be merged.
|
40
|
+
* They behave very similar to the Ruby `#tap` method.
|
41
|
+
* Instead of an ActiveRecord instance, you can also pass the record’s id to a ReturningInteraction. This is possible thanks to ActiveInteraction’s record input filter type.
|
42
|
+
* Convention for input arguments: Don’t nest object attrs in a hash since ActiveInteraction doesn’t have good input filter error reporting on nested values. It’s better to have all attrs as flat list, merged with the returned record. (NOTE: This may be addressed with ActiveInteraction v5)
|
@@ -5,10 +5,9 @@
|
|
5
5
|
* Update CHANGELOG.md
|
6
6
|
* Commit changes
|
7
7
|
* Prepare new release
|
8
|
-
*
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
* The bump command will commit the new version and tag the commit.
|
8
|
+
* Assign new version in `lib/makwa/version.rb`
|
9
|
+
* Commit the change with "Bump makwa to <version>"
|
10
|
+
* Tag the commit of the new version with `v<version>`
|
11
|
+
* Push the changes
|
13
12
|
* Distribute new release
|
14
|
-
* `gem
|
13
|
+
* `gem push makwa` - This will push the new release to rubygems.org.
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# Makwa Interaction Usage Examples
|
2
|
+
|
3
|
+
## Specify your Application interactions
|
4
|
+
|
5
|
+
All specific interactions will inherit from your app-wide interactions:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# app/interactions/application_interaction.rb
|
9
|
+
class ApplicationInteraction < Makwa::Interaction
|
10
|
+
def debug(txt)
|
11
|
+
# Uncomment the next line for detailed debug output
|
12
|
+
# puts indent + txt
|
13
|
+
end
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
# app/interactions/application_interaction.rb
|
19
|
+
class ApplicationReturningInteraction < Makwa::ReturningInteraction
|
20
|
+
def debug(txt)
|
21
|
+
# Uncomment the next line for detailed debug output
|
22
|
+
# puts indent + txt
|
23
|
+
end
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
## Implement a regular interaction to wrap a 3rd party service
|
28
|
+
|
29
|
+
This example interaction wraps an email sending service:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
# app/interactions/infrastructure/send_email.rb
|
33
|
+
module Infrastructure
|
34
|
+
class SendEmail < ApplicationInteraction
|
35
|
+
string :recipient_email
|
36
|
+
string :subject
|
37
|
+
string :body
|
38
|
+
|
39
|
+
validates :recipient_email, presence: true, format: {with: /.+@.+/}
|
40
|
+
validates :subject, presence: true
|
41
|
+
|
42
|
+
def execute
|
43
|
+
ThirdPartyEmailService.send(
|
44
|
+
to: recipient_email,
|
45
|
+
subject: subject,
|
46
|
+
body: body
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
## Implement a ReturningInteraction to create a user
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# app/users/create.rb
|
57
|
+
module Users
|
58
|
+
class Create < ApplicationReturningInteraction
|
59
|
+
returning :user
|
60
|
+
|
61
|
+
string :first_name
|
62
|
+
string :last_name
|
63
|
+
string :email
|
64
|
+
record :user
|
65
|
+
|
66
|
+
validates :first_name, presence: true
|
67
|
+
validates :last_name, presence: true
|
68
|
+
validates :email, presence: true, format: {with: /.+@.+/}
|
69
|
+
|
70
|
+
def execute_returning
|
71
|
+
user.update(inputs.except(:user))
|
72
|
+
return_if_errors!
|
73
|
+
|
74
|
+
compose(
|
75
|
+
Infrastructure::SendEmail, # See the example above for details
|
76
|
+
recipient_email: user.email,
|
77
|
+
subject: "Welcome to Makwa",
|
78
|
+
body: "Lorem ipsum..."
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
Use this interaction from the controller:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# app/controllers/users_controller.rb
|
89
|
+
class UsersController < ApplicationController
|
90
|
+
def new
|
91
|
+
@user = User.new
|
92
|
+
end
|
93
|
+
|
94
|
+
def create
|
95
|
+
# Differences: Pass in the `:user` input and call `#run_returning!` instead of `#run`.
|
96
|
+
@user = Users::Create.run_returning!(
|
97
|
+
{user: User.new}.merge(params.fetch(:user, {}))
|
98
|
+
)
|
99
|
+
|
100
|
+
if @user.errors_empty?
|
101
|
+
redirect_to(@user)
|
102
|
+
else
|
103
|
+
render(:edit)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
## Implement a ReturningInteraction to update a user
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# app/users/update.rb
|
113
|
+
module Users
|
114
|
+
class Update < ApplicationReturningInteraction
|
115
|
+
returning :user
|
116
|
+
|
117
|
+
string :first_name
|
118
|
+
string :last_name
|
119
|
+
string :email
|
120
|
+
record :user
|
121
|
+
|
122
|
+
validates :first_name, presence: true
|
123
|
+
validates :last_name, presence: true
|
124
|
+
validates :email, presence: true, format: {with: /.+@.+/}
|
125
|
+
|
126
|
+
def execute_returning
|
127
|
+
user.update(inputs.except(:user))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
Use this interaction from the controller:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
# app/controllers/users_controller.rb
|
137
|
+
class UsersController < ApplicationController
|
138
|
+
before_action :load_user
|
139
|
+
|
140
|
+
def edit
|
141
|
+
end
|
142
|
+
|
143
|
+
def update
|
144
|
+
@user = Users::Create.run_returning!(
|
145
|
+
{user: @user}.merge(params.fetch(:user, {}))
|
146
|
+
)
|
147
|
+
|
148
|
+
if @user.errors_empty?
|
149
|
+
redirect_to(@user)
|
150
|
+
else
|
151
|
+
render(:edit)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
## Usage conventions
|
158
|
+
|
159
|
+
Interactions follow these conventions:
|
160
|
+
|
161
|
+
* **Code location**: Interactions are stored in app/interactions.
|
162
|
+
* **Naming**: Interaction names always start with a verb, optionally followed by an Object noun. The Interaction’s parent namespaces provide additional context. When referring to ActiveRecord models in an interaction’s parent namespace, use plural form. This is to avoid naming conflicts with ActiveRecord models. Examples:
|
163
|
+
* `users/create.rb`
|
164
|
+
* `facilitator/groups/close.rb`
|
165
|
+
* `nw_app_structure/nw_patch_items/nw_tables/change/prepare_form_object.rb`
|
166
|
+
* **Inheritance**: Interactions inherit from ApplicationInteraction, ApplicationReturningInteraction, or one of their descendants.
|
167
|
+
* **Invocation**: You can invoke an Interaction in one of the following ways:
|
168
|
+
* `.run` - always returns the Interaction outcome. You can then query the outcome with `#errors_empy?`, `#errors_any?`, `#result` and `#errors`. This is the primary way of invoking Interactions. Example: `outcome = NwAppStructure::NwPatches::Apply.run(id: "1234abcd")`
|
169
|
+
* `.run!` - the bang version returns the Interaction’s return value if successful, and raises an exception if not successful. This can be used as a convenience method where we want to assure that the interaction executes successfully, and where we want easy access to the return value.
|
170
|
+
* **ReturningInteractions** can only be invoked with `.run_returning!`.
|
171
|
+
* **Input param safety**: Interactions validate, restrict, and coerce their input args. That means you don't need strong params. You can use raw controller params via `user_params: params.to_unsafe_h[:user]`.
|
172
|
+
* **Error handling**:
|
173
|
+
* Rely on ActiveModel validations to add errors to the interaction.
|
174
|
+
* Use `errors.add` and `errors.merge!` to manually add errors to an interaction.
|
175
|
+
* When composing interactions, errors in nested interactions bubble to the top.
|
176
|
+
* Errors can be used for flow control, e.g., via `#return_if_errors!`.
|
177
|
+
* **Outcome**: Regular interactions that are invoked with `.run` return an outcome:
|
178
|
+
* Outcome can be tested for presence/absence of any errors. Please use `#errors_empty?` and `#errors_any?` instead of `#valid?`. See caveat below related to `#valid?` for details.
|
179
|
+
* Outcome has a `#result` (return value of the #execute method).
|
180
|
+
* Outcome exposes any errors via an `ActiveModel::Errors` compatible API.
|
181
|
+
* **Input arguments**:
|
182
|
+
* Input arguments to an interaction are always wrapped in a Hash. The hash keys correspond to the interaction’s input filters.
|
183
|
+
* As a general guideline, interactions receive the same types of arguments as the corresponding controller action would expect as `params`: Only basic Ruby data types like Hashes, Arrays, Strings, Numbers, Dates, Times, etc. This convention provides the following benefits:
|
184
|
+
* Provides the simplest API possible.
|
185
|
+
* Makes it easy to invoke an Interaction from a controller action: Just forward the params as is.
|
186
|
+
* Makes it easy to invoke an interaction from the console. You can type all input args as literals.
|
187
|
+
* Makes it easy to pass test data into an interaction.
|
188
|
+
* Makes it easy to serialize the invocation of an interaction, e.g., for a Sidekiq background job.
|
189
|
+
* We do not pass in ActiveRecord instances, but their ids instead. We rely on ActiveRecord caching to prevent multiple DB reads when passing the same record id into nested interactions. Exceptions:
|
190
|
+
* You can pass a descendent of ActiveModel, e.g., an ActiveRecord instance as the returned input to a ReturningInteraction. Use the record input filter type. It accepts both the ActiveRecord instance, as well as its id attribute. That way, you can still pass in basic Ruby types, e.g., in the console when invoking the interaction.
|
191
|
+
* In some use cases with nested interactions, we may choose to pass in an ActiveRecord instance to work around persistence concerns.
|
192
|
+
* When an interaction is concerned with an ActiveRecord instance, we pass the record’s id under the :id hash key (unless it’s a ReturningInteraction).
|
193
|
+
|
194
|
+
## Caveat: Don’t use #valid?
|
195
|
+
|
196
|
+
We need to address the issue where the supposedly non-destructive ActiveModel method `#valid?` is actually destructive. This affects both ActiveModel::Validations as well as ActiveInteraction. The `#valid?` method actually clears out any existing errors and re-runs ActiveModel validations. This causes any errors added outside of an ActiveModel::Validation to disappear, resulting in `#valid?` returning true when it shouldn’t.
|
197
|
+
|
198
|
+
Don't use `#valid?`, use `#errors_empty?` instead.
|
199
|
+
|
200
|
+
Don't use `#invalid?`, use `#errors_any?` instead.
|
201
|
+
|
202
|
+
Rails source code at activemodel/lib/active_model/validations.rb, line 334:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
def valid?(context = nil)
|
206
|
+
current_context, self.validation_context = validation_context, context
|
207
|
+
errors.clear
|
208
|
+
run_validations!
|
209
|
+
ensure
|
210
|
+
self.validation_context = current_context
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
The official Rails way to circumvent the clearing of errors is to use errors.any? or errors.empty?
|
215
|
+
|
216
|
+
We wrap this implementation detail (calling errors.any?) in a method that clearly communicates intent, prevents well intentioned devs from changing `#errors.empty?` to `#valid?`, and helps us audit code to make sure that we're not using the default `#valid?` method in connection with Interactions (Both on the interaction itself, and on any ActiveRecord instances it touches).
|
data/lib/makwa/interaction.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Makwa
|
4
4
|
class Interaction < ::ActiveInteraction::Base
|
5
|
-
class Interrupt < Object.const_get("::ActiveInteraction::Interrupt")
|
5
|
+
class Interrupt < Object.const_get(:"::ActiveInteraction::Interrupt")
|
6
6
|
end
|
7
7
|
|
8
8
|
#
|
@@ -28,7 +28,7 @@ module Makwa
|
|
28
28
|
#
|
29
29
|
|
30
30
|
# Log execution of interaction, caller, and inputs
|
31
|
-
set_callback :
|
31
|
+
set_callback :filter, :before, ->(interaction) {
|
32
32
|
debug("Executing interaction #{interaction.class.name} #{interaction.id_marker}")
|
33
33
|
calling_interaction = interaction.calling_interaction
|
34
34
|
debug(" ↳ called from #{calling_interaction} #{interaction.id_marker}") if calling_interaction.present?
|
@@ -41,10 +41,10 @@ module Makwa
|
|
41
41
|
# Log interaction's outcome and errors if any.
|
42
42
|
set_callback :execute, :after, ->(interaction) {
|
43
43
|
if interaction.errors_empty?
|
44
|
-
debug(" ↳ outcome: succeeded
|
44
|
+
debug(" ↳ outcome: succeeded #{interaction.id_marker}")
|
45
45
|
else
|
46
|
-
debug(" ↳ outcome: failed
|
47
|
-
debug(" ↳ errors: #{interaction.errors.details}
|
46
|
+
debug(" ↳ outcome: failed #{interaction.id_marker}")
|
47
|
+
debug(" ↳ errors: #{interaction.errors.details} #{interaction.id_marker}")
|
48
48
|
end
|
49
49
|
}
|
50
50
|
|
@@ -46,10 +46,11 @@ module Makwa
|
|
46
46
|
# Run validations (explicitly, don't rely on #valid?)
|
47
47
|
validate
|
48
48
|
if errors_any?
|
49
|
+
return_filter = self.class.instance_variable_get(:@return_filter)
|
49
50
|
# Add errors and values to the result object (so that the form can render them) and return the result object
|
50
51
|
return result
|
51
52
|
.tap { |r| r.errors.merge!(errors) }
|
52
|
-
.tap { |r| r.assign_attributes(inputs.except(
|
53
|
+
.tap { |r| r.assign_attributes(inputs.except(return_filter)) }
|
53
54
|
end
|
54
55
|
|
55
56
|
# Otherwise run the body of the interaction (along with any callbacks) ...
|
data/lib/makwa/version.rb
CHANGED
data/makwa.gemspec
CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.summary = "Interactions for Ruby on Rails apps."
|
16
16
|
spec.homepage = "https://github.com/animikii/makwa"
|
17
17
|
spec.license = "MIT"
|
18
|
-
spec.required_ruby_version = ">= 2.
|
18
|
+
spec.required_ruby_version = ">= 2.7.0"
|
19
19
|
|
20
20
|
spec.metadata["homepage_uri"] = spec.homepage
|
21
21
|
spec.metadata["source_code_uri"] = "https://github.com/animikii/makwa"
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
end
|
29
29
|
spec.require_paths = ["lib"]
|
30
30
|
|
31
|
-
spec.add_dependency "active_interaction", "~>
|
31
|
+
spec.add_dependency "active_interaction", "~> 5.2.0"
|
32
32
|
|
33
33
|
spec.add_development_dependency "standard"
|
34
34
|
spec.add_development_dependency "byebug"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: makwa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jo Hund
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2022-11-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: active_interaction
|
@@ -17,14 +17,14 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version:
|
20
|
+
version: 5.2.0
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version:
|
27
|
+
version: 5.2.0
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: standard
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -69,7 +69,10 @@ files:
|
|
69
69
|
- Rakefile
|
70
70
|
- bin/console
|
71
71
|
- bin/setup
|
72
|
+
- doc/about_interactions.md
|
73
|
+
- doc/features_and_design_considerations.md
|
72
74
|
- doc/how_to_release_new_version.md
|
75
|
+
- doc/usage_examples.md
|
73
76
|
- lib/makwa.rb
|
74
77
|
- lib/makwa/interaction.rb
|
75
78
|
- lib/makwa/returning_interaction.rb
|
@@ -90,14 +93,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
93
|
requirements:
|
91
94
|
- - ">="
|
92
95
|
- !ruby/object:Gem::Version
|
93
|
-
version: 2.
|
96
|
+
version: 2.7.0
|
94
97
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
98
|
requirements:
|
96
99
|
- - ">="
|
97
100
|
- !ruby/object:Gem::Version
|
98
101
|
version: '0'
|
99
102
|
requirements: []
|
100
|
-
rubygems_version: 3.
|
103
|
+
rubygems_version: 3.1.6
|
101
104
|
signing_key:
|
102
105
|
specification_version: 4
|
103
106
|
summary: Interactions for Ruby on Rails apps.
|