clean_actions 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +289 -0
- data/lib/clean_actions/action.rb +71 -0
- data/lib/clean_actions/action_failure.rb +14 -0
- data/lib/clean_actions/base.rb +6 -0
- data/lib/clean_actions/configuration.rb +20 -0
- data/lib/clean_actions/error_reporter.rb +9 -0
- data/lib/clean_actions/fail_with.rb +25 -0
- data/lib/clean_actions/isolation_level_validator.rb +26 -0
- data/lib/clean_actions/transaction_runner.rb +83 -0
- data/lib/clean_actions/typed_returns.rb +35 -0
- data/lib/clean_actions/version.rb +5 -0
- data/lib/clean_actions.rb +22 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b6ce7dad0e85ad8963eaeae13b7ebed2286e4cfb337aefea479bfad654f8b01c
|
4
|
+
data.tar.gz: a2e81f2e47694e43b8c8c417c4b3849f1f5f76ac87c13762f683363178059ab5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0137b1b214dbc703c3b1e0c339e96be565748370f8c4d387d516bfcdca147b6107543e89b560f772418040c46c1c1222127218aba65d5621e758572f316b6574
|
7
|
+
data.tar.gz: e8be5458cc24adc7cd75b5fc08a78a66b475f2d06298c106eaea20794702fe34cf235ee554659b8ac6e5c91a6a4095149511d5c0f8619a0166409542b593c8e9
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 DmitryTsepelev
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,289 @@
|
|
1
|
+
# CleanActions
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/clean_actions.svg)](https://rubygems.org/gems/clean_actions)
|
4
|
+
[![Tests status](https://github.com/DmitryTsepelev/clean_actions/actions/workflows/test.yml/badge.svg)](https://github.com/DmitryTsepelev/clean_actions/actions/workflows/test.yml)
|
5
|
+
![](https://ruby-gem-downloads-badge.herokuapp.com/clean_actions?type=total)
|
6
|
+
|
7
|
+
A modern modular service object toolkit for Rails, that respects database transactions and adds type checks to returned values.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class AddItemToCart < CleanActions::Base
|
11
|
+
includes Dry::Initializer
|
12
|
+
|
13
|
+
option :user
|
14
|
+
option :item
|
15
|
+
|
16
|
+
# This will report an error if someone accidentally returns wrong instance from #perform_actions.
|
17
|
+
returns OrderItem
|
18
|
+
|
19
|
+
# Such checks are happening inside the transaction right before #perform_actions, so
|
20
|
+
# you can halt early.
|
21
|
+
fail_with(:banned_user) { @user.banned? }
|
22
|
+
|
23
|
+
# This method is executed inside the database transaction.
|
24
|
+
# If transaction was opened by another action, which called this one - savepoint won't be created.
|
25
|
+
# Last line will be used as a returned value.
|
26
|
+
def perform_actions
|
27
|
+
@order = CreateOrder.call(user: @user) # if CreateOrder fails - transaction will be rolled back
|
28
|
+
@order.order_items.create!(item: @item) # if something else fails here - transaction will be rolled back as well
|
29
|
+
end
|
30
|
+
|
31
|
+
# This method will be called for each action after whole transaction commits successfully.
|
32
|
+
def after_commit
|
33
|
+
ItemAddedSubscription.trigger(order: @order)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
Add this line to your application's Gemfile:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
gem 'clean_actions'
|
44
|
+
```
|
45
|
+
|
46
|
+
## Writing your actions
|
47
|
+
|
48
|
+
Inherit your actions from `CleanActions::Base`, which by defaut includes [typed returns](/README.md#Typed-Returns) and [fail_with](/README.md#Fail-With).
|
49
|
+
|
50
|
+
> If you want to exclude something — inherit from `CleanActions::Action` and configure all includes you need.
|
51
|
+
|
52
|
+
You should implement at least one of two methods—`#perform_actions` or `#after_commit`:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class AddItemToCart < CleanActions::Base
|
56
|
+
def perform_actions
|
57
|
+
@order = CreateOrder.call(user: @user)
|
58
|
+
@order_item = @order.order_items
|
59
|
+
.create_with(quantity: 0)
|
60
|
+
.find_or_create_by!(item: @item)
|
61
|
+
end
|
62
|
+
|
63
|
+
def after_commit
|
64
|
+
NotifyUserAboutUpdatedOrderItemJob.perform_later(order_item: @order_item)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
When first action is called, it will be wrapped to the database transaction, and all actions called by it will be inside the same transaction. All `#perform_actions` will happen inside the transaction (and rolled back if needed). After that, in case of successful commit, all `#after_commit` actions will happen in order.
|
70
|
+
|
71
|
+
## Error handling
|
72
|
+
|
73
|
+
If something goes wrong and transaction will raise an error—it will cause transaction to be rolled back. Errors should not be used as a way to manage a control flow, so all unhandled exceptions raised inside actions, will be reraised.
|
74
|
+
|
75
|
+
However, if you do expect an error—it's better to represent it as a returned value. Use `#fail!(:reason)` for that:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class AddItemToCart < CleanActions::Base
|
79
|
+
def perform_actions
|
80
|
+
fail!(:shop_is_closed)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
AddItemToCart.call # => CleanActions::ActionFailure(reason: :shop_is_closed)
|
85
|
+
```
|
86
|
+
|
87
|
+
## Typed Returns
|
88
|
+
|
89
|
+
Have you ever been in situation, when it's not clear, what will be returned by the class? Do you have some type system in your project? While you are setting it up—use typed returns:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class FetchOrder < CleanActions::Base
|
93
|
+
returns Order
|
94
|
+
|
95
|
+
option :order_id
|
96
|
+
|
97
|
+
def perform_actions
|
98
|
+
User.find(order_id)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
FetchOrder.call(42) # => "expected FetchOrder to return Order, returned User" is logged
|
103
|
+
```
|
104
|
+
|
105
|
+
The last line of `#perform_actions` will be returned. Note that if you have this module on but configure nothing—action will return `nil`.
|
106
|
+
|
107
|
+
## Isolation levels
|
108
|
+
|
109
|
+
By default transactions are executed in `READ COMMITTED` level. You can override it for a specific aciton:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class FetchOrder < CleanActions::Base
|
113
|
+
with_isolation_level :repeatable_read
|
114
|
+
|
115
|
+
option :order_id
|
116
|
+
|
117
|
+
def perform_actions
|
118
|
+
# actions
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
FetchOrder.call(42) # => "expected FetchOrder to return Order, returned User" is logged
|
123
|
+
```
|
124
|
+
|
125
|
+
Also, you can configure it for the whole project:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
CleanActions.config.isolation_level = :serializable
|
129
|
+
```
|
130
|
+
|
131
|
+
## Savepoints
|
132
|
+
|
133
|
+
If you want to run one action inside another but want a nested one be inside the ([SAVEPOINT](https://www.postgresql.org/docs/current/sql-savepoint.html))—use `with_savepoint`:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
class AddItemToCart < CleanActions::Base
|
137
|
+
def perform_actions
|
138
|
+
@order = CreateOrder.call(user: @user, with_savepoint: true)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
Note that `after_commit` still happens when the transaction from the root action is commited.
|
144
|
+
|
145
|
+
## Error configuration
|
146
|
+
|
147
|
+
When something weird happens during the action execution, the message is sent to the Rails log. Also, errors are _raised_ in development and test environments. To change that you can use `.config` object:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
CleanActions.config.raise_errors = true
|
151
|
+
```
|
152
|
+
|
153
|
+
Here is a list of errors affected by this config:
|
154
|
+
|
155
|
+
- type mismatch from (Typed Returns)[/README.md#Typed-Returns];
|
156
|
+
- action with (#before_transaction)[/README.md#before_transaction] is called inside the transaction;
|
157
|
+
- invalid isolation levels;
|
158
|
+
- action calls from unexpected places.
|
159
|
+
|
160
|
+
## Advanced Lifecycle
|
161
|
+
|
162
|
+
This section contains some additional hooks to improve your actions.
|
163
|
+
|
164
|
+
### before_transaction
|
165
|
+
|
166
|
+
If you want to do something outside the transaction (e.g., some IO operation)—use `before_transaction`:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class SyncData < CleanActions::Base
|
170
|
+
def before_transaction
|
171
|
+
@response = ApiClient.fetch
|
172
|
+
end
|
173
|
+
|
174
|
+
def perform_actions
|
175
|
+
# use response
|
176
|
+
end
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
Please note, that error will be risen if this action will be called from another action (and transaction will be already in progress):
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
class OtherAction < CleanActions::Base
|
184
|
+
def perform_actions
|
185
|
+
SyncData.call
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
OtherAction.call # => "SyncData#before_transaction was called inside the transaction" is logged
|
190
|
+
```
|
191
|
+
|
192
|
+
⚠️ Do not call other actions from this method!
|
193
|
+
|
194
|
+
### before_actions
|
195
|
+
|
196
|
+
If you want to do something before action — use `#before_action` callback, that is run inside the transaction but before `#perform_actions`:
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
class AddItemToCart < CleanActions::Base
|
200
|
+
def before_actions
|
201
|
+
@order = Order.find(order_id)
|
202
|
+
end
|
203
|
+
|
204
|
+
def perform_actions
|
205
|
+
# use order
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
⚠️ Do not call other actions from this method!
|
211
|
+
|
212
|
+
### fail_with
|
213
|
+
|
214
|
+
Fail with is a syntax sugar over `#fail!` to decouple pre–checks from the execution logic. Take a look at the improved example from the [Error Handling](/README.md#Error-Handling) section:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
class AddItemToCart < CleanActions::Base
|
218
|
+
fail_with(:shop_is_closed) { Time.now.hour.in?(10..18) }
|
219
|
+
|
220
|
+
def perform_actions
|
221
|
+
# only when shop is open
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
If you want to check that action can be called successfully (at least, preconditions are met) — you can use `#dry_call`, which will run _all_ preconditions and return all failures:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
class CheckNumber < CleanActions::Base
|
230
|
+
fail_with(:fail1) { @value == 1 }
|
231
|
+
fail_with(:fail_odd) { @value.odd? }
|
232
|
+
|
233
|
+
def initialize(value:)
|
234
|
+
@value = value
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
CheckNumber.dry_call(value: 1) # => [CleanActions::ActionFailure.new(:fail_odd), CleanActions::ActionFailure.new(:fail1)]
|
239
|
+
```
|
240
|
+
|
241
|
+
⚠️ Do not call other actions from this method!
|
242
|
+
|
243
|
+
### rollback
|
244
|
+
|
245
|
+
Actions rollback things inside `#perform_actions` in case of failure because of the database transactions. However, what if you want to rollback something non–transactional?
|
246
|
+
|
247
|
+
Well, if you sent an email or enqueued background job—you cannot do much,. Just in case, you want do something—here is a `#rollback` method that happens only when action fails.
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
class DumbCounter < CleanActions::Base
|
251
|
+
def perform_actions
|
252
|
+
Thread.current[:counter] ||= 0
|
253
|
+
Thread.current[:counter] += 1
|
254
|
+
fail!(:didnt_i_say_its_a_dumb_counter)
|
255
|
+
end
|
256
|
+
|
257
|
+
def rollback
|
258
|
+
Thread.current[:counter] ||= 0
|
259
|
+
Thread.current[:counter] -= 1
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
DumbCounter.call
|
264
|
+
Thread.current[:counter] # => 0
|
265
|
+
```
|
266
|
+
|
267
|
+
### ensure
|
268
|
+
|
269
|
+
Opened file inside `#perform_actions` or want to do some other cleanup even when action fails? Use `#ensure`:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
class UseFile < CleanActions::Base
|
273
|
+
def perform_actions
|
274
|
+
@file = File.open # ...
|
275
|
+
end
|
276
|
+
|
277
|
+
def ensure
|
278
|
+
@file.close
|
279
|
+
end
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
## Contributing
|
284
|
+
|
285
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/DmitryTsepelev/clean_actions.
|
286
|
+
|
287
|
+
## License
|
288
|
+
|
289
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module CleanActions
|
2
|
+
class Action
|
3
|
+
class << self
|
4
|
+
def call(with_savepoint: false, **kwargs)
|
5
|
+
new(**kwargs).call(with_savepoint: with_savepoint)
|
6
|
+
end
|
7
|
+
|
8
|
+
def before_actions(&block)
|
9
|
+
before_actions_blocks << block
|
10
|
+
end
|
11
|
+
|
12
|
+
def before_actions_blocks
|
13
|
+
@before_actions_blocks ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_isolation_level(isolation_level)
|
17
|
+
IsolationLevelValidator.validate(isolation_level)
|
18
|
+
|
19
|
+
@isolation_level = isolation_level
|
20
|
+
end
|
21
|
+
|
22
|
+
def isolation_level
|
23
|
+
@isolation_level ||= CleanActions.config.isolation_level
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(with_savepoint: false)
|
28
|
+
if TransactionRunner.action_calls_restricted_by
|
29
|
+
ErrorReporter.report("calling action #{self.class.name} is resticted inside ##{TransactionRunner.action_calls_restricted_by}")
|
30
|
+
end
|
31
|
+
|
32
|
+
TransactionRunner.restrict_action_calls_by(:before_transaction) { perform_before_transaction }
|
33
|
+
|
34
|
+
TransactionRunner.new(self).run(with_savepoint: with_savepoint) do
|
35
|
+
TransactionRunner.restrict_action_calls_by(:before_actions) do
|
36
|
+
self.class.before_actions_blocks.each { |b| instance_eval(&b) }
|
37
|
+
end
|
38
|
+
|
39
|
+
perform_actions
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def fail!(reason)
|
44
|
+
raise ActionFailure.new(reason)
|
45
|
+
end
|
46
|
+
|
47
|
+
def perform_actions
|
48
|
+
end
|
49
|
+
|
50
|
+
def after_commit
|
51
|
+
end
|
52
|
+
|
53
|
+
def ensure
|
54
|
+
end
|
55
|
+
|
56
|
+
def rollback
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def perform_before_transaction
|
62
|
+
return unless respond_to?(:before_transaction)
|
63
|
+
|
64
|
+
if Thread.current[:transaction_started]
|
65
|
+
ErrorReporter.report("#{self.class.name}#before_transaction was called inside the transaction")
|
66
|
+
end
|
67
|
+
|
68
|
+
before_transaction
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module CleanActions
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :raise_errors
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@raise_errors = Rails.env.development? || Rails.env.test?
|
7
|
+
end
|
8
|
+
|
9
|
+
def isolation_level=(isolation_level)
|
10
|
+
IsolationLevelValidator.validate(isolation_level, allow_serializable: true)
|
11
|
+
@isolation_level = isolation_level
|
12
|
+
end
|
13
|
+
|
14
|
+
def isolation_level
|
15
|
+
@isolation_level ||= :read_committed
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :raise_errors?, :raise_errors
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module CleanActions
|
2
|
+
module FailWith
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def dry_call
|
8
|
+
self.class.before_actions_blocks.each_with_object([]) do |b, failures|
|
9
|
+
instance_eval(&b)
|
10
|
+
rescue CleanActions::ActionFailure => f
|
11
|
+
failures << f
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def fail_with(failure_reason, &block)
|
17
|
+
before_actions { fail!(failure_reason) if instance_eval(&block) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def dry_call(**kwargs)
|
21
|
+
new(**kwargs).dry_call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CleanActions
|
2
|
+
class IsolationLevelValidator
|
3
|
+
VALID_ISOLATION_LEVELS = %i[read_uncommited read_committed repeatable_read]
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def validate(isolation_level, allow_serializable: false)
|
7
|
+
if isolation_level == :serializable
|
8
|
+
unless allow_serializable
|
9
|
+
ErrorReporter.report("serializable isolation should only be used for a whole project, please use global config")
|
10
|
+
end
|
11
|
+
|
12
|
+
return
|
13
|
+
end
|
14
|
+
|
15
|
+
return if VALID_ISOLATION_LEVELS.include?(isolation_level)
|
16
|
+
|
17
|
+
ErrorReporter.report("invalid isolation level #{isolation_level} for #{name}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def can_be_nested(isolation_level)
|
21
|
+
CleanActions.config.isolation_level == :serializable ||
|
22
|
+
VALID_ISOLATION_LEVELS.index(isolation_level) <= VALID_ISOLATION_LEVELS.index(Thread.current[:root_isolation_level])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module CleanActions
|
2
|
+
class TransactionRunner
|
3
|
+
class << self
|
4
|
+
def restrict_action_calls_by(method)
|
5
|
+
Thread.current[:action_calls_restricted_by] = method
|
6
|
+
yield
|
7
|
+
ensure
|
8
|
+
Thread.current[:action_calls_restricted_by] = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def action_calls_restricted_by
|
12
|
+
Thread.current[:action_calls_restricted_by]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(action)
|
17
|
+
@action = action
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(with_savepoint: false, &block)
|
21
|
+
performed_actions << @action
|
22
|
+
|
23
|
+
if Thread.current[:transaction_started]
|
24
|
+
unless IsolationLevelValidator.can_be_nested(action_isolation_level)
|
25
|
+
ErrorReporter.report <<~MSG
|
26
|
+
action #{@action.class.name} requires #{action_isolation_level}, run inside #{Thread.current[:root_isolation_level]}
|
27
|
+
MSG
|
28
|
+
end
|
29
|
+
|
30
|
+
if with_savepoint
|
31
|
+
return ActiveRecord::Base.transaction(requires_new: true) { block.call }
|
32
|
+
else
|
33
|
+
return block.call
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
start_transaction(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
delegate :restrict_action_calls_by, to: :class
|
43
|
+
|
44
|
+
def start_transaction(&block)
|
45
|
+
Thread.current[:transaction_started] = true
|
46
|
+
Thread.current[:root_isolation_level] = action_isolation_level
|
47
|
+
|
48
|
+
ActiveRecord::Base.transaction(isolation: action_isolation_level) do
|
49
|
+
block.call.tap { restrict_action_calls_by(:after_commit) { run_after_commit_actions } }
|
50
|
+
rescue => e
|
51
|
+
run_rollback_blocks
|
52
|
+
raise e unless e.is_a?(ActionFailure)
|
53
|
+
|
54
|
+
e
|
55
|
+
end
|
56
|
+
ensure
|
57
|
+
Thread.current[:root_isolation_level] = nil
|
58
|
+
Thread.current[:transaction_started] = false
|
59
|
+
run_ensure_blocks
|
60
|
+
Thread.current[:performed_actions] = []
|
61
|
+
end
|
62
|
+
|
63
|
+
def action_isolation_level
|
64
|
+
@action.class.isolation_level
|
65
|
+
end
|
66
|
+
|
67
|
+
def run_after_commit_actions
|
68
|
+
performed_actions.each(&:after_commit)
|
69
|
+
end
|
70
|
+
|
71
|
+
def run_ensure_blocks
|
72
|
+
performed_actions.each(&:ensure)
|
73
|
+
end
|
74
|
+
|
75
|
+
def run_rollback_blocks
|
76
|
+
performed_actions.each(&:rollback)
|
77
|
+
end
|
78
|
+
|
79
|
+
def performed_actions
|
80
|
+
Thread.current[:performed_actions] ||= []
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module CleanActions
|
2
|
+
module TypedReturns
|
3
|
+
def self.included(base)
|
4
|
+
base.prepend(PrependedMethods)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module PrependedMethods
|
9
|
+
def call(**)
|
10
|
+
returned_value = super
|
11
|
+
|
12
|
+
return returned_value if returned_value.is_a?(ActionFailure)
|
13
|
+
|
14
|
+
if self.class.returned_classes.nil?
|
15
|
+
returned_value = nil
|
16
|
+
elsif self.class.returned_classes.none? { returned_value.is_a?(_1) }
|
17
|
+
ErrorReporter.report(
|
18
|
+
"expected #{self.class.name} to return #{self.class.returned_classes.map(&:name).join(", ")}, " \
|
19
|
+
"returned #{returned_value.inspect}"
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
returned_value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
attr_reader :returned_classes
|
29
|
+
|
30
|
+
def returns(*klasses)
|
31
|
+
@returned_classes = klasses
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
require "clean_actions/configuration"
|
6
|
+
require "clean_actions/error_reporter"
|
7
|
+
require "clean_actions/isolation_level_validator"
|
8
|
+
require "clean_actions/fail_with"
|
9
|
+
require "clean_actions/typed_returns"
|
10
|
+
require "clean_actions/action_failure"
|
11
|
+
require "clean_actions/transaction_runner"
|
12
|
+
require "clean_actions/action"
|
13
|
+
require "clean_actions/base"
|
14
|
+
require "clean_actions/version"
|
15
|
+
|
16
|
+
module CleanActions
|
17
|
+
class << self
|
18
|
+
def config
|
19
|
+
@config ||= Configuration.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clean_actions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- DmitryTsepelev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-09-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: prometheus-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- dmitry.a.tsepelev@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- CHANGELOG.md
|
63
|
+
- LICENSE.txt
|
64
|
+
- README.md
|
65
|
+
- lib/clean_actions.rb
|
66
|
+
- lib/clean_actions/action.rb
|
67
|
+
- lib/clean_actions/action_failure.rb
|
68
|
+
- lib/clean_actions/base.rb
|
69
|
+
- lib/clean_actions/configuration.rb
|
70
|
+
- lib/clean_actions/error_reporter.rb
|
71
|
+
- lib/clean_actions/fail_with.rb
|
72
|
+
- lib/clean_actions/isolation_level_validator.rb
|
73
|
+
- lib/clean_actions/transaction_runner.rb
|
74
|
+
- lib/clean_actions/typed_returns.rb
|
75
|
+
- lib/clean_actions/version.rb
|
76
|
+
homepage: https://github.com/DmitryTsepelev/clean_actions
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata:
|
80
|
+
bug_tracker_uri: https://github.com/DmitryTsepelev/clean_actions/issues
|
81
|
+
changelog_uri: https://github.com/DmitryTsepelev/clean_actions/blob/master/CHANGELOG.md
|
82
|
+
documentation_uri: https://github.com/DmitryTsepelev/clean_actions/blob/master/README.md
|
83
|
+
homepage_uri: https://github.com/DmitryTsepelev/clean_actions
|
84
|
+
source_code_uri: https://github.com/DmitryTsepelev/clean_actions
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.7.0
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubygems_version: 3.4.10
|
101
|
+
signing_key:
|
102
|
+
specification_version: 4
|
103
|
+
summary: A modern modular service object toolkit for Rails, that respects database
|
104
|
+
transactions and adds type checks to returned values.
|
105
|
+
test_files: []
|