cuprum 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '0797c743296d8ae77c0cebf8db95ed48bb7701ac'
4
- data.tar.gz: 78eef973b33c1ae167ac74f7e04169142ef69e45
3
+ metadata.gz: 4420d672d1c4560ed615b9d40100423cffba35bc
4
+ data.tar.gz: 76697dbef40ba29900b3cfc53716f244f8c007c5
5
5
  SHA512:
6
- metadata.gz: 356d580bea16226d831453bcb98cfe32bc6d38b264b53c6935cb91adccfc9f627ed416269374c42dc513187d34add5aa5a3298cfc8b4ef897dd785a58436bfdb
7
- data.tar.gz: 0e181b037184bdd6e0ea774cc1861cea9997ba6d01bee5cba4b92e99ce5348d9a1b66f536b634e91269cc9dfc4a4b0c68172d7be76f32f33ff663d65eef008a9
6
+ metadata.gz: 5e9afc3c3bb45b356098825bb077e7d1e0764cec2b1bc1e75e8fb1ea334f3707041b187fadc69ddbb556135f03d8b474d417615620c71b89048d19436d8e532a
7
+ data.tar.gz: 69acd4b378b7828f656d53e5293ea01bf6a174f2247cabfde5e3b86400c121e6f7026f60985a3ffd5c8b2ae5eb94728295f3b32e563c7c1afac9c5e0c416a97b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ The "Name Not Found For NullFunction" Update.
6
+
7
+ Added the `Cuprum::warn` helper, which prints a warning message. By default, `::warn` delegates to `Kernel#warn`, but can be configured (e.g. to call a Logger) by setting `Cuprum::warning_proc=` with a Proc that accepts one argument (the message to display).
8
+
9
+ ## Operations
10
+
11
+ The implementation of `Cuprum::Operation` has been extracted to a module at `Cuprum::Operation::Mixin`, allowing users to easily convert an existing function class or instance to an operation.
12
+
13
+ ## Results
14
+
15
+ Implemented `Cuprum::Result#==` as a fuzzy comparison, allowing a result to be equal to any object with the same value and status.
16
+
17
+ Implemented `Cuprum::Result#empty?`, which returns true for a new result and false for a result with a value, with non-empty errors, a result with set status, or a halted result.
18
+
19
+ ## Utilities
20
+
21
+ Added the `Cuprum::Utils::InstanceSpy` module to empower testing of code that calls a function without providing a reference, such as some chained functions.
22
+
23
+ ## Built In Functions
24
+
25
+ Added the `NullFunction` and `NullOperation` predefined classes, which do nothing when called and return a result with no errors and a value of nil.
26
+
27
+ Added the `IdentityFunction` and `IdentityOperation` predefined classes, which return the value or result which which they were called.
28
+
3
29
  ## 0.4.0
4
30
 
5
31
  The "Halt And Catch Fire" Update.
data/DEVELOPMENT.md CHANGED
@@ -1,13 +1,18 @@
1
1
  # Development
2
2
 
3
+ - Rename Cuprum::Function to Cuprum::Command.
4
+ - Extract Cuprum::BasicCommand (excludes chaining functionality).
5
+
6
+ ## Core
7
+
3
8
  ## Function
4
9
 
5
- - #build_errors method
6
10
  - Predefined functions/operations:
7
- - NullFunction
8
11
  - IdentityFunction
9
12
  - MapFunction
10
13
  - RetryFunction
14
+ - allow_result_argument? - defaults to false. if false, there is one argument,
15
+ and the argument is a Result, process the value instead.
11
16
 
12
17
  ## Operation
13
18
 
@@ -28,3 +33,5 @@ Chaining Case Study: |
28
33
  Create Content
29
34
  Create ContentVersion
30
35
  Tags.each { FindOrCreate Tag }
36
+
37
+ ## Testing
data/README.md CHANGED
@@ -25,6 +25,8 @@ Hi, I'm Rob Smith, a Ruby Engineer and the developer of this library. I use thes
25
25
 
26
26
  ## Functions
27
27
 
28
+ require 'cuprum/function'
29
+
28
30
  [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FFunction)
29
31
 
30
32
  Functions are the core feature of Cuprum. In a nutshell, each Cuprum::Function is a functional object that encapsulates a business logic operation. A Function provides a consistent interface and tracking of result value and status. This minimizes boilerplate and allows for interchangeability between different implementations or strategies for managing your data and processes.
@@ -323,6 +325,8 @@ If the `#halt` method is called as part of a Function block or `#process` method
323
325
 
