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