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 +4 -4
- data/CHANGELOG.md +26 -0
- data/DEVELOPMENT.md +9 -2
- data/README.md +138 -0
- data/lib/cuprum.rb +31 -4
- data/lib/cuprum/built_in.rb +6 -0
- data/lib/cuprum/built_in/identity_function.rb +31 -0
- data/lib/cuprum/built_in/identity_operation.rb +27 -0
- data/lib/cuprum/built_in/null_function.rb +18 -0
- data/lib/cuprum/built_in/null_operation.rb +16 -0
- data/lib/cuprum/function.rb +68 -33
- data/lib/cuprum/operation.rb +115 -78
- data/lib/cuprum/result.rb +78 -0
- data/lib/cuprum/utils.rb +6 -0
- data/lib/cuprum/utils/instance_spy.rb +139 -0
- data/lib/cuprum/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4420d672d1c4560ed615b9d40100423cffba35bc
|
4
|
+
data.tar.gz: 76697dbef40ba29900b3cfc53716f244f8c007c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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,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
|
data/lib/cuprum/function.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
341
|
-
|
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
|
-
|
344
|
-
|
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
|
-
|
344
|
+
Cuprum.warn(result_not_empty_warning) unless result.empty?
|
349
345
|
|
350
|
-
|
346
|
+
convert_value_to_result(other)
|
351
347
|
else
|
352
348
|
result.value = other
|
353
|
-
end # if-else
|
354
349
|
|
355
|
-
|
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
|
data/lib/cuprum/operation.rb
CHANGED
@@ -34,84 +34,121 @@ module Cuprum
|
|
34
34
|
#
|
35
35
|
# @see Cuprum::Function
|
36
36
|
class Operation < Cuprum::Function
|
37
|
-
#
|
38
|
-
#
|
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
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
called?
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
@
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
data/lib/cuprum/utils.rb
ADDED
@@ -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, █ 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
|
data/lib/cuprum/version.rb
CHANGED
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
|
+
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-
|
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:
|