324
326
  ## Operations
325
327
 
328
+ require 'cuprum/operation'
329
+
326
330
  [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation)
327
331
 
328
332
  An Operation is like a Function, but with two key differences. First, an Operation retains a reference to the result object from the most recent time the operation was called and delegates the methods defined by `Cuprum::Result` to the most recent result. This allows a called Operation to replace a `Cuprum::Result` in any code that expects or returns a result. Second, the `#call` method returns the operation instance, rather than the result itself.
@@ -379,6 +383,12 @@ Clears the most recent result and resets `#called?` to false. This frees the res
379
383
 
380
384
  [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#reset!-instance_method)
381
385
 
386
+ ### The Operation Mixin
387
+
388
+ [Module Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation%2FMixin)
389
+
390
+ The implementation of `Cuprum::Operation` is defined by the `Cuprum::Operation::Mixin` module, which provides the methods defined above. Any function class or instance can be converted to an operation by including (for a class) or extending (for an instance) the operation mixin.
391
+
382
392
  ## Results
383
393
 
384
394
  [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FResult)
@@ -426,3 +436,131 @@ True if the function did not generate any errors, otherwise false.
426
436
  True if the function generated one or more errors, otherwise false.
427
437
 
428
438
  [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#failure%3F-instance_method)
439
+
440
+ #### `#==`
441
+
442
+ ==(other) #=> true, false
443
+
444
+ Performs a fuzzy comparison with the other object. At a minimum, the other object must respond to `#value` and `#success?`, and the values of `other.value` and `other.success?` must be equal to the corresponding value on the result. In addition, if the `#failure?`, `#errors`, or `#halted?` methods are defined on the other object, then the value of each defined method is compared to the value on the result. Returns true if all values match, otherwise returns false.
445
+
446
+ #### `#empty?`
447
+
448
+ empty?() #=> true, false
449
+
450
+ Helper method that returns true for a new result. The method returns false if `result.value` is not nil, if `result.errors` is not empty, if the status has been manually set with `#success!` or `#failure!`, or if the result has been halted.
451
+
452
+ ## Utilities
453
+
454
+ Cuprum provides these utility modules to grant additional functionality under specific circumstances.
455
+
456
+ ### InstanceSpy
457
+
458
+ require 'cuprum/utils/instance_spy'
459
+
460
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FUtils%2FInstanceSpy)
461
+
462
+ Utility module for instrumenting calls to the #call method of any instance of a function class. This can be used to unobtrusively test the functionality of code that calls a function without providing a reference to the function instance, such as chained functions or methods that create and call a function instance.
463
+
464
+ #### `::spy_on`
465
+
466
+ spy_on(function_class) #=> InstanceSpy
467
+ spy_on(function_class) { |spy| ... } #=> nil
468
+
469
+ Finds or creates a spy object for the given module or class. Each time that the #call method is called for an object of the given type, the spy's #call method will be invoked with the same arguments and block. If `#spy_on` is called with a block, the instance spy will be yielded to the block; otherwise, the spy will be returned.
470
+
471
+ # Observing calls to instances of a function.
472
+ spy = Cuprum::Utils::InstanceSpy.spy_on(CustomFunction)
473
+
474
+ expect(spy).to receive(:call).with(1, 2, 3, :four => '4')
475
+
476
+ CustomFunction.new.call(1, 2, 3, :four => '4')
477
+
478
+ # Observing calls to a chained function.
479
+ spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedFunction)
480
+
481
+ expect(spy).to receive(:call)
482
+
483
+ Cuprum::Function.new {}.
484
+ chain { |result| ChainedFunction.new.call(result) }.
485
+ call
486
+
487
+ # Block syntax
488
+ Cuprum::Utils::InstanceSpy.spy_on(CustomFunction) do |spy|
489
+ expect(spy).to receive(:call)
490
+
491
+ CustomFunction.new.call
492
+ end # spy_on
493
+
494
+ [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Utils/InstanceSpy#spy_on%3F-instance_method)
495
+
496
+ #### `::clear_spies`
497
+
498
+ clear_spies() #=> nil
499
+
500
+ Retires all spies. Subsequent calls to the #call method on function instances will not be mirrored to existing spy objects. Calling this method after each test or example that uses an instance spy is recommended.
501
+
502
+ after(:example) { Cuprum::Utils::InstanceSpy.clear_spies }
503
+
504
+ [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Utils/InstanceSpy#clear_spies%3F-instance_method)
505
+
506
+ ## Built In Functions
507
+
508
+ Cuprum includes a small number of predefined functions and their equivalent operations.
509
+
510
+ ### IdentityFunction
511
+
512
+ require 'cuprum/built_in/identity_function'
513
+
514
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityFunction)
515
+
516
+ A pregenerated function that returns the value or result with which it was called.
517
+
518
+ function = Cuprum::BuiltIn::IdentityFunction.new
519
+ result = function.call('expected value')
520
+ result.value
521
+ #=> 'expected value'
522
+ result.success?
523
+ #=> true
524
+
525
+ ### IdentityOperation
526
+
527
+ require 'cuprum/built_in/identity_operation'
528
+
529
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityOperation)
530
+
531
+ A pregenerated operation that sets its result to the value or result with which it was called.
532
+
533
+ operation = Cuprum::BuiltIn::IdentityFunction.new.call('expected value')
534
+ operation.value
535
+ #=> 'expected value'
536
+ operation.success?
537
+ #=> true
538
+
539
+ ### NullFunction
540
+
541
+ require 'cuprum/built_in/null_function'
542
+
543
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullFunction)
544
+
545
+ A pregenerated function that does nothing when called.
546
+
547
+ function = Cuprum::BuiltIn::NullFunction.new
548
+ result = function.call
549
+ result.value
550
+ #=> nil
551
+ result.success?
552
+ #=> true
553
+
554
+ ### NullOperation
555
+
556
+ require 'cuprum/built_in/null_operation'
557
+
558
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullFunction)
559
+
560
+ A pregenerated operation that does nothing when called.
561
+
562
+ operation = Cuprum::BuiltIn::NullOperation.new.call
563
+ operation.value
564
+ #=> nil
565
+ operation.success?
566
+ #=> true
data/lib/cuprum.rb CHANGED
@@ -5,8 +5,35 @@ module Cuprum
5
5
  autoload :Operation, 'cuprum/operation'
