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,14 @@
1
+ require 'cuprum'
2
+
3
+ module Cuprum
4
+ # Error class for calling a Command that was not given a definition block
5
+ # or have a #process method defined.
6
+ class NotImplementedError < StandardError
7
+ # Error message for a NotImplementedError.
8
+ DEFAULT_MESSAGE = 'no implementation defined for command'.freeze
9
+
10
+ def initialize message = nil
11
+ super(message || DEFAULT_MESSAGE)
12
+ end # constructor
13
+ end # class
14
+ end # module
@@ -1,4 +1,4 @@
1
- require 'cuprum/function'
1
+ require 'cuprum/command'
2
2
 
3
3
  module Cuprum
4
4
  # Functional object that with syntactic sugar for tracking the last result.
@@ -32,13 +32,13 @@ module Cuprum
32
32
  # implementation block to the constructor or by creating a subclass that
33
33
  # overwrites the #process method.
34
34
  #
35
- # @see Cuprum::Function
36
- class Operation < Cuprum::Function
35
+ # @see Cuprum::Command
36
+ class Operation < Cuprum::Command
37
37
  # Module-based implementation of the Operation methods. Use this to convert
38
38
  # an already-defined function into an operation.
39
39
  #
40
40
  # @example
41
- # class CustomOperation < CustomFunction
41
+ # class CustomOperation < CustomCommand
42
42
  # include Cuprum::Operation::Mixin
43
43
  # end # class
44
44
  module Mixin
@@ -60,11 +60,11 @@ module Cuprum
60
60
  # @yield If a block argument is given, it will be passed to the
61
61
  # implementation.
62
62
  #
63
- # @raise [NotImplementedError] Unless a block was passed to the
64
- # constructor or the #process method was overriden by a Function
63
+ # @raise [Cuprum::NotImplementedError] Unless a block was passed to the
64
+ # constructor or the #process method was overriden by a Command
65
65
  # subclass.
66
66
  #
67
- # @see Cuprum::Function#call
67
+ # @see Cuprum::Command#call
68
68
  def call *args, &block
69
69
  reset! if called? # Clear reference to most recent result.
70
70
 
@@ -1,4 +1,3 @@
1
- require 'cuprum/built_in/null_function'
2
1
  require 'cuprum/utils'
3
2
 
4
3
  module Cuprum::Utils
@@ -9,26 +8,26 @@ module Cuprum::Utils
9
8
  # call a function instance.
10
9
  #
11
10
  # @example Observing calls to instances of a function.
12
- # spy = Cuprum::Utils::InstanceSpy.spy_on(CustomFunction)
11
+ # spy = Cuprum::Utils::InstanceSpy.spy_on(CustomCommand)
13
12
  #
14
13
  # expect(spy).to receive(:call).with(1, 2, 3, :four => '4')
15
14
  #
16
- # CustomFunction.new.call(1, 2, 3, :four => '4')
15
+ # CustomCommand.new.call(1, 2, 3, :four => '4')
17
16
  #
18
17
  # @example Observing calls to a chained function.
19
- # spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedFunction)
18
+ # spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedCommand)
20
19
  #
21
20
  # expect(spy).to receive(:call)
22
21
  #
23
- # Cuprum::Function.new {}.
24
- # chain { |result| ChainedFunction.new.call(result) }.
22
+ # Cuprum::Command.new {}.
23
+ # chain { |result| ChainedCommand.new.call(result) }.
25
24
  # call
26
25
  #
27
26
  # @example Block syntax
28
- # Cuprum::Utils::InstanceSpy.spy_on(CustomFunction) do |spy|
27
+ # Cuprum::Utils::InstanceSpy.spy_on(CustomCommand) do |spy|
29
28
  # expect(spy).to receive(:call)
30
29
  #
31
- # CustomFunction.new.call
30
+ # CustomCommand.new.call
32
31
  # end # spy_on
