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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.tool-versions +1 -0
- data/.travis.sh +19 -0
- data/.travis.yml +30 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +1369 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/micro/case.rb +185 -0
- data/lib/micro/case/error.rb +56 -0
- data/lib/micro/case/result.rb +174 -0
- data/lib/micro/case/safe.rb +19 -0
- data/lib/micro/case/strict.rb +13 -0
- data/lib/micro/case/utils.rb +19 -0
- data/lib/micro/case/version.rb +7 -0
- data/lib/micro/case/with_activemodel_validation.rb +41 -0
- data/lib/micro/cases.rb +16 -0
- data/lib/micro/cases/flow.rb +96 -0
- data/lib/micro/cases/safe/flow.rb +18 -0
- data/lib/u-case.rb +3 -0
- data/lib/u-case/with_activemodel_validation.rb +5 -0
- data/lib/u-case/with_validation.rb +6 -0
- data/test.sh +11 -0
- data/u-case.gemspec +33 -0
- metadata +126 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/lib/micro/case.rb
ADDED
@@ -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
|