6
6
  autoload :Result, 'cuprum/result'
7
7
 
8
- # @return [String] The current version of the gem.
9
- def self.version
10
- VERSION
11
- end # class method version
8
+ DEFAULT_WARNING_PROC = ->(message) { Kernel.warn message }
9
+ private_constant :DEFAULT_WARNING_PROC
10
+
11
+ class << self
12
+ # @return [Proc] The proc called to display a warning message. By default,
13
+ # delegates to Kernel#warn. Set this to configure the warning behavior
14
+ # (e.g. to call a Logger).
15
+ attr_writer :warning_proc
16
+
17
+ # @return [String] The current version of the gem.
18
+ def version
19
+ VERSION
20
+ end # method version
21
+
22
+ # Displays a warning message. By default, delegates to Kernel#warn. The
23
+ # warning behavior can be configured (e.g. to call a Logger) using the
24
+ # #warning_proc= method.
25
+ #
26
+ # @param message [String] The warning message to display.
27
+ #
28
+ # @see #warning_proc=
29
+ def warn message
30
+ warning_proc.call(message)
31
+ end # method warn
32
+
33
+ private
34
+
35
+ def warning_proc
36
+ @warning_proc ||= DEFAULT_WARNING_PROC
37
+ end # method warning_proc
38
+ end # eigenclass
12
39
  end # module
