u-case 2.1.1 → 2.5.0

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.
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'kind'
3
4
  require 'micro/attributes'
4
- # frozen_string_literal: true
5
5
 
6
6
  module Micro
7
7
  class Case
8
8
  require 'micro/case/version'
9
+ require 'micro/case/utils'
9
10
  require 'micro/case/result'
10
11
  require 'micro/case/error'
11
12
  require 'micro/case/safe'
@@ -42,35 +43,81 @@ module Micro
42
43
  instance
43
44
  end
44
45
 
45
- def self.__get_flow__
46
- @__flow
46
+ def self.__call_and_set_transition__(result, arg)
47
+ if arg.respond_to?(:keys)
48
+ result.__set_transitions_accessible_attributes__(arg.keys)
49
+ end
50
+
51
+ __new__(result, arg).call
47
52
  end
48
53
 
49
- private_class_method def self.__set_flow__(reducer, args)
50
- def self.use_cases; __get_flow__.use_cases; end
54
+ FLOW_STEP = 'Flow_Step'.freeze
51
55
 
52
- self.class_eval('def use_cases; self.class.use_cases; end')
56
+ private_constant :FLOW_STEP
53
57
 
54
- reducer.build(args)
55
- end
58
+ def self.__call!
59
+ return const_get(FLOW_STEP) if const_defined?(FLOW_STEP, false)
56
60
 
57
- def self.flow(*args)
58
- @__flow ||= __set_flow__(Flow::Reducer, args)
61
+ class_eval("class #{FLOW_STEP} < #{self.name}; private def __call; __call_use_case; end; end")
59
62
  end
60
63
 
61
64
  def self.call!
62
- return const_get(:Flow_Step) if const_defined?(:Flow_Step)
65
+ self
66
+ end
67
+
68
+ def self.inherited(subclass)
69
+ subclass.attributes(self.attributes_data({}))
70
+ subclass.extend ::Micro::Attributes.const_get('Macros::ForSubclasses'.freeze)
71
+
72
+ if self.send(:__flow_use_cases) && !subclass.name.to_s.end_with?(FLOW_STEP)
73
+ raise "Wooo, you can't do this! Inherits from a use case which has an inner flow violates "\
74
+ "one of the project principles: Solve complex business logic, by allowing the composition of use cases. "\
75
+ "Instead of doing this, declare a new class/constant with the steps needed.\n\n"\
76
+ "Related issue: https://github.com/serradura/u-case/issues/19\n"
77
+ end
78
+ end
79
+
80
+ def self.__flow_reducer
81
+ Flow::Reducer
82
+ end
83
+
84
+ def self.__flow_get
85
+ return @__flow if defined?(@__flow)
86
+ end
87
+
88
+ private_class_method def self.__flow_use_cases
89
+ return @__flow_use_cases if defined?(@__flow_use_cases)
90
+ end
91
+
92
+ private_class_method def self.__flow_use_cases_get
93
+ Array(__flow_use_cases)
94
+ .map { |use_case| use_case == self ? self.__call! : use_case }
95
+ end
96
+
97
+ private_class_method def self.__flow_use_cases_set(args)
98
+ @__flow_use_cases = args
99
+ end
100
+
101
+ private_class_method def self.__flow_set(args)
102
+ return if __flow_get
103
+
104
+ def self.use_cases; __flow_get.use_cases; end
105
+
106
+ self.class_eval('def use_cases; self.class.use_cases; end')
107
+
108
+ @__flow = __flow_reducer.build(args)
109
+ end
63
110
 
64
- const_set(:Flow_Step, Class.new(self) do
65
- private def __call
66
- __call_use_case
67
- end
68
- end)
111
+ def self.__flow_set!
112
+ __flow_set(__flow_use_cases_get) if !__flow_get && __flow_use_cases
113
+ end
114
+
115
+ def self.flow(*args)
116
+ __flow_use_cases_set(args)
69
117
  end
70
118
 
71
119
  def initialize(input)
72
- @__input = input
73
- self.attributes = input
120
+ __setup_use_case(input)
74
121
  end
75
122
 
76
123
  def call!
@@ -83,15 +130,23 @@ module Micro
83
130
 
84
131
  def __set_result__(result)
