u-case 3.0.0.rc1

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.
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "micro/case"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kind'
4
+ require 'micro/attributes'
5
+
6
+ require 'micro/case/version'
7
+
8
+ module Micro
9
+ class Case
10
+ require 'micro/case/utils'
11
+ require 'micro/case/result'
12
+ require 'micro/case/error'
13
+ require 'micro/case/safe'
14
+ require 'micro/case/strict'
15
+
16
+ require 'micro/cases'
17
+
18
+ include Micro::Attributes.without(:strict_initialize)
19
+
20
+ def self.call(options = {})
21
+ new(options).call
22
+ end
23
+
24
+ def self.to_proc
25
+ Proc.new { |arg| call(arg) }
26
+ end
27
+
28
+ def self.call!
29
+ self
30
+ end
31
+
32
+ def self.flow(*args)
33
+ @__flow_use_cases = args
34
+ end
35
+
36
+ def self.inherited(subclass)
37
+ subclass.attributes(self.attributes_data({}))
38
+ subclass.extend ::Micro::Attributes.const_get('Macros::ForSubclasses'.freeze)
39
+
40
+ if self.send(:__flow_use_cases) && !subclass.name.to_s.end_with?(FLOW_STEP)
41
+ raise "Wooo, you can't do this! Inherits from a use case which has an inner flow violates "\
42
+ "one of the project principles: Solve complex business logic, by allowing the composition of use cases. "\
43
+ "Instead of doing this, declare a new class/constant with the steps needed.\n\n"\
44
+ "Related issue: https://github.com/serradura/u-case/issues/19\n"
45
+ end
46
+ end
47
+
48
+ def self.__new__(result, arg)
49
+ instance = new(arg)
50
+ instance.__set_result__(result)
51
+ instance
52
+ end
53
+
54
+ def self.__call_and_set_transition__(result, arg)
55
+ input =
56
+ arg.is_a?(Hash) ? result.__set_transitions_accessible_attributes__(arg) : arg
57
+
58
+ __new__(result, input).call
59
+ end
60
+
61
+ def self.__flow_builder
62
+ Cases::Flow
63
+ end
64
+
65
+ def self.__flow_get
66
+ return @__flow if defined?(@__flow)
67
+ end
68
+
69
+ private_class_method def self.__flow_set(args)
70
+ return if __flow_get
71
+
72
+ def self.use_cases; __flow_get.use_cases; end
73
+
74
+ self.class_eval('def use_cases; self.class.use_cases; end')
75
+
76
+ @__flow = __flow_builder.build(args)
77
+ end
78
+
79
+ FLOW_STEP = 'Flow_Step'.freeze
80
+
81
+ private_constant :FLOW_STEP
82
+
83
+ def self.__call!
84
+ return const_get(FLOW_STEP) if const_defined?(FLOW_STEP, false)
85
+
86
+ class_eval("class #{FLOW_STEP} < #{self.name}; private def __call; __call_use_case; end; end")
87
+ end
88
+
89
+ private_class_method def self.__flow_use_cases
90
+ return @__flow_use_cases if defined?(@__flow_use_cases)
91
+ end
92
+
93
+ private_class_method def self.__flow_use_cases_get
94
+ Array(__flow_use_cases)
95
+ .map { |use_case| use_case == self ? self.__call! : use_case }
96
+ end
97
+
98
+ def self.__flow_set!
99
+ __flow_set(__flow_use_cases_get) if !__flow_get && __flow_use_cases
100
+ end
101
+
102
+ def initialize(input)
103
+ __setup_use_case(input)
104
+ end
105
+
106
+ def call!
107
+ raise NotImplementedError
108
+ end
109
+
110
+ def call
111
+ __call
112
+ end
113
+
114
+ def __set_result__(result)
115
+ raise Error::InvalidResultInstance unless result.is_a?(Result)
116
+ raise Error::ResultIsAlreadyDefined if defined?(@__result)
117
+
118
+ @__result = result
119
+ end
120
+
121
+ private
122
+
123
+ def __setup_use_case(input)
124
+ self.class.__flow_set!
125
+
126
+ @__input = input
127
+
128
+ self.attributes = input
129
+ end
130
+
131
+ def __call
132
+ return __call_use_case_flow if __call_use_case_flow?
133
+
134
+ __call_use_case
135
+ end
136
+
137
+ def __call_use_case
138
+ result = call!
139
+
140
+ return result if result.is_a?(Result)
141
+
142
+ raise Error::UnexpectedResult.new(self.class)
143
+ end
144
+
145
+ def __call_use_case_flow?
146
+ self.class.__flow_get
147
+ end
148
+
149
+ def __call_use_case_flow
150
+ self.class.__flow_get.call(@__input)
151
+ end
152
+
153
+ def Success(type = :ok, result: nil)
154
+ value = result || type
155
+
156
+ __get_result(true, value, type)
157
+ end
158
+
159
+ MapFailureType = -> (value, type) do
160
+ return type if type != :error
161
+ return value if value.is_a?(Symbol)
162
+ return :exception if value.is_a?(Exception)
163
+
164
+ type
165
+ end
166
+
167
+ def Failure(type = :error, result: nil)
168
+ value = result || type
169
+
170
+ type = MapFailureType.call(value, type)
171
+
172
+ __get_result(false, value, type)
173
+ end
174
+
175
+ def __result__
176
+ @__result ||= Result.new
177
+ end
178
+
179
+ def __get_result(is_success, value, type)
180
+ __result__.__set__(is_success, value, type, self)
181
+ end
182
+
183
+ private_constant :MapFailureType
184
+ end
185
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ class Case
5
+ module Error
6
+ class UnexpectedResult < TypeError
7
+ MESSAGE = '#call! must return an instance of Micro::Case::Result'.freeze
8
+
9
+ def initialize(klass); super(klass.name + MESSAGE); end
10
+ end
11
+
12
+ class ResultIsAlreadyDefined < ArgumentError
13
+ def initialize; super('result is already defined'.freeze); end
14
+ end
15
+
16
+ class InvalidResultType < TypeError
17
+ def initialize; super('type must be a Symbol'.freeze); end
18
+ end
19
+
20
+ class InvalidResultData < TypeError
21
+ end
22
+
23
+ class InvalidSuccessResult < InvalidResultData
24
+ def initialize(object)
25
+ super("Success(result: #{object.inspect}) must be a Hash or Symbol")
26
+ end
27
+ end
28
+
29
+ class InvalidFailureResult < InvalidResultData
30
+ def initialize(object)
31
+ super("Failure(result: #{object.inspect}) must be a Hash, Symbol or an Exception")
32
+ end
33
+ end
34
+
35
+ class InvalidResultInstance < ArgumentError
36
+ def initialize; super('argument must be an instance of Micro::Case::Result'.freeze); end
37
+ end
38
+
39
+ class InvalidUseCase < TypeError
40
+ def initialize; super('use case must be a kind or an instance of Micro::Case'.freeze); end
41
+ end
42
+
43
+ class InvalidInvocationOfTheThenMethod < StandardError
44
+ def initialize; super('Invalid invocation of the Micro::Case::Result#then method'); end
45
+ end
46
+
47
+ class InvalidAccessToTheUseCaseObject < StandardError
48
+ def initialize; super('only a failure result can access its use case object'.freeze); end
49
+ end
50
+
51
+ def self.by_wrong_usage?(exception)
52
+ exception.is_a?(InvalidResultData) || exception.is_a?(Error::UnexpectedResult) || exception.is_a?(ArgumentError)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Micro
6
+ class Case
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
+
16
+ attr_reader :type, :data
17
+
18
+ alias_method :value, :data
19
+
20
+ def initialize
21
+ @__transitions__ = []
22
+ @__transitions_accessible_attributes__ = {}
23
+ end
24
+
25
+ def to_ary
26
+ [data, type]
27
+ end
28
+
29
+ def [](key)
30
+ data[key]
31
+ end
32
+
33
+ def values_at(*keys)
34
+ data.values_at(*keys)
35
+ end
36
+
37
+ def success?
38
+ @success
39
+ end
40
+
41
+ def failure?
42
+ !success?
43
+ end
44
+
45
+ def use_case
46
+ return @use_case if failure?
47
+
48
+ raise Error::InvalidAccessToTheUseCaseObject
49
+ end
50
+
51
+ def on_success(expected_type = nil)
52
+ yield(data) if success_type?(expected_type)
53
+
54
+ self
55
+ end
56
+
57
+ def on_failure(expected_type = nil)
58
+ return self unless failure_type?(expected_type)
59
+
60
+ hook_data = expected_type.nil? ? self : data
61
+
62
+ yield(hook_data, @use_case)
63
+
64
+ self
65
+ end
66
+
67
+ def on_exception(expected_exception = nil)
68
+ return self unless failure_type?(:exception)
69
+
70
+ if !expected_exception || (Kind.is(Exception, expected_exception) && data.fetch(:exception).is_a?(expected_exception))
71
+ yield(data, @use_case)
72
+ end
73
+
74
+ self
75
+ end
76
+
77
+ def then(arg = nil, attributes = nil, &block)
78
+ can_yield_self = respond_to?(:yield_self)
79
+
80
+ if block
81
+ raise Error::InvalidInvocationOfTheThenMethod if arg
82
+ raise NotImplementedError if !can_yield_self
83
+
84
+ yield_self(&block)
85
+ else
86
+ return yield_self if !arg && can_yield_self
87
+
88
+ raise Error::InvalidInvocationOfTheThenMethod if !is_a_use_case?(arg)
89
+
90
+ return self if failure?
91
+
92
+ input = attributes.is_a?(Hash) ? self.data.merge(attributes) : self.data
93
+
94
+ arg.__call_and_set_transition__(self, input)
95
+ end
96
+ end
97
+
98
+ def transitions
99
+ @__transitions__.clone
100
+ end
101
+
102
+ FetchData = -> (data, is_success) do
103
+ return data if data.is_a?(Hash)
104
+ return { data => true } if data.is_a?(Symbol)
105
+ return { exception: data } if data.is_a?(Exception)
106
+
107
+ err = is_success ? :InvalidSuccessResult : :InvalidFailureResult
108
+
109
+ raise Micro::Case::Error.const_get(err), data
110
+ end
111
+
112
+ def __set__(is_success, data, type, use_case)
113
+ raise Error::InvalidResultType unless type.is_a?(Symbol)
114
+ raise Error::InvalidUseCase if !is_a_use_case?(use_case)
115
+
116
+ @success, @type, @use_case = is_success, type, use_case
117
+
118
+ @data = FetchData.call(data, is_success)
119
+
120
+ __set_transition__ unless @@transition_tracking_disabled
121
+
122
+ self
123
+ end
124
+
125
+ def __set_transitions_accessible_attributes__(attributes_data)
126
+ return attributes_data if @@transition_tracking_disabled
127
+
128
+ __set_transitions_accessible_attributes__!(attributes_data)
129
+ end
130
+
131
+ private
132
+
133
+ def success_type?(expected_type)
134
+ success? && (expected_type.nil? || expected_type == type)
135
+ end
136
+
137
+ def failure_type?(expected_type)
138
+ failure? && (expected_type.nil? || expected_type == type)
139
+ end
140
+
141
+ def is_a_use_case?(arg)
142
+ (arg.is_a?(Class) && arg < ::Micro::Case) || arg.is_a?(::Micro::Case)
143
+ end
144
+
145
+ def __set_transitions_accessible_attributes__!(attributes_data)
146
+ attributes = Utils.symbolize_hash_keys(attributes_data)
147
+
148
+ __update_transitions_accessible_attributes__(attributes)
149
+ end
150
+
151
+ def __update_transitions_accessible_attributes__(attributes)
152
+ @__transitions_accessible_attributes__.merge!(attributes)
153
+ @__transitions_accessible_attributes__
154
+ end
155
+
156
+ def __set_transition__
157
+ use_case_class = @use_case.class
158
+ use_case_attributes = Utils.symbolize_hash_keys(@use_case.attributes)
159
+
160
+ __update_transitions_accessible_attributes__(use_case_attributes)
161
+
162
+ result = @success ? :success : :failure
163
+
164
+ @__transitions__ << {
165
+ use_case: { class: use_case_class, attributes: use_case_attributes },
166
+ result => { type: @type, result: data },
167
+ accessible_attributes: @__transitions_accessible_attributes__.keys
168
+ }
169
+ end
170
+
171
+ private_constant :FetchData
172
+ end
173
+ end
174
+ end