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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 655e43f9df76abfb9b93f04cf55ea437635662ffcfef58b78d1227f63d8560af
4
- data.tar.gz: 8343148513834477223867d864b9270f85109780b64ccbcd182373694cc2bd6b
3
+ metadata.gz: 3519bcb78e2b13c959698975e01d44ff031c16abc4d650096054264d14cdf580
4
+ data.tar.gz: dbd636da4a27423d023889ee8ea6863ac192d10930aa9a107309708218efcc7e
5
5
  SHA512:
6
- metadata.gz: ff3ce715ed177028996635b2bac116604447266d0952f7ef8ed33c1b69f3d727b3171903310233768d086af583b3103d258910cc34365b012ef57450f6faccf5
7
- data.tar.gz: 07012d6bbd9be48f99646389d020c90c92a9efdf02da014f66e3cccc04540fb122bf7ddbc26a2cbc5799a9c1d1671fb7399d2e0278f50b2a734004d7b816e73a
6
+ metadata.gz: b6dfb9945b340e4c944e3feeadc54edf3b2e150e1f9725c5764667cd9e80a573e613a72ba2654bfc87280d2cb749fc5cdc06ec93b4830d7e4c4b639b0c4631f1
7
+ data.tar.gz: d77f0cef4644bf1dfa8b96dbad0f89031910857a0f79577aec47d37202a2c8a6dc6de81779fce873d524a387321286c5000d1c2be9fb0c2843c6bdc83591436c
data/.rubocop.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 3.2
3
+ NewCops: enable
3
4
 
4
5
  Layout/LineLength:
5
6
  Max: 120
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][#merge] -- Merges the output of the step with the input args. Best used with keyword arguments.
29
- * [tap][#tap] -- Similar to Ruby `Kernel#tap`, discards the return value of the step
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][#valid] -- Runs a Dry::Schema or Dry::Validation::Contract on the input, and transforms the validation Result to a Result monad.
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`][#use] step with the [Validation
177
- extension][#validation]. Before attempting to run the provided transaction, it
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][#valid] step adapter, Dry::Transaction::Extra has
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`][#use] and [`maybe`][#maybe] steps:
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 "The object provided to the step (#{txn}) does not implement a `validator` method."
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
- # However, it first checks to see if the transaction implements a
17
- # `validator` method (usually provided by the validation_dsl
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
- # normal. If it fails, it continues on with the next step, passing
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}".to_sym
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
- raise NoValidatorError, txn unless txn.respond_to? :validator
39
+ txn_class = txn.is_a?(Class) ? txn : txn.class
40
+ raise NoValidatorError, txn unless txn_class.respond_to? :validator
43
41
 
44
- result = txn.validator.new.call(*args).to_monad
45
- result.bind { txn.call(*args) }
46
- .or { Success(*args) }
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}".to_sym
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, &block)
69
- validator(contract || Class.new(Dry::Validation::Contract, &block))
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
@@ -3,7 +3,7 @@
3
3
  module Dry
4
4
  module Transaction
5
5
  module Extra
6
- VERSION = "0.1.0"
6
+ VERSION = "0.1.2"
7
7
  end
8
8
  end
9
9
  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/tap"
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.0
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: 2023-01-24 00:00:00.000000000 Z
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.4.1
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: []