use_cases 0.3.8 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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