use_cases 0.3.8 → 1.0.4

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: ad9722f48d9d6f073a9699cb042c246d935994b4384e340e3b985c35ea6a697b
4
- data.tar.gz: 8d3cc6daeef1b64fb86a96a9e6ff13a3b95ef328ff2a4e453696cae0724a1572
3
+ metadata.gz: 0c1ed9f7d86b9109062b207489f37e67f84b93e676d7e81b930f219abbd47224
4
+ data.tar.gz: 2b9234f7075cb8240836875845d5d61036caa9a3c7feeb72abf31d2dd5830f91
5
5
  SHA512:
6
- metadata.gz: aefef04cfa79694d593848a6c245ad3d282416fdf28de6a934581f8fa15bfadf1faf5fccf1624a272b997473619af36841464feb8b1f26b6329ee8333a688e4b
7
- data.tar.gz: 00bb31d99dfd4e9f1d98d99778b1263e4d32949dc5270af41f78d19dba6a460afeba44797219984dcbe964c516c150e2c0ee1db1392645760de14a7599ae2876
6
+ metadata.gz: fe3e66534633de66dff8811dad8a3d2c9f0f350d392db60a7e91989d11a861510e32937abc77f47ff8d74b9439a8c6bb321e18d655f655af0778bf676efcc28f
7
+ data.tar.gz: 4da7071b68b859462feda741591b9862330f9b7dbe5ad127a76ff6052ac13bf7622241620c17a3ef20ee52e6f8b16c67e44b398f01621643df646b6d94759e62
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.1] - 2021-12-19
4
+
5
+ - Async published events are now suffixed by `".aync"`
6
+
7
+ ## [1.0.0] - 2021-12-19
8
+
9
+ - Fixed some minor bugs that have been pending todos.
10
+ - Deprecated the `UseCases::Base` superclass as a DSL injection method. You must now use the `UseCase` module.
11
+ - Added the `publish` option for steps, which allows the publishing of an event when a step is completed.
12
+
3
13
  ## [0.1.0] - 2021-09-21
4
14
 
5
15
  - Added the basic DSL of them with the following modules:
@@ -9,7 +19,7 @@
9
19
  - Notifications [WIP]
10
20
  - StackRunner
11
21
  - Stack
12
- - StepResult
22
+ - UseCases::Result
13
23
  - Steps
14
24
  - Validate
15
25
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- use_cases (0.3.7)
4
+ use_cases (1.0.2)
5
5
  activesupport
6
6
  dry-events
7
7
  dry-matcher
data/README.md CHANGED
@@ -4,17 +4,17 @@
4
4
 
5
5
  # UseCases
6
6
 
7
- ## Currently Unstable! Use at your own risk.
7
+ `UseCases` is a dry-ecosystem-based gem that implements a DSL for the use case pattern using the railway programming paradigm.
8
8
 
