use_cases 0.2.5

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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +23 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +19 -0
  6. data/.rubocop_todo.yml +28 -0
  7. data/CHANGELOG.md +20 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/Gemfile +20 -0
  10. data/Gemfile.lock +129 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +265 -0
  13. data/Rakefile +22 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +8 -0
  16. data/lib/use_case.rb +57 -0
  17. data/lib/use_cases/authorize.rb +20 -0
  18. data/lib/use_cases/base.rb +8 -0
  19. data/lib/use_cases/dsl.rb +35 -0
  20. data/lib/use_cases/errors.rb +9 -0
  21. data/lib/use_cases/module_optins.rb +43 -0
  22. data/lib/use_cases/notifications.rb +51 -0
  23. data/lib/use_cases/params.rb +15 -0
  24. data/lib/use_cases/prepare.rb +19 -0
  25. data/lib/use_cases/rspec/matchers.rb +31 -0
  26. data/lib/use_cases/stack.rb +51 -0
  27. data/lib/use_cases/stack_runner.rb +60 -0
  28. data/lib/use_cases/step_active_job_adapter.rb +34 -0
  29. data/lib/use_cases/step_adapters/abstract.rb +99 -0
  30. data/lib/use_cases/step_adapters/authorize.rb +22 -0
  31. data/lib/use_cases/step_adapters/check.rb +22 -0
  32. data/lib/use_cases/step_adapters/enqueue.rb +18 -0
  33. data/lib/use_cases/step_adapters/map.rb +18 -0
  34. data/lib/use_cases/step_adapters/step.rb +18 -0
  35. data/lib/use_cases/step_adapters/tee.rb +20 -0
  36. data/lib/use_cases/step_adapters/try.rb +20 -0
  37. data/lib/use_cases/step_adapters.rb +25 -0
  38. data/lib/use_cases/step_result.rb +55 -0
  39. data/lib/use_cases/transaction.rb +25 -0
  40. data/lib/use_cases/validate.rb +104 -0
  41. data/lib/use_cases/version.rb +5 -0
  42. data/lib/use_cases.rb +7 -0
  43. data/use_cases.gemspec +42 -0
  44. metadata +200 -0
