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