@@ -0,0 +1,6 @@
1
+ require 'cuprum'
2
+
3
+ module Cuprum
4
+ # Namespace for predefined function and operation classes.
5
+ module BuiltIn; end
6
+ end # module
@@ -0,0 +1,31 @@
1
+ require 'cuprum/built_in'
2
+ require 'cuprum/function'
3
+
4
+ module Cuprum::BuiltIn
5
+ # A predefined function that returns the value or result it was called with.
6
+ #
7
+ # @example With a value.
8
+ # result = IdentityFunction.new.call('custom value')
9
+ # result.value
10
+ # #=> 'custom value'
11
+ # result.success?
12
+ # #=> true
13
+ #
14
+ # @example With a result.
15
+ # errors = ['errors.messages.unknown']
16
+ # value = Cuprum::Result.new('result value', :errors => errors)
17
+ # result = IdentityFunction.new.call(value)
18
+ # result.value
19
+ # #=> 'result value'
20
+ # result.success?
21
+ # #=> false
22
+ # result.errors
23
+ # #=> ['errors.messages.unknown']
24
+ class IdentityFunction < Cuprum::Function
25
+ private
26
+
27
+ def process value = nil
28
+ value
29
+ end # method process
30
+ end # class
31
+ end # module
@@ -0,0 +1,27 @@
1
+ require 'cuprum/built_in/identity_function'
2
+ require 'cuprum/operation'
3
+
4
+ module Cuprum::BuiltIn
5
+ # A predefined operation that returns the value or result it was called with.
6
+ #
7
+ # @example With a value.
8
+ # operation = IdentityOperation.new.call('custom value')
9
+ # operation.value
10
+ # #=> 'custom value'
11
+ # operation.success?
12
+ # #=> true
13
+ #
14
+ # @example With a result.
15
+ # errors = ['errors.messages.unknown']
16
+ # value = Cuprum::Result.new('result value', :errors => errors)
17
+ # operation = IdentityOperation.new.call(value)
18
+ # operation.value
19
+ # #=> 'result value'
20
+ # operation.success?
21
+ # #=> false
22
+ # operation.errors
23
+ # #=> ['errors.messages.unknown']
24
+ class IdentityOperation < Cuprum::BuiltIn::IdentityFunction
25
+ include Cuprum::Operation::Mixin
26
+ end # class
27
+ end # module
@@ -0,0 +1,18 @@
1
+ require 'cuprum/built_in'
2
+ require 'cuprum/function'
3
+
4
+ module Cuprum::BuiltIn
5
+ # A predefined function that does nothing when called.
6
+ #
7
+ # @example
8
+ # result = NullFunction.new.call
9
+ # result.value
10
+ # #=> nil
11
+ # result.success?
12
+ # #=> true
13
+ class NullFunction < Cuprum::Function
14
+ private
15
+
16
+ def process *_args; end
17
+ end # class
18
+ end # module
@@ -0,0 +1,16 @@
1
+ require 'cuprum/built_in/null_function'
2
+ require 'cuprum/operation'
3
+
4
+ module Cuprum::BuiltIn
5
+ # A predefined operation that does nothing when called.
6
+ #
7
+ # @example
8
+ # operation = NullOperation.new.call
9
+ # operation.value
10
+ # #=> nil
11
+ # operation.success?
12
+ # #=> true
13
+ class NullOperation < Cuprum::BuiltIn::NullFunction
14
+ include Cuprum::Operation::Mixin
15
+ end # class
16
+ end # module
@@ -104,7 +104,7 @@ module Cuprum
104
104
  #
105
105
  # result = collatz_function.new(16)
106
106
  # result.value #=> 8
107
- class Function
107
+ class Function # rubocop:disable Metrics/ClassLength
108
108
  # Error class for calling a Function that was not given a definition block
109
109
  # or have a #process method defined.
110
110
  class NotImplementedError < StandardError
@@ -146,13 +146,9 @@ module Cuprum
146
146
  # subclass.
147
147
  def call *args, &block
148
148
  call_chained_functions do
149
- Cuprum::Result.new(:errors => build_errors).tap do |result|
150
- @result = result
151
-
149
+ wrap_result do |result|
152
150
  merge_results(result, process(*args, &block))
153
-
154
- @result = nil
155
- end # tap
151
+ end # method wrap_result
156
152
  end # call_chained_functions
157
153
  end # method call
158
154
 
@@ -195,9 +191,9 @@ module Cuprum
195
191
  #
196
192
  # @return [Cuprum::Function] The chained function.
197
193
  def chain function = nil, on: nil, &block
198
- proc = convert_function_or_proc_to_proc(block || function)
199
-
200
- chain_function(proc, :on => on)
194
+ clone.tap do |fn|
195
+ fn.chained_functions << build_chain_link(block || function, :on => on)
196
+ end # tap
201
197
  end # method chain
202
198
 
203
199
  # Shorthand for function.chain(:on => :failure). Registers a function or
@@ -218,9 +214,7 @@ module Cuprum
218
214
  #
219
215
  # @see #chain
220
216
  def else function = nil, &block
221
- proc = convert_function_or_proc_to_proc(block || function)
222
-
223
- chain_function(proc, :on => :failure)
217
+ chain(function, :on => :failure, &block)
224
218
  end # method else
225
219
 
226
220
  # Shorthand for function.chain(:on => :success). Registers a function or
@@ -241,28 +235,24 @@ module Cuprum
241
235
  #
242
236
  # @see #chain
243
237
  def then function = nil, &block
244
- proc = convert_function_or_proc_to_proc(block || function)
245
-
246
- chain_function(proc, :on => :success)
238
+ chain(function, :on => :success, &block)
247
239
  end # method then
248
240
 
249
241
  protected
250
242
 