data/lib/use_case.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require "dry/events"
5
+ require "dry/monads/do"
6
+ require "dry/monads/do/all"
7
+ require "dry/matcher/result_matcher"
8
+
9
+ require "use_cases/authorize"
10
+ require "use_cases/dsl"
11
+ require "use_cases/errors"
12
+ require "use_cases/validate"
13
+ require "use_cases/stack"
14
+ require "use_cases/params"
15
+ require "use_cases/stack_runner"
16
+ require "use_cases/step_result"
17
+ require "use_cases/notifications"
18
+ require "use_cases/prepare"
19
+ require "use_cases/step_adapters"
20
+ require "use_cases/module_optins"
21
+
22
+ module UseCase
23
+ extend UseCases::ModuleOptins
24
+
25
+ def self.included(base)
26
+ super
27
+ base.class_eval do
28
+ include Dry::Monads[:result]
29
+ include Dry::Monads::Do.for(:call)
30
+ include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
31
+
32
+ extend UseCases::DSL
33
+ extend UseCases::ModuleOptins
34
+
35
+ include UseCases::StepAdapters
36
+ include UseCases::Notifications
37
+ end
38
+ end
39
+
40
+ attr_reader :stack
41
+
42
+ def initialize(*)
43
+ @stack = UseCases::Stack.new(self.class.__steps__).bind(self)
44
+ # self.class.bind_step_subscriptions
45
+ end
46
+
47
+ def call(params, current_user = nil)
48
+ params = UseCases::Params.new(params)
49
+ do_call(params, current_user)
50
+ end
51
+
52
+ private
53
+
54
+ def do_call(*args)
55
+ UseCases::StackRunner.new(stack).call(*args)
56
+ end
57
+ end
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_case"
4
+ module UseCases
5
+ class Base
6
+ include UseCase
7
+ end
8
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector/methods"
4
+
5
+ module UseCases
6
+ module DSL
7
+ include ActiveSupport::Inflector
8
+
9
+ def register_adapter(step_class)
10
+ step_name = underscore(demodulize(step_class.name))
11
+
12
+ define_singleton_method(step_name) do |name, options = {}|
13
+ __steps__ << step_class.new(name, nil, options)
14
+ end
15
+ end
16
+
17
+ def __steps__
18
+ @__steps__ ||= []
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
+ end
35
+ end
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/authorize"
4
+ require "use_cases/prepare"
5
+ require "use_cases/transaction"
6
+ require "use_cases/validate"
7
+
8
+ module UseCases
9
+ module ModuleOptins
10
+ attr_accessor :options
11
+
12
+ 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
20
+
21
+ def included(base)
22
+ super
23
+ @modules ||= []
24
+ return if @modules.empty?
25
+
26
+ base.include(*@modules)
27
+ @modules = nil
28
+ end
29
+
30
+ def inherited(base)
31
+ super
32
+ @modules ||= []
33
+ return if @modules.empty?
34
+
35
+ base.include(*@modules)
36
+ @modules = nil
37
+ end
38
+
39
+ def descendants
40
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "byebug"
4
+
5
+ module UseCases
6
+ module Notifications
7
+ def self.included(base)
8
+ base.extend DSL
9
+ end
10
+
11
+ module DSL
12
+ def subscribe_to_step(event_id, listener)
13
+ step_subscriptions << StepSubscription.new(event_id, listener)
14
+ end
15
+
16
+ def bind_step_subscriptions
17
+ step_subscriptions.each { |subscription| subscription.bind(__steps__) }
18
+ end
19
+
20
+ def step_subscriptions
21
+ @step_subscriptions ||= []
22
+ end
23
+ end
24
+
25
+ class StepSubscription
26
+ attr_reader :event_id, :listener, :steps
27
+
28
+ def initialize(event_id, listener)
29
+ @event_id = event_id
30
+ @listener = listener
31
+ end
32
+
33
+ def bind(steps)
34
+ @steps = steps
35
+
36
+ step = steps.find { |s| s.name == step_name }
37
+ step.subscribe(event_predicate, listener)
38
+ end
39
+
40
+ def event_predicate
41
+ predicate = event_id.to_s.gsub(step_name.to_s, "")
42
+
43
+ "step#{predicate}".to_sym
44
+ end
45
+
46
+ def step_name
47
+ steps.map(&:name).find { |step_name| step_name.start_with?(event_id.to_s) }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/hash_with_indifferent_access"
4
+
5
+ module UseCases
6
+ class Params < ActiveSupport::HashWithIndifferentAccess
7
+ def initialize(params)
8
+ if defined?(Rails) && params.is_a?(ActionController::Parameters)
9
+ super(params.permit!.to_h)
10
+ else
11
+ super(params)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec"
4
+
5
+ RSpec::Matchers.define(:fail_with_code) do |expected_code|
6
+ match do |test_subject|
7
+ expect(test_subject.failure?).to be true
8
+ expect(test_subject.failure.first).to eq expected_code
9
+ end
10
+ end
11
+
12
+ RSpec::Matchers.define(:fail_with_payload) do |expected_result|
13
+ match do |test_subject|
14
+ expect(test_subject.failure?).to be true
15
+ expect(test_subject.failure.last).to eq expected_result
16
+ end
17
+ end
18
+
19
+ RSpec::Matchers.define(:fail_with) do |*expected_failure|
20
+ match do |test_subject|
21
+ expect(test_subject.failure?).to be true
22
+ expect(test_subject.failure).to eq expected_failure
23
+ end
24
+ end
25
+
26
+ RSpec::Matchers.define(:succeed_with) do |expected_result|
27
+ match do |test_subject|
28
+ expect(test_subject.success?).to be true
29
+ expect(test_subject.success).to eq expected_result
30
+ end
31
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UseCases
4
+ class Stack
5
+ attr_reader :steps
6
+
7
+ attr_accessor :prev_step_result, :current_step
8
+
9
+ def initialize(steps)
10
+ @steps = steps
11
+ end
12
+
13
+ def bind(object)
14
+ steps.map! { |step| step.bind(object) }
15
+ self
16
+ end
17
+
18
+ def call(initial_value = nil)
19
+ steps.reduce(initial_value) do |prev_result, current_step|
20
+ self.current_step = current_step
21
+ self.prev_step_result = prev_result
22
+
23
+ yield
24
+ end
25
+ end
26
+
27
+ def in_first_step?
28
+ steps.find_index(current_step).zero?
29
+ end
30
+
31
+ def previous_result_empty?
32
+ prev_step_result.nil?
33
+ end
34
+
35
+ def previous_step_value
36
+ prev_step_result.value
37
+ end
38
+
39
+ def step_names
40
+ steps.map(&:name)
41
+ end
42
+
43
+ def include_step?(step_name)
44
+ step_names.include?(step_name)
45
+ end
46
+
47
+ def find_step(step_name)
48
+ steps.find { |step| step.name == step_name }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UseCases
4
+ class StackRunner
5
+ attr_reader :stack
6
+
7
+ def initialize(stack)
8
+ @stack = stack
9
+ end
10
+
11
+ def call(*args, &around_block)
12
+ return around_block.call { do_call(*args) } if around_block
13
+
14
+ do_call(*args)
15
+ end
16
+
17
+ private
18
+
19
+ def do_call(*args)
20
+ stack.call do
21
+ result = _run_step(stack, args)
22
+
23
+ return result if result.failure?
24
+
25
+ result
26
+ end
27
+ end
28
+
29
+ def _run_step(stack, args)
30
+ step = stack.current_step
31
+ expected_args_count = step.args_count
32
+ step_args = _assert_step_arguments_with_count(stack, args)
33
+
34
+ raise MissingStepError, "Missing ##{step.name} implementation." if step.missing?
35
+
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
+ step.call(*step_args)
42
+ end
43
+
44
+ def _assert_step_arguments_with_count(stack, args)
45
+ step_args_count = stack.current_step.args_count
46
+
47
+ if _should_prepend_previous_step_result_to_args?(stack)
48
+ prev_step_result_value = stack.previous_step_value
49
+
50
+ args = [prev_step_result_value] + args
51
+ end
52
+
53
+ args.first(step_args_count)
54
+ end
55
+
56
+ def _should_prepend_previous_step_result_to_args?(stack)
57
+ !stack.previous_result_empty? && !stack.in_first_step?
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector"
4
+
5
+ return unless defined? ActiveJob
6
+
7
+ module UseCases
8
+ class StepActiveJobAdapter < ActiveJob::Base
9
+ def perform(use_case_name, step_name, *args)
10
+ args = deserialize_step_arguments(args)
11
+
12
+ use_case = ActiveSupport::Inflector.constantize(use_case_name)
13
+ use_case.new.send(step_name, *args)
14
+ end
15
+
16
+ def deserialize_step_arguments(args)
17
+ args.map { |arg| arg.is_a?(Hash) && arg.delete("_serialized_by_use_case") ? arg.deserialize : arg }
18
+ end
19
+
20
+ def self.serialize_step_arguments(args)
21
+ args.select.with_index do |arg, index|
22
+ ActiveJob::Arguments.send(:serialize_argument, arg)
23
+
24
+ rescue ActiveJob::SerializationError => _e
25
+ arg.serialize.merge("_serialized_by_use_case" => true)
26
+
27
+ rescue NoMethodError => _e
28
+ puts "[WARNING] #{arg.class} (index = #{index})" \
29
+ "is not serializable and does not repond to #serialize and will be ignored."
30
+ false
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads/all"
4
+
5
+ module UseCases
6
+ module StepAdapters
7
+ class Abstract
8
+ 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
+ include Dry::Monads[:result]
20
+
21
+ attr_reader :name, :object, :failure, :options
22
+
23
+ def initialize(name, *args, **options)
24
+ @name = name
25
+ @object = args.first
26
+ @options = options
27
+ end
28
+
29
+ def previous_step_result
30
+ object.stack.prev_step_result
31
+ end
32
+
33
+ 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
47
+ end
48
+
49
+ def do_call(*args)
50
+ callable_proc.call(*args)
51
+ end
52
+
53
+ def bind(object)
54
+ self.class.new(name, object, options)
55
+ end
56
+
57
+ def callable_proc
58
+ callable_object.method(callable_method)
59
+ end
60
+
61
+ def callable_object
62
+ case options[:with]
63
+ when NilClass, FalseClass then object
64
+ when String then object.send(options[:with])
65
+ else options[:with]
66
+ end
67
+ end
68
+
69
+ def callable_method
70
+ case options[:with]
71
+ when NilClass, FalseClass then name
72
+ else :call
73
+ end
74
+ end
75
+
76
+ def external?
77
+ options[:with].present?
78
+ end
79
+
80
+ def args_count
81
+ callable_proc.parameters.count
82
+ end
83
+
84
+ def missing?
85
+ !callable_object.respond_to?(callable_method, true)
86
+ end
87
+
88
+ def before_call(*args); end
89
+
90
+ def after_call_success(*args); end
91
+
92
+ def after_call_failure(*args); end
93
+
94
+ def around_call(*_args, &blk)
95
+ blk.call
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,22 @@
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] || :check_failure
16
+ failure_message = options[:failure_message] || "Failed"
17
+
18
+ result ? Success(prev_result) : Failure([failure_code, failure_message])
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/abstract"
4
+
5
+ module UseCases
6
+ module StepAdapters
7
+ class Check < UseCases::StepAdapters::Abstract
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] || :check_failure
16
+ failure_message = options[:failure_message] || "Failed"
17
+
18
+ result ? Success(prev_result) : Failure([failure_code, failure_message])
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UseCases
4
+ module StepAdapters
5
+ class Enqueue < UseCases::StepAdapters::Tee
6
+ def do_call(*base_args)
7
+ args = [object.class.name, name.to_s, *base_args]
8
+ args = ::UseCases::StepActiveJobAdapter.serialize_step_arguments(args)
9
+
10
+ job_options = options.slice(:queue, :wait, :wait_until, :priority)
11
+
12
+ ::UseCases::StepActiveJobAdapter.set(job_options).perform_later(*args)
13
+
14
+ Success(previous_step_result.value)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/abstract"
4
+
5
+ module UseCases
6
+ module StepAdapters
7
+ class Map < UseCases::StepAdapters::Abstract
8
+ class InvalidReturnValue < StandardError; end
9
+
10
+ def do_call(*args)
11
+ result = super(*args)
12
+ raise InvalidReturnValue, "The return value should not be a Monad." if result.is_a?(Dry::Monads::Result)
13
+
14
+ Success(result)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/abstract"
4
+
5
+ module UseCases
6
+ module StepAdapters
7
+ class Step < UseCases::StepAdapters::Abstract
8
+ class InvalidReturnValue < StandardError; end
9
+
10
+ def do_call(*args)
11
+ result = super(*args)
12
+ raise InvalidReturnValue, "Return value should be a Monad" unless result.is_a?(Dry::Monads::Result)
13
+
14
+ result
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/abstract"
4
+
5
+ module UseCases
6
+ module StepAdapters
7
+ class Tee < UseCases::StepAdapters::Abstract
8
+ class InvalidReturnValue < StandardError; end
9
+
10
+ def do_call(*args)
11
+ result = super(*args)
12
+ rescue StandardError => _e
13
+ raise InvalidReturnValue, "For a tee step, a Monad will have no effect." if result.is_a?(Dry::Monads::Result)
14
+
15
+ prev_result = previous_step_result.value
16
+ Success(prev_result)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_cases/step_adapters/abstract"
4
+
5
+ module UseCases
6
+ module StepAdapters
7
+ class Try < UseCases::StepAdapters::Abstract
8
+ class InvalidReturnValue < StandardError; end
9
+
10
+ def do_call(*args)
11
+ result = super(*args)
12
+ raise InvalidReturnValue, "The return value should not be a Monad." if result.is_a?(Dry::Monads::Result)
13
+
14
+ Success(result)
15
+ rescue options[:catch] || StandardError => e
16
+ Failure([options[:failure], e.message])
17
+ end
18
+ end
19
+ end
20
+ end