results 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/results.png)](http://badge.fury.io/rb/results)
|
3
3
|
[![Build Status](https://travis-ci.org/ms-ati/results.png)](https://travis-ci.org/ms-ati/results)
|
4
4
|
[![Dependency Status](https://gemnasium.com/ms-ati/results.png)](https://gemnasium.com/ms-ati/results)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/ms-ati/results.png)](https://codeclimate.com/github/ms-ati/results)
|
6
6
|
[![Coverage Status](https://coveralls.io/repos/ms-ati/results/badge.png)](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
|