results 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/README.md +197 -4
- data/lib/results.rb +234 -2
- data/lib/results/version.rb +1 -1
- data/spec/example_usages_spec.rb +182 -0
- data/spec/results_unit_spec.rb +618 -0
- metadata +5 -3
- data/spec/results_spec.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7f5a6ff736c00051a79b53e0b228db6b051d37e
|
4
|
+
data.tar.gz: 7d0e3982b3422b2f9dd1d09918472019874136b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9582877f5ad6ef1716468e1f44f5c71195ec05622ade4ac061789f5da7e23dfe95b2583f2fa60d5ef85937d881797ef4ae8cf1f8e00e918a214bfa166f985b73
|
7
|
+
data.tar.gz: 53de975391c145a5dd6d22252a667ed87bbec00abc99e594ff292e4a712332c79669e923fff256bb676ef73d3ac53bcb7283a27347dd103ad23351148c84fb76
|
data/.rspec
ADDED
data/README.md
CHANGED
@@ -1,22 +1,215 @@
|
|
1
|
-
|
1
|
+
# Results
|
2
2
|
[](http://badge.fury.io/rb/results)
|
3
3
|
[](https://travis-ci.org/ms-ati/results)
|
4
4
|
[](https://gemnasium.com/ms-ati/results)
|
5
5
|
[](https://codeclimate.com/github/ms-ati/results)
|
6
6
|
[](https://coveralls.io/r/ms-ati/results)
|
7
7
|
|
8
|
-
A functional combinator of results which are either Good or Bad
|
8
|
+
A functional combinator of results which are either {Results::Good Good} or {Results::Bad Bad}.
|
9
|
+
|
10
|
+
Inspired by the [ScalaUtils][1] library's [Or and Every][2] classes, whose APIs are documented
|
11
|
+
[here][3] and [here][4].
|
9
12
|
|
10
13
|
[1]: http://www.scalautils.org
|
11
14
|
[2]: http://www.scalautils.org/user_guide/OrAndEvery
|
15
|
+
[3]: http://doc.scalatest.org/2.1.3/index.html#org.scalautils.Or
|
16
|
+
[4]: http://doc.scalatest.org/2.1.3/index.html#org.scalautils.Every
|
17
|
+
|
18
|
+
## Table of Contents
|
19
|
+
|
20
|
+
* [Usage](#usage)
|
21
|
+
* [Basic validation](#basic-validation)
|
22
|
+
* [Chained filters and validations](#chained-filters-and-validations)
|
23
|
+
* [Accumulating multiple bad results](#accumulating-multiple-bad-results)
|
24
|
+
* [Multiple filters and validations of a single input](#multiple-filters-and-validations-of-a-single-input)
|
25
|
+
* [Combine results of multiple inputs](#combine-results-of-multiple-inputs)
|
26
|
+
* [TODO](#todo)
|
12
27
|
|
13
28
|
## Usage
|
14
29
|
|
30
|
+
### Basic validation
|
31
|
+
|
32
|
+
By default, `Results` will transform an `ArgumentError` into a `Bad`, allowing built-in
|
33
|
+
numeric conversions to work directly as validations.
|
34
|
+
|
15
35
|
```ruby
|
16
|
-
|
36
|
+
def parseAge(str)
|
37
|
+
Results.new(str) { |v| Integer(v) }
|
38
|
+
end
|
39
|
+
|
40
|
+
parseAge('1') # => #<struct Results::Good value=1>
|
41
|
+
parseAge('abc') # => #<struct Results::Bad why=[#<struct Results::Because error="invalid value for integer", input="abc">]>
|
42
|
+
```
|
43
|
+
|
44
|
+
### Chained filters and validations
|
45
|
+
|
46
|
+
Once you have a `Good` or `Bad`, you can chain additional boolean filters using `#when` and `#when_not`.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
def parseAge21To45(str)
|
50
|
+
# Syntax workaround due to no chaining on blocks - Open to suggestions!
|
51
|
+
_ = parseAge(str)
|
52
|
+
_ = _.when ('under 45') { |v| v < 45 }
|
53
|
+
_ = _.when_not('under 21') { |v| v < 21 }
|
54
|
+
end
|
55
|
+
|
56
|
+
parseAge21To45('29') # => #<struct Results::Good value=29>
|
57
|
+
parseAge21To45('65') # => #<struct Results::Bad why=[#<struct Results::Because error="not under 45", input=65>]>
|
58
|
+
parseAge21To45('1') # => #<struct Results::Bad why=[#<struct Results::Because error="under 21", input=1>]>
|
59
|
+
```
|
60
|
+
|
61
|
+
Want to save your favorite filters? You can use the provided class `Filter`,
|
62
|
+
or any object with the same duck-type, meaning it responds to `#call` and `#message`.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# You can use the provided class Filter
|
66
|
+
under_45 = Results::Filter.new('under 45') { |v| v < 45 }
|
67
|
+
|
68
|
+
# ...or do something funky like this...
|
69
|
+
under_21 = lambda { |v| v < 21 }.tap { |l| l.define_singleton_method(:message) { 'under 21' } }
|
70
|
+
|
71
|
+
# Both work the same way
|
72
|
+
parseAge('65')
|
73
|
+
.when(under_45)
|
74
|
+
.when_not(under_21) # => #<struct Results::Bad why=[#<struct Results::Because error="not under 45", input=65>]>
|
75
|
+
```
|
76
|
+
|
77
|
+
You can also chain validation functions (returning `Good` or `Bad` instead of `Boolean`) using `#validate`.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
def parseAgeRange(str)
|
81
|
+
parseAge(str).validate do |v|
|
82
|
+
case v
|
83
|
+
when 21...45 then Results::Good.new(v)
|
84
|
+
else Results::Bad.new('not between 21 and 45', v)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
parseAgeRange('29') # => #<struct Results::Good value=29>
|
90
|
+
parseAgeRange('65') # => #<struct Results::Bad why=[#<struct Results::Because error="not between 21 and 45", input=65>]>
|
91
|
+
```
|
92
|
+
|
93
|
+
For convenience, the `#when` and `#when_not` methods can also accept a lambda for
|
94
|
+
the error message, to format the error message based on the input value.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
parseAge('65').when(lambda { |v| "#{v} is not under 45" }) { |v| v < 45 }
|
98
|
+
# => #<struct Results::Bad why=[#<struct Results::Because error="65 is not under 45", input=65>]>
|
99
|
+
```
|
100
|
+
|
101
|
+
In a similar vein, if you already have a `Filter` or compatible duck-type
|
102
|
+
(see above), it's easy turn it into a basic validation function returning
|
103
|
+
`Good` or `Bad` via convenience functions `Results.when` and `Results.when_not`.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
Results.when_not(under_21).call(16)
|
107
|
+
# => #<struct Results::Bad why=[#<struct Results::Because error="under 21", input=16>]>
|
108
|
+
```
|
109
|
+
|
110
|
+
Note that this is equivalent to:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
Results.new(16).when_not(under_21)
|
114
|
+
```
|
115
|
+
|
116
|
+
The benefit of `Results.when` is for cases where the value (here, 16) is not yet known.
|
117
|
+
|
118
|
+
Experience has shown that many filters that are written are simply
|
119
|
+
predicates called on value objects, such as `Numeric#zero?` or `String#empty?` or
|
120
|
+
even `Object#nil?`.
|
121
|
+
|
122
|
+
For these cases, you can use the convenience function `Results.predicate`.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
# validates non-nil, non-empty
|
126
|
+
def valid?(str)
|
127
|
+
Results.new(str)
|
128
|
+
.when_not(Results.predicate :nil?)
|
129
|
+
.when_not(Results.predicate :empty?)
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
Or even simpler, just pass the symbol of the predicate name directly to `#when` or `#when_not`.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
# same as above
|
137
|
+
def valid_short?(str)
|
138
|
+
Results.new(str).when_not(:nil?).when_not(:empty?)
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
### Accumulating multiple bad results
|
143
|
+
|
144
|
+
So, now the interesting parts (Yes, the earlier sections were a bit slow,
|
145
|
+
but it picks up a bit here):
|
146
|
+
|
147
|
+
#### Multiple filters and validations of a single input
|
148
|
+
|
149
|
+
The way we've done things so far, even if you chained multiple filters and validations
|
150
|
+
together, if more than one would fail for some input, you would only see the first
|
151
|
+
`Bad`, and none of the the later filters would be run.
|
152
|
+
|
153
|
+
Now, instead, we're going to accumulate all the failures for a single input.
|
154
|
+
|
155
|
+
One simple way is to intersperse the `#and` method between your chained `#when` calls.
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
# Good still works as before
|
159
|
+
Results.new(0).when(:integer?).and.when(:zero?) # => #<struct Results::Good value=0>
|
160
|
+
|
161
|
+
# Bad accumulates multiple failures
|
162
|
+
Results.new(1.23).when(:integer?).and.when(:zero?) # => #<struct Results::Bad why=[
|
163
|
+
# #<struct Results::Because error="not integer", input=1.23>,
|
164
|
+
# #<struct Results::Because error="not zero", input=1.23>]>
|
165
|
+
```
|
166
|
+
|
167
|
+
You can also call `#when_all` and`#when_all_not` with a collection of filters.
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
filters = [:integer?, :zero?, Results::Filter.new('greater than 2') { |n| n > 2 }]
|
171
|
+
r = Results.new(1.23).when_all(filters) # => #<struct Results::Bad why=[
|
172
|
+
# #<struct Results::Because error="not integer", input=1.23>,
|
173
|
+
# #<struct Results::Because error="not zero", input=1.23>,
|
174
|
+
# #<struct Results::Because error="not greater than 2", input=1.23>]>
|
175
|
+
```
|
176
|
+
|
177
|
+
For a collection of validation functions, you can use `#validate_all` in a similar fashion.
|
178
|
+
|
179
|
+
#### Combine results of multiple inputs
|
180
|
+
|
181
|
+
If you have two results, the simplest way to combine them is with `#zip`. If both results
|
182
|
+
are good, it returns a `Good` containing an array of both values. However, if any results
|
183
|
+
are bad, it returns a `Bad` containing all the failures.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
good = Results::Good.new(1)
|
187
|
+
bad1 = Results::Bad.new('not nonzero', 0)
|
188
|
+
bad2 = Results::Bad.new('not integer', 1.23)
|
189
|
+
|
190
|
+
good.zip(good) # => #<struct Results::Good value=[1, 1]>
|
191
|
+
|
192
|
+
good.zip(bad1).zip(bad2) # => #<struct Results::Bad why=[
|
193
|
+
# #<struct Results::Because error="not nonzero", input=0>,
|
194
|
+
# #<struct Results::Because error="not integer", input=1.23>]>
|
195
|
+
```
|
196
|
+
|
197
|
+
If you have a collection of results, you can combine them with `Results.combine`. If all
|
198
|
+
results are good, it returns a single `Good` containing a collection of all the values.
|
199
|
+
However, if any results are bad, it returns a single `Bad` containing all the failures.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
all_good_results = [good, good, good]
|
203
|
+
some_bad_results = [bad1, good, bad2]
|
204
|
+
|
205
|
+
Results.combine(all_good_results) # => #<struct Results::Good value=[1, 1, 1]>
|
206
|
+
|
207
|
+
Results.combine(some_bad_results) # => #<struct Results::Bad why=[
|
208
|
+
# #<struct Results::Because error="not nonzero", input=0>,
|
209
|
+
# #<struct Results::Because error="not integer", input=1.23>]>
|
17
210
|
```
|
18
211
|
|
19
|
-
|
212
|
+
NOTE: this section is under construction...
|
20
213
|
|
21
214
|
## TODO
|
22
215
|
|
data/lib/results.rb
CHANGED
@@ -1,4 +1,236 @@
|
|
1
|
+
require 'rescuer'
|
2
|
+
|
1
3
|
module Results
|
2
|
-
|
3
|
-
|
4
|
+
DEFAULT_EXCEPTIONS_TO_RESCUE_AS_BADS = [ArgumentError]
|
5
|
+
DEFAULT_EXCEPTION_MESSAGE_TRANSFORMS = {
|
6
|
+
ArgumentError => lambda do |m|
|
7
|
+
r = /\Ainvalid (value|string) for ([A-Z][a-z]+)(\(\))?(: ".*")?\Z/
|
8
|
+
m.gsub(r) { |_| "invalid value for #{$2}".downcase }
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
def new(input_or_proc)
|
13
|
+
exceptions_as_bad = DEFAULT_EXCEPTIONS_TO_RESCUE_AS_BADS
|
14
|
+
exceptions_xforms = DEFAULT_EXCEPTION_MESSAGE_TRANSFORMS
|
15
|
+
|
16
|
+
rescued = Rescuer.new(*exceptions_as_bad) do
|
17
|
+
call_or_yield_or_return(input_or_proc) { |input| block_given? ? yield(input) : input }
|
18
|
+
end
|
19
|
+
|
20
|
+
from_rescuer(rescued, input_or_proc, exceptions_xforms)
|
21
|
+
end
|
22
|
+
module_function :new
|
23
|
+
|
24
|
+
def from_rescuer(success_or_failure, input, exception_message_transforms = DEFAULT_EXCEPTION_MESSAGE_TRANSFORMS)
|
25
|
+
success_or_failure.transform(
|
26
|
+
lambda { |v| Good.new(v) },
|
27
|
+
lambda { |e| Bad.new(transform_exception_message(e, exception_message_transforms), input) }
|
28
|
+
).get
|
29
|
+
end
|
30
|
+
module_function :from_rescuer
|
31
|
+
|
32
|
+
def transform_exception_message(exception, exception_message_transforms = DEFAULT_EXCEPTION_MESSAGE_TRANSFORMS)
|
33
|
+
_, f = exception_message_transforms.find { |klass, _| klass === exception }
|
34
|
+
message = exception && exception.message
|
35
|
+
f && f.call(message) || message
|
36
|
+
end
|
37
|
+
module_function :transform_exception_message
|
38
|
+
|
39
|
+
def when(*args)
|
40
|
+
lambda { |v| Results.new(v).when(*args) }
|
41
|
+
end
|
42
|
+
module_function :when
|
43
|
+
|
44
|
+
def when_not(*args)
|
45
|
+
lambda { |v| Results.new(v).when_not(*args) }
|
46
|
+
end
|
47
|
+
module_function :when_not
|
48
|
+
|
49
|
+
# TODO: Extract this simple util somewhere else
|
50
|
+
HeadCombiner = Struct.new(:elements) do
|
51
|
+
def initialize(*args)
|
52
|
+
head, *tail = args
|
53
|
+
super(head.is_a?(HeadCombiner) ? head.elements + tail : args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def combine(*args)
|
58
|
+
flat_args = (args.size == 1 && args.first.is_a?(Enumerable)) ? args.first : args
|
59
|
+
raise ArgumentError, 'no results to combine' if flat_args.empty?
|
60
|
+
flat_args.inject { |res, nxt| res.zip(nxt).map { |vs| HeadCombiner.new(*vs) } }.map { |hc| hc.elements }
|
61
|
+
end
|
62
|
+
module_function :combine
|
63
|
+
|
64
|
+
def predicate(method_name)
|
65
|
+
Filter.new(method_name.to_s.gsub(/\?\Z/, '')) { |v| v.send(method_name) }
|
66
|
+
end
|
67
|
+
module_function :predicate
|
68
|
+
|
69
|
+
class Filter
|
70
|
+
def initialize(msg_or_proc, &filter_block)
|
71
|
+
raise ArgumentError, 'invalid message' if msg_or_proc.nil?
|
72
|
+
raise ArgumentError, 'no block given' if filter_block.nil?
|
73
|
+
@msg_or_proc, @filter_block = msg_or_proc, filter_block
|
74
|
+
end
|
75
|
+
|
76
|
+
def call(value)
|
77
|
+
@filter_block.call(value)
|
78
|
+
end
|
79
|
+
|
80
|
+
def message
|
81
|
+
@msg_or_proc
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
Good = Struct.new(:value) do
|
86
|
+
def map
|
87
|
+
flat_map { |v| Good.new(yield v) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def flat_map
|
91
|
+
yield(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
alias_method :validate, :flat_map
|
95
|
+
|
96
|
+
def when(msg_or_proc_or_filter)
|
97
|
+
validate do |v|
|
98
|
+
predicate, msg_or_proc = extract_predicate_and_message(msg_or_proc_or_filter, v) { yield v }
|
99
|
+
predicate ? self : Bad.new(Results.call_or_yield_or_return(msg_or_proc, v) { |msg| 'not ' + msg }, v)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def when_not(msg_or_proc_or_filter)
|
104
|
+
validate do |v|
|
105
|
+
predicate, msg_or_proc = extract_predicate_and_message(msg_or_proc_or_filter, v) { yield v }
|
106
|
+
!predicate ? self : Bad.new(Results.call_or_yield_or_return(msg_or_proc, v), v)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def and
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def when_all(*filters)
|
115
|
+
filters.flatten.inject(self) { |prev_result, filter| prev_result.and.when(filter) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def when_all_not(*filters)
|
119
|
+
filters.flatten.inject(self) { |prev_result, filter| prev_result.and.when_not(filter) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def validate_all(*validations)
|
123
|
+
validations.flatten.inject(self) { |prev_result, validation| prev_result.and.validate(&validation) }
|
124
|
+
end
|
125
|
+
|
126
|
+
def zip(other)
|
127
|
+
case other
|
128
|
+
when Good then Good.new([value, other.value])
|
129
|
+
when Bad then other
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def extract_predicate_and_message(msg_or_proc_or_filter, v)
|
136
|
+
if msg_or_proc_or_filter.is_a? Symbol
|
137
|
+
p = Results.predicate(msg_or_proc_or_filter)
|
138
|
+
[p.call(v), p.message]
|
139
|
+
elsif msg_or_proc_or_filter.respond_to?(:call) && msg_or_proc_or_filter.respond_to?(:message)
|
140
|
+
[msg_or_proc_or_filter.call(v), msg_or_proc_or_filter.message]
|
141
|
+
else
|
142
|
+
[yield, msg_or_proc_or_filter]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
Bad = Struct.new(:why) do
|
149
|
+
def initialize(*args)
|
150
|
+
flat_args = args.flatten
|
151
|
+
super( flat_args.all? { |a| a.is_a? Because } ? flat_args : [Because.new(*flat_args)] )
|
152
|
+
end
|
153
|
+
|
154
|
+
def map
|
155
|
+
self
|
156
|
+
end
|
157
|
+
|
158
|
+
def flat_map
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
alias_method :validate, :flat_map
|
163
|
+
|
164
|
+
def when(msg_or_proc)
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def when_not(msg_or_proc)
|
169
|
+
self
|
170
|
+
end
|
171
|
+
|
172
|
+
def and
|
173
|
+
And.new(self)
|
174
|
+
end
|
175
|
+
|
176
|
+
And = Struct.new(:prev_bad) do
|
177
|
+
def when(*args, &blk)
|
178
|
+
accumulate(:when, *args, &blk)
|
179
|
+
end
|
180
|
+
|
181
|
+
def when_not(*args, &blk)
|
182
|
+
accumulate(:when_not, *args, &blk)
|
183
|
+
end
|
184
|
+
|
185
|
+
def validate(*args, &blk)
|
186
|
+
accumulate(:validate, *args, &blk)
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def accumulate(method, *args, &blk)
|
192
|
+
next_result = Good.new(input).send(method, *args, &blk)
|
193
|
+
case next_result
|
194
|
+
when Good then prev_bad
|
195
|
+
when Bad then Bad.new(prev_bad.why + next_result.why)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def input
|
200
|
+
prev_bad.why.last.input
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def when_all(*filters)
|
205
|
+
filters.flatten.inject(self) { |prev_result, filter| prev_result.and.when(filter) }
|
206
|
+
end
|
207
|
+
|
208
|
+
def when_all_not(*filters)
|
209
|
+
filters.flatten.inject(self) { |prev_result, filter| prev_result.and.when_not(filter) }
|
210
|
+
end
|
211
|
+
|
212
|
+
def validate_all(*validations)
|
213
|
+
validations.flatten.inject(self) { |prev_result, validation| prev_result.and.validate(&validation) }
|
214
|
+
end
|
215
|
+
|
216
|
+
def zip(other)
|
217
|
+
case other
|
218
|
+
when Good then self
|
219
|
+
when Bad then Bad.new(why + other.why)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
Because = Struct.new(:error, :input)
|
225
|
+
|
226
|
+
# Helper which will call its argument, or yield it to a block, or simply return it,
|
227
|
+
# depending on what is possible with the given input
|
228
|
+
def self.call_or_yield_or_return(proc_or_value, *args)
|
229
|
+
if proc_or_value.respond_to?(:call)
|
230
|
+
proc_or_value.call(*args)
|
231
|
+
else
|
232
|
+
block_given? ? yield(proc_or_value) : proc_or_value
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
4
236
|
end
|
data/lib/results/version.rb
CHANGED
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'results'
|
3
|
+
|
4
|
+
##
|
5
|
+
# These specs ensure that published usage examples continue to work.
|
6
|
+
##
|
7
|
+
describe 'Example usages' do
|
8
|
+
|
9
|
+
def parseAge(str)
|
10
|
+
Results.new(str) { |v| Integer(v) }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'Basic validation' do
|
14
|
+
|
15
|
+
context 'parseAge("1")' do
|
16
|
+
subject { parseAge('1').inspect }
|
17
|
+
it { is_expected.to eq '#<struct Results::Good value=1>' }
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'parseAge("abc")' do
|
21
|
+
subject { parseAge('abc').inspect }
|
22
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="invalid value for integer", input="abc">]>' }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def parseAge21To45(str)
|
28
|
+
# Syntax workaround due to lack of support for chaining on blocks
|
29
|
+
_ = parseAge(str)
|
30
|
+
_ = _.when ('under 45') { |v| v < 45 }
|
31
|
+
_ = _.when_not('under 21') { |v| v < 21 }
|
32
|
+
end
|
33
|
+
|
34
|
+
under_45 = Results::Filter.new('under 45') { |v| v < 45 }
|
35
|
+
|
36
|
+
under_21 = lambda { |v| v < 21 }.tap { |l| l.define_singleton_method(:message) { 'under 21' } }
|
37
|
+
|
38
|
+
def parseAgeRange(str)
|
39
|
+
parseAge(str).validate do |v|
|
40
|
+
case v
|
41
|
+
when 21...45 then Results::Good.new(v)
|
42
|
+
else Results::Bad.new('not between 21 and 45', v)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid?(str)
|
48
|
+
Results.new(str)
|
49
|
+
.when_not(Results.predicate :nil?)
|
50
|
+
.when_not(Results.predicate :empty?)
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid_short?(str)
|
54
|
+
Results.new(str)
|
55
|
+
.when_not(:nil?)
|
56
|
+
.when_not(:empty?)
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'Chained filters and validations' do
|
60
|
+
|
61
|
+
context 'parseAge21To45("29")' do
|
62
|
+
subject { parseAge21To45('29').inspect }
|
63
|
+
it { is_expected.to eq '#<struct Results::Good value=29>' }
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'parseAge21To45("65")' do
|
67
|
+
subject { parseAge21To45('65').inspect }
|
68
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="not under 45", input=65>]>' }
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'parseAge21To45("1")' do
|
72
|
+
subject { parseAge21To45('1').inspect }
|
73
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="under 21", input=1>]>' }
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'parseAgeRange("29")' do
|
77
|
+
subject { parseAgeRange('29').inspect }
|
78
|
+
it { is_expected.to eq '#<struct Results::Good value=29>' }
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'parseAgeRange("65")' do
|
82
|
+
subject { parseAgeRange('65').inspect }
|
83
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="not between 21 and 45", input=65>]>' }
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'parseAge("65").when(under_45).when_not(under_21)' do
|
87
|
+
subject { parseAge('65').when(under_45).when_not(under_21).inspect }
|
88
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="not under 45", input=65>]>' }
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'parseAge("16").when(under_45).when_not(under_21)' do
|
92
|
+
subject {
|
93
|
+
parseAge('16')
|
94
|
+
.when(under_45)
|
95
|
+
.when_not(under_21).inspect
|
96
|
+
}
|
97
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="under 21", input=16>]>' }
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'parseAge("65").when(lambda { |v| "#{v} is not under 45" }) { |v| v < 45 }' do
|
101
|
+
subject { parseAge('65').when(lambda { |v| "#{v} is not under 45" }) { |v| v < 45 }.inspect }
|
102
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="65 is not under 45", input=65>]>' }
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'Results.when_not(under_21).call(16)' do
|
106
|
+
subject { Results.when_not(under_21).call(16).inspect }
|
107
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="under 21", input=16>]>' }
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'valid?(nil)' do
|
111
|
+
subject { valid?(nil).inspect }
|
112
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="nil", input=nil>]>' }
|
113
|
+
end
|
114
|
+
|
115
|
+
context 'valid?("")' do
|
116
|
+
subject { valid?('').inspect }
|
117
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="empty", input="">]>' }
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'valid_short?(nil)' do
|
121
|
+
subject { valid_short?(nil).inspect }
|
122
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="nil", input=nil>]>' }
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
describe 'Accumulating multiple bad results' do
|
128
|
+
|
129
|
+
describe 'Multiple filters and validations of a single input' do
|
130
|
+
|
131
|
+
context 'Results.new(1.23).when(:integer?).and.when(:zero?)' do
|
132
|
+
subject { Results.new(1.23).when(:integer?).and.when(:zero?).inspect }
|
133
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="not integer", input=1.23>, ' +
|
134
|
+
'#<struct Results::Because error="not zero", input=1.23>]>' }
|
135
|
+
end
|
136
|
+
|
137
|
+
let(:filters) { [:integer?, :zero?, Results::Filter.new('greater than 2') { |n| n > 2 }] }
|
138
|
+
|
139
|
+
context 'Results.new(1.23).when_all(filters)' do
|
140
|
+
subject { Results.new(1.23).when_all(filters).inspect }
|
141
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="not integer", input=1.23>, ' +
|
142
|
+
'#<struct Results::Because error="not zero", input=1.23>, ' +
|
143
|
+
'#<struct Results::Because error="not greater than 2", input=1.23>]>' }
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
describe 'Combine results of multiple inputs' do
|
149
|
+
let(:good) { Results::Good.new(1) }
|
150
|
+
let(:bad1) { Results::Bad.new('not nonzero', 0) }
|
151
|
+
let(:bad2) { Results::Bad.new('not integer', 1.23) }
|
152
|
+
|
153
|
+
context 'good.zip(good)' do
|
154
|
+
subject { good.zip(good).inspect }
|
155
|
+
it { is_expected.to eq '#<struct Results::Good value=[1, 1]>' }
|
156
|
+
end
|
157
|
+
|
158
|
+
context 'good.zip(bad1).zip(bad2)' do
|
159
|
+
subject { good.zip(bad1).zip(bad2).inspect }
|
160
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="not nonzero", input=0>, ' +
|
161
|
+
'#<struct Results::Because error="not integer", input=1.23>]>' }
|
162
|
+
end
|
163
|
+
|
164
|
+
let(:all_good_results) { [good, good, good] }
|
165
|
+
let(:some_bad_results) { [bad1, good, bad2] }
|
166
|
+
|
167
|
+
context 'Results.combine(all_good_results)' do
|
168
|
+
subject { Results.combine(all_good_results).inspect }
|
169
|
+
it { is_expected.to eq '#<struct Results::Good value=[1, 1, 1]>' }
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'Results.combine(some_bad_results)' do
|
173
|
+
subject { Results.combine(some_bad_results).inspect }
|
174
|
+
it { is_expected.to eq '#<struct Results::Bad why=[#<struct Results::Because error="not nonzero", input=0>, ' +
|
175
|
+
'#<struct Results::Because error="not integer", input=1.23>]>' }
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
@@ -0,0 +1,618 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'results'
|
3
|
+
|
4
|
+
describe Results do
|
5
|
+
|
6
|
+
##
|
7
|
+
# Construct indirectly with a value or block which may raise an exception
|
8
|
+
##
|
9
|
+
describe '.new' do
|
10
|
+
|
11
|
+
context 'with non-callable value and no block' do
|
12
|
+
subject { Results.new(1) }
|
13
|
+
it { is_expected.to eq Results::Good.new(1) }
|
14
|
+
end
|
15
|
+
|
16
|
+
shared_examples 'exception handler' do
|
17
|
+
context 'when does *not* raise' do
|
18
|
+
subject { when_does_not_raise }
|
19
|
+
it { is_expected.to eq Results::Good.new(1) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when *does* raise' do
|
23
|
+
let(:std_err) { StandardError.new('abc') }
|
24
|
+
|
25
|
+
context 'with defaults' do
|
26
|
+
context 'when raises ArgumentError' do
|
27
|
+
subject { when_raises_arg_err }
|
28
|
+
it { is_expected.to eq Results::Bad.new('invalid value for integer', input_raises_arg_err) }
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when raises StandardError' do
|
32
|
+
subject { lambda { when_raises_std_err } }
|
33
|
+
it { is_expected.to raise_error(StandardError, 'abc') }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'with callable value' do
|
40
|
+
let(:input_does_not_raise) { lambda { 1 } }
|
41
|
+
let(:input_raises_arg_err) { lambda { Integer('abc') } }
|
42
|
+
let(:input_raises_std_err) { lambda { raise std_err } }
|
43
|
+
|
44
|
+
let(:when_does_not_raise) { Results.new(input_does_not_raise) }
|
45
|
+
let(:when_raises_arg_err) { Results.new(input_raises_arg_err) }
|
46
|
+
let(:when_raises_std_err) { Results.new(input_raises_std_err) }
|
47
|
+
|
48
|
+
it_behaves_like 'exception handler'
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with block' do
|
52
|
+
let(:input_does_not_raise) { 1 }
|
53
|
+
let(:input_raises_arg_err) { 'abc' }
|
54
|
+
let(:input_raises_std_err) { 'dummy' }
|
55
|
+
|
56
|
+
let(:when_does_not_raise) { Results.new(input_does_not_raise) { |v| v } }
|
57
|
+
let(:when_raises_arg_err) { Results.new(input_raises_arg_err) { |v| Integer(v) } }
|
58
|
+
let(:when_raises_std_err) { Results.new(input_raises_std_err) { |_| raise std_err } }
|
59
|
+
|
60
|
+
it_behaves_like 'exception handler'
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Construct directly by wrapping an existing Rescuer::Success or Rescuer::Failure
|
67
|
+
##
|
68
|
+
describe '.from_rescuer' do
|
69
|
+
|
70
|
+
context 'when success' do
|
71
|
+
let(:success) { Rescuer::Success.new(1) }
|
72
|
+
subject { Results.from_rescuer(success, 1) }
|
73
|
+
it { is_expected.to eq Results::Good.new(1) }
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'when failure' do
|
77
|
+
let(:input) { 'abc' }
|
78
|
+
let(:failure) { Rescuer::Failure.new(StandardError.new('failure message')) }
|
79
|
+
subject { Results.from_rescuer(failure, input) }
|
80
|
+
it { is_expected.to eq Results::Bad.new('failure message', 'abc') }
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Make a validation function from a filter using .when
|
87
|
+
##
|
88
|
+
describe '.when' do
|
89
|
+
|
90
|
+
shared_examples 'validation from a filter' do
|
91
|
+
context 'when passes' do
|
92
|
+
subject { Results.when(is_zero).call(0) }
|
93
|
+
it { is_expected.to eq Results::Good.new(0) }
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when fails' do
|
97
|
+
subject { Results.when(is_zero).call(1) }
|
98
|
+
it { is_expected.to eq Results::Bad.new('not zero', 1) }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'when instance of Results::Filter' do
|
103
|
+
let(:is_zero) { Results::Filter.new('zero') { |n| n.zero? } }
|
104
|
+
it_behaves_like 'validation from a filter'
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'when a duck-type of #call and #message' do
|
108
|
+
let(:is_zero) { lambda { |n| n.zero? }.tap { |l| l.define_singleton_method(:message) { 'zero' } } }
|
109
|
+
it_behaves_like 'validation from a filter'
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Make a validation function from a filter using .when_not
|
116
|
+
##
|
117
|
+
describe '.when_not' do
|
118
|
+
|
119
|
+
shared_examples 'validation from a filter' do
|
120
|
+
context 'when passes' do
|
121
|
+
subject { Results.when_not(is_zero).call(1) }
|
122
|
+
it { is_expected.to eq Results::Good.new(1) }
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'when fails' do
|
126
|
+
subject { Results.when_not(is_zero).call(0) }
|
127
|
+
it { is_expected.to eq Results::Bad.new('zero', 0) }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'when instance of Results::Filter' do
|
132
|
+
let(:is_zero) { Results::Filter.new('zero') { |n| n.zero? } }
|
133
|
+
it_behaves_like 'validation from a filter'
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'when a duck-type of #call and #message' do
|
137
|
+
let(:is_zero) { lambda { |n| n.zero? }.tap { |l| l.define_singleton_method(:message) { 'zero' } } }
|
138
|
+
it_behaves_like 'validation from a filter'
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Combine a collection of multiple results
|
145
|
+
##
|
146
|
+
describe '.combine' do
|
147
|
+
let(:all_goods) { Array.new(4) { |n| Results::Good.new(n) } }
|
148
|
+
let(:some_bads) { all_goods.map { |r| r.when('even') { |v| v % 2 == 0 } } }
|
149
|
+
|
150
|
+
context 'when all good results' do
|
151
|
+
subject { Results.combine(all_goods) }
|
152
|
+
it { is_expected.to eq Results::Good.new([0, 1, 2, 3]) }
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'when some bad results' do
|
156
|
+
subject { Results.combine(some_bads) }
|
157
|
+
it { is_expected.to eq Results::Bad.new([1, 3].map { |v| Results::Because.new('not even', v) }) }
|
158
|
+
end
|
159
|
+
|
160
|
+
context 'when splatted all good results' do
|
161
|
+
subject { Results.combine(*all_goods) }
|
162
|
+
it { is_expected.to eq Results::Good.new([0, 1, 2, 3]) }
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'when good results are arrays' do
|
166
|
+
subject { Results.combine(all_goods.map { |r| r.map { |n| [n] } }) }
|
167
|
+
it { is_expected.to eq Results::Good.new([[0], [1], [2], [3]]) }
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'when empty' do
|
171
|
+
subject { lambda { Results.combine([]) } }
|
172
|
+
it { is_expected.to raise_error(ArgumentError, 'no results to combine') }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Make a filter from the symbol name of a predicate method using .predicate
|
178
|
+
##
|
179
|
+
describe '.predicate' do
|
180
|
+
context 'when symbol ends in ?, message strips the ?' do
|
181
|
+
subject { Results.predicate(:zero?).message }
|
182
|
+
it { is_expected.to eq 'zero' }
|
183
|
+
end
|
184
|
+
|
185
|
+
context 'when passes' do
|
186
|
+
subject { Results.predicate(:zero?).call(0) }
|
187
|
+
it { is_expected.to eq true }
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'when fails' do
|
191
|
+
subject { Results.predicate(:zero?).call(1) }
|
192
|
+
it { is_expected.to eq false }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# Transform exception message formatting via configured lambdas
|
198
|
+
##
|
199
|
+
describe '.transform_exception_message' do
|
200
|
+
|
201
|
+
context 'with defaults' do
|
202
|
+
context 'when argument error due to invalid integer' do
|
203
|
+
subject { Results.transform_exception_message(begin; Integer('abc'); rescue => e; e; end) }
|
204
|
+
it { is_expected.to eq 'invalid value for integer' }
|
205
|
+
end
|
206
|
+
|
207
|
+
context 'when argument error due to invalid float' do
|
208
|
+
subject { Results.transform_exception_message(begin; Float('abc'); rescue => e; e; end) }
|
209
|
+
it { is_expected.to eq 'invalid value for float' }
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
##
|
216
|
+
# Construct directly as Good
|
217
|
+
##
|
218
|
+
describe Results::Good do
|
219
|
+
let(:value) { 1 }
|
220
|
+
let(:good) { Results::Good.new(value) }
|
221
|
+
|
222
|
+
describe '#map' do
|
223
|
+
context 'with a function' do
|
224
|
+
subject { good.map { |v| v + 1 } }
|
225
|
+
it { is_expected.to eq Results::Good.new(2) }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
describe '#flat_map' do
|
230
|
+
context 'with a function' do
|
231
|
+
subject { good.flat_map { |v| Results::Good.new(v + 1) } }
|
232
|
+
it { is_expected.to eq Results::Good.new(2) }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe '#when' do
|
237
|
+
context 'with true predicate' do
|
238
|
+
subject { good.when('dummy') { |_| true } }
|
239
|
+
it { is_expected.to be good }
|
240
|
+
end
|
241
|
+
|
242
|
+
context 'with false predicate and string error message, prepends "not"' do
|
243
|
+
subject { good.when('true') { |_| false } }
|
244
|
+
it { is_expected.to eq Results::Bad.new('not true', value) }
|
245
|
+
end
|
246
|
+
|
247
|
+
context 'with false predicate and callable error message' do
|
248
|
+
subject { good.when(lambda { |v| "#{v} was not true" }) { |_| false } }
|
249
|
+
it { is_expected.to eq Results::Bad.new('1 was not true', value) }
|
250
|
+
end
|
251
|
+
|
252
|
+
context 'with true predicate given by name' do
|
253
|
+
subject { good.when(:nonzero?) }
|
254
|
+
it { is_expected.to be good }
|
255
|
+
end
|
256
|
+
|
257
|
+
context 'with false predicate given by name' do
|
258
|
+
subject { good.when(:zero?) }
|
259
|
+
it { is_expected.to eq Results::Bad.new('not zero', value) }
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
describe '#when_not' do
|
264
|
+
context 'with false predicate' do
|
265
|
+
subject { good.when_not('dummy') { |_| false } }
|
266
|
+
it { is_expected.to be good }
|
267
|
+
end
|
268
|
+
|
269
|
+
context 'with true predicate and string error message' do
|
270
|
+
subject { good.when_not('evaluated as true') { |_| true } }
|
271
|
+
it { is_expected.to eq Results::Bad.new('evaluated as true', value) }
|
272
|
+
end
|
273
|
+
|
274
|
+
context 'with failing predicate and callable error message' do
|
275
|
+
subject { good.when_not(lambda { |v| "#{v} evaluated as true" }) { |_| true } }
|
276
|
+
it { is_expected.to eq Results::Bad.new('1 evaluated as true', value) }
|
277
|
+
end
|
278
|
+
|
279
|
+
context 'with true predicate given by name' do
|
280
|
+
subject { good.when_not(:nonzero?) }
|
281
|
+
it { is_expected.to eq Results::Bad.new('nonzero', value) }
|
282
|
+
end
|
283
|
+
|
284
|
+
context 'with false predicate given by name' do
|
285
|
+
subject { good.when_not(:zero?) }
|
286
|
+
it { is_expected.to be good }
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
describe '#validate' do
|
291
|
+
context 'with return of good' do
|
292
|
+
subject { good.validate { |_| good } }
|
293
|
+
it { is_expected.to be good }
|
294
|
+
end
|
295
|
+
|
296
|
+
context 'with return of bad' do
|
297
|
+
subject { good.validate { |v| Results::Bad.new("no good: #{v}", v) } }
|
298
|
+
it { is_expected.to eq Results::Bad.new('no good: 1', value) }
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
describe '#and' do
|
303
|
+
subject { good.and }
|
304
|
+
it { is_expected.to be good }
|
305
|
+
end
|
306
|
+
|
307
|
+
describe '#when_all' do
|
308
|
+
context 'with filters which are all true' do
|
309
|
+
let(:filters_true) { Array.new(2) { |n| Results::Filter.new("f#{n}") { |_| true } } }
|
310
|
+
|
311
|
+
context 'passed as array' do
|
312
|
+
subject { good.when_all(filters_true) }
|
313
|
+
it { is_expected.to be good }
|
314
|
+
end
|
315
|
+
|
316
|
+
context 'passed splatted' do
|
317
|
+
subject { good.when_all(*filters_true) }
|
318
|
+
it { is_expected.to be good }
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
context 'with filters which are all false' do
|
323
|
+
let(:filters_false) { Array.new(2) { |n| Results::Filter.new("f#{n}") { |_| false } } }
|
324
|
+
let(:expected_bad) { Results::Bad.new(
|
325
|
+
Results::Because.new('not f0', value),
|
326
|
+
Results::Because.new('not f1', value)) }
|
327
|
+
|
328
|
+
context 'passed as array' do
|
329
|
+
subject { good.when_all(filters_false) }
|
330
|
+
it { is_expected.to eq expected_bad }
|
331
|
+
end
|
332
|
+
|
333
|
+
context 'passed splatted' do
|
334
|
+
subject { good.when_all(*filters_false) }
|
335
|
+
it { is_expected.to eq expected_bad }
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
describe '#when_all_not' do
|
341
|
+
context 'with filters which are all true' do
|
342
|
+
let(:filters_true) { Array.new(2) { |n| Results::Filter.new("f#{n}") { |_| true } } }
|
343
|
+
let(:expected_bad) { Results::Bad.new(
|
344
|
+
Results::Because.new('f0', value),
|
345
|
+
Results::Because.new('f1', value)) }
|
346
|
+
|
347
|
+
context 'passed as array' do
|
348
|
+
subject { good.when_all_not(filters_true) }
|
349
|
+
it { is_expected.to eq expected_bad }
|
350
|
+
end
|
351
|
+
|
352
|
+
context 'passed splatted' do
|
353
|
+
subject { good.when_all_not(*filters_true) }
|
354
|
+
it { is_expected.to eq expected_bad }
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
context 'with filters which are all false' do
|
359
|
+
let(:filters_false) { Array.new(2) { |n| Results::Filter.new("f#{n}") { |_| false } } }
|
360
|
+
|
361
|
+
context 'passed as array' do
|
362
|
+
subject { good.when_all_not(filters_false) }
|
363
|
+
it { is_expected.to be good }
|
364
|
+
end
|
365
|
+
|
366
|
+
context 'passed splatted' do
|
367
|
+
subject { good.when_all_not(*filters_false) }
|
368
|
+
it { is_expected.to be good }
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
describe '#validate_all' do
|
374
|
+
context 'with validations all returning good' do
|
375
|
+
let(:validations_good) { Array.new(2) { |n| lambda { |_| Results::Good.new(n) } } }
|
376
|
+
subject { good.validate_all(validations_good) }
|
377
|
+
it { is_expected.to eq good }
|
378
|
+
end
|
379
|
+
|
380
|
+
context 'with validations all returning bad' do
|
381
|
+
let(:validations_bad) { Array.new(2) { |n| lambda { |i| Results::Bad.new("v#{n}", i) } } }
|
382
|
+
let(:expected_bad) { Results::Bad.new(
|
383
|
+
Results::Because.new('v0', value),
|
384
|
+
Results::Because.new('v1', value)) }
|
385
|
+
|
386
|
+
subject { good.validate_all(validations_bad) }
|
387
|
+
it { is_expected.to eq expected_bad }
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
describe '#zip' do
|
392
|
+
context 'when other is good' do
|
393
|
+
subject { good.zip(good) }
|
394
|
+
it { is_expected.to eq Results::Good.new([value, value]) }
|
395
|
+
end
|
396
|
+
|
397
|
+
context 'when other is bad' do
|
398
|
+
let(:bad) { Results::Bad.new('not ok', value) }
|
399
|
+
subject { good.zip(bad) }
|
400
|
+
it { is_expected.to be bad }
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
##
|
406
|
+
# Construct directly as Bad
|
407
|
+
##
|
408
|
+
describe Results::Bad do
|
409
|
+
let(:msg) { 'epic fail' }
|
410
|
+
let(:input) { 'abc' }
|
411
|
+
let(:bad) { Results::Bad.new(msg, input) }
|
412
|
+
|
413
|
+
describe '#map' do
|
414
|
+
context 'with any function' do
|
415
|
+
subject { bad.map { |_| 'dummy' } }
|
416
|
+
it { is_expected.to be bad }
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
describe '#flat_map' do
|
421
|
+
context 'with any function' do
|
422
|
+
subject { bad.flat_map { |_| Results::Bad.new('dummy', 0) } }
|
423
|
+
it { is_expected.to be bad }
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
describe '#when' do
|
428
|
+
context 'with any predicate' do
|
429
|
+
subject { bad.when('dummy') { |_| true } }
|
430
|
+
it { is_expected.to be bad }
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
describe '#when_not' do
|
435
|
+
context 'with any predicate' do
|
436
|
+
subject { bad.when_not('dummy') { |_| true } }
|
437
|
+
it { is_expected.to be bad }
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
describe '#validate' do
|
442
|
+
context 'with any function' do
|
443
|
+
subject { bad.validate { |_| Results::Good.new(2) } }
|
444
|
+
it { is_expected.to be bad }
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
describe '#and' do
|
449
|
+
describe '#when' do
|
450
|
+
context 'filter is true' do
|
451
|
+
subject { bad.and.when('filter') { |_| true } }
|
452
|
+
it { is_expected.to be bad }
|
453
|
+
end
|
454
|
+
|
455
|
+
context 'filter is false' do
|
456
|
+
subject { bad.and.when('filter') { |_| false } }
|
457
|
+
it { is_expected.to eq Results::Bad.new(Results::Because.new(msg, input),
|
458
|
+
Results::Because.new('not filter', input)) }
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
describe '#when_not' do
|
463
|
+
context 'filter is true' do
|
464
|
+
subject { bad.and.when_not('filter') { |_| true } }
|
465
|
+
it { is_expected.to eq Results::Bad.new(Results::Because.new(msg, input),
|
466
|
+
Results::Because.new('filter', input)) }
|
467
|
+
end
|
468
|
+
|
469
|
+
context 'filter is false' do
|
470
|
+
subject { bad.and.when_not('filter') { |_| false } }
|
471
|
+
it { is_expected.to be bad }
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
describe '#when_all' do
|
477
|
+
context 'with filters which are all true' do
|
478
|
+
let(:filters_true) { Array.new(2) { |n| Results::Filter.new("f#{n}") { |_| true } } }
|
479
|
+
|
480
|
+
context 'passed as array' do
|
481
|
+
subject { bad.when_all(filters_true) }
|
482
|
+
it { is_expected.to be bad }
|
483
|
+
end
|
484
|
+
|
485
|
+
context 'passed splatted' do
|
486
|
+
subject { bad.when_all(*filters_true) }
|
487
|
+
it { is_expected.to be bad }
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
context 'with filters which are all false' do
|
492
|
+
let(:filters_false) { Array.new(2) { |n| Results::Filter.new("f#{n}") { |_| false } } }
|
493
|
+
let(:expected_bad) { Results::Bad.new(
|
494
|
+
Results::Because.new(msg, input),
|
495
|
+
Results::Because.new('not f0', input),
|
496
|
+
Results::Because.new('not f1', input)) }
|
497
|
+
|
498
|
+
context 'passed as array' do
|
499
|
+
subject { bad.when_all(filters_false) }
|
500
|
+
it { is_expected.to eq expected_bad }
|
501
|
+
end
|
502
|
+
|
503
|
+
context 'passed splatted' do
|
504
|
+
subject { bad.when_all(*filters_false) }
|
505
|
+
it { is_expected.to eq expected_bad }
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
describe '#when_all_not' do
|
511
|
+
context 'with filters which are all true' do
|
512
|
+
let(:filters_true) { Array.new(2) { |n| Results::Filter.new("f#{n}") { |_| true } } }
|
513
|
+
let(:expected_bad) { Results::Bad.new(
|
514
|
+
Results::Because.new(msg, input),
|
515
|
+
Results::Because.new('f0', input),
|
516
|
+
Results::Because.new('f1', input)) }
|
517
|
+
|
518
|
+
context 'passed as array' do
|
519
|
+
subject { bad.when_all_not(filters_true) }
|
520
|
+
it { is_expected.to eq expected_bad }
|
521
|
+
end
|
522
|
+
|
523
|
+
context 'passed splatted' do
|
524
|
+
subject { bad.when_all_not(*filters_true) }
|
525
|
+
it { is_expected.to eq expected_bad }
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
context 'with filters which are all false' do
|
530
|
+
let(:filters_false) { Array.new(2) { |n| Results::Filter.new("f#{n}") { |_| false } } }
|
531
|
+
|
532
|
+
context 'passed as array' do
|
533
|
+
subject { bad.when_all_not(filters_false) }
|
534
|
+
it { is_expected.to be bad }
|
535
|
+
end
|
536
|
+
|
537
|
+
context 'passed splatted' do
|
538
|
+
subject { bad.when_all_not(*filters_false) }
|
539
|
+
it { is_expected.to be bad }
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
describe '#validate_all' do
|
545
|
+
context 'with validations all returning good' do
|
546
|
+
let(:validations_good) { Array.new(2) { |n| lambda { |_| Results::Good.new(n) } } }
|
547
|
+
subject { bad.validate_all(validations_good) }
|
548
|
+
it { is_expected.to be bad }
|
549
|
+
end
|
550
|
+
|
551
|
+
context 'with validations all returning bad' do
|
552
|
+
let(:validations_bad) { Array.new(2) { |n| lambda { |i| Results::Bad.new("v#{n}", i) } } }
|
553
|
+
let(:expected_bad) { Results::Bad.new(
|
554
|
+
Results::Because.new(msg, input),
|
555
|
+
Results::Because.new('v0', input),
|
556
|
+
Results::Because.new('v1', input)) }
|
557
|
+
|
558
|
+
subject { bad.validate_all(validations_bad) }
|
559
|
+
it { is_expected.to eq expected_bad }
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
describe '#zip' do
|
564
|
+
context 'when other is good' do
|
565
|
+
subject { bad.zip(Results::Good.new(2)) }
|
566
|
+
it { is_expected.to be bad }
|
567
|
+
end
|
568
|
+
|
569
|
+
context 'when other is bad' do
|
570
|
+
subject { bad.zip(bad) }
|
571
|
+
it { is_expected.to eq Results::Bad.new(bad.why + bad.why) }
|
572
|
+
end
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
##
|
577
|
+
# Helper class Filter for use with #when and #when_not
|
578
|
+
##
|
579
|
+
describe Results::Filter do
|
580
|
+
|
581
|
+
context 'with string message and block' do
|
582
|
+
let(:filter_under_45) { Results::Filter.new('under 45') { |v| v < 45 } }
|
583
|
+
|
584
|
+
context '#call when block returns false' do
|
585
|
+
subject { filter_under_45.call(45) }
|
586
|
+
it { is_expected.to be false }
|
587
|
+
end
|
588
|
+
|
589
|
+
context '#call when block returns true' do
|
590
|
+
subject { filter_under_45.call(44) }
|
591
|
+
it { is_expected.to be true }
|
592
|
+
end
|
593
|
+
|
594
|
+
context '#message' do
|
595
|
+
subject { filter_under_45.message }
|
596
|
+
it { is_expected.to eq 'under 45' }
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
context 'with callable message' do
|
601
|
+
let(:filter_callable_msg) { Results::Filter.new(lambda { |v| "value: #{v}" }) { |v| v } }
|
602
|
+
subject { filter_callable_msg.message.call(1) }
|
603
|
+
it { is_expected.to eq 'value: 1' }
|
604
|
+
end
|
605
|
+
|
606
|
+
context 'with nil message' do
|
607
|
+
subject { lambda { Results::Filter.new(nil) { |v| v } } }
|
608
|
+
it { is_expected.to raise_error(ArgumentError, 'invalid message') }
|
609
|
+
end
|
610
|
+
|
611
|
+
context 'with *no* block' do
|
612
|
+
subject { lambda { Results::Filter.new('dummy') } }
|
613
|
+
it { is_expected.to raise_error(ArgumentError, 'no block given') }
|
614
|
+
end
|
615
|
+
|
616
|
+
end
|
617
|
+
|
618
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: results
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc Siegel
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-05-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rescuer
|
@@ -116,6 +116,7 @@ extensions: []
|
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
118
|
- ".gitignore"
|
119
|
+
- ".rspec"
|
119
120
|
- ".ruby-gemset"
|
120
121
|
- ".ruby-version"
|
121
122
|
- ".travis.yml"
|
@@ -126,7 +127,8 @@ files:
|
|
126
127
|
- lib/results.rb
|
127
128
|
- lib/results/version.rb
|
128
129
|
- results.gemspec
|
129
|
-
- spec/
|
130
|
+
- spec/example_usages_spec.rb
|
131
|
+
- spec/results_unit_spec.rb
|
130
132
|
- spec/spec_helper.rb
|
131
133
|
homepage: http://ms-ati.github.com/results/
|
132
134
|
licenses:
|
data/spec/results_spec.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'results'
|
3
|
-
|
4
|
-
describe Results do
|
5
|
-
|
6
|
-
##
|
7
|
-
# Construct indirectly by wrapping a block which may raise an exception
|
8
|
-
##
|
9
|
-
describe '.new' do
|
10
|
-
end
|
11
|
-
|
12
|
-
##
|
13
|
-
# Construct directly as Good
|
14
|
-
##
|
15
|
-
describe Results::Good do
|
16
|
-
end
|
17
|
-
|
18
|
-
##
|
19
|
-
# Construct directly as Bad
|
20
|
-
##
|
21
|
-
describe Results::Bad do
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|