cuprum 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,212 @@
1
+ require 'cuprum/not_implemented_error'
2
+
3
+ module Cuprum
4
+ # Functional object that encapsulates a business logic operation with a
5
+ # standardized interface and returns a result object.
6
+ class BasicCommand
7
+ # Returns a new instance of Cuprum::BasicCommand.
8
+ #
9
+ # @yield [*arguments, **keywords, &block] If a block is given, the
10
+ # #call method will wrap the block and set the result #value to the return
11
+ # value of the block. This overrides the implementation in #process, if
12
+ # any.
13
+ def initialize &implementation
14
+ define_singleton_method :process, &implementation if implementation
15
+ end # method initialize
16
+
17
+ # @overload call(*arguments, **keywords, &block)
18
+ # Executes the logic encoded in the constructor block, or the #process
19
+ # method if no block was passed to the constructor, and returns a
20
+ # Cuprum::Result object with the return value of the block or #process,
21
+ # the success or failure status, and any errors generated.
22
+ #
23
+ # @param arguments [Array] Arguments to be passed to the implementation.
24
+ #
25
+ # @param keywords [Hash] Keywords to be passed to the implementation.
26
+ #
27
+ # @return [Cuprum::Result] The result object for the command.
28
+ #
29
+ # @yield If a block argument is given, it will be passed to the
30
+ # implementation.
31
+ #
32
+ # @raise [Cuprum::NotImplementedError] Unless a block was passed to the
33
+ # constructor or the #process method was overriden by a Command
34
+ # subclass.
35
+ def call *args, &block
36
+ wrap_result { |result| merge_results(result, process(*args, &block)) }
37
+ end # method call
38
+
39
+ private
40
+
41
+ # @!visibility public
42
+ #
43
+ # Generates an empty errors object. When the function is called, the result
44
+ # will have its #errors property initialized to the value returned by
45
+ # #build_errors. By default, this is an array. If you want to use a custom
46
+ # errors object type, override this method in a subclass.
47
+ #
48
+ # @return [Array] An empty errors object.
49
+ def build_errors
50
+ []
51
+ end # method build_errors
52
+
53
+ def convert_value_to_result value
54
+ return nil unless value_is_result?(value)
55
+
56
+ if value.respond_to?(:result) && value_is_result?(value.result)
57
+ return value.result
58
+ end # if
59
+
60
+ value
61
+ end # method convert_value_to_result
62
+
63
+ # @!visibility public
64
+ #
65
+ # Provides a reference to the current result's errors object. Messages or
66
+ # error objects added to this will be included in the #errors method of the
67
+ # returned result object.
68
+ #
69
+ # @return [Array, Object] The errors object.
70
+ #
71
+ # @see Cuprum::Result#errors.
72
+ #
73
+ # @note This is a private method, and only available when executing the
74
+ # function implementation as defined in the constructor block or the
75
+ # #process method.
76
+ def errors
77
+ @result&.errors
78
+ end # method errors
79
+
80
+ # @!visibility public
81
+ #
82
+ # Marks the current result as failed. Calling #failure? on the returned
83
+ # result object will evaluate to true, whether or not the result has any
84
+ # errors.
85
+ #
86
+ # @see Cuprum::Result#failure!.
87
+ #
88
+ # @note This is a private method, and only available when executing the
89
+ # function implementation as defined in the constructor block or the
90
+ # #process method.
91
+ def failure!
92
+ @result&.failure!
93
+ end # method failure!
94
+
95
+ # @!visibility public
96
+ #
97
+ # Marks the current result as halted.
98
+ #
99
+ # @see Cuprum::Result#halt!.
100
+ #
101
+ # @note This is a private method, and only available when executing the
102
+ # function implementation as defined in the constructor block or the
103
+ # #process method.
104
+ def halt!
105
+ @result&.halt!
106
+ end # method halt!
107
+
108
+ # :nocov:
109
+ def humanize_list list, empty_value: ''
110
+ return empty_value if list.size.zero?
111
+
112
+ return list.first.to_s if list.size == 1
113
+
114
+ return "#{list.first} and #{list.last}" if list.size == 2
115
+
116
+ "#{list[0...-1].join ', '}, and #{list.last}"
117
+ end # method humanize_list
118
+ # :nocov:
119
+
120
+ def merge_results result, other
121
+ if value_is_result?(other)
122
+ Cuprum.warn(result_not_empty_warning) unless result.empty?
123
+
124
+ convert_value_to_result(other)
125
+ else
126
+ result.value = other
127
+
128
+ result
129
+ end # if-else
130
+ end # method merge_results
131
+
132
+ # @!visibility public
133
+ # @overload process(*arguments, **keywords, &block)
134
+ # The implementation of the function, to be executed when the #call method
135
+ # is called. Can add errors to or set the status of the result, and the
136
+ # value of the result will be set to the value returned by #process. Do
137
+ # not call this method directly.
138
+ #
139
+ # @param arguments [Array] The arguments, if any, passed from #call.
140
+ #
141
+ # @param keywords [Hash] The keywords, if any, passed from #call.
142
+ #
143
+ # @yield The block, if any, passed from #call.
144
+ #
145
+ # @return [Object] the value of the result object to be returned by #call.
146
+ #
147
+ # @raise [Cuprum::NotImplementedError] Unless a block was passed to the
148
+ # constructor or the #process method was overriden by a Command
149
+ # subclass.
150
+ #
151
+ # @note This is a private method.
152
+ def process *_args
153
+ raise Cuprum::NotImplementedError, nil, caller(1..-1)
154
+ end # method process
155
+
156
+ def result_not_empty_warning # rubocop:disable Metrics/MethodLength
157
+ warnings = []
158
+
159
+ unless @result.errors.empty?
160
+ warnings << "there were already errors #{@result.errors.inspect}"
161
+ end # unless
162
+
163
+ status = @result.send(:status)
164
+ unless status.nil?
165
+ warnings << "the status was set to #{status.inspect}"
166
+ end # unless
167
+
168
+ if @result.halted?
169
+ warnings << 'the function was halted'
170
+ end # if
171
+
172
+ message = '#process returned a result, but '
173
+ message <<
174
+ humanize_list(warnings, :empty_value => 'the result was not empty')
175
+
176
+ message
177
+ end # method result_not_empty_warning
178
+
179
+ # @!visibility public
180
+ #
181
+ # Marks the current result as passing. Calling #success? on the returned
182
+ # result object will evaluate to true, whether or not the result has any
183
+ # errors.
184
+ #
185
+ # @see Cuprum::Result#success!.
186
+ #
187
+ # @note This is a private method, and only available when executing the
188
+ # function implementation as defined in the constructor block or the
189
+ # #process method.
190
+ def success!
191
+ @result&.success!
192
+ end # method success!
193
+
194
+ def value_is_result? value
195
+ value.respond_to?(:value) && value.respond_to?(:success?)
196
+ end # method value
197
+
198
+ def wrap_result
199
+ result = Cuprum::Result.new(:errors => build_errors)
200
+
201
+ begin
202
+ @result = result
203
+
204
+ result = yield result
205
+ ensure
206
+ @result = nil
207
+ end # begin-ensure
208
+
209
+ result
210
+ end # method wrap_result
211
+ end # class
212
+ end # module
@@ -1,11 +1,11 @@
1
1
  require 'cuprum/built_in'
