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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a01474a6d7d669f7d02f6ddec9ab653710b4215399e83a429a099b520122838b
4
- data.tar.gz: 53d47eff5c8f981119af924a15f6d93a23435af552edb14ee66216e598979ec9
3
+ metadata.gz: 05d88c5995c722bae7c3769dfafe68dcf56389462b9d0ae6dc1a1fb330bab923
4
+ data.tar.gz: 6cb8f9358894fa15ab384a760a07bc37a1e6ac2c14dc72286421024b4fd5fb6d
5
5
  SHA512:
6
- metadata.gz: e20a524264523fdbdc2715994a06ce34d52ca8761f9a8bff5b1a51d92e463a7f5d9850bc35a944c3b67898a82d5530c3512e70497137da571d2240623247e86e
7
- data.tar.gz: 88833f8324ad7167e89af8b91d9c71b505b516b7502de9ac74fe70d213e5c69d6b586055f24bb1a89740d46edfc7400d051f2e2d49437cd7217c1e0d88a7b260
6
+ metadata.gz: 7a5426ce17f47958382bcd42cb203524e2625c9fe67a3f1b3cb3afc371592bc1ca11b4fd8db705d2710bf0c7b75202b762ec8f272d0f3cd5d234581e9c0e63e9
7
+ data.tar.gz: d71ba78909dd101447aa104d62eb49444c09713bcde3403969ad45a5b566ce10f0da4d7031c53d9aa546cbe1b3f33de6115e060f4a9663b6960e952bc4393d99
data/.gitignore CHANGED
@@ -8,3 +8,5 @@
8
8
 
9
9
  Gemfile.lock
10
10
  .byebug_history
11
+
12
+ .DS_Store
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
- Interactions for Ruby on Rails.
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 'makwa'
16
+ gem "makwa"
11
17
  ```
12
18
 
13
19
  And then execute:
14
20
 
15
- $ bundle install
21
+ ```shell
22
+ $ bundle install
23
+ ```
16
24
 
17
25
  Or install it yourself as:
18
26
 
19
- $ gem install makwa
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
- ## Usage
171
+ ### Further reading
22
172
 
23
- TODO: Write usage instructions here
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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/[USERNAME]/makwa.
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
- * Bump version via one of
9
- * `gem bump --tag --version major`
10
- * `gem bump --tag --version minor`
11
- * `gem bump --tag --version patch`
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
12
+ * Build the gem with `gem build`
13
13
  * Distribute new release
14
- * `gem release` - This will push the new release to rubygems.org.
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).
@@ -28,7 +28,7 @@ module Makwa
28
28
  #
29
29
 
30
30
  # Log execution of interaction, caller, and inputs
31
- set_callback :type_check, :before, ->(interaction) {
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 (id##{interaction.object_id})")
44
+ debug(" ↳ outcome: succeeded #{interaction.id_marker}")
45
45
  else
46
- debug(" ↳ outcome: failed (id##{interaction.object_id})")
47
- debug(" ↳ errors: #{interaction.errors.details} (id##{interaction.object_id})")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Makwa
4
- VERSION = "0.2.1"
4
+ VERSION = "1.0.1"
5
5
  end
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.4.0"
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", "~> 4.0"
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.2.1
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: 2021-11-03 00:00:00.000000000 Z
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: '4.0'
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: '4.0'
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.4.0
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.0.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.