33
32
  module InstanceSpy
34
33
  # Minimal class that implements a #call method to mirror method calls to
@@ -52,13 +51,13 @@ module Cuprum::Utils
52
51
  # spy's #call method will be invoked with the same arguments and block.
53
52
  #
54
53
  # @param function_class [Class, Module] The type of function to spy on.
55
- # Must be either a Module, or a Class that extends Cuprum::Function.
54
+ # Must be either a Module, or a Class that extends Cuprum::Command.
56
55
  #
57
56
  # @raise [ArgumentError] If the argument is neither a Module nor a Class
58
- # that extends Cuprum::Function.
57
+ # that extends Cuprum::Command.
59
58
  #
60
59
  # @note Calling this method for the first time will prepend the
61
- # Cuprum::Utils::InstanceSpy module to Cuprum::Function.
60
+ # Cuprum::Utils::InstanceSpy module to Cuprum::Command.
62
61
  #
63
62
  # @overload spy_on(function_class)
64
63
  # @return [Cuprum::Utils::InstanceSpy::Spy] The instance spy.
@@ -107,17 +106,17 @@ module Cuprum::Utils
107
106
  return if function_class.is_a?(Module) && !function_class.is_a?(Class)
108
107
 
109
108
  return if function_class.is_a?(Class) &&
110
- function_class <= Cuprum::Function
109
+ function_class <= Cuprum::Command
111
110
 
112
111
  raise ArgumentError,
113
- 'must be a class inheriting from Cuprum::Function',
112
+ 'must be a class inheriting from Cuprum::Command',
114
113
  caller(1..-1)
115
114
  end # method guard_spy_class!
116
115
 
117
116
  def instrument_call!
118
- return if Cuprum::Function < Cuprum::Utils::InstanceSpy
117
+ return if Cuprum::Command < Cuprum::Utils::InstanceSpy
119
118
 
120
- Cuprum::Function.prepend(Cuprum::Utils::InstanceSpy)
119
+ Cuprum::Command.prepend(Cuprum::Utils::InstanceSpy)
121
120
  end # method instrument_call!
122
121
 
123
122
  def spies
@@ -129,7 +128,7 @@ module Cuprum::Utils
129
128
  end # method spies_for
130
129
  end # eigenclass
131
130
 
132
- # (see Cuprum::Function#call)
131
+ # (see Cuprum::Command#call)
133
132
  def call *args, &block
134
133
  Cuprum::Utils::InstanceSpy.send(:call_spies_for, self, *args, &block)
135
134
 
@@ -8,7 +8,7 @@ module Cuprum
8
8
  # Major version.
9
9
  MAJOR = 0
10
10
  # Minor version.
11
- MINOR = 5
11
+ MINOR = 6
12
12
  # Patch version.
13
13
  PATCH = 0
14
14
  # Prerelease version.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cuprum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob "Merlin" Smith
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-17 00:00:00.000000000 Z
11
+ date: 2017-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -80,8 +80,9 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.15'
83
- description: A lightweight, functional-lite toolkit for making business logic a first-class
84
- citizen of your application.
83
+ description: An opinionated implementation of the Command pattern for Ruby applications.
84
+ Cuprum wraps your business logic in a consistent, object-oriented interface and
85
+ features status and error management, composability and control flow management.
85
86
  email:
86
87
  - merlin@sleepingkingstudios.com
87
88
  executables: []
@@ -93,12 +94,15 @@ files:
93
94
  - LICENSE
94
95
  - README.md
95
96
  - lib/cuprum.rb
97
+ - lib/cuprum/basic_command.rb
96
98
  - lib/cuprum/built_in.rb
97
- - lib/cuprum/built_in/identity_function.rb
99
+ - lib/cuprum/built_in/identity_command.rb
98
100
  - lib/cuprum/built_in/identity_operation.rb
