results 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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