85
132
  raise Error::InvalidResultInstance unless result.is_a?(Result)
86
- raise Error::ResultIsAlreadyDefined if @__result
133
+ raise Error::ResultIsAlreadyDefined if defined?(@__result)
87
134
 
88
135
  @__result = result
89
136
  end
90
137
 
91
138
  private
92
139
 
140
+ def __setup_use_case(input)
141
+ self.class.__flow_set!
142
+
143
+ @__input = input
144
+
145
+ self.attributes = input
146
+ end
147
+
93
148
  def __call
94
- return self.class.__get_flow__.call(@__input) if self.class.__get_flow__
149
+ return __call_use_case_flow if __call_use_case_flow?
95
150
 
96
151
  __call_use_case
97
152
  end
@@ -104,21 +159,25 @@ module Micro
104
159
  raise Error::UnexpectedResult.new(self.class)
105
160
  end
106
161
 
162
+ def __call_use_case_flow?
163
+ self.class.__flow_get
164
+ end
165
+
166
+ def __call_use_case_flow
167
+ self.class.__flow_get.call(@__input)
168
+ end
169
+
107
170
  def Success(arg = :ok)
108
171
  value, type = block_given? ? [yield, arg] : [arg, :ok]
109
172
 
110
- __get_result__.__set__(true, value, type, nil)
173
+ __get_result_with(true, value, type)
111
174
  end
112
175
 
113
176
  def Failure(arg = :error)
114
177
  value = block_given? ? yield : arg
115
178
  type = __map_failure_type(value, block_given? ? arg : :error)
116
179
 
117
- __get_result__.__set__(false, value, type, self)
118
- end
119
-
120
- def __get_result__
121
- @__result ||= Result.new
180
+ __get_result_with(false, value, type)
122
181
  end
123
182
 
124
183
  def __map_failure_type(arg, type)
@@ -128,5 +187,13 @@ module Micro
128
187
 
129
188
  type
130
189
  end
190
+
191
+ def __get_result__
192
+ @__result ||= Result.new
193
+ end
194
+
195
+ def __get_result_with(is_success, value, type)
196
+ __get_result__.__set__(is_success, value, type, self)
197
+ end
131
198
  end
132
199
  end
@@ -9,20 +9,36 @@ module Micro
9
9
  def initialize(klass); super(klass.name + MESSAGE); end
10
10
  end
11
11
 
12
- ResultIsAlreadyDefined = ArgumentError.new('result is already defined'.freeze)
12
+ class ResultIsAlreadyDefined < ArgumentError
13
+ def initialize; super('result is already defined'.freeze); end
14
+ end
13
15
 
14
- InvalidResultType = TypeError.new('type must be a Symbol'.freeze)
15
- InvalidResultInstance = ArgumentError.new('argument must be an instance of Micro::Case::Result'.freeze)
16
+ class InvalidResultType < TypeError
17
+ def initialize; super('type must be a Symbol'.freeze); end
18
+ end
16
19
 
17
- InvalidUseCase = TypeError.new('use case must be a kind or an instance of Micro::Case'.freeze)
18
- InvalidUseCases = ArgumentError.new('argument must be a collection of `Micro::Case` classes'.freeze)
20
+ class InvalidResultInstance < ArgumentError
21
+ def initialize; super('argument must be an instance of Micro::Case::Result'.freeze); end
22
+ end
19
23
 
20
- UndefinedFlow = ArgumentError.new("This class hasn't declared its flow. Please, use the `flow()` macro to define one.".freeze)
24
+ class InvalidUseCase < TypeError
25
+ def initialize; super('use case must be a kind or an instance of Micro::Case'.freeze); end
26
+ end
21
27
 
22
- class InvalidAccessToTheUseCaseObject < StandardError
23
- MSG = 'only a failure result can access its use case object'.freeze
28
+ class InvalidUseCases < ArgumentError
29
+ def initialize; super('argument must be a collection of `Micro::Case` classes'.freeze); end
30
+ end
24
31
 