99
- - lib/cuprum/built_in/null_function.rb
101
+ - lib/cuprum/built_in/null_command.rb
100
102
  - lib/cuprum/built_in/null_operation.rb
101
- - lib/cuprum/function.rb
103
+ - lib/cuprum/chaining.rb
104
+ - lib/cuprum/command.rb
105
+ - lib/cuprum/not_implemented_error.rb
102
106
  - lib/cuprum/operation.rb
103
107
  - lib/cuprum/result.rb
104
108
  - lib/cuprum/utils.rb
@@ -127,5 +131,5 @@ rubyforge_project:
127
131
  rubygems_version: 2.6.13
128
132
  signing_key:
129
133
  specification_version: 4
130
- summary: A lightweight, functional-lite toolkit.
134
+ summary: An opinionated implementation of the Command pattern.
131
135
  test_files: []
@@ -1,447 +0,0 @@
1
- require 'cuprum/result'
2
-
3
- module Cuprum
4
- # Functional object that encapsulates a business logic operation with a
5
- # consistent interface and tracking of result value and status.
6
- #
7
- # A Function can be defined either by passing a block to the constructor, or
8
- # by defining a subclass of Function and implementing the #process method.
9
- #
10
- # @example A Function with a block
11
- # double_function = Cuprum::Function.new { |int| 2 * int }
12
- # result = double_function.call(5)
13
- #
14
- # result.value #=> 10
15
- #
16
- # @example A Function subclass
17
- # class MultiplyFunction < Cuprum::Function
18
- # def initialize multiplier
19
- # @multiplier = multiplier
20
- # end # constructor
21
- #
22
- # private
23
- #
24
- # def process int
25
- # int * @multiplier
26
- # end # method process
27
- # end # class
28
- #
29
- # triple_function = MultiplyFunction.new(3)
30
- # result = triple_function.call(5)
31
- #
32
- # result.value #=> 15
33
- #
34
- # @example A Function with errors
35
- # class DivideFunction < Cuprum::Function
36
- # def initialize divisor
37
- # @divisor = divisor
38
- # end # constructor
39
- #
40
- # private
41
- #
42
- # def process int
43
- # if @divisor.zero?
44
- # errors << 'errors.messages.divide_by_zero'
45
- #
46
- # return
47
- # end # if
48
- #
49
- # int / @divisor
50
- # end # method process
51
- # end # class
52
- #
53
- # halve_function = DivideFunction.new(2)
54
- # result = halve_function.call(10)
55
- #
56
- # result.errors #=> []
57
- # result.value #=> 5
58
- #
59
- # function_with_errors = DivideFunction.new(0)
60
- # result = function_with_errors.call(10)
61
- #
62
- # result.errors #=> ['errors.messages.divide_by_zero']
63
- # result.value #=> nil
64
- #
65
- # @example Function Chaining
66
- # class AddFunction < Cuprum::Function
67
- # def initialize addend
68
- # @addend = addend
69
- # end # constructor
70
- #
71
- # private
72
- #
73
- # def process int
74
- # int + @addend
75
- # end # method process
76
- # end # class
77
- #
78
- # double_and_add_one = MultiplyFunction.new(2).chain(AddFunction.new(1))
79
- # result = double_and_add_one(5)
80
- #
81
- # result.value #=> 5
82
- #
83
- # @example Conditional Chaining With #then And #else
84
- # class EvenFunction < Cuprum::Function
85
- # private
86
- #
87
- # def process int
88
- # errors << 'errors.messages.not_even' unless int.even?
89
- #
90
- # int
91
- # end # method process
92
- # end # class
93
- #
94
- # # The next step in a Collatz sequence is determined as follows:
95
- # # - If the number is even, divide it by 2.
96
- # # - If the number is odd, multiply it by 3 and add 1.
97
- # collatz_function =
98
- # EvenFunction.new.
99
- # then(DivideFunction.new(2)).
100
- # else(MultiplyFunction.new(3).chain(AddFunction.new(1)))
101
- #
102
- # result = collatz_function.new(5)
103
- # result.value #=> 16
104
- #
105
- # result = collatz_function.new(16)
106
- # result.value #=> 8
107
- class Function # rubocop:disable Metrics/ClassLength
108
- # Error class for calling a Function that was not given a definition block
109
- # or have a #process method defined.
110
- class NotImplementedError < StandardError
111
- # Error message for a NotImplementedError.
112
- DEFAULT_MESSAGE = 'no implementation defined for function'.freeze
113
-
114
- def initialize message = nil
115
- super(message || DEFAULT_MESSAGE)
116
- end # constructor
117
- end # class
118
-
119
- # Returns a new instance of Cuprum::Function.
120
- #
121
- # @yield [*arguments, **keywords, &block] If a block is given, the
122
- # #call method will wrap the block and set the result #value to the return
123
- # value of the block. This overrides the implementation in #process, if
124
- # any.
125
- def initialize &implementation
126
- define_singleton_method :process, &implementation if implementation
127
- end # method initialize
128
-
129
- # @overload call(*arguments, **keywords, &block)
130
- # Executes the logic encoded in the constructor block, or the #process
131
- # method if no block was passed to the constructor, and returns a
132
- # Cuprum::Result object with the return value of the block or #process,
133
- # the success or failure status, and any errors generated.
134
- #
135
- # @param arguments [Array] Arguments to be passed to the implementation.
136
- #
137
- # @param keywords [Hash] Keywords to be passed to the implementation.
138
- #
139
- # @return [Cuprum::Result] The result object for the function.
140
- #
141
- # @yield If a block argument is given, it will be passed to the
142
- # implementation.
143
- #
144
- # @raise [NotImplementedError] Unless a block was passed to the
145
- # constructor or the #process method was overriden by a Function
146
- # subclass.
147
- def call *args, &block
148
- call_chained_functions do
149
- wrap_result do |result|
150
- merge_results(result, process(*args, &block))
151
- end # method wrap_result
152
- end # call_chained_functions
153
- end # method call
154
-
155
- # Registers a function or block to run after the current function, or after
156
- # the last chained function if the current function already has one or more
157
- # chained function(s). This creates and modifies a copy of the current
158
- # function.
159
- #
160
- # @param on [Symbol] Sets a condition on when the chained function can run,
161
- # based on the status of the previous function. Valid values are :success,
162
- # :failure, and :always. A value of :success will constrain the function
163
- # to run only if the previous function succeeded. A value of :failure will
164
- # constrain the function to run only if the previous function failed. A
165
- # value of :always will ensure the function is always run, even if the
166
- # function chain has been halted. If no value is given, the function will
167
- # run whether the previous function was a success or a failure, but not if
168
- # the function chain has been halted.
169
- #
170
- # @overload chain(function, on: nil)
171
- # The function will be passed the #value of the previous function result
172
- # as its parameter, and the result of the chained function will be
173
- # returned (or passed to the next chained function, if any).
174
- #
175
- # @param function [Cuprum::Function] The function to call after the
176
- # current or last chained function.
177
- #
178
- # @overload chain(on: :nil, &block)
179
- # The block will be passed the #result of the previous function as its
180
- # parameter. If your use case depends on the status of the previous
181
- # function or on any errors generated, use the block form of #chain.
182
- #
183
- # If the block returns a Cuprum::Result (or an object responding to #value
184
- # and #success?), the block result will be returned (or passed to the next
185
- # chained function, if any). If the block returns any other value
186
- # (including nil), the #result of the previous function will be returned
187
- # or passed to the next function.
188
- #
189
- # @yieldparam result [Cuprum::Result] The #result of the previous
190
- # function.
191
- #
192
- # @return [Cuprum::Function] The chained function.
193
- def chain function = nil, on: nil, &block
194
- clone.tap do |fn|
195
- fn.chained_functions << build_chain_link(block || function, :on => on)
196
- end # tap
197
- end # method chain
198
-
199
- # Shorthand for function.chain(:on => :failure). Registers a function or
200
- # block to run after the current function. The chained function will only
201
- # run if the previous function was unsuccessfully run.
202
- #
203
- # @overload else(function)
204
- #
205
- # @param function [Cuprum::Function] The function to call after the
206
- # current or last chained function.
207
- #
208
- # @overload else(&block)
209
- #
210
- # @yieldparam result [Cuprum::Result] The #result of the previous
211
- # function.
212
- #
213
- # @return [Cuprum::Function] The chained function.
214
- #
215
- # @see #chain
216
- def else function = nil, &block
217
- chain(function, :on => :failure, &block)
218
- end # method else
219
-
220
- # Shorthand for function.chain(:on => :success). Registers a function or
221
- # block to run after the current function. The chained function will only
222
- # run if the previous function was successfully run.
223
- #
224
- # @overload then(function)
225
- #
226
- # @param function [Cuprum::Function] The function to call after the
227
- # current or last chained function.
228
- #
229
- # @overload then(&block)
230
- #
231
- # @yieldparam result [Cuprum::Result] The #result of the previous
232
- # function.
233
- #
234
- # @return [Cuprum::Function] The chained function.
235
- #
236
- # @see #chain
237
- def then function = nil, &block
238
- chain(function, :on => :success, &block)
239
- end # method then
240
-
241
- protected
242
-
243
- def chained_functions
244
- @chained_functions ||= []
245
- end # method chained_functions
246
-
247
- private
248
-
249
- def build_chain_link function_or_proc, on: nil
250
- {
251
- :proc => convert_function_or_proc_to_proc(function_or_proc),
252
- :on => on
253
- } # end hash
254
- end # method build_chain_link
255
-
256
- # @!visibility public
257
- #
258
- # Generates an empty errors object. When the function is called, the result
259
- # will have its #errors property initialized to the value returned by
260
- # #build_errors.By default, this is an array. If you want to use a custom
261
- # errors object type, override this method in a subclass.
262
- #
263
- # @return [Array] an empty errors object.
264
- def build_errors
265
- []
266
- end # method build_errors
267
-
268
- def call_chained_functions
269
- chained_functions.reduce(yield) do |result, hsh|
270
- next result if skip_chained_function?(result, :on => hsh[:on])
271
-
272
- value = hsh.fetch(:proc).call(result)
273
-
274
- convert_value_to_result(value) || result
275
- end # reduce
276
- end # method call_chained_functions
277
-
278
- def convert_function_or_proc_to_proc function_or_proc
279
- return function_or_proc if function_or_proc.is_a?(Proc)
280
-
281
- ->(result) { function_or_proc.call(result) }
282
- end # method convert_function_or_proc_to_proc
283
-
284
- def convert_value_to_result value
285
- return nil unless value_is_result?(value)
286
-
287
- if value.respond_to?(:result) && value_is_result?(value.result)
288
- return value.result
289
- end # if
290
-
291
- value
292
- end # method convert_value_to_result
293
-
294
- # @!visibility public
295
- #
296
- # Provides a reference to the current result's errors object. Messages or
297
- # error objects added to this will be included in the #errors method of the
298
- # returned result object.
299
- #
300
- # @return [Array, Object] the errors object.
301
- #
302
- # @see Cuprum::Result#errors.
303
- #
304
- # @note This is a private method, and only available when executing the
305
- # function implementation as defined in the constructor block or the
306
- # #process method.
307
- def errors
308
- @result&.errors
309
- end # method errors
310
-
311
- # @!visibility public
312
- #
313
- # Marks the current result as failed. Calling #failure? on the returned
314
- # result object will evaluate to true, whether or not the result has any
315
- # errors.
316
- #
317
- # @see Cuprum::Result#failure!.
318
- #
319
- # @note This is a private method, and only available when executing the
320
- # function implementation as defined in the constructor block or the
321
- # #process method.
322
- def failure!
323
- @result&.failure!
324
- end # method failure!
325
-
326
- def halt!
327
- @result&.halt!
328
- end # method halt!
329
-
330
- # :nocov:
331
- def humanize_list list, empty_value: ''
332
- return empty_value if list.size.zero?
333
-
334
- return list.first.to_s if list.size == 1
335
-
336
- return "#{list.first} and #{list.last}" if list.size == 2
337
-
338
- "#{list[0...-1].join ', '}, and #{list.last}"
339
- end # method humanize_list
340
- # :nocov:
341
-
342
- def merge_results result, other
343
- if value_is_result?(other)
344
- Cuprum.warn(result_not_empty_warning) unless result.empty?
345
-
346
- convert_value_to_result(other)
347
- else
348
- result.value = other
349
-
350
- result
351
- end # if-else
352
- end # method merge_results
353
-
354
- # @!visibility public
355
- # @overload process(*arguments, **keywords, &block)
356
- # The implementation of the function, to be executed when the #call method
357
- # is called. Can add errors to or set the status of the result, and the
358
- # value of the result will be set to the value returned by #process. Do
359
- # not call this method directly.
360
- #
361
- # @param arguments [Array] The arguments, if any, passed from #call.
362
- #
363
- # @param keywords [Hash] The keywords, if any, passed from #call.
364
- #
365
- # @yield The block, if any, passed from #call.
366
- #
367
- # @return [Object] the value of the result object to be returned by #call.
368
- #
369
- # @raise NotImplementedError
370
- #
371
- # @note This is a private method.
372
- def process *_args
373
- raise NotImplementedError, nil, caller(1..-1)
374
- end # method process
375
-
376
- def result_not_empty_warning # rubocop:disable Metrics/MethodLength
377
- warnings = []
378
-
379
- unless @result.errors.empty?
380
- warnings << "there were already errors #{@result.errors.inspect}"
381
- end # unless
382
-
383
- status = @result.send(:status)
384
- unless status.nil?
385
- warnings << "the status was set to #{status.inspect}"
386
- end # unless
387
-
388
- if @result.halted?
389
- warnings << 'the function was halted'
390
- end # if
391
-
392
- message = '#process returned a result, but '
393
- message <<
394
- humanize_list(warnings, :empty_value => 'the result was not empty')
395
-
396
- message
397
- end # method result_not_empty_warning
398
-
399
- def skip_chained_function? last_result, on:
400
- return false if on == :always
401
-
402
- return true if last_result.respond_to?(:halted?) && last_result.halted?
403
-
404
- case on
405
- when :success
406
- !last_result.success?
407
- when :failure
408
- !last_result.failure?
409
- end # case
410
- end # method skip_chained_function?
411
-
412
- # @!visibility public
413
- #
414
- # Marks the current result as passing. Calling #success? on the returned
415
- # result object will evaluate to true, whether or not the result has any
416
- # errors.
417
- #
418
- # @see Cuprum::Result#success!.
419
- #
420
- # @note This is a private method, and only available when executing the
421
- # function implementation as defined in the constructor block or the
422
- # #process method.
423
- def success!
424
- @result&.success!
425
- end # method success!
426
-
427
- def value_is_result? value
428
- value.respond_to?(:value) && value.respond_to?(:success?)
429
- end # method value
430
-
431
- def wrap_result
432
- value = nil
433
-
434
- Cuprum::Result.new(:errors => build_errors).tap do |result|
435
- begin
436
- @result = result
437
-
438
- value = yield result
439
- ensure
440
- @result = nil
441
- end # begin-ensure
442
- end # tap
443
-
444
- value
445
- end # method wrap_result
446
- end # class
447
- end # module