251
- def chain_function proc, on: nil
252
- hsh = { :proc => proc }
253
- hsh[:on] = on if on
254
-
255
- clone.tap do |fn|
256
- fn.chained_functions << hsh
257
- end # tap
258
- end # method chain_function
259
-
260
243
  def chained_functions
261
244
  @chained_functions ||= []
262
245
  end # method chained_functions
263
246
 
264
247
  private
265
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
+
266
256
  # @!visibility public
267
257
  #
268
258
  # Generates an empty errors object. When the function is called, the result
@@ -337,22 +327,28 @@ module Cuprum
337
327
  @result&.halt!
338
328
  end # method halt!
339
329
 
340
- def merge_errors result, other
341
- return unless other.respond_to?(:errors)
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
342
335
 
343
- result.errors += other.errors
344
- end # method merge_errors
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:
345
341
 
346
342
  def merge_results result, other
347
343
  if value_is_result?(other)
348
- result.value = other.value
344
+ Cuprum.warn(result_not_empty_warning) unless result.empty?
349
345
 
350
- merge_errors(result, other)
346
+ convert_value_to_result(other)
351
347
  else
352
348
  result.value = other
353
- end # if-else
354
349
 
355
- result
350
+ result
351
+ end # if-else
356
352
  end # method merge_results
357
353
 
358
354
  # @!visibility public
@@ -377,6 +373,29 @@ module Cuprum
377
373
  raise NotImplementedError, nil, caller(1..-1)
378
374
  end # method process
379
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
+
380
399
  def skip_chained_function? last_result, on:
381
400
  return false if on == :always
382
401
 
@@ -408,5 +427,21 @@ module Cuprum
408
427
  def value_is_result? value
409
428
  value.respond_to?(:value) && value.respond_to?(:success?)
410
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
411
446
  end # class
412
447
  end # module
@@ -34,84 +34,121 @@ module Cuprum
34
34
  #
35
35
  # @see Cuprum::Function
36
36
  class Operation < Cuprum::Function
37
- # @return [Cuprum::Result] The result from the most recent call of the
38
- # operation.
39
- attr_reader :result
40
-
41
- # @overload call(*arguments, **keywords, &block)
42
- # Executes the logic encoded in the constructor block, or the #process
43
- # method if no block was passed to the constructor, and returns the
44
- # operation object.
37
+ # Module-based implementation of the Operation methods. Use this to convert
38
+ # an already-defined function into an operation.
45
39
  #
