cuprum 0.5.0 → 0.6.0

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,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