clean_actions 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ # Change log
2
+
3
+ ## main
4
+
5
+ # 0.1.0 (21-09-2023)
6
+
7
+ - Initial version ([@DmitryTsepelev])
8
+
9
+ [@DmitryTsepelev]: https://github.com/DmitryTsepelev
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,14 @@
1
+ module CleanActions
2
+ class ActionFailure < ActiveRecord::Rollback
3
+ attr_reader :reason
4
+
5
+ def initialize(reason)
6
+ @reason = reason
7
+ super
8
+ end
9
+
10
+ def ==(other)
11
+ reason == other.reason
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ module CleanActions
2
+ class Base < Action
3
+ include TypedReturns
4
+ include FailWith
5
+ end
6
+ 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,9 @@
1
+ module CleanActions
2
+ class ErrorReporter
3
+ def self.report(message)
4
+ Rails.logger.info(message)
5
+
6
+ raise message if CleanActions.config.raise_errors?
7
+ end
8
+ end
9
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CleanActions
4
+ VERSION = "0.1.0"
5
+ 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: []