46
- # @param arguments [Array] Arguments to be passed to the implementation.
47
- #
48
- # @param keywords [Hash] Keywords to be passed to the implementation.
49
- #
50
- # @return [Cuprum::Operation] the called operation.
51
- #
52
- # @yield If a block argument is given, it will be passed to the
53
- # implementation.
54
- #
55
- # @raise [NotImplementedError] Unless a block was passed to the
56
- # constructor or the #process method was overriden by a Function
57
- # subclass.
58
- #
59
- # @see Cuprum::Function#call
60
- def call *args, &block
61
- reset! if called? # Clear reference to most recent result.
62
-
63
- @result = super
64
-
65
- self
66
- end # method call
67
-
68
- # @return [Boolean] true if the operation has been called and has a
69
- # reference to the most recent result; otherwise false.
70
- def called?
71
- !result.nil?
72
- end # method called?
73
-
74
- # @return [Array] the errors from the most recent result, or nil if the
75
- # operation has not been called.
76
- def errors
77
- super || (called? ? result.errors : nil)
78
- end # method errors
79
-
80
- # @return [Boolean] true if the most recent result had errors, or false if
81
- # the most recent result had no errors or if the operation has not been
82
- # called.
83
- def failure?
84
- called? ? result.failure? : false
85
- end # method success?
86
-
87
- # @return [Boolean] true if the most recent was halted, otherwise false.
88
- def halted?
89
- called? ? result.halted? : false
90
- end # method halted?
91
-
92
- # Clears the reference to the most recent call of the operation, if any.
93
- # This allows the result and any referenced data to be garbage collected.
94
- # Use this method to clear any instance variables or state internal to the
95
- # operation (an operation should never have external state apart from the
96
- # last result).
97
- #
98
- # If the operation cannot be run more than once, this method should raise an
99
- # error.
100
- def reset!
101
- @result = nil
102
- end # method reset
103
-
104
- # @return [Boolean] true if the most recent result had no errors, or false
105
- # if the most recent result had errors or if the operation has not been
106
- # called.
107
- def success?
108
- called? ? result.success? : false
109
- end # method success?
110
-
111
- # @return [Object] the value of the most recent result, or nil if the
112
- # operation has not been called.
113
- def value
114
- called? ? result.value : nil
115
- end # method value
40
+ # @example
41
+ # class CustomOperation < CustomFunction
42
+ # include Cuprum::Operation::Mixin
43
+ # end # class
44
+ module Mixin
45
+ # @return [Cuprum::Result] The result from the most recent call of the
46
+ # operation.
47
+ attr_reader :result
48
+
49
+ # @overload call(*arguments, **keywords, &block)
50
+ # Executes the logic encoded in the constructor block, or the #process
51
+ # method if no block was passed to the constructor, and returns the
52
+ # operation object.
53
+ #
54
+ # @param arguments [Array] Arguments to be passed to the implementation.
55
+ #
56
+ # @param keywords [Hash] Keywords to be passed to the implementation.
57
+ #
58
+ # @return [Cuprum::Operation] the called operation.
59
+ #
60
+ # @yield If a block argument is given, it will be passed to the
61
+ # implementation.
62
+ #
63
+ # @raise [NotImplementedError] Unless a block was passed to the
64
+ # constructor or the #process method was overriden by a Function
65
+ # subclass.
66
+ #
67
+ # @see Cuprum::Function#call
68
+ def call *args, &block
69
+ reset! if called? # Clear reference to most recent result.
70
+
71
+ @result = super
72
+
73
+ self
74
+ end # method call
75
+
76
+ # @return [Boolean] true if the operation has been called and has a
77
+ # reference to the most recent result; otherwise false.
78
+ def called?
79
+ !result.nil?
80
+ end # method called?
81
+
82
+ # @return [Array] the errors from the most recent result, or nil if the
83
+ # operation has not been called.
84
+ def errors
85
+ super || (called? ? result.errors : nil)
86
+ end # method errors
87
+
88
+ # @return [Boolean] true if the most recent result had errors, or false if
89
+ # the most recent result had no errors or if the operation has not been
90
+ # called.
91
+ def failure?
92
+ called? ? result.failure? : false
93
+ end # method success?
94
+
95
+ # @return [Boolean] true if the most recent was halted, otherwise false.
96
+ def halted?
97
+ called? ? result.halted? : false
98
+ end # method halted?
99
+
100
+ # Clears the reference to the most recent call of the operation, if any.
101
+ # This allows the result and any referenced data to be garbage collected.
102
+ # Use this method to clear any instance variables or state internal to the
103
+ # operation (an operation should never have external state apart from the
104
+ # last result).
105
+ #
106
+ # If the operation cannot be run more than once, this method should raise
107
+ # an error.
108
+ def reset!
109
+ @result = nil
110
+ end # method reset
111
+
112
+ # @return [Boolean] true if the most recent result had no errors, or false
113
+ # if the most recent result had errors or if the operation has not been
114
+ # called.
115
+ def success?
116
+ called? ? result.success? : false
117
+ end # method success?
118
+
119
+ # @return [Object] the value of the most recent result, or nil if the
120
+ # operation has not been called.
121
+ def value
122
+ called? ? result.value : nil
123
+ end # method value
124
+ end # module
125
+ include Mixin
126
+
127
+ # @!method call
128
+ # (see Cuprum::Operation::Mixin#call)
129
+
130
+ # @!method called?
131
+ # (see Cuprum::Operation::Mixin#called?)
132
+
133
+ # @!method errors
134
+ # (see Cuprum::Operation::Mixin#errors)
135
+
136
+ # @!method failure?
137
+ # (see Cuprum::Operation::Mixin#failure?)
138
+
139
+ # @!method halted?
140
+ # (see Cuprum::Operation::Mixin#halted?)
141
+
142
+ # @!method reset!
143
+ # (see Cuprum::Operation::Mixin#reset!)
144
+
145
+ # @!method result
146
+ # (see Cuprum::Operation::Mixin#result)
147
+
148
+ # @!method success?
149
+ # (see Cuprum::Operation::Mixin#success?)
150
+
151
+ # @!method value
152
+ # (see Cuprum::Operation::Mixin#value)
116
153
  end # class
117
154
  end # module
data/lib/cuprum/result.rb CHANGED
@@ -21,6 +21,51 @@ module Cuprum
21
21
  # called.
22
22
  attr_accessor :errors
23
23
 
