dry-transaction-extra 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/README.md +14 -7
- data/lib/dry/transaction/extra/active_record_rescues.rb +26 -0
- data/lib/dry/transaction/extra/perform_later.rb +58 -0
- data/lib/dry/transaction/extra/steps/async.rb +55 -0
- data/lib/dry/transaction/extra/steps/maybe.rb +19 -16
- data/lib/dry/transaction/extra/steps/use.rb +3 -3
- data/lib/dry/transaction/extra/validation_dsl.rb +41 -2
- data/lib/dry/transaction/extra/version.rb +1 -1
- data/lib/dry/transaction/extra.rb +13 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3519bcb78e2b13c959698975e01d44ff031c16abc4d650096054264d14cdf580
|
4
|
+
data.tar.gz: dbd636da4a27423d023889ee8ea6863ac192d10930aa9a107309708218efcc7e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b6dfb9945b340e4c944e3feeadc54edf3b2e150e1f9725c5764667cd9e80a573e613a72ba2654bfc87280d2cb749fc5cdc06ec93b4830d7e4c4b639b0c4631f1
|
7
|
+
data.tar.gz: d77f0cef4644bf1dfa8b96dbad0f89031910857a0f79577aec47d37202a2c8a6dc6de81779fce873d524a387321286c5000d1c2be9fb0c2843c6bdc83591436c
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -25,11 +25,18 @@ require "dry-transaction-extra"
|
|
25
25
|
|
26
26
|
Dry::Transaction::Extra defines a few extra steps you can use:
|
27
27
|
|
28
|
-
* [merge]
|
29
|
-
|
28
|
+
* [merge](#merge) -- Merges the output of the step with the input args. Best
|
29
|
+
used with keyword arguments.
|
30
|
+
* [tap](#tap) -- Similar to Ruby `Kernel#tap`, discards the return value of the step
|
30
31
|
and returns the original input. If the step fails, then returns the Failure
|
31
32
|
instead.
|
32
|
-
* [valid]
|
33
|
+
* [valid](#valid) -- Runs a Dry::Schema or Dry::Validation::Contract on the
|
34
|
+
input, and transforms the validation Result to a Result monad.
|
35
|
+
* [use](#use) -- Invokes another transaction (or any other callable), and
|
36
|
+
merges the result.
|
37
|
+
* [maybe](#maybe) -- Optionally invokes another transaction by first
|
38
|
+
attempting to invoke the validator. If the validation fails, it continues to
|
39
|
+
the next step without failing.
|
33
40
|
|
34
41
|
#### `merge`
|
35
42
|
|
@@ -173,8 +180,8 @@ valid ParamsValidator
|
|
173
180
|
|
174
181
|
#### `maybe`
|
175
182
|
|
176
|
-
Maybe combines the [`use`]
|
177
|
-
extension]
|
183
|
+
Maybe combines the [`use`]( #use ) step with the [Validation
|
184
|
+
extension]( #validation ). Before attempting to run the provided transaction, it
|
178
185
|
first runs its defined validator. If that validation passes, then it invokes
|
179
186
|
the transaction. If the validation fails, however, then the transaction
|
180
187
|
continues on, silently ignoring the failure. This is useful in several
|
@@ -227,7 +234,7 @@ class CreateUser
|
|
227
234
|
|
228
235
|
#### Validation
|
229
236
|
|
230
|
-
In addition to the [valid]
|
237
|
+
In addition to the [valid]( #valid ) step adapter, Dry::Transaction::Extra has
|
231
238
|
support for an explicit "pre-flight" validation that runs as the first step.
|
232
239
|
|
233
240
|
```ruby
|
@@ -263,7 +270,7 @@ MyTransaction.new.call(args)
|
|
263
270
|
MyTransaction.call(args)
|
264
271
|
```
|
265
272
|
|
266
|
-
This is particularly useful when invoking transactions via the [`use`]
|
273
|
+
This is particularly useful when invoking transactions via the [`use`]( #use ) and [`maybe`]( #maybe ) steps:
|
267
274
|
|
268
275
|
```
|
269
276
|
use MyTransaction
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transaction
|
5
|
+
module Extra
|
6
|
+
module ActiveRecordRescues
|
7
|
+
RESCUE_ERRORS = [
|
8
|
+
ActiveRecord::RecordInvalid,
|
9
|
+
ActiveRecord::RecordNotFound,
|
10
|
+
ActiveRecord::RecordNotUnique
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
def with_broadcast(args)
|
14
|
+
super
|
15
|
+
rescue *RESCUE_ERRORS => e
|
16
|
+
error = adapter.options[:message] || e
|
17
|
+
Failure(
|
18
|
+
Dry::Transaction::StepFailure.call(self, error) do
|
19
|
+
publish(:step_failed, step_name: name, args: args, value: error)
|
20
|
+
end
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transaction
|
5
|
+
module Extra
|
6
|
+
module PerformLater
|
7
|
+
def self.extended(klass)
|
8
|
+
klass.extend Dry::Core::ClassAttributes
|
9
|
+
|
10
|
+
klass.defines :transaction_job
|
11
|
+
end
|
12
|
+
|
13
|
+
include Dry::Monads::Result::Mixin
|
14
|
+
|
15
|
+
def set(options = {})
|
16
|
+
ConfiguredJob.new(transaction_job, self, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def perform_later(*args)
|
20
|
+
if validator
|
21
|
+
result = validator.new.call(*args).to_monad
|
22
|
+
return result unless result.success?
|
23
|
+
|
24
|
+
args = [result.value!.to_h]
|
25
|
+
end
|
26
|
+
|
27
|
+
Dry::Monads::Success(transaction_job.new(transaction_class_name: name, args:).enqueue)
|
28
|
+
end
|
29
|
+
|
30
|
+
class ConfiguredJob
|
31
|
+
def initialize(job_class, transaction, options = {})
|
32
|
+
@job_class = job_class
|
33
|
+
@transaction = transaction
|
34
|
+
@options = options
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform_later(*args)
|
38
|
+
if validator
|
39
|
+
result = validator.new.call(*args).to_monad
|
40
|
+
return result unless result.success?
|
41
|
+
|
42
|
+
args = [result.value!.to_h]
|
43
|
+
end
|
44
|
+
|
45
|
+
Dry::Monads::Success(
|
46
|
+
transaction_job.new(transaction_class_name: @transaction.name, args:)
|
47
|
+
.enqueue(@options)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def validator
|
52
|
+
@transaction.validator
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transaction
|
5
|
+
module Extra
|
6
|
+
module Steps
|
7
|
+
# Executes the step in a background job. Argument is an ActiveJob or anything that
|
8
|
+
# implements `#perform_later`. This can include other Transactions when using the
|
9
|
+
# :perform_later extension.
|
10
|
+
#
|
11
|
+
# If the provided transaction implements a `validate` step, then that validator will be
|
12
|
+
# called on the input before the job is enqueued. This prevents us from enqueuing jobs with
|
13
|
+
# garbage arguemnts that can never be run, and limits the params passed through the message
|
14
|
+
# body into only those relevant to the job.
|
15
|
+
#
|
16
|
+
# Additionally, ActiveJob only allows for the serialization of a few types of values into
|
17
|
+
# the message, Strings, Numbers and ActiveRecord::Model instances (via globalid). Anything
|
18
|
+
# else will raise an ActiveJob::SerializationError. Calling the validator beforehand helps
|
19
|
+
# strip those out as well.
|
20
|
+
#
|
21
|
+
# Accepts an optional argument of delay:, to allow for jobs to be performed later, instead
|
22
|
+
# of as soon as possible.
|
23
|
+
#
|
24
|
+
# Usage:
|
25
|
+
#
|
26
|
+
# async GuestsCleanupJob # A job
|
27
|
+
# async Guests::CleanStale # A transaction
|
28
|
+
# async DoThisLater, delay: 5.minutes # Optional delay for the job
|
29
|
+
#
|
30
|
+
module Async
|
31
|
+
module DSL
|
32
|
+
def async(job, delay: nil)
|
33
|
+
method_name = job.name.underscore.intern
|
34
|
+
step method_name
|
35
|
+
define_method method_name do |input = {}|
|
36
|
+
job = job.set(wait: delay) if delay
|
37
|
+
|
38
|
+
if (validator = job&.validator)
|
39
|
+
result = validator.new.call(input)
|
40
|
+
# If the validator failed, don't enqueue the job, but don't
|
41
|
+
# also fail the step
|
42
|
+
job.perform_later(**result.to_h) if result.success?
|
43
|
+
else
|
44
|
+
job.perform_later(**input)
|
45
|
+
end
|
46
|
+
|
47
|
+
Success(input)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -8,42 +8,45 @@ module Dry
|
|
8
8
|
module DSL
|
9
9
|
class NoValidatorError < ArgumentError
|
10
10
|
def initialize(txn)
|
11
|
-
super
|
11
|
+
super("The object provided to the step (#{txn}) does not implement a `validator` method.")
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
# Just like the `use` step, this invokes another transaction.
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# extension), and if so, will call it before invoking the
|
19
|
-
# transaction.
|
15
|
+
# Just like the `use` step, this invokes another transaction. However, it first checks
|
16
|
+
# to see if the transaction implements a `validator` method (usually provided by the
|
17
|
+
# validation_dsl extension), and if so, will call it before invoking the transaction.
|
20
18
|
#
|
21
|
-
# If the validation succeeds, then it invokes the transaction as
|
22
|
-
#
|
23
|
-
# the original input through.
|
19
|
+
# If the validation succeeds, then it invokes the transaction as normal. If it fails, it
|
20
|
+
# continues on with the next step, passing the original input through.
|
24
21
|
#
|
25
22
|
# @example
|
26
23
|
#
|
27
24
|
# step :create_user
|
28
25
|
# maybe VerifyEmail
|
29
26
|
#
|
30
|
-
def maybe(txn_or_container, key = nil, as: nil)
|
27
|
+
def maybe(txn_or_container, key = nil, as: nil, **)
|
31
28
|
if key
|
32
29
|
container = txn_or_container
|
33
|
-
method_name = as || "#{container.name}.#{key}"
|
30
|
+
method_name = as || :"#{container.name}.#{key}"
|
34
31
|
else
|
35
32
|
txn = txn_or_container
|
36
33
|
method_name = as || txn.name.to_sym
|
37
34
|
end
|
38
35
|
|
39
|
-
merge(method_name, as
|
36
|
+
merge(method_name, as:, **)
|
40
37
|
define_method method_name do |*args|
|
41
38
|
txn = container[key] if key
|
42
|
-
|
39
|
+
txn_class = txn.is_a?(Class) ? txn : txn.class
|
40
|
+
raise NoValidatorError, txn unless txn_class.respond_to? :validator
|
43
41
|
|
44
|
-
result =
|
45
|
-
result.
|
46
|
-
|
42
|
+
result = txn_class.validator.new.call(*args)
|
43
|
+
if result.failure?
|
44
|
+
# publish(:maybe_failed, step_name: method_name, args:, value: result)
|
45
|
+
Rails.logger.debug "Skipping #{txn} because of errors: #{result.errors.to_h}" if defined?(Rails)
|
46
|
+
return Success(*args)
|
47
|
+
end
|
48
|
+
|
49
|
+
txn.call(*args)
|
47
50
|
end
|
48
51
|
rescue NoMethodError => e
|
49
52
|
raise e unless e.name == :name
|
@@ -38,16 +38,16 @@ module Dry
|
|
38
38
|
#
|
39
39
|
# # => { user: #<User: id=1> }
|
40
40
|
#
|
41
|
-
def use(txn_or_container, key = nil, as: nil)
|
41
|
+
def use(txn_or_container, key = nil, as: nil, **)
|
42
42
|
if key
|
43
43
|
container = txn_or_container
|
44
|
-
method_name = as || "#{container.name}.#{key}"
|
44
|
+
method_name = as || :"#{container.name}.#{key}"
|
45
45
|
else
|
46
46
|
txn = txn_or_container
|
47
47
|
method_name = as || txn.name.to_sym
|
48
48
|
end
|
49
49
|
|
50
|
-
merge(method_name, as
|
50
|
+
merge(method_name, as:, **)
|
51
51
|
define_method method_name do |*args|
|
52
52
|
txn = container[key] if key
|
53
53
|
txn.call(*args)
|
@@ -26,6 +26,45 @@ module Dry
|
|
26
26
|
# # => #<Dry::Validation::Result{name: "Jane"} errors={}>
|
27
27
|
klass.defines :validator
|
28
28
|
|
29
|
+
# Allows overriding the default validation contract class. This is useful if you want to
|
30
|
+
# use a different Contract class with a different configuration.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
#
|
34
|
+
# module MyApp
|
35
|
+
# module Types
|
36
|
+
# include Dry.Types()
|
37
|
+
#
|
38
|
+
# Container = Dry::Schema::TypeContainer.new
|
39
|
+
# Container.register("params.email", String.constrained(format: /@/))
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# class Contract < Dry::Validation::Contract
|
43
|
+
# config.types = Types::Container
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# module ApplicationTransaction
|
48
|
+
# include Dry::Transaction
|
49
|
+
# include Dry::Transaction::Extra
|
50
|
+
#
|
51
|
+
# load_extensions :validation
|
52
|
+
#
|
53
|
+
# validation_contract_class MyApp::Contract
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# class MyTransaction
|
57
|
+
# include ApplicationTransaction
|
58
|
+
#
|
59
|
+
# validate do
|
60
|
+
# params do
|
61
|
+
# # Now the custom `:email` type is available in this schema
|
62
|
+
# required(:email).filled(:email)
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
klass.defines :validation_contract_class
|
67
|
+
|
29
68
|
require "dry/validation"
|
30
69
|
Dry::Validation.load_extensions(:monads)
|
31
70
|
end
|
@@ -65,8 +104,8 @@ module Dry
|
|
65
104
|
# validate NewUserContract
|
66
105
|
# end
|
67
106
|
#
|
68
|
-
def validate(contract = nil, &
|
69
|
-
validator(contract || Class.new(Dry::Validation::Contract, &
|
107
|
+
def validate(contract = nil, &)
|
108
|
+
validator(contract || Class.new(validation_contract_class || Dry::Validation::Contract, &))
|
70
109
|
|
71
110
|
valid(validator.new, name: "validate")
|
72
111
|
end
|
@@ -5,19 +5,23 @@ require_relative "extra/version"
|
|
5
5
|
require "dry/monads"
|
6
6
|
require "dry/transaction"
|
7
7
|
|
8
|
-
require_relative "extra/steps/
|
8
|
+
require_relative "extra/steps/async"
|
9
9
|
require_relative "extra/steps/maybe"
|
10
10
|
require_relative "extra/steps/merge"
|
11
|
+
require_relative "extra/steps/tap"
|
11
12
|
require_relative "extra/steps/use"
|
12
13
|
require_relative "extra/steps/valid"
|
13
14
|
|
15
|
+
require_relative "extra/active_record_rescues"
|
14
16
|
require_relative "extra/class_callable"
|
17
|
+
require_relative "extra/perform_later"
|
15
18
|
require_relative "extra/validation_dsl"
|
16
19
|
|
17
20
|
module Dry
|
18
21
|
module Transaction
|
19
22
|
module Extra
|
20
23
|
def self.included(klass)
|
24
|
+
klass.extend Extra::Steps::Async::DSL
|
21
25
|
klass.extend Extra::Steps::Maybe::DSL
|
22
26
|
klass.extend Extra::Steps::Use::DSL
|
23
27
|
klass.extend Extra::Steps::Valid::DSL
|
@@ -31,6 +35,14 @@ module Dry
|
|
31
35
|
klass.register_extension :class_callable do
|
32
36
|
klass.extend ClassCallable
|
33
37
|
end
|
38
|
+
|
39
|
+
klass.register_extension :perform_later do
|
40
|
+
klass.extend PerformLater
|
41
|
+
end
|
42
|
+
|
43
|
+
klass.register_extension :active_record_rescues do
|
44
|
+
Dry::Transaction::Step.prepend(ActiveRecordRescues)
|
45
|
+
end
|
34
46
|
end
|
35
47
|
|
36
48
|
adapters = Dry::Transaction::StepAdapters
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dry-transaction-extra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Sadauskas
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-04-11 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: dry-monads
|
@@ -98,7 +97,10 @@ files:
|
|
98
97
|
- Rakefile
|
99
98
|
- dry-transaction-extra.gemspec
|
100
99
|
- lib/dry/transaction/extra.rb
|
100
|
+
- lib/dry/transaction/extra/active_record_rescues.rb
|
101
101
|
- lib/dry/transaction/extra/class_callable.rb
|
102
|
+
- lib/dry/transaction/extra/perform_later.rb
|
103
|
+
- lib/dry/transaction/extra/steps/async.rb
|
102
104
|
- lib/dry/transaction/extra/steps/maybe.rb
|
103
105
|
- lib/dry/transaction/extra/steps/merge.rb
|
104
106
|
- lib/dry/transaction/extra/steps/tap.rb
|
@@ -113,7 +115,6 @@ licenses:
|
|
113
115
|
metadata:
|
114
116
|
homepage_uri: https://github.com/paul/dry-transaction-extra
|
115
117
|
source_code_uri: https://github.com/paul/dry-transaction-extra
|
116
|
-
post_install_message:
|
117
118
|
rdoc_options: []
|
118
119
|
require_paths:
|
119
120
|
- lib
|
@@ -128,8 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
129
|
- !ruby/object:Gem::Version
|
129
130
|
version: '0'
|
130
131
|
requirements: []
|
131
|
-
rubygems_version: 3.
|
132
|
-
signing_key:
|
132
|
+
rubygems_version: 3.6.2
|
133
133
|
specification_version: 4
|
134
134
|
summary: Extra steps and functionality for Dry::Transaction
|
135
135
|
test_files: []
|