25
- def initialize(message = MSG); super; end
32
+ class InvalidInvocationOfTheThenMethod < StandardError
33
+ def initialize; super('Invalid invocation of the Micro::Case::Result#then method'); end
34
+ end
35
+
36
+ class UndefinedFlow < ArgumentError
37
+ def initialize; super("This class hasn't declared its flow. Please, use the `flow()` macro to define one.".freeze); end
38
+ end
39
+
40
+ class InvalidAccessToTheUseCaseObject < StandardError
41
+ def initialize; super('only a failure result can access its use case object'.freeze); end
26
42
  end
27
43
 
28
44
  module ByWrongUsage
@@ -5,7 +5,7 @@ module Micro
5
5
  module Flow
6
6
  module ClassMethods
7
7
  def __flow__
8
- @__flow
8
+ return @__flow if defined?(@__flow)
9
9
  end
10
10
 
11
11
  def flow(*args)
@@ -23,19 +23,18 @@ module Micro
23
23
 
24
24
  def initialize(use_cases)
25
25
  @use_cases = use_cases
26
+ @first_use_case = use_cases[0]
27
+ @next_use_cases = use_cases[1..-1]
26
28
  end
27
29
 
28
30
  def call(arg = {})
29
31
  memo = arg.is_a?(Hash) ? arg.dup : {}
30
32
 
31
- @use_cases.reduce(initial_result(arg)) do |result, use_case|
32
- break result if result.failure?
33
+ first_result = first_use_case_result(arg)
33
34
 
34
- value = result.value
35
- input = value.is_a?(Hash) ? memo.tap { |data| data.merge!(value) } : value
35
+ return first_result if @next_use_cases.empty?
36
36
 
37
- use_case_result(use_case, result, input)
38
- end
37
+ next_use_cases_result(first_result, memo)
39
38
  end
40
39
 
41
40
  def >>(arg)
@@ -52,16 +51,8 @@ module Micro
52
51
 
53
52
  private
54
53
 
55
- def use_case_result(use_case, result, input)
56
- use_case.__new__(result, input).call
57
- end
58
-
59
- def initial_result(arg)
60
- return arg.call if arg_to_call?(arg)
61
- return arg if arg.is_a?(Micro::Case::Result)
62
-
63
- result = ::Micro::Case::Result.new
64
- result.__set__(true, arg, :ok, nil)
54
+ def is_a_result?(arg)
55
+ arg.is_a?(Micro::Case::Result)
65
56
  end
66
57
 
67
58
  def arg_to_call?(arg)
@@ -69,6 +60,44 @@ module Micro
69
60
  return true if arg.is_a?(Class) && (arg < ::Micro::Case || arg < ::Micro::Case::Flow)
70
61
  return false
71
62
  end
63
+
64
+ def call_arg(arg)
65
+ output = arg.call
66
+
67
+ is_a_result?(output) ? output.value : output
68
+ end
69
+
70
+ def first_use_case_input(arg)
71
+ return call_arg(arg) if arg_to_call?(arg)
72
+ return arg.value if is_a_result?(arg)
73
+
74
+ arg
75
+ end
76
+
77
+ def first_use_case_result(arg)
78
+ input = first_use_case_input(arg)
79
+
80
+ result = ::Micro::Case::Result.new
81
+
82
+ @first_use_case.__call_and_set_transition__(result, input)
83
+ end
84
+
85
+ def next_use_case_result(use_case, result, input)
86
+ use_case.__new__(result, input).call
87
+ end
88
+
89
+ def next_use_cases_result(first_result, memo)
90
+ @next_use_cases.reduce(first_result) do |result, use_case|
91
+ break result if result.failure?
92
+
93
+ value = result.value
94
+ input = value.is_a?(Hash) ? memo.tap { |data| data.merge!(value) } : value
95
+
96
+ result.__set_transitions_accessible_attributes__(memo.keys)
97
+
98
+ next_use_case_result(use_case, result, input)
99
+ end
100
+ end
72
101
  end
73
102
  end
74
103
  end
@@ -1,8 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module Micro
4
6
  class Case
5
7
  class Result
8
+ Kind::Types.add(self)
9
+
10
+ @@transition_tracking_disabled = false
11
+
12
+ def self.disable_transition_tracking
13
+ @@transition_tracking_disabled = true
14
+ end
15
+
6
16
  class Data
7
17
  attr_reader :value, :type
8
18
 
