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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 33039153c74e13756f5a15c64d552fc64e1f82be
4
- data.tar.gz: d169c2ae7b34c76a40560b88a6cbd90a76615baf
3
+ metadata.gz: a7f5a6ff736c00051a79b53e0b228db6b051d37e
4
+ data.tar.gz: 7d0e3982b3422b2f9dd1d09918472019874136b9
5
5
  SHA512:
6
- metadata.gz: 4ef21d48dd71bbabfd3aac219eebd0b217a526606e59125e3939500c678344f8e1356d8b961a9c384ee5da41a654b50fa71b073b4620496c3a5743b33a902d3b
7
- data.tar.gz: 200415d4e0fd09f67ff95e3b8c58b2ebd26cb502fc2117e65fa852ff381f5aaed3275782115bd586e48fe553ab6f398c0f235efad2933babb31e3f158c821a1e
6
+ metadata.gz: 9582877f5ad6ef1716468e1f44f5c71195ec05622ade4ac061789f5da7e23dfe95b2583f2fa60d5ef85937d881797ef4ae8cf1f8e00e918a214bfa166f985b73
7
+ data.tar.gz: 53de975391c145a5dd6d22252a667ed87bbec00abc99e594ff292e4a712332c79669e923fff256bb676ef73d3ac53bcb7283a27347dd103ad23351148c84fb76
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/README.md CHANGED
@@ -1,22 +1,215 @@
1
- = Results =
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 inspired by the [ScalaUtils][1] [Or and Every][1] classes.
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
- # Coming soon...
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
- More coming soon...
212
+ NOTE: this section is under construction...
20
213
 
21
214
  ## TODO
22
215
 
@@ -1,4 +1,236 @@
1
+ require 'rescuer'
2
+
1
3
  module Results
2
- Good = Struct.new(:value)
3
- Bad = Struct.new(:error)
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
@@ -1,4 +1,4 @@
1
1
  module Results
2
2
  # The current version of this gem
3
- VERSION = '0.0.1'
3
+ VERSION = '0.0.2'
4
4
  end
@@ -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.1
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-04-22 00:00:00.000000000 Z
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/results_spec.rb
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:
@@ -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