u-case 1.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 +13 -0
- data/.travis.yml +29 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +704 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/micro/case.rb +12 -0
- data/lib/micro/case/base.rb +78 -0
- data/lib/micro/case/error.rb +35 -0
- data/lib/micro/case/flow.rb +56 -0
- data/lib/micro/case/flow/reducer.rb +90 -0
- data/lib/micro/case/result.rb +54 -0
- data/lib/micro/case/safe.rb +14 -0
- data/lib/micro/case/strict.rb +13 -0
- data/lib/micro/case/version.rb +7 -0
- data/lib/micro/case/with_validation.rb +17 -0
- data/lib/u-case.rb +3 -0
- data/lib/u-case/with_validation.rb +3 -0
- data/test.sh +11 -0
- data/u-case.gemspec +44 -0
- metadata +110 -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,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'micro/attributes'
|
4
|
+
|
5
|
+
require 'micro/case/version'
|
6
|
+
require 'micro/case/error'
|
7
|
+
require 'micro/case/result'
|
8
|
+
require 'micro/case/base'
|
9
|
+
require 'micro/case/safe'
|
10
|
+
require 'micro/case/strict'
|
11
|
+
require 'micro/case/flow/reducer'
|
12
|
+
require 'micro/case/flow'
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Case
|
5
|
+
class Base
|
6
|
+
include Micro::Attributes.without(:strict_initialize)
|
7
|
+
|
8
|
+
def self.to_proc
|
9
|
+
Proc.new { |arg| call(arg) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.>>(use_case)
|
13
|
+
Flow[self, use_case]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.&(use_case)
|
17
|
+
Flow::Safe[self, use_case]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.call(options = {})
|
21
|
+
new(options).call
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.__new__(result, arg)
|
25
|
+
instance = new(arg)
|
26
|
+
instance.__set_result__(result)
|
27
|
+
instance
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.__failure_type(arg, type)
|
31
|
+
return type if type != :error
|
32
|
+
|
33
|
+
case arg
|
34
|
+
when Exception then :exception
|
35
|
+
when Symbol then arg
|
36
|
+
else type
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def call!
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
def call
|
45
|
+
__call
|
46
|
+
end
|
47
|
+
|
48
|
+
def __set_result__(result)
|
49
|
+
raise Error::InvalidResultInstance unless result.is_a?(Result)
|
50
|
+
raise Error::ResultIsAlreadyDefined if @__result
|
51
|
+
@__result = result
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def __call
|
57
|
+
result = call!
|
58
|
+
return result if result.is_a?(Result)
|
59
|
+
raise Error::UnexpectedResult.new(self.class)
|
60
|
+
end
|
61
|
+
|
62
|
+
def __get_result__
|
63
|
+
@__result ||= Result.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def Success(arg = :ok)
|
67
|
+
value, type = block_given? ? [yield, arg] : [arg, :ok]
|
68
|
+
__get_result__.__set__(true, value, type, nil)
|
69
|
+
end
|
70
|
+
|
71
|
+
def Failure(arg = :error)
|
72
|
+
value = block_given? ? yield : arg
|
73
|
+
type = self.class.__failure_type(value, block_given? ? arg : :error)
|
74
|
+
__get_result__.__set__(false, value, type, self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module 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
|
+
ResultIsAlreadyDefined = ArgumentError.new('result is already defined'.freeze)
|
13
|
+
|
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
|
+
|
17
|
+
InvalidUseCase = TypeError.new('use case must be a kind or an instance of Micro::Case::Base'.freeze)
|
18
|
+
InvalidUseCases = ArgumentError.new('argument must be a collection of `Micro::Case::Base` classes'.freeze)
|
19
|
+
|
20
|
+
UndefinedFlow = ArgumentError.new("This class hasn't declared its flow. Please, use the `flow()` macro to define one.".freeze)
|
21
|
+
|
22
|
+
class InvalidAccessToTheUseCaseObject < StandardError
|
23
|
+
MSG = 'only a failure result can access its use case object'.freeze
|
24
|
+
|
25
|
+
def initialize(message = MSG); super; end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ByWrongUsage
|
29
|
+
def self.check(exception)
|
30
|
+
exception.is_a?(Error::UnexpectedResult) || exception.is_a?(ArgumentError)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Case
|
5
|
+
module Flow
|
6
|
+
module ClassMethods
|
7
|
+
def __flow__
|
8
|
+
@__flow
|
9
|
+
end
|
10
|
+
|
11
|
+
def flow(*args)
|
12
|
+
@__flow= flow_reducer.build(args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(options = {})
|
16
|
+
new(options).call
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
CONSTRUCTOR = <<-RUBY
|
21
|
+
def initialize(options)
|
22
|
+
@options = options
|
23
|
+
flow= self.class.__flow__
|
24
|
+
raise Error::UndefinedFlow unless flow
|
25
|
+
end
|
26
|
+
RUBY
|
27
|
+
|
28
|
+
private_constant :ClassMethods, :CONSTRUCTOR
|
29
|
+
|
30
|
+
def self.included(base)
|
31
|
+
def base.flow_reducer; Reducer; end
|
32
|
+
base.extend(ClassMethods)
|
33
|
+
base.class_eval(CONSTRUCTOR)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.[](*args)
|
37
|
+
Reducer.build(args)
|
38
|
+
end
|
39
|
+
|
40
|
+
def call
|
41
|
+
self.class.__flow__.call(@options)
|
42
|
+
end
|
43
|
+
|
44
|
+
module Safe
|
45
|
+
def self.included(base)
|
46
|
+
base.send(:include, Micro::Case::Flow)
|
47
|
+
def base.flow_reducer; SafeReducer; end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.[](*args)
|
51
|
+
SafeReducer.build(args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Case
|
5
|
+
module Flow
|
6
|
+
class Reducer
|
7
|
+
attr_reader :use_cases
|
8
|
+
|
9
|
+
def self.map_use_cases(arg)
|
10
|
+
return arg.use_cases if arg.is_a?(Reducer)
|
11
|
+
return arg.__flow__.use_cases if arg.is_a?(Class) && arg < Micro::Case::Flow
|
12
|
+
Array(arg)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.build(args)
|
16
|
+
use_cases = Array(args).flat_map { |arg| map_use_cases(arg) }
|
17
|
+
|
18
|
+
raise Error::InvalidUseCases if use_cases.any? { |klass| !(klass < ::Micro::Case::Base) }
|
19
|
+
|
20
|
+
new(use_cases)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(use_cases)
|
24
|
+
@use_cases = use_cases
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(arg = {})
|
28
|
+
@use_cases.reduce(initial_result(arg)) do |result, use_case|
|
29
|
+
break result if result.failure?
|
30
|
+
use_case.__new__(result, result.value).call
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def >>(arg)
|
35
|
+
self.class.build(use_cases + self.class.map_use_cases(arg))
|
36
|
+
end
|
37
|
+
|
38
|
+
def &(arg)
|
39
|
+
raise NoMethodError, "undefined method `&' for #{self.inspect}. Please, use the method `>>' to avoid this error."
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_proc
|
43
|
+
Proc.new { |arg| call(arg) }
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def initial_result(arg)
|
49
|
+
return arg.call if arg_to_call?(arg)
|
50
|
+
return arg if arg.is_a?(Micro::Case::Result)
|
51
|
+
result = Micro::Case::Result.new
|
52
|
+
result.__set__(true, arg, :ok, nil)
|
53
|
+
end
|
54
|
+
|
55
|
+
def arg_to_call?(arg)
|
56
|
+
return true if arg.is_a?(Micro::Case::Base) || arg.is_a?(Reducer)
|
57
|
+
return true if arg.is_a?(Class) && (arg < Micro::Case::Base || arg < Micro::Case::Flow)
|
58
|
+
return false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class SafeReducer < Reducer
|
63
|
+
def call(arg = {})
|
64
|
+
@use_cases.reduce(initial_result(arg)) do |result, use_case|
|
65
|
+
break result if result.failure?
|
66
|
+
use_case_result(use_case, result)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
alias_method :&, :>>
|
71
|
+
|
72
|
+
def >>(arg)
|
73
|
+
raise NoMethodError, "undefined method `>>' for #{self.inspect}. Please, use the method `&' to avoid this error."
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def use_case_result(use_case, result)
|
79
|
+
begin
|
80
|
+
instance = use_case.__new__(result, result.value)
|
81
|
+
instance.call
|
82
|
+
rescue => exception
|
83
|
+
raise exception if Error::ByWrongUsage.check(exception)
|
84
|
+
result.__set__(false, exception, :exception, instance)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Case
|
5
|
+
class Result
|
6
|
+
attr_reader :value, :type
|
7
|
+
|
8
|
+
def __set__(is_success, value, type, use_case)
|
9
|
+
raise Error::InvalidResultType unless type.is_a?(Symbol)
|
10
|
+
raise Error::InvalidUseCase if !is_success && !is_an_use_case?(use_case)
|
11
|
+
|
12
|
+
@success, @value, @type, @use_case = is_success, value, type, use_case
|
13
|
+
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def success?
|
18
|
+
@success
|
19
|
+
end
|
20
|
+
|
21
|
+
def failure?
|
22
|
+
!success?
|
23
|
+
end
|
24
|
+
|
25
|
+
def use_case
|
26
|
+
return @use_case if failure?
|
27
|
+
|
28
|
+
raise Error::InvalidAccessToTheUseCaseObject
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_success(arg = nil)
|
32
|
+
self.tap { yield(value) if success_type?(arg) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_failure(arg = nil)
|
36
|
+
self.tap{ yield(value, @use_case) if failure_type?(arg) }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def success_type?(arg)
|
42
|
+
success? && (arg.nil? || arg == type)
|
43
|
+
end
|
44
|
+
|
45
|
+
def failure_type?(arg)
|
46
|
+
failure? && (arg.nil? || arg == type)
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_an_use_case?(arg)
|
50
|
+
(arg.is_a?(Class) && arg < Case::Base) || arg.is_a?(Case::Base)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Case
|
5
|
+
class Strict < Case::Base
|
6
|
+
include Micro::Attributes::Features::StrictInitialize
|
7
|
+
|
8
|
+
class Safe < Case::Safe
|
9
|
+
include Micro::Attributes::Features::StrictInitialize
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'micro/case'
|
4
|
+
|
5
|
+
module Micro
|
6
|
+
module Case
|
7
|
+
class Base
|
8
|
+
include Micro::Attributes::Features::ActiveModelValidations
|
9
|
+
|
10
|
+
def call
|
11
|
+
return Failure(:validation_error) { self.errors } unless valid?
|
12
|
+
|
13
|
+
__call
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/u-case.rb
ADDED