24
+ # rubocop:disable Metrics/AbcSize
25
+ # rubocop:disable Metrics/CyclomaticComplexity
26
+ # rubocop:disable Metrics/MethodLength
27
+ # rubocop:disable Metrics/PerceivedComplexity
28
+
29
+ # Compares the other object to the result.
30
+ #
31
+ # @param other [#value, #success?] An object responding to, at minimum,
32
+ # #value and #success?. If present, the #failure?, #errors and #halted?
33
+ # values will also be compared.
34
+ #
35
+ # @return [Boolean] True if all present values match the result, otherwise
36
+ # false.
37
+ def == other
38
+ return false unless other.respond_to?(:value) && other.value == value
39
+
40
+ unless other.respond_to?(:success?) && other.success? == success?
41
+ return false
42
+ end # unless
43
+
44
+ if other.respond_to?(:failure?) && other.failure? != failure?
45
+ return false
46
+ end # if
47
+
48
+ if other.respond_to?(:errors) && other.errors != errors
49
+ return false
50
+ end # if
51
+
52
+ if other.respond_to?(:halted?) && other.halted? != halted?
53
+ return false
54
+ end # if
55
+
56
+ true
57
+ end # method ==
58
+ # rubocop:enable Metrics/AbcSize
59
+ # rubocop:enable Metrics/CyclomaticComplexity
60
+ # rubocop:enable Metrics/MethodLength
61
+ # rubocop:enable Metrics/PerceivedComplexity
62
+
63
+ # @return [Boolean] true if the result is empty, i.e. has no value or errors
64
+ # and does not have its status set or is halted.
65
+ def empty?
66
+ value.nil? && errors.empty? && @status.nil? && !halted?
67
+ end # method empty?
68
+
24
69
  # Marks the result as a failure, whether or not the function generated any
25
70
  # errors.
26
71
  #
@@ -68,5 +113,38 @@ module Cuprum
68
113
  def success?
69
114
  @status == :success || (@status.nil? && errors.empty?)
70
115
  end # method success?
116
+
117
+ # @api private
118
+ def update other_result
119
+ return self if other_result.nil?
120
+
121
+ self.value = other_result.value
122
+
123
+ update_status(other_result)
124
+
125
+ update_errors(other_result)
126
+
127
+ halt! if other_result.halted?
128
+
129
+ self
130
+ end # method update
131
+
132
+ protected
133
+
134
+ attr_reader :status
135
+
136
+ private
137
+
138
+ def update_errors other_result
139
+ return if other_result.errors.empty?
140
+
141
+ @errors += other_result.errors
142
+ end # method update_errors
143
+
144
+ def update_status other_result
145
+ return if status || !errors.empty?
146
+
147
+ @status = other_result.status
148
+ end # method update_status
71
149
  end # class
72
150
  end # module