2
- require 'cuprum/function'
2
+ require 'cuprum/command'
3
3
 
4
4
  module Cuprum::BuiltIn
5
5
  # A predefined function that returns the value or result it was called with.
6
6
  #
7
7
  # @example With a value.
8
- # result = IdentityFunction.new.call('custom value')
8
+ # result = IdentityCommand.new.call('custom value')
9
9
  # result.value
10
10
  # #=> 'custom value'
11
11
  # result.success?
@@ -14,14 +14,14 @@ module Cuprum::BuiltIn
14
14
  # @example With a result.
15
15
  # errors = ['errors.messages.unknown']
16
16
  # value = Cuprum::Result.new('result value', :errors => errors)
17
- # result = IdentityFunction.new.call(value)
17
+ # result = IdentityCommand.new.call(value)
18
18
  # result.value
19
19
  # #=> 'result value'
20
20
  # result.success?
21
21
  # #=> false
22
22
  # result.errors
23
23
  # #=> ['errors.messages.unknown']
24
- class IdentityFunction < Cuprum::Function
24
+ class IdentityCommand < Cuprum::Command
25
25
  private
26
26
 
27
27
  def process value = nil
@@ -1,4 +1,4 @@
1
- require 'cuprum/built_in/identity_function'
1
+ require 'cuprum/built_in/identity_command'
2
2
  require 'cuprum/operation'