9
- `UseCases` is a gem based on the [dry-transaction](https://dry-rb.org/gems/dry-transaction/) DSL that implements macros commonly used internally by Ring Twice.
10
-
11
- `UseCases` does not however use `dry-transaction` behind the scenes. Instead it relies on other `dry` libraries like [dry-validation](https://dry-rb.org/gems/dry-validation/), [dry-events](https://dry-rb.org/gems/dry-validation/) and [dry-monads](https://dry-rb.org/gems/dry-validation/) to implement a DSL that can be flexible enough for our needs.
9
+ It's concept is largely based on `dry-transaction` but does not use it behind the scenes. Instead it relies on other `dry` libraries like [dry-validation](https://dry-rb.org/gems/dry-validation/), [dry-events](https://dry-rb.org/gems/dry-validation/) and [dry-monads](https://dry-rb.org/gems/dry-validation/) to implement a DSL that can be flexible enough for your needs.
12
10
 
13
11
  ## Why `UseCases` came about:
14
12
 
15
13
  1. It allows you to use `dry-validation` without much gymastics.
16
14
  2. It abstracts common steps like **authorization** and **validation** into macros.
17
15
  3. It solves what we consider a problem of `dry-transaction`. The way it funnels down `input` through `Dry::Monads` payloads alone. `UseCases` offers more flexibility in a way that still promotes functional programming values.
16
+ 4. It implements a simple pub/sub mechanism which can be async when `ActiveJob` is a project dependency.
17
+ 5. It implements an `enqueue` mechanism to delay execution of steps, also using `ActiveJob` as a dependency.
18
18
 
19
19
  ## Installation
20
20
 
@@ -34,25 +34,34 @@ Or install it yourself as:
34
34
 
35
35
  ## Usage
36
36
 
37
- To fully understand `UseCases`, make sure to read [dry-transaction](https://dry-rb.org/gems/dry-transaction/0.13/)'s documentation first.
37
+ To get a good basis to get started on `UseCases`, make sure to read [dry-transaction](https://dry-rb.org/gems/dry-transaction/0.13/)'s documentation first.
38
38
 
39
39
  ### Validations
40
40
 
41
41
  See [dry-validation](https://dry-rb.org/gems/dry-validation/)
42
42
 
43
- ### Step Adapters
44
-
45
- https://dry-rb.org/gems/dry-transaction/0.13/step-adapters/
43
+ ### Creating a Use Case
46
44
 
47
45
  **Basic Example**
48
46
 
49
47
  ```ruby
50
- class YourCase < UseCases::Base
51
- params {}
48
+ class DeleteUser
49
+ include UseCase
50
+
51
+ check :current_user_is_user?
52
+ step :build_user
53
+ map :persist_user
54
+
55
+ private
56
+
57
+ def current_user_is_user?(params, current_user)
58
+ end
59
+
60
+ def build_user(params, current_user)
61
+ end
52
62
 
53
- step :do_something
54
63
 
55
- def do_something(params, current_user)
64
+ def do_something(user, params, current_user)
56
65
  params[:should_fail] ? Failure([:failed, "failed"]) : Success("it succeeds!")
57
66
  end
58
67
  end
data/lib/use_case.rb CHANGED
@@ -6,20 +6,16 @@ require "dry/monads/do"
6
6
  require "dry/monads/do/all"
7
7
  require "dry/matcher/result_matcher"
8
8
 
9
- require "use_cases/authorize"
10
9
  require "use_cases/dsl"
11
- require "use_cases/errors"
12
- require "use_cases/validate"
13
10
  require "use_cases/stack"
14
11
  require "use_cases/params"
15
12
  require "use_cases/stack_runner"
16
- require "use_cases/step_result"
17
- require "use_cases/notifications"
18
- require "use_cases/prepare"
13
+ require "use_cases/result"
19
14
  require "use_cases/step_adapters"
20
15
  require "use_cases/module_optins"
21
16
 
22
17
  module UseCase
18
+ extend UseCases::DSL
23
19
  extend UseCases::ModuleOptins
24
20
 
25
21
  def self.included(base)
@@ -33,7 +29,6 @@ module UseCase
33
29
  extend UseCases::ModuleOptins
34
30
 
35
31
  include UseCases::StepAdapters
36
- include UseCases::Notifications
37
32
  end
38
33
  end
39
34
 
@@ -41,7 +36,6 @@ module UseCase
41
36
 
42
37
  def initialize(*)
43
38
  @stack = UseCases::Stack.new(self.class.__steps__).bind(self)
44
- # self.class.bind_step_subscriptions
45
39
  end
46
40
 
47
41
  def call(params, current_user = nil)
data/lib/use_cases/dsl.rb CHANGED
@@ -17,19 +17,5 @@ module UseCases
17
17
  def __steps__
18
18
  @__steps__ ||= []
19
19
  end
20
-
21
- def subscribe(listeners)
22
- @listeners = listeners
23
-
24
- if listeners.is_a?(Hash)
25
- listeners.each do |step_name, listener|
26
- __steps__.detect { |step| step.name == step_name }.subscribe(listener)
27
- end
28
- else
29
- __steps__.each do |step|
30
- step.subscribe(listeners)
31
- end
32
- end
33
- end
34
20
  end
35
21
  end
@@ -0,0 +1,19 @@
1
+
2
+ return unless defined? ActiveJob
3
+
4
+ module UseCases
5
+ module Events
6
+ class PublishJob < ActiveJob::Base
7
+ def perform(publish_key, payload)
8
+ publish_key += '.async'
9
+ UseCases.publisher.publish(publish_key, payload)
10
+ end
11
+
12
+ private
13
+
14
+ def publisher
15
+ UseCases.publisher
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ require 'dry/events/publisher'
2
+
3
+ module UseCases
4
+ module Events
5
+ class Publisher
6
+ include Dry::Events::Publisher[:use_cases]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module UseCases
2
+ module Events
3
+ class Subscriber
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/check"
4
+
5
+ module UseCases
6
+ module ModuleOptins
7
+ module Authorized
8
+ def self.included(base)
9
+ super
10
+ base.class_eval do
11
+ extend DSL
12
+ end
13
+ end
14
+
15
+ module DSL
16
+ DEFAULT_OPTIONS = {
17
+ failure: :unauthorized,
18
+ failure_message: "Not Authorized",
19
+ merge_input_as: :resource
20
+ }
21
+
22
+ def authorize(name, options = {})
23
+ options = DEFAULT_OPTIONS.merge(options)
24
+
25
+ check name, options
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/tee"
4
+
5
+ module UseCases
6
+ module ModuleOptins
7
+ module Prepared
8
+ def self.included(base)
9
+ super
10
+ base.class_eval do
11
+ extend DSL
12
+ end
13
+ end
14
+
15
+ module DSL
16
+ def prepare(name, options = {})
17
+ __steps__.unshift StepAdapters::Tee.new(name, nil, options)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/events/publisher"
4
+ require "use_cases/events/subscriber"
5
+ require "use_cases/events/publish_job"
6
+
7
+ module UseCases
8
+ module ModuleOptins
9
+ module Publishing
10
+ def self.included(base)
11
+ super
12
+ base.prepend DoCallPatch
13
+ end
14
+
15
+ module DoCallPatch
16
+ def do_call(*args)
17
+ super(*args, method(:publish_step_result).to_proc)
18
+ end
19
+
20
+ def publish_step_result(step_result, step_object)
21
+ publish_key = step_object.options[:publish]
22
+ publish_key += step_result.success? ? ".success" : ".failure"
23
+ payload = step_result.value!
24
+ return unless publish_key
25
+
26
+ register_event(publish_key)
27
+ peform_publish(publish_key, payload)
28
+ end
29
+
30
+ def register_event(publish_key)
31
+ return if UseCases.publisher.class.events[publish_key]
32
+
33
+ UseCases.publisher.class.register_event(publish_key)
34
+ end
35
+
36
+ def peform_publish(publish_key, payload)
37
+ UseCases.publisher.publish(publish_key, payload)
38
+ return unless defined? UseCases::Events::PublishJob
39
+
40
+ UseCases::Events::PublishJob.perform_later(publish_key, payload.to_json)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UseCases
4
+ module ModuleOptins
5
+ module Transactional
6
+ class TransactionHandlerUndefined < StandardError; end
7
+
8
+ class TransactionHandlerInvalid < StandardError; end
9
+
10
+ def self.included(base)
11
+ super
12
+ base.prepend DoCallPatch
13
+ end
14
+
15
+ module DoCallPatch
16
+ def do_call(*args)
17
+ unless respond_to?(:transaction_handler)
18
+ raise TransactionHandlerUndefined, "when using *transactional*, make sure to include a transaction handler in your dependencies."
19
+ end
20
+
21
+ raise TransactionHandlerInvalid, "Make sure your transaction_handler implements #transaction." unless transaction_handler.respond_to?(:transaction)
22
+
23
+ transaction_handler.transaction { super }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/validation/contract"
4
+
5
+ module UseCases
6
+ module ModuleOptins
7
+ module Validated
8
+ class NoValidationError < StandardError; end
9
+
10
+ def self.included(base)
11
+ super
12
+ base.class_eval do
13
+ extend DSL
14
+ extend ClassMethods
15
+ prepend CallPatch
16
+ end
17
+ end
18
+
19
+ module CallPatch
20
+ def call(*args)
21
+ unless stack.include_step?(:validate)
22
+ raise NoValidationError,
23
+ "Make sure to define params validations by using *params*" \
24
+ "*schema*, *json*, *rule* or *option* macros in your use case."
25
+ end
26
+
27
+ super
28
+ end
29
+ end
30
+
31
+ module DSL
32
+ def params(*args, &blk)
33
+ _setup_validation
34
+
35
+ _contract_class.params(*args, &blk)
36
+ end
37
+
38
+ def schema(*args, &blk)
39
+ _setup_validation
40
+
41
+ _contract_class.schema(*args, &blk)
42
+ end
43
+
44
+ def rule(*args, &blk)
45
+ _setup_validation
46
+
47
+ _contract_class.rule(*args, &blk)
48
+ end
49
+
50
+ def json(*args, &blk)
51
+ _setup_validation
52
+
53
+ _contract_class.json(*args, &blk)
54
+ end
55
+
56
+ def option(*args, &blk)
57
+ _contract_class.option(*args, &blk)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def validate(params, *_args)
64
+ return Failure([:validation_error, "*params* must be a hash."]) unless params.respond_to?(:merge)
65
+
66
+ validation = contract.call(params)
67
+
68
+ if validation.success?
69
+ params.merge!(validation.to_h)
70
+ Success(validation.to_h)
71
+ else
72
+ Failure([:validation_error, validation.errors.to_h])
73
+ end
74
+ end
75
+
76
+ def contract
77
+ return self.class._contract_class.new if self.class._contract_class_defined?
78
+ end
79
+
80
+ module ClassMethods
81
+ def _setup_validation
82
+ _define_contract_class unless _contract_class_defined?
83
+ _define_validation_step unless _validation_step_defined?
84
+ end
85
+
86
+ def _define_validation_step
87
+ step :validate
88
+ end
89
+
90
+ def _contract_class
91
+ self::Contract
92
+ end
93
+
94
+ def _define_contract_class
95
+ const_set(:Contract, Class.new(Dry::Validation::Contract))
96
+ end
97
+
98
+ def _contract_class_name
99
+ "#{name}::Contract"
100
+ end
101
+
102
+ def _contract_class_defined?
103
+ Object.const_defined? _contract_class_name
104
+ end
105
+
106
+ def _validation_step_defined?
107
+ __steps__.map(&:name).include?(:validate)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -1,39 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "use_cases/authorize"
4
- require "use_cases/prepare"
5
- require "use_cases/transaction"
6
- require "use_cases/validate"
3
+ require "use_cases/module_optins/prepared"
4
+ require "use_cases/module_optins/transactional"
5
+ require "use_cases/module_optins/validated"
6
+ require "use_cases/module_optins/publishing"
7
+ require "use_cases/module_optins/authorized"
7
8
 
8
9
  module UseCases
9
10
  module ModuleOptins
10
11
  attr_accessor :options
11
12
 
13
+ OPTINS = {
14
+ authorized: Authorized,
15
+ transactional: Transactional,
16
+ validated: Validated,
17
+ prepared: Prepared,
18
+ publishing: Publishing
19
+ }
20
+
12
21
  def [](*options)
13
- @modules = []
14
- @modules << UseCases::Authorize if options.include?(:authorized)
15
- @modules << UseCases::Transaction if options.include?(:transactional)
16
- @modules << UseCases::Validate if options.include?(:validated)
17
- @modules << UseCases::Prepare if options.include?(:prepared)
18
- self
19
- end
22
+ modules = [self]
20
23
 
21
- def included(base)
22
- super
23
- @modules ||= []
24
- return if @modules.empty?
24
+ OPTINS.each do |key, module_constant|
25
+ modules << module_constant if options.include?(key)
26
+ end
25
27
 
26
- base.include(*@modules)
27
- @modules = nil
28
- end
28
+ supermodule = Module.new
29
29
 
30
- def inherited(base)
31
- super
32
- @modules ||= []
33
- return if @modules.empty?
30
+ supermodule.define_singleton_method(:included) do |base|
31
+ base.include(*modules)
32
+ end
34
33
 
35
- base.include(*@modules)
36
- @modules = nil
34
+ supermodule
37
35
  end
38
36
 
39
37
  def descendants
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UseCases
4
- class StepResult < Dry::Monads::Result
4
+ class Result < Dry::Monads::Result
5
5
  attr_reader :step, :result
6
6
 
7
7
  def initialize(step, result)
@@ -14,7 +14,7 @@ module UseCases
14
14
  return result if result_not_monad?
15
15
  return nil if result_empty?
16
16
 
17
- result.success? ? result.value! : result
17
+ result.success? ? result.value! : result.failure
18
18
  end
19
19
  alias value! value
20
20
 
@@ -23,25 +23,23 @@ module UseCases
23
23
  end
24
24
 
25
25
  def failure?
26
- value.is_a?(Dry::Monads::Result::Failure)
27
- end
28
-
29
- def failure
30
- failure? && value.failure
26
+ result.is_a?(Dry::Monads::Result::Failure)
31
27
  end
32
28
 
33
29
  def success
34
30
  success? && value
35
31
  end
36
32
 
33
+ def failure
34
+ failure? && value
35
+ end
36
+
37
37
  def nil?
38
38
  value.nil?
39
39
  end
40
40
 
41
41
  def to_result
42
- value.to_result if failure?
43
-
44
- result
42
+ self
45
43
  end
46
44
 
47
45
  def result_empty?
@@ -51,5 +49,13 @@ module UseCases
51
49
  def result_not_monad?
52
50
  !result.is_a?(Dry::Monads::Result)
53
51
  end
52
+
53
+ def inspect
54
+ if failure?
55
+ "UseCases::Result.Failure(#{value.inspect})"
56
+ else
57
+ "UseCases::Result.Success(#{value.inspect})"
58
+ end
59
+ end
54
60
  end
55
61
  end
@@ -38,13 +38,15 @@ RSpec::Matchers.define(:be_failure_with) do |*expected_failure|
38
38
  expect(test_subject.failure).to eql expected_failure
39
39
  end
40
40
 
41
+ expected_result, expected_code = expect_failure
42
+
41
43
  failure_message do |test_subject|
42
44
  if test_subject.failure?
43
- "the use case was expected to fail with #{expected_result.inspect} but it returned #{test_subject.failure.inspect}"
45
+ "the use case was expected to fail with #{expected_code} and #{expected_result.inspect} but it returned #{test_subject.failure.inspect}"
44
46
  else
45
47
  "the use case was expected to fail but it succeeded with #{test_subject.success.inspect}"
46
48
  end
47
- end
49
+ end
48
50
  end
49
51
 
50
52
  RSpec::Matchers.define(:be_successful_with) do |expected_result|
@@ -59,5 +61,5 @@ RSpec::Matchers.define(:be_successful_with) do |expected_result|
59
61
  else
60
62
  "the use case was expected to succeed but it failed with #{test_subject.failure.inspect}"
61
63
  end
62
- end
64
+ end
63
65
  end
@@ -8,9 +8,7 @@ module UseCases
8
8
  @stack = stack
9
9
  end
10
10
 
11
- def call(*args, &around_block)
12
- return around_block.call { do_call(*args) } if around_block
13
-
11
+ def call(*args)
14
12
  do_call(*args)
15
13
  end
16
14
 
@@ -19,6 +17,7 @@ module UseCases
19
17
  def do_call(*args)
20
18
  stack.call do
21
19
  result = _run_step(stack, args)
20
+ args.last.call(result, stack.current_step) if args.last.is_a? Proc
22
21
 
23
22
  return result if result.failure?
24
23
 
@@ -28,29 +27,21 @@ module UseCases
28
27
 
29
28
  def _run_step(stack, args)
30
29
  step = stack.current_step
31
- expected_args_count = step.args_count
32
- step_args = _assert_step_arguments_with_count(stack, args)
30
+ step_args = _assert_step_arguments(stack, args)
33
31
 
34
32
  raise MissingStepError, "Missing ##{step.name} implementation." if step.missing?
35
33
 
36
- if expected_args_count != step_args.count
37
- raise StepArgumentError,
38
- "##{step.name} expects #{expected_args_count} arguments it only received #{step_args.count}, make sure your previous step Success() statement has a payload."
39
- end
40
-
41
34
  step.call(*step_args)
42
35
  end
43
36
 
44
- def _assert_step_arguments_with_count(stack, args)
45
- step_args_count = stack.current_step.args_count
46
-
37
+ def _assert_step_arguments(stack, args)
47
38
  if _should_prepend_previous_step_result_to_args?(stack)
48
39
  prev_step_result_value = stack.previous_step_value
49
40
 
50
41
  args = [prev_step_result_value] + args
51
42
  end
52
43
 
53
- args.first(step_args_count)
44
+ args
54
45
  end
55
46
 
56
47
  def _should_prepend_previous_step_result_to_args?(stack)
@@ -16,7 +16,7 @@ module UseCases
16
16
  def deserialize_step_arguments(args)
17
17
  args.map do |arg|
18
18
  if arg.is_a?(Hash) && arg.delete("_serialized_by_use_case")
19
- arg.delete('_class').constantize.new(arg)
19
+ arg.delete("_class").constantize.new(arg)
20
20
  else
21
21
  arg
22
22
  end
@@ -1,21 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/monads/all"
4
+ require "byebug"
4
5
 
5
6
  module UseCases
6
7
  module StepAdapters
7
8
  class Abstract
8
9
  include Dry::Monads
9
-
10
- # include Dry::Events::Publisher[name || object_id]
11
-
12
- # def self.inherited(subclass)
13
- # super
14
- # subclass.register_event(:step)
15
- # subclass.register_event(:step_succeeded)
16
- # subclass.register_event(:step_failed)
17
- # end
18
-
19
10
  include Dry::Monads[:result]
20
11
 
21
12
  attr_reader :name, :object, :failure, :options
@@ -31,22 +22,11 @@ module UseCases
31
22
  end
32
23
 
33
24
  def call(*args)
34
- around_call(name, args: args) do
35
- before_call(name, args: args)
36
-
37
- result = StepResult.new(self, do_call(*args))
38
-
39
- if result.success?
40
- after_call_success(name, args: args, value: result.value)
41
- else
42
- after_call_failure(name, args: args, value: result.value)
43
- end
44
-
45
- result
46
- end
25
+ UseCases::Result.new(self, do_call(*args))
47
26
  end
48
27
 
49
- def do_call(*args)
28
+ def do_call(*initial_args)
29
+ args = callable_args(initial_args)
50
30
  callable_proc.call(*args)
51
31
  end
52
32
 
@@ -54,45 +34,71 @@ module UseCases
54
34
  self.class.new(name, object, options)
55
35
  end
56
36
 
37
+ def callable_args(args)
38
+ if callable_args_count > args.count
39
+ raise StepArgumentError,
40
+ "##{step.name} expects #{expected_args_count} arguments it only received #{step_args.count}, make sure your previous step Success() statement has a payload."
41
+ end
42
+
43
+ return args.first(callable_args_count) unless external? && selects_external_args?
44
+
45
+ hashed_args(args)
46
+ end
47
+
48
+ def hashed_args(args)
49
+ {
50
+ params: args.first,
51
+ current_user: args.last,
52
+ previous_step_result: args.last
53
+ }.slice(pass_option)
54
+ end
55
+
57
56
  def callable_proc
58
57
  callable_object.method(callable_method).to_proc
59
58
  end
60
59
 
61
60
  def callable_object
62
- case options[:with]
61
+ case with_option
63
62
  when NilClass, FalseClass then object
64
- when String, Symbol then object.send(options[:with])
65
- else options[:with]
63
+ when Symbol then object.send(with_option)
64
+ when String then UseCases.config.container[with_option]
65
+ else with_option
66
66
  end
67
67
  end
68
68
 
69
69
  def callable_method
70
- case options[:with]
70
+ case with_option
71
71
  when NilClass, FalseClass then name
72
72
  else :call
73
73
  end
74
74
  end
75
75
 
76
- def external?
77
- options[:with].present?
76
+ def with_option
77
+ options[:with]
78
78
  end
79
79
 
80
- def args_count
81
- callable_proc.parameters.count
80
+ def pass_option
81
+ options[:pass]
82
82
  end
83
83
 
84
- def missing?
85
- !callable_object.respond_to?(callable_method, true)
84
+ def previous_input_param_name
85
+ options[:merge_input_as] || :input
86
86
  end
87
87
 
88
- def before_call(*args); end
88
+ def external?
89
+ with_option.present?
90
+ end
89
91
 
90
- def after_call_success(*args); end
92
+ def selects_external_args?
93
+ pass_option.present?
94
+ end
91
95
 
92
- def after_call_failure(*args); end
96
+ def callable_args_count
97
+ callable_proc.parameters.count
98
+ end
93
99
 
94
- def around_call(*_args, &blk)
95
- blk.call
100
+ def missing?
101
+ !callable_object.respond_to?(callable_method, true)
96
102
  end
97
103
  end
98
104
  end
@@ -11,6 +11,7 @@ require "use_cases/step_active_job_adapter"
11
11
  module UseCases
12
12
  module StepAdapters
13
13
  def self.included(base)
14
+ super
14
15
  base.class_eval do
15
16
  register_adapter StepAdapters::Step
16
17
  register_adapter StepAdapters::Tee
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UseCases
4
- VERSION = "0.3.8"
4
+ VERSION = "1.0.4"
5
5
  end
data/lib/use_cases.rb CHANGED
@@ -1,11 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry/configurable"
4
+
3
5
  require_relative "use_case"
4
6
  require_relative "use_cases/version"
5
- require_relative "use_cases/base"
6
7
 
7
8
  module UseCases
8
- def self.register_type(type_name, klass)
9
+ class StepArgumentError < ArgumentError; end
10
+
11
+ class MissingStepError < NoMethodError; end
12
+
13
+ class PreviousStepInvalidReturn < StandardError; end
14
+
15
+ extend Dry::Configurable
16
+
17
+ setting :container, reader: true
18
+ setting :publisher, ::UseCases::Events::Publisher.new, reader: true
19
+ setting :subscribers, [], reader: true
20
+
21
+ def self.configure(&blk)
22
+ super
23
+ finalize_subscriptions!
24
+ end
25
+
26
+ def self.finalize_subscriptions!
27
+ subscribe(*subscribers)
28
+ return unless UseCases::Events::Subscriber.respond_to?(:descendants)
29
+
30
+ usecase_subscriber_children = UseCases::Events::Subscriber.descendants
31
+ subscribe(*usecase_subscriber_children)
32
+ subscribers.concat(usecase_subscriber_children)
33
+ end
9
34
 
35
+ def self.subscribe(*subscribers)
36
+ subscribers.each { |subscriber| publisher.subscribe(subscriber) }
10
37
  end
11
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: use_cases
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ring Twice
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-06 00:00:00.000000000 Z
11
+ date: 2022-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -145,30 +145,30 @@ files:
145
145
  - bin/setup
146
146
  - lib/use_case.rb
147
147
  - lib/use_cases.rb
148
- - lib/use_cases/authorize.rb
149
- - lib/use_cases/base.rb
150
148
  - lib/use_cases/dsl.rb
151
- - lib/use_cases/errors.rb
149
+ - lib/use_cases/events/publish_job.rb
150
+ - lib/use_cases/events/publisher.rb
151
+ - lib/use_cases/events/subscriber.rb
152
152
  - lib/use_cases/module_optins.rb
153
- - lib/use_cases/notifications.rb
153
+ - lib/use_cases/module_optins/authorized.rb
154
+ - lib/use_cases/module_optins/prepared.rb
155
+ - lib/use_cases/module_optins/publishing.rb
156
+ - lib/use_cases/module_optins/transactional.rb
157
+ - lib/use_cases/module_optins/validated.rb
154
158
  - lib/use_cases/params.rb
155
- - lib/use_cases/prepare.rb
159
+ - lib/use_cases/result.rb
156
160
  - lib/use_cases/rspec/matchers.rb
157
161
  - lib/use_cases/stack.rb
158
162
  - lib/use_cases/stack_runner.rb
159
163
  - lib/use_cases/step_active_job_adapter.rb
160
164
  - lib/use_cases/step_adapters.rb
161
165
  - lib/use_cases/step_adapters/abstract.rb
162
- - lib/use_cases/step_adapters/authorize.rb
163
166
  - lib/use_cases/step_adapters/check.rb
164
167
  - lib/use_cases/step_adapters/enqueue.rb
165
168
  - lib/use_cases/step_adapters/map.rb
166
169
  - lib/use_cases/step_adapters/step.rb
167
170
  - lib/use_cases/step_adapters/tee.rb
168
171
  - lib/use_cases/step_adapters/try.rb
169
- - lib/use_cases/step_result.rb
170
- - lib/use_cases/transaction.rb
171
- - lib/use_cases/validate.rb
172
172
  - lib/use_cases/version.rb
173
173
  - use_cases.gemspec
174
174
  homepage: https://github.com/listminut/use_cases
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module UseCases
4
- module Authorize
5
- class NoAuthorizationError < StandardError; end
6
-
7
- def self.included(base)
8
- base.class_eval do
9
- extend DSL
10
- end
11
- end
12
-
13
- module DSL
14
- def authorize(step_name, options = {})
15
- options[:failure] = :unauthorized
16
- check step_name, **options
17
- end
18
- end
19
- end
20
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "use_case"
4
- module UseCases
5
- class Base
6
- include UseCase
7
- end
8
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module UseCases
4
- class StepArgumentError < ArgumentError; end
5
-
6
- class MissingStepError < NoMethodError; end
7
-
8
- class PreviousStepInvalidReturn < StandardError; end
9
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module UseCases
4
- module Notifications
5
- def self.included(base)
6
- base.extend DSL
7
- end
8
-
9
- module DSL
10
- def subscribe_to_step(event_id, listener)
11
- step_subscriptions << StepSubscription.new(event_id, listener)
12
- end
13
-
14
- def bind_step_subscriptions
15
- step_subscriptions.each { |subscription| subscription.bind(__steps__) }
16
- end
17
-
18
- def step_subscriptions
19
- @step_subscriptions ||= []
20
- end
21
- end
22
-
23
- class StepSubscription
24
- attr_reader :event_id, :listener, :steps
25
-
26
- def initialize(event_id, listener)
27
- @event_id = event_id
28
- @listener = listener
29
- end
30
-
31
- def bind(steps)
32
- @steps = steps
33
-
34
- step = steps.find { |s| s.name == step_name }
35
- step.subscribe(event_predicate, listener)
36
- end
37
-
38
- def event_predicate
39
- predicate = event_id.to_s.gsub(step_name.to_s, "")
40
-
41
- "step#{predicate}".to_sym
42
- end
43
-
44
- def step_name
45
- steps.map(&:name).find { |step_name| step_name.start_with?(event_id.to_s) }
46
- end
47
- end
48
- end
49
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "use_cases/step_adapters/tee"
4
-
5
- module UseCases
6
- module Prepare
7
- def self.included(base)
8
- base.class_eval do
9
- extend DSL
10
- end
11
- end
12
-
13
- module DSL
14
- def prepare(name, options = {})
15
- __steps__.unshift StepAdapters::Tee.new(name, nil, options)
16
- end
17
- end
18
- end
19
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "use_cases/step_adapters/check"
4
-
5
- module UseCases
6
- module StepAdapters
7
- class Authorize < UseCases::StepAdapters::Check
8
- class InvalidReturnValue < StandardError; end
9
-
10
- def do_call(*args)
11
- result = super(*args)
12
- prev_result = previous_step_result.value
13
- raise InvalidReturnValue, "The return value should not be a Monad." if result.is_a?(Dry::Monads::Result)
14
-
15
- failure_code = options[:failure] || :unauthorized
16
- failure_message = options[:failure_message] || "Not Authorized"
17
-
18
- result ? Success(prev_result) : Failure([failure_code, failure_message])
19
- end
20
- end
21
- end
22
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module UseCases
4
- module Transaction
5
- class TransactionHandlerUndefined < StandardError; end
6
-
7
- class TransactionHandlerInvalid < StandardError; end
8
-
9
- def self.included(base)
10
- base.prepend DoCallPatch
11
- end
12
-
13
- module DoCallPatch
14
- def do_call(*args)
15
- unless respond_to?(:transaction_handler)
16
- raise TransactionHandlerUndefined, "when using *transactional*, make sure to include a transaction handler in your dependencies."
17
- end
18
-
19
- raise TransactionHandlerInvalid, "Make sure your transaction_handler implements #transaction." unless transaction_handler.respond_to?(:transaction)
20
-
21
- transaction_handler.transaction { super }
22
- end
23
- end
24
- end
25
- end
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry/validation/contract"
4
-
5
- module UseCases
6
- module Validate
7
- class NoValidationError < StandardError; end
8
-
9
- def self.included(base)
10
- base.class_eval do
11
- extend DSL
12
- extend ClassMethods
13
- prepend CallPatch
14
- end
15
- end
16
-
17
- module CallPatch
18
- def call(*args)
19
- unless stack.include_step?(:validate)
20
- raise NoValidationError,
21
- "Make sure to define params validations by using *params*" \
22
- "*schema*, *json*, *rule* or *option* macros in your use case."
23
- end
24
-
25
- super
26
- end
27
- end
28
-
29
- module DSL
30
- def params(*args, &blk)
31
- _setup_validation
32
-
33
- _contract_class.params(*args, &blk)
34
- end
35
-
36
- def schema(*args, &blk)
37
- _setup_validation
38
-
39
- _contract_class.schema(*args, &blk)
40
- end
41
-
42
- def rule(*args, &blk)
43
- _setup_validation
44
-
45
- _contract_class.rule(*args, &blk)
46
- end
47
-
48
- def json(*args, &blk)
49
- _setup_validation
50
-
51
- _contract_class.json(*args, &blk)
52
- end
53
-
54
- def option(*args, &blk)
55
- _contract_class.option(*args, &blk)
56
- end
57
- end
58
-
59
- private
60
-
61
- def validate(params, current_user)
62
- return Failure([:validation_error, "*params* must be a hash."]) unless params.respond_to?(:merge)
63
-
64
- validation = contract.call(params)
65
-
66
- if validation.success?
67
- params.merge!(validation.to_h)
68
- Success(validation.to_h)
69
- else
70
- Failure([:validation_error, validation.errors.to_h])
71
- end
72
- end
73
-
74
- def contract
75
- return self.class._contract_class.new if self.class._contract_class_defined?
76
- end
77
-
78
- module ClassMethods
79
- def _setup_validation
80
- _define_contract_class unless _contract_class_defined?
81
- _define_validation_step unless _validation_step_defined?
82
- end
83
-
84
- def _define_validation_step
85
- step :validate
86
- end
87
-
88
- def _contract_class
89
- self::Contract
90
- end
91
-
92
- def _define_contract_class
93
- const_set(:Contract, Class.new(Dry::Validation::Contract))
94
- end
95
-
96
- def _contract_class_name
97
- "#{name}::Contract"
98
- end
99
-
100
- def _contract_class_defined?
101
- Object.const_defined? _contract_class_name
102
- end
103
-
104
- def _validation_step_defined?
105
- __steps__.map(&:name).include?(:validate)
106
- end
107
- end
108
- end
109
- end