use_cases 0.2.5

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