@@ -17,12 +27,19 @@ module Micro
17
27
 
18
28
  attr_reader :value, :type
19
29
 
30
+ def initialize
31
+ @__transitions__ = {}
32
+ @__transitions_accessible_attributes__ = Set.new
33
+ end
34
+
20
35
  def __set__(is_success, value, type, use_case)
21
36
  raise Error::InvalidResultType unless type.is_a?(Symbol)
22
- raise Error::InvalidUseCase if !is_success && !is_a_use_case?(use_case)
37
+ raise Error::InvalidUseCase if !is_a_use_case?(use_case)
23
38
 
24
39
  @success, @value, @type, @use_case = is_success, value, type, use_case
25
40
 
41
+ __set_transition__ unless @@transition_tracking_disabled
42
+
26
43
  self
27
44
  end
28
45
 
@@ -41,7 +58,9 @@ module Micro
41
58
  end
42
59
 
43
60
  def on_success(expected_type = nil)
44
- self.tap { yield(value) if success_type?(expected_type) }
61
+ yield(value) if success_type?(expected_type)
62
+
63
+ self
45
64
  end
46
65
 
47
66
  def on_failure(expected_type = nil)
@@ -49,11 +68,62 @@ module Micro
49
68
 
50
69
  data = expected_type.nil? ? Data.new(value, type).tap(&:freeze) : value
51
70
 
52
- self.tap { yield(data, @use_case) }
71
+ yield(data, @use_case)
72
+
73
+ self
74
+ end
75
+
76
+ def on_exception(expected_exception = nil)
77
+ return self unless failure_type?(:exception)
78
+
79
+ if !expected_exception || (Kind.is(Exception, expected_exception) && value.is_a?(expected_exception))
80
+ yield(value, @use_case)
81
+ end
82
+
83
+ self
84
+ end
85
+
86
+ def then(arg = nil, &block)
87
+ can_yield_self = respond_to?(:yield_self)
88
+
89
+ if block
90
+ raise Error::InvalidInvocationOfTheThenMethod if arg
91
+ raise NotImplementedError if !can_yield_self
92
+
93
+ yield_self(&block)
94
+ else
95
+ return yield_self if !arg && can_yield_self
96
+
97
+ raise Error::InvalidInvocationOfTheThenMethod if !is_a_use_case?(arg)
98
+
99
+ return self if failure?
100
+
101
+ arg.__call_and_set_transition__(self, self.value)
102
+ end
103
+ end
104
+
105
+ def transitions
106
+ return [] if @__transitions__.empty?
107
+
108
+ @__transitions__.map { |_use_case, transition| transition }
109
+ end
110
+
111
+ def __set_transitions_accessible_attributes__(attribute_names)
112
+ return if @@transition_tracking_disabled
113
+
114
+ __set_transitions_accessible_attributes__!(
115
+ attribute_names.map!(&:to_sym)
116
+ )
53
117
  end
54
118
 
55
119
  private
56
120
 
121
+ def __set_transitions_accessible_attributes__!(attribute_names)
122
+ @__transitions_accessible_attributes__.merge(
123
+ attribute_names
124
+ )
125
+ end
126
+
57
127
  def success_type?(expected_type)
58
128
  success? && (expected_type.nil? || expected_type == type)
59
129
  end
@@ -65,6 +135,22 @@ module Micro
65
135
  def is_a_use_case?(arg)
66
136
  (arg.is_a?(Class) && arg < ::Micro::Case) || arg.is_a?(::Micro::Case)
67
137
  end
138
+
139
+ def __set_transition__
140
+ use_case_class = @use_case.class
141
+ use_case_attributes = Utils.symbolize_hash_keys(@use_case.attributes)
142
+
143
+ __set_transitions_accessible_attributes__!(use_case_attributes.keys)
144
+
145
+ result = @success ? :success : :failure
146
+ transition = {
147
+ use_case: { class: use_case_class, attributes: use_case_attributes },
148
+ result => { type: @type, value: @value },
149
+ accessible_attributes: @__transitions_accessible_attributes__.to_a
150
+ }
151
+
152
+ @__transitions__[use_case_class] = transition
153
+ end
68
154
  end
69
155
  end
70
156
  end