@@ -0,0 +1,6 @@
1
+ require 'cuprum'
2
+
3
+ module Cuprum
4
+ # Namespace for utility modules.
5
+ module Utils; end
6
+ end # module
@@ -0,0 +1,139 @@
1
+ require 'cuprum/built_in/null_function'
2
+ require 'cuprum/utils'
3
+
4
+ module Cuprum::Utils
5
+ # Utility module for instrumenting calls to the #call method of any instance
6
+ # of a function class. This can be used to unobtrusively test the
7
+ # functionality of code that calls a function without providing a reference to
8
+ # the function instance, such as chained functions or methods that create and
9
+ # call a function instance.
10
+ #
11
+ # @example Observing calls to instances of a function.
12
+ # spy = Cuprum::Utils::InstanceSpy.spy_on(CustomFunction)
13
+ #
14
+ # expect(spy).to receive(:call).with(1, 2, 3, :four => '4')
15
+ #
16
+ # CustomFunction.new.call(1, 2, 3, :four => '4')
17
+ #
18
+ # @example Observing calls to a chained function.
19
+ # spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedFunction)
20
+ #
21
+ # expect(spy).to receive(:call)
22
+ #
23
+ # Cuprum::Function.new {}.
24
+ # chain { |result| ChainedFunction.new.call(result) }.
25
+ # call
26
+ #
27
+ # @example Block syntax
28
+ # Cuprum::Utils::InstanceSpy.spy_on(CustomFunction) do |spy|
29
+ # expect(spy).to receive(:call)
30
+ #
31
+ # CustomFunction.new.call
32
+ # end # spy_on
33
+ module InstanceSpy
34
+ # Minimal class that implements a #call method to mirror method calls to
35
+ # instances of an instrumented function class.
36
+ class Spy
37
+ # Empty method that accepts any arguments and an optional block.
38
+ def call *_args, &block; end
39
+ end # class
40
+
41
+ class << self
42
+ # Retires all spies. Subsequent calls to the #call method on function
43
+ # instances will not be mirrored to existing spy objects.
44
+ def clear_spies
45
+ Thread.current[:cuprum_instance_spies] = nil
46
+
47
+ nil
48
+ end # method clear_spies
49
+
50
+ # Finds or creates a spy object for the given module or class. Each time
51
+ # that the #call method is called for an object of the given type, the
52
+ # spy's #call method will be invoked with the same arguments and block.
53
+ #
54
+ # @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.
56
+ #
57
+ # @raise [ArgumentError] If the argument is neither a Module nor a Class
58
+ # that extends Cuprum::Function.
59
+ #
60
+ # @note Calling this method for the first time will prepend the
61
+ # Cuprum::Utils::InstanceSpy module to Cuprum::Function.
62
+ #
63
+ # @overload spy_on(function_class)
64
+ # @return [Cuprum::Utils::InstanceSpy::Spy] The instance spy.
65
+ #
66
+ # @overload spy_on(function_class, &block)
67
+ # Yields the instance spy to the block, and returns nil.
68
+ #
69
+ # @yield [Cuprum::Utils::InstanceSpy::Spy] The instance spy.
70
+ #
71
+ # @return [nil] nil.
72
+ def spy_on function_class
73
+ guard_spy_class!(function_class)
74
+
75
+ instrument_call!
76
+
77
+ if block_given?
78
+ begin
79
+ instance_spy = assign_spy(function_class)
80
+
81
+ yield instance_spy
82
+ end # begin-ensure
83
+ else
84
+ assign_spy(function_class)
85
+ end # if-else
86
+ end # method spy_on
87
+
88
+ private
89
+
90
+ def assign_spy function_class
91
+ existing_spy = spies[function_class]
92
+
93
+ return existing_spy if existing_spy
94
+
95
+ spies[function_class] = build_spy
96
+ end # method assign_spy
97
+
98
+ def build_spy
99
+ Cuprum::Utils::InstanceSpy::Spy.new
100
+ end # method build_spy
101
+
102
+ def call_spies_for function, *args, &block
103
+ spies_for(function).each { |spy| spy.call(*args, &block) }
104
+ end # method call_spies_for
105
+
106
+ def guard_spy_class! function_class
107
+ return if function_class.is_a?(Module) && !function_class.is_a?(Class)
108
+
109
+ return if function_class.is_a?(Class) &&
110
+ function_class <= Cuprum::Function
111
+
112
+ raise ArgumentError,
113
+ 'must be a class inheriting from Cuprum::Function',
114
+ caller(1..-1)
115
+ end # method guard_spy_class!
116
+
117
+ def instrument_call!
118
+ return if Cuprum::Function < Cuprum::Utils::InstanceSpy
119
+
120
+ Cuprum::Function.prepend(Cuprum::Utils::InstanceSpy)
121
+ end # method instrument_call!
122
+
123
+ def spies
124
+ Thread.current[:cuprum_instance_spies] ||= {}
125
+ end # method spies
126
+
127
+ def spies_for function
128
+ spies.select { |mod, _| function.is_a?(mod) }.map { |_, spy| spy }
129
+ end # method spies_for
130
+ end # eigenclass
131
+
132
+ # (see Cuprum::Function#call)
133
+ def call *args, &block
134
+ Cuprum::Utils::InstanceSpy.send(:call_spies_for, self, *args, &block)
135
+
136
+ super
137
+ end # method call
138
+ end # module
139
+ end # module
@@ -8,7 +8,7 @@ module Cuprum
8
8
  # Major version.
9
9
  MAJOR = 0
10
10
  # Minor version.
11
- MINOR = 4
11
+ MINOR = 5
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.4.0
4
+ version: 0.5.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-09-04 00:00:00.000000000 Z
11
+ date: 2017-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -93,9 +93,16 @@ files:
93
93
  - LICENSE
94
94
  - README.md
95
95
  - lib/cuprum.rb
96
+ - lib/cuprum/built_in.rb
97
+ - lib/cuprum/built_in/identity_function.rb
98
+ - lib/cuprum/built_in/identity_operation.rb
99
+ - lib/cuprum/built_in/null_function.rb
100
+ - lib/cuprum/built_in/null_operation.rb
96
101
  - lib/cuprum/function.rb
97
102
  - lib/cuprum/operation.rb
98
103
  - lib/cuprum/result.rb
104
+ - lib/cuprum/utils.rb
105
+ - lib/cuprum/utils/instance_spy.rb
99
106
  - lib/cuprum/version.rb
100
107
  homepage: http://sleepingkingstudios.com
101
108
  licenses: