makwa 0.2.1 → 1.0.1
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 +50 -0
- data/doc/features_and_design_considerations.md +95 -0
- data/doc/how_to_release_new_version.md +6 -6
- data/doc/usage_examples.md +222 -0
- data/lib/makwa/interaction.rb +4 -4
- 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: 05d88c5995c722bae7c3769dfafe68dcf56389462b9d0ae6dc1a1fb330bab923
|
4
|
+
data.tar.gz: 6cb8f9358894fa15ab384a760a07bc37a1e6ac2c14dc72286421024b4fd5fb6d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a5426ce17f47958382bcd42cb203524e2625c9fe67a3f1b3cb3afc371592bc1ca11b4fd8db705d2710bf0c7b75202b762ec8f272d0f3cd5d234581e9c0e63e9
|
7
|
+
data.tar.gz: d71ba78909dd101447aa104d62eb49444c09713bcde3403969ad45a5b566ce10f0da4d7031c53d9aa546cbe1b3f33de6115e060f4a9663b6960e952bc4393d99
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,14 +1,23 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.0.1] - 2022-12-01
|
4
|
+
|
5
|
+
- Fixes reference to nested class (symbol -> string)
|
6
|
+
|
7
|
+
## [1.0.0] - 2022-11-26
|
8
|
+
|
9
|
+
- 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.
|
10
|
+
- Adds documentation.
|
11
|
+
|
3
12
|
## [0.2.1] - 2021-11-03
|
4
13
|
|
5
14
|
- Fixes bug where the `returning` attribute was not ignored correctly when assigning attrs under certain error conditions.
|
6
15
|
|
7
16
|
## [0.2.0] - 2021-10-27
|
8
17
|
|
9
|
-
- Adds implementation
|
10
|
-
- Updates ReturningInteraction to copy errors and values to returned object on invalid inputs
|
18
|
+
- Adds implementation.
|
19
|
+
- Updates ReturningInteraction to copy errors and values to returned object on invalid inputs.
|
11
20
|
|
12
21
|
## [0.1.0] - 2021-09-23
|
13
22
|
|
14
|
-
- Initial release
|
23
|
+
- 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,50 @@
|
|
1
|
+
# About Interactions
|
2
|
+
|
3
|
+
## When to use interactions
|
4
|
+
|
5
|
+
When to use an interaction:
|
6
|
+
|
7
|
+
* Integration with Rails CRUD forms:
|
8
|
+
* FormObject preparation.
|
9
|
+
* FormParams sanitization/transformation
|
10
|
+
* special validations
|
11
|
+
* CRUD persistence operations
|
12
|
+
* post-persistence actions like sending an email, or triggering another interaction
|
13
|
+
* Decoupling behavior from ActiveRecord classes:
|
14
|
+
* Replace **ALL** ActiveRecord `:after_...` callbacks with interactions. This refers to all after callbacks, not just save.
|
15
|
+
* 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.
|
16
|
+
* 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.
|
17
|
+
* Implement complex domain behaviours by composing sub tasks into higher level processes.
|
18
|
+
* Wrap a 3rd party service so that we can swap it out in a single place if needed.
|
19
|
+
|
20
|
+
## ReturningInteractions
|
21
|
+
|
22
|
+
ReturningInteractions are a special kind of interaction, optimized for usage with Rails forms.
|
23
|
+
|
24
|
+
### Motivation
|
25
|
+
|
26
|
+
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).
|
27
|
+
|
28
|
+
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.
|
29
|
+
|
30
|
+
### How they are different from regular interactions
|
31
|
+
|
32
|
+
ReturningInteractions inherit from ActiveInteraction, however, they are different in the following ways:
|
33
|
+
|
34
|
+
* They will always return the specified input, no matter what happens after you invoke them:
|
35
|
+
* If input validations fail, and the `#execute_returning` method is never reached.
|
36
|
+
* If associated ActiveModel validations fail.
|
37
|
+
* If exceptions are rescued.
|
38
|
+
* If the `#execute_returning` method returns early.
|
39
|
+
* If it follows the happy path, everything goes as expected, independently of what the `#execute_returning` method returns.
|
40
|
+
* They merge any errors added to the returned input argument before returning it.
|
41
|
+
* They inherit from ReturningInteraction.
|
42
|
+
* They have an additional returning macro to specify which one of the inputs is guaranteed to be returned.
|
43
|
+
* The `#execute` method is replaced with `#execute_returning`.
|
44
|
+
|
45
|
+
### Additional notes
|
46
|
+
|
47
|
+
* They require the returned input argument to implement the ActiveModel::Errors interface so that any errors can be merged.
|
48
|
+
* They behave very similar to the Ruby `#tap` method.
|
49
|
+
* 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.
|
50
|
+
* 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)
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# Features and design considerations
|
2
|
+
|
3
|
+
## Input validation and coercion
|
4
|
+
|
5
|
+
All input arguments are validated and coerced before the interaction is invoked. Depending on the use case we can use the following validations:
|
6
|
+
|
7
|
+
* ActiveInteraction input filters - Check for presence of input argument keys, their types, and can set default values for optional input arguments.
|
8
|
+
* ActiveModel validations in the Interaction. - Use the full power of ActiveModel::Validation to check for specific values, or higher level business rules that are specific to the interaction.
|
9
|
+
* ActiveModel validations in the ActiveRecord instance passed to a ReturningInteraction - For shared validations that apply to all related interactions.
|
10
|
+
* Dry-validation based contracts for advanced validations of deeply nested data structures.
|
11
|
+
|
12
|
+
The validation solutions listed above offer the following features:
|
13
|
+
|
14
|
+
* Type checking.
|
15
|
+
* Ability to implement complex validation rules.
|
16
|
+
* Ability to look at other input args when validating an arg.
|
17
|
+
* Composability: Input validations can be composed to prevent duplication and allow re-use. Dry validation offers this temporary workaround for composing rules: https://github.com/dry-rb/dry-validation/issues/593#issuecomment-631597226
|
18
|
+
* Type coercion, e.g., for Rails request params that need to be cast from String to Integer.
|
19
|
+
* Runtime validation (since we don’t know what arguments are passed to an Interaction at compile time).
|
20
|
+
* Default values can be assigned when using ActiveInteraction input filters.
|
21
|
+
|
22
|
+
Below are some options for input argument validation:
|
23
|
+
|
24
|
+
* ActiveModel Validations: https://api.rubyonrails.org/classes/ActiveModel/Validations.html
|
25
|
+
* ActiveInteraction filters: https://github.com/AaronLasseigne/active_interaction#filters
|
26
|
+
* dry-validation contracts: https://dry-rb.org/gems/dry-validation/
|
27
|
+
|
28
|
+
## Composability
|
29
|
+
|
30
|
+
Interactions can be composed to facilitate re-use and modularity.
|
31
|
+
|
32
|
+
* Errors raised in composed interactions are merged into the parent interaction.
|
33
|
+
* Execution in parent interaction stops when a composed interaction fails.
|
34
|
+
* Composed interactions act like the #run! bang method with exception handling built in.
|
35
|
+
|
36
|
+
Code example:
|
37
|
+
|
38
|
+
```
|
39
|
+
def execute
|
40
|
+
r1 = compose(Nested::Service1, arg1: 123, arg2: "a string")
|
41
|
+
r2 = compose(Nested::Service2, arg1: r1)
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
Note that ActiveInteraction input filters can also be reused via `import_filters OtherInteraction`.
|
46
|
+
|
47
|
+
## Exception handling and error reporting
|
48
|
+
|
49
|
+
* Errors (not Ruby Exceptions!) added in an interaction are accessible to the caller for testing and notification purposes.
|
50
|
+
* Errors are addable manually inside the interaction via errors.add(:base, text: "something went wrong", key: :something_went_wrong).
|
51
|
+
* Errors are mergeable from other sources, e.g., ActiveRecord objects or nested interactions via errors.merge!(@user.errors).
|
52
|
+
* Early exit of the interaction is possible if an unrecoverable error condition is detected. E.g., via return_if_errors! or return.
|
53
|
+
|
54
|
+
## Localization
|
55
|
+
|
56
|
+
Success and error messages are customizable via Rails' default I18n mechanisms.
|
57
|
+
|
58
|
+
## Integration with Rails forms
|
59
|
+
|
60
|
+
ReturningInteractions, are a specialized subclass of ActiveInteraction, work well with processing params submitted from Rails forms, and with possible re-rendering of the form if there are any errors.
|
61
|
+
|
62
|
+
## Dependency injection
|
63
|
+
|
64
|
+
Dependencies can be injected into an interaction, mostly for testing. Example: We inject a fake `Git` library when testing code that makes git commits. Other examples: `File`, `Time`, `TwitterApi`, etc.
|
65
|
+
|
66
|
+
## Serializability
|
67
|
+
|
68
|
+
Invocation of an interaction, with its input arguments, can be serialized in a simple, robust, and performant way. We accomplish this by limiting input arguments to basic Ruby types: Hashes, Arrays, Strings, Numbers, and Booleans.
|
69
|
+
|
70
|
+
## Logging
|
71
|
+
|
72
|
+
Interactions print detailed info to the log. Output includes:
|
73
|
+
|
74
|
+
* Name of invoked interaction
|
75
|
+
* caller
|
76
|
+
* input args
|
77
|
+
* any errors
|
78
|
+
|
79
|
+
Example:
|
80
|
+
|
81
|
+
```
|
82
|
+
Executing interaction Niiwin::NwLoader::InitialLoad (id#70361484811280)
|
83
|
+
↳ inputs: {} (id#70361484811280)
|
84
|
+
Executing interaction Niiwin::NwLoader::NwConfigs::Load (id#70361484766000)
|
85
|
+
↳ called from niiwin/nw_loader/initial_load.rb:14:in `initial_load_nw_config' (id#70361484766000)
|
86
|
+
↳ inputs: {} (id#70361484766000)
|
87
|
+
↳ outcome: succeeded (id#70361484766000)
|
88
|
+
Executing interaction Niiwin::NwLoader::NwConfigs::Validate (id#70361476717620)
|
89
|
+
↳ called from niiwin/nw_loader/initial_load.rb:15:in `initial_load_nw_config' (id#70361476717620)
|
90
|
+
↳ inputs: {} (id#70361476717620)
|
91
|
+
↳ outcome: succeeded (id#70361476717620)
|
92
|
+
↳ outcome: succeeded (id#70361484811280)
|
93
|
+
```
|
94
|
+
|
95
|
+
You can turn off logging by overriding the `ApplicationInteraction#debug` method.
|
@@ -5,10 +5,10 @@
|
|
5
5
|
* Update CHANGELOG.md
|
6
6
|
* Commit changes
|
7
7
|
* Prepare new release
|
8
|
-
*
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
*
|
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
|
12
|
+
* Build the gem with `gem build`
|
13
13
|
* Distribute new release
|
14
|
-
* `gem
|
14
|
+
* `gem push makwa-<version>.gem` - This will push the new release to rubygems.org.
|
@@ -0,0 +1,222 @@
|
|
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_returning_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
|
+
You can then use this interaction to send emails:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
Infrastructure::SendEmail.run!(recipient_email: "email@test.com", subject: "Email Subject", body: "Email Body")
|
57
|
+
```
|
58
|
+
|
59
|
+
## Implement a ReturningInteraction to create a user
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# app/users/create.rb
|
63
|
+
module Users
|
64
|
+
class Create < ApplicationReturningInteraction
|
65
|
+
returning :user
|
66
|
+
|
67
|
+
string :first_name
|
68
|
+
string :last_name
|
69
|
+
string :email
|
70
|
+
record :user
|
71
|
+
|
72
|
+
validates :first_name, presence: true
|
73
|
+
validates :last_name, presence: true
|
74
|
+
validates :email, presence: true, format: {with: /.+@.+/}
|
75
|
+
|
76
|
+
def execute_returning
|
77
|
+
user.update(inputs.except(:user))
|
78
|
+
return_if_errors!
|
79
|
+
|
80
|
+
compose(
|
81
|
+
Infrastructure::SendEmail, # See the example above for details
|
82
|
+
recipient_email: user.email,
|
83
|
+
subject: "Welcome to Makwa",
|
84
|
+
body: "Lorem ipsum..."
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
Use this interaction from the controller:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
# app/controllers/users_controller.rb
|
95
|
+
class UsersController < ApplicationController
|
96
|
+
def new
|
97
|
+
@user = User.new
|
98
|
+
end
|
99
|
+
|
100
|
+
def create
|
101
|
+
# Differences: Pass in the `:user` input and call `#run_returning!` instead of `#run`.
|
102
|
+
@user = Users::Create.run_returning!(
|
103
|
+
{user: User.new}.merge(params.fetch(:user, {}))
|
104
|
+
)
|
105
|
+
|
106
|
+
if @user.errors_empty?
|
107
|
+
redirect_to(@user)
|
108
|
+
else
|
109
|
+
render(:edit)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
## Implement a ReturningInteraction to update a user
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
# app/users/update.rb
|
119
|
+
module Users
|
120
|
+
class Update < ApplicationReturningInteraction
|
121
|
+
returning :user
|
122
|
+
|
123
|
+
string :first_name
|
124
|
+
string :last_name
|
125
|
+
string :email
|
126
|
+
record :user
|
127
|
+
|
128
|
+
validates :first_name, presence: true
|
129
|
+
validates :last_name, presence: true
|
130
|
+
validates :email, presence: true, format: {with: /.+@.+/}
|
131
|
+
|
132
|
+
def execute_returning
|
133
|
+
user.update(inputs.except(:user))
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
Use this interaction from the controller:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
# app/controllers/users_controller.rb
|
143
|
+
class UsersController < ApplicationController
|
144
|
+
before_action :load_user
|
145
|
+
|
146
|
+
def edit
|
147
|
+
end
|
148
|
+
|
149
|
+
def update
|
150
|
+
@user = Users::Update.run_returning!(
|
151
|
+
{user: @user}.merge(params.fetch(:user, {}))
|
152
|
+
)
|
153
|
+
|
154
|
+
if @user.errors_empty?
|
155
|
+
redirect_to(@user)
|
156
|
+
else
|
157
|
+
render(:edit)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
## Usage conventions
|
164
|
+
|
165
|
+
Interactions follow these conventions:
|
166
|
+
|
167
|
+
* **Code location**: Interactions are stored in app/interactions.
|
168
|
+
* **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:
|
169
|
+
* `users/create.rb`
|
170
|
+
* `facilitator/groups/close.rb`
|
171
|
+
* `nw_app_structure/nw_patch_items/nw_tables/change/prepare_form_object.rb`
|
172
|
+
* **Inheritance**: Interactions inherit from ApplicationInteraction, ApplicationReturningInteraction, or one of their descendants.
|
173
|
+
* **Invocation**: You can invoke an Interaction in one of the following ways:
|
174
|
+
* `.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")`
|
175
|
+
* `.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.
|
176
|
+
* **ReturningInteractions** can only be invoked with `.run_returning!`.
|
177
|
+
* **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]`.
|
178
|
+
* **Error handling**:
|
179
|
+
* Rely on ActiveModel validations to add errors to the interaction.
|
180
|
+
* Use `errors.add` and `errors.merge!` to manually add errors to an interaction.
|
181
|
+
* When composing interactions, errors in nested interactions bubble to the top.
|
182
|
+
* Errors can be used for flow control, e.g., via `#return_if_errors!`.
|
183
|
+
* **Outcome**: Regular interactions that are invoked with `.run` return an outcome:
|
184
|
+
* 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.
|
185
|
+
* Outcome has a `#result` (return value of the #execute method).
|
186
|
+
* Outcome exposes any errors via an `ActiveModel::Errors` compatible API.
|
187
|
+
* **Input arguments**:
|
188
|
+
* Input arguments to an interaction are always wrapped in a Hash. The hash keys correspond to the interaction’s input filters.
|
189
|
+
* 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:
|
190
|
+
* Provides the simplest API possible.
|
191
|
+
* Makes it easy to invoke an Interaction from a controller action: Just forward the params as is.
|
192
|
+
* Makes it easy to invoke an interaction from the console. You can type all input args as literals.
|
193
|
+
* Makes it easy to pass test data into an interaction.
|
194
|
+
* Makes it easy to serialize the invocation of an interaction, e.g., for a Sidekiq background job.
|
195
|
+
* 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:
|
196
|
+
* You can pass a descendant 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.
|
197
|
+
* In some use cases with nested interactions, we may choose to pass in an ActiveRecord instance to work around persistence concerns.
|
198
|
+
* 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).
|
199
|
+
|
200
|
+
## Caveat: Don’t use #valid?
|
201
|
+
|
202
|
+
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.
|
203
|
+
|
204
|
+
Don't use `#valid?`, use `#errors_empty?` instead.
|
205
|
+
|
206
|
+
Don't use `#invalid?`, use `#errors_any?` instead.
|
207
|
+
|
208
|
+
Rails source code at activemodel/lib/active_model/validations.rb, line 334:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
def valid?(context = nil)
|
212
|
+
current_context, self.validation_context = validation_context, context
|
213
|
+
errors.clear
|
214
|
+
run_validations!
|
215
|
+
ensure
|
216
|
+
self.validation_context = current_context
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
The official Rails way to circumvent the clearing of errors is to use errors.any? or errors.empty?
|
221
|
+
|
222
|
+
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
@@ -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
|
|
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.1
|
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-12-01 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.
|