3
3
 
4
4
  module Cuprum::BuiltIn
@@ -21,7 +21,7 @@ module Cuprum::BuiltIn
21
21
  # #=> false
22
22
  # operation.errors
23
23
  # #=> ['errors.messages.unknown']
24
- class IdentityOperation < Cuprum::BuiltIn::IdentityFunction
24
+ class IdentityOperation < Cuprum::BuiltIn::IdentityCommand
25
25
  include Cuprum::Operation::Mixin
26
26
  end # class
27
27
  end # module
@@ -1,16 +1,16 @@
1
1
  require 'cuprum/built_in'
2
- require 'cuprum/function'
2
+ require 'cuprum/command'
3
3
 
4
4
  module Cuprum::BuiltIn
5
- # A predefined function that does nothing when called.
5
+ # A predefined command that does nothing when called.
6
6
  #
7
7
  # @example
8
- # result = NullFunction.new.call
8
+ # result = NullCommand.new.call
9
9
  # result.value
10
10
  # #=> nil
11
11
  # result.success?
12
12
  # #=> true
13
- class NullFunction < Cuprum::Function
13
+ class NullCommand < Cuprum::Command
14
14
  private
15
15
 
16
16
  def process *_args; end
@@ -1,4 +1,4 @@
1
- require 'cuprum/built_in/null_function'
1
+ require 'cuprum/built_in/null_command'
2
2
  require 'cuprum/operation'
3
3
 
4
4
  module Cuprum::BuiltIn
@@ -10,7 +10,7 @@ module Cuprum::BuiltIn
10
10
  # #=> nil
11
11
  # operation.success?
12
12
  # #=> true
13
- class NullOperation < Cuprum::BuiltIn::NullFunction
13
+ class NullOperation < Cuprum::BuiltIn::NullCommand
14
14
  include Cuprum::Operation::Mixin
15
15
  end # class
16
16
  end # module
@@ -0,0 +1,142 @@
1
+ require 'cuprum'
2
+
3
+ module Cuprum
4
+ # Mixin to implement command chaining functionality for a command class.
5
+ # Chaining commands allows you to define complex logic by composing it from
6
+ # simpler commands, including branching logic and error handling.
7
+ #
8
+ # @see Cuprum::Command
9
+ module Chaining
10
+ # (see Cuprum::BasicCommand#call)
11
+ def call *args, &block
12
+ call_chained_functions(super)
13
+ end # method call
14
+
15
+ # Registers a function or block to run after the current function, or after
16
+ # the last chained function if the current function already has one or more
17
+ # chained function(s). This creates and modifies a copy of the current
18
+ # function.
19
+ #
20
+ # @param on [Symbol] Sets a condition on when the chained function can run,
21
+ # based on the status of the previous function. Valid values are :success,
22
+ # :failure, and :always. A value of :success will constrain the function
23
+ # to run only if the previous function succeeded. A value of :failure will
24
+ # constrain the function to run only if the previous function failed. A
25
+ # value of :always will ensure the function is always run, even if the
26
+ # function chain has been halted. If no value is given, the function will
27
+ # run whether the previous function was a success or a failure, but not if
28
+ # the function chain has been halted.
29
+ #
30
+ # @overload chain(function, on: nil)
31
+ # The function will be passed the #value of the previous function result
32
+ # as its parameter, and the result of the chained function will be
33
+ # returned (or passed to the next chained function, if any).
34
+ #
35
+ # @param function [Cuprum::Command] The function to call after the
36
+ # current or last chained function.
37
+ #
38
+ # @overload chain(on: :nil, &block)
39
+ # The block will be passed the #result of the previous function as its
40
+ # parameter. If your use case depends on the status of the previous
41
+ # function or on any errors generated, use the block form of #chain.
42
+ #
43
+ # If the block returns a Cuprum::Result (or an object responding to #value
44
+ # and #success?), the block result will be returned (or passed to the next
45
+ # chained function, if any). If the block returns any other value
46
+ # (including nil), the #result of the previous function will be returned
47
+ # or passed to the next function.
48
+ #
49
+ # @yieldparam result [Cuprum::Result] The #result of the previous
50
+ # function.
51
+ #
52
+ # @return [Cuprum::Command] The chained function.
53
+ def chain function = nil, on: nil, &block
54
+ clone.tap do |fn|
55
+ fn.chained_functions <<
56
+ {
57
+ :proc => convert_function_or_proc_to_proc(block || function),
58
+ :on => on
59
+ } # end hash
60
+ end # tap
61
+ end # method chain
62
+
63
+ # Shorthand for function.chain(:on => :failure). Registers a function or
64
+ # block to run after the current function. The chained function will only
65
+ # run if the previous function was unsuccessfully run.
66
+ #
67
+ # @overload else(function)
68
+ #
69
+ # @param function [Cuprum::Command] The function to call after the
70
+ # current or last chained function.
71
+ #
72
+ # @overload else(&block)
73
+ #
74
+ # @yieldparam result [Cuprum::Result] The #result of the previous
75
+ # function.
76
+ #
77
+ # @return [Cuprum::Command] The chained function.
78
+ #
79
+ # @see #chain
80
+ def else function = nil, &block
81
+ chain(function, :on => :failure, &block)
82
+ end # method else
83
+
84
+ # Shorthand for function.chain(:on => :success). Registers a function or
85
+ # block to run after the current function. The chained function will only
86
+ # run if the previous function was successfully run.
87
+ #
88
+ # @overload then(function)
89
+ #
90
+ # @param function [Cuprum::Command] The function to call after the
91
+ # current or last chained function.
92
+ #
93
+ # @overload then(&block)
94
+ #
95
+ # @yieldparam result [Cuprum::Result] The #result of the previous
96
+ # function.
97
+ #
98
+ # @return [Cuprum::Command] The chained function.
99
+ #
100
+ # @see #chain
101
+ def then function = nil, &block
102
+ chain(function, :on => :success, &block)
103
+ end # method then
104
+
105
+ protected
106
+
107
+ def chained_functions
108
+ @chained_functions ||= []
109
+ end # method chained_functions
110
+
111
+ private
112
+
113
+ def call_chained_functions first_result
114
+ chained_functions.reduce(first_result) do |result, hsh|
115
+ next result if skip_chained_function?(result, :on => hsh[:on])
116
+
117
+ value = hsh.fetch(:proc).call(result)
118
+
119
+ convert_value_to_result(value) || result
120
+ end # reduce
121
+ end # method call_chained_functions
122
+
123
+ def convert_function_or_proc_to_proc function_or_proc
124
+ return function_or_proc if function_or_proc.is_a?(Proc)
125
+
126
+ ->(result) { function_or_proc.call(result) }
127
+ end # method convert_function_or_proc_to_proc
128
+
129
+ def skip_chained_function? last_result, on:
130
+ return false if on == :always
131
+
132
+ return true if last_result.respond_to?(:halted?) && last_result.halted?
133
+
134
+ case on
135
+ when :success
136
+ !last_result.success?
137
+ when :failure
138
+ !last_result.failure?
139
+ end # case
140
+ end # method skip_chained_function?
141
+ end # module
142
+ end # modue
@@ -0,0 +1,113 @@
1
+ require 'cuprum/basic_command'
2
+ require 'cuprum/chaining'
3
+ require 'cuprum/not_implemented_error'
4
+ require 'cuprum/result'
5
+
6
+ module Cuprum
7
+ # Functional object that encapsulates a business logic operation with a
8
+ # consistent interface and tracking of result value and status.
9
+ #
10
+ # A Command can be defined either by passing a block to the constructor, or
11
+ # by defining a subclass of Command and implementing the #process method.
12
+ #
13
+ # @example A Command with a block
14
+ # double_function = Cuprum::Command.new { |int| 2 * int }
15
+ # result = double_function.call(5)
16
+ #
17
+ # result.value #=> 10
18
+ #
19
+ # @example A Command subclass
20
+ # class MultiplyCommand < Cuprum::Command
21
+ # def initialize multiplier
22
+ # @multiplier = multiplier
23
+ # end # constructor
24
+ #
25
+ # private
26
+ #
27
+ # def process int
28
+ # int * @multiplier
29
+ # end # method process
30
+ # end # class
31
+ #
32
+ # triple_function = MultiplyCommand.new(3)
33
+ # result = triple_function.call(5)
34
+ #
35
+ # result.value #=> 15
36
+ #
37
+ # @example A Command with errors
38
+ # class DivideCommand < Cuprum::Command
39
+ # def initialize divisor
40
+ # @divisor = divisor
41
+ # end # constructor
42
+ #
43
+ # private
44
+ #
45
+ # def process int
46
+ # if @divisor.zero?
47
+ # errors << 'errors.messages.divide_by_zero'
48
+ #
49
+ # return
50
+ # end # if
51
+ #
52
+ # int / @divisor
53
+ # end # method process
54
+ # end # class
55
+ #
56
+ # halve_function = DivideCommand.new(2)
57
+ # result = halve_function.call(10)
58
+ #
59
+ # result.errors #=> []
60
+ # result.value #=> 5
61
+ #
62
+ # function_with_errors = DivideCommand.new(0)
63
+ # result = function_with_errors.call(10)
64
+ #
65
+ # result.errors #=> ['errors.messages.divide_by_zero']
66
+ # result.value #=> nil
67
+ #
68
+ # @example Command Chaining
69
+ # class AddCommand < Cuprum::Command
70
+ # def initialize addend
71
+ # @addend = addend
72
+ # end # constructor
73
+ #
74
+ # private
75
+ #
76
+ # def process int
77
+ # int + @addend
78
+ # end # method process
79
+ # end # class
80
+ #
81
+ # double_and_add_one = MultiplyCommand.new(2).chain(AddCommand.new(1))
82
+ # result = double_and_add_one(5)
83
+ #
84
+ # result.value #=> 5
85
+ #
86
+ # @example Conditional Chaining With #then And #else
87
+ # class EvenCommand < Cuprum::Command
88
+ # private
89
+ #
90
+ # def process int
91
+ # errors << 'errors.messages.not_even' unless int.even?
92
+ #
93
+ # int
94
+ # end # method process
95
+ # end # class
96
+ #
97
+ # # The next step in a Collatz sequence is determined as follows:
98
+ # # - If the number is even, divide it by 2.
99
+ # # - If the number is odd, multiply it by 3 and add 1.
100
+ # collatz_function =
101
+ # EvenCommand.new.
102
+ # then(DivideCommand.new(2)).
103
+ # else(MultiplyCommand.new(3).chain(AddCommand.new(1)))
104
+ #
105
+ # result = collatz_function.new(5)
106
+ # result.value #=> 16
107
+ #
108
+ # result = collatz_function.new(16)
109
+ # result.value #=> 8
110
+ class Command < Cuprum::BasicCommand
111
+ include Cuprum::Chaining
112
+ end # class
113
+ end # module