camertron-option 1.0.0
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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +82 -0
- data/Rakefile +11 -0
- data/camertron-option.gemspec +20 -0
- data/ci/option.gemspec +20 -0
- data/gem-public_cert.pem +21 -0
- data/lib/option.rb +268 -0
- data/lib/option/version.rb +3 -0
- data/spec/acceptance_spec.rb +33 -0
- data/spec/option_spec.rb +283 -0
- data/spec/spec_helper.rb +23 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 18e340be2a52ace7173a04dbaab005f4254c1b54
|
4
|
+
data.tar.gz: 85d9e7fe331c69594acf0dddca8f86b1a99079e0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84163abd2e4109372bab89dd9426bcb0aba01ca4a533f1d98aa3279c0952aa1e5660583332adeed20ae2121f3421b9097b818b5449609944cf91c65d906d5b8a
|
7
|
+
data.tar.gz: 9dd4ce0407da20c4bff121cc8614deef94bae544b42b6a2110451ebe7533296e606b01bcccb60a151a154fbf84cc4334c83e23351dfe3d8d9513a9cd778bac2e
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Rob Ares
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Option
|
2
|
+
|
3
|
+
A Ruby port of Scala's Option monad. Tries to be faithful
|
4
|
+
but also pragmatic in RE: to duck typing.
|
5
|
+
|
6
|
+
Blog post: http://robares.com/2012/09/16/ruby-port-of-scala-option/
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'option'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install option
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
Generally, you want to use the Option(A) wrapper method to box
|
25
|
+
your value. This will make the right decision as to what your initial
|
26
|
+
value should be:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
foo = Option("bar")
|
30
|
+
```
|
31
|
+
|
32
|
+
This will allow you to now manipulate the value in the box via various means:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
|
36
|
+
# get the value
|
37
|
+
foo.get #=> "bar"
|
38
|
+
|
39
|
+
# return a default if the box is None
|
40
|
+
None.get_or_else { "default" } #=> "default"
|
41
|
+
|
42
|
+
# map the value to another option
|
43
|
+
foo.map { |v| v.upcase } #=> Some("BAR")
|
44
|
+
|
45
|
+
# does the value meet a requirement?
|
46
|
+
foo.exists? { |v| v == "bar" } #=> true
|
47
|
+
|
48
|
+
# return the value or nil depending on the state
|
49
|
+
foo.or_nil #=> "bar"
|
50
|
+
|
51
|
+
# chain values
|
52
|
+
foo.map { |v| v * 2 }.map { |v| v.upcase }.get_or_else { "missing" } #=> BARBAR
|
53
|
+
|
54
|
+
# attempt to extract a value but default if None
|
55
|
+
None.fold(-> { "missing" }) { |v| v.upcase } #=> missing
|
56
|
+
|
57
|
+
# filter values returning an option
|
58
|
+
foo.filter { |v| v == "baz" } #=> None
|
59
|
+
|
60
|
+
# Side-effects
|
61
|
+
foo.inside { |v| v.upcase! } #=> Some("BAR")
|
62
|
+
|
63
|
+
# use in a for loop
|
64
|
+
for value in foo
|
65
|
+
puts value #=> bar
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
## Build Status
|
70
|
+
[](http://travis-ci.org/rares/option)
|
71
|
+
|
72
|
+
## Code Climate
|
73
|
+
[](https://codeclimate.com/github/rares/option)
|
74
|
+
|
75
|
+
## Contributing
|
76
|
+
|
77
|
+
1. Fork it
|
78
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
79
|
+
3. Implement your feature. Patches not considered without accompanying tests.
|
80
|
+
4. Commit your changes (`git commit -am 'Added some feature'`)
|
81
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
82
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/option/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Rob Ares"]
|
6
|
+
gem.email = ["rob.ares@gmail.com"]
|
7
|
+
gem.description = %q{Ruby port of Scala's Option Monad}
|
8
|
+
gem.summary = %q{Option attempts to be faithful to the useful parts of the scala api. We lose the type safety but still is quite useful when dealing with optional values.}
|
9
|
+
gem.homepage = "http://www.github.com/rares/option"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
14
|
+
gem.name = "camertron-option"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Option::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency "rake", "= 0.9.2.2"
|
19
|
+
gem.add_development_dependency "minitest", "= 3.4.0"
|
20
|
+
end
|
data/ci/option.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../../lib/option/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Rob Ares"]
|
6
|
+
gem.email = ["rob.ares@gmail.com"]
|
7
|
+
gem.description = %q{Ruby port of Scala's Option Monad}
|
8
|
+
gem.summary = %q{Option attempts to be faithful to the useful parts of the scala api. We lose the type safety but still is quite useful when dealing with optional values.}
|
9
|
+
gem.homepage = "http://www.github.com/rares/option"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
14
|
+
gem.name = "option"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Option::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency "rake", "= 0.9.2.2"
|
19
|
+
gem.add_development_dependency "minitest", "= 3.4.0"
|
20
|
+
end
|
data/gem-public_cert.pem
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDcDCCAligAwIBAgIBATANBgkqhkiG9w0BAQUFADA/MREwDwYDVQQDDAhyb2Iu
|
3
|
+
YXJlczEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYDY29t
|
4
|
+
MB4XDTE0MDMwMzE1MzYyMloXDTE1MDMwMzE1MzYyMlowPzERMA8GA1UEAwwIcm9i
|
5
|
+
LmFyZXMxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkWA2Nv
|
6
|
+
bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALbltpKu3b5+nvN46Eew
|
7
|
+
IDzCxoi9wbrbt2I9zsuF0i6BxH8It5ce+4dGHpzUTK7Kw6HHesUUL2xePE7aaf4D
|
8
|
+
SfYOLqE/qVam/JqnRBHNWvBBZl/qXtcm4TS8HpO1EXaQZ8uubYnaAKF6Drgv3eUq
|
9
|
+
Y/GZ9XwyVSeRJDKDtjrWLhwi+SJENbWbUIu2HelNCoscQmza9rGdciVz65IDKWxO
|
10
|
+
vA7kzCVhykGxCHb/K2ICslo4+49+zB7owtvrrUWN/tuPX5CkLwNG42B3zGED+dgI
|
11
|
+
Jpm12B3DuTrSn2sd1brj+PiqmR9no1fPDKPLZ9IoOZsL+gUSo2zyCciKSAFmIhUz
|
12
|
+
agMCAwEAAaN3MHUwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFPRT
|
13
|
+
NwXu52FT6zVrl9P0Ils7MbCOMB0GA1UdEQQWMBSBEnJvYi5hcmVzQGdtYWlsLmNv
|
14
|
+
bTAdBgNVHRIEFjAUgRJyb2IuYXJlc0BnbWFpbC5jb20wDQYJKoZIhvcNAQEFBQAD
|
15
|
+
ggEBAHlZQvEnbCVAM+peq/BDA/l4Kxzo7Pf/k1hm76P5xwThW5DDsnA0TN/tj5zN
|
16
|
+
vagQHqC1rjSUyZdpsmirMATm4DpiwyIwxrSxzLHO4kSJNsX3hB9wcSdeiVc7gH1a
|
17
|
+
2rCnN1ZcvSM1lcVkRW3BX4ycyl86MkvHrgP9ytdA1tZgHQM30ZAw6h86si4TOMwJ
|
18
|
+
OIuMBzMI/Ssl3C8DJlN6UlkytPjppqD/3qukepuVOm0gbKqAOlGbDEK/FfSzV0Mr
|
19
|
+
77c8ApML1B7wubSs7fykaPuXZX4VL2Gsus1znAJtqiIpuGw3cupy9mApeesCI6kO
|
20
|
+
290ocpWJbAZG98uB33L0VfcVIGs=
|
21
|
+
-----END CERTIFICATE-----
|
data/lib/option.rb
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
module OptionHelpers
|
2
|
+
class OptionMatcher
|
3
|
+
attr_reader :return_value
|
4
|
+
|
5
|
+
def initialize(option)
|
6
|
+
@option = option
|
7
|
+
@return_value = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def case(type)
|
11
|
+
if type.is_a?(OptionType)
|
12
|
+
if @option.present?
|
13
|
+
option_val = @option.get
|
14
|
+
if option_val.is_a?(type.type)
|
15
|
+
@return_value = yield(option_val)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
elsif type == SomeClass
|
19
|
+
if @option.present?
|
20
|
+
@return_value = yield(@option.get)
|
21
|
+
end
|
22
|
+
elsif type.is_a?(NoneClass)
|
23
|
+
if !@option.present?
|
24
|
+
@return_value = yield(@option)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
raise TypeError, "can't match an Option against a #{type.to_s}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def else
|
32
|
+
@return_value = yield(@option)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class OptionType
|
37
|
+
attr_reader :type
|
38
|
+
|
39
|
+
def initialize(type)
|
40
|
+
@type = type
|
41
|
+
end
|
42
|
+
|
43
|
+
class << self
|
44
|
+
def for_class(klass)
|
45
|
+
case klass
|
46
|
+
when Class
|
47
|
+
option_type_cache[klass] ||= OptionType.new(klass)
|
48
|
+
else
|
49
|
+
raise TypeError, "Must be a Class"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def option_type_cache
|
56
|
+
@option_type_cache ||= {}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class OptionClass
|
63
|
+
|
64
|
+
def or_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
class << self
|
68
|
+
def [](type)
|
69
|
+
OptionHelpers::OptionType.for_class(type)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def ==(that)
|
74
|
+
case that
|
75
|
+
when OptionClass then or_nil == that.or_nil
|
76
|
+
else or_nil == that
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def match
|
81
|
+
matcher = OptionHelpers::OptionMatcher.new(self)
|
82
|
+
yield matcher
|
83
|
+
matcher.return_value
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def assert_option(result)
|
89
|
+
case result
|
90
|
+
when OptionClass then return result
|
91
|
+
else raise TypeError, "Must be an Option"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class SomeClass < OptionClass
|
97
|
+
|
98
|
+
def initialize(value)
|
99
|
+
@value = value
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_a
|
103
|
+
[get]
|
104
|
+
end
|
105
|
+
|
106
|
+
def get
|
107
|
+
@value
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_or_else(&blk)
|
111
|
+
get
|
112
|
+
end
|
113
|
+
|
114
|
+
def each(&blk)
|
115
|
+
blk.call(get)
|
116
|
+
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def or_nil
|
121
|
+
get
|
122
|
+
end
|
123
|
+
|
124
|
+
def present?
|
125
|
+
true
|
126
|
+
end
|
127
|
+
|
128
|
+
def empty?
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
def map(&blk)
|
133
|
+
self.class.new(blk.call(get))
|
134
|
+
end
|
135
|
+
|
136
|
+
def flat_map(&blk)
|
137
|
+
assert_option(blk.call(get))
|
138
|
+
end
|
139
|
+
|
140
|
+
def fold(if_empty, &blk)
|
141
|
+
blk.call(get)
|
142
|
+
end
|
143
|
+
|
144
|
+
def exists?(&blk)
|
145
|
+
!! blk.call(get)
|
146
|
+
end
|
147
|
+
|
148
|
+
def include?(value)
|
149
|
+
get == value
|
150
|
+
end
|
151
|
+
|
152
|
+
def filter(&blk)
|
153
|
+
exists?(&blk) ? self : None
|
154
|
+
end
|
155
|
+
|
156
|
+
def inside(&blk)
|
157
|
+
blk.call(get)
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
def or_else(&blk)
|
162
|
+
self
|
163
|
+
end
|
164
|
+
|
165
|
+
def flatten
|
166
|
+
case get
|
167
|
+
when OptionClass then get.flatten
|
168
|
+
else self
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def error(*argv)
|
173
|
+
self
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class NoneClass < OptionClass
|
178
|
+
def [](type)
|
179
|
+
raise TypeError, "can't specify a type of NoneClass"
|
180
|
+
end
|
181
|
+
|
182
|
+
def dup
|
183
|
+
raise TypeError, "can't dup NoneClass"
|
184
|
+
end
|
185
|
+
|
186
|
+
def clone
|
187
|
+
raise TypeError, "can't clone NoneClass"
|
188
|
+
end
|
189
|
+
|
190
|
+
def to_a
|
191
|
+
[]
|
192
|
+
end
|
193
|
+
|
194
|
+
def get
|
195
|
+
raise IndexError, "None.get"
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_or_else(&blk)
|
199
|
+
blk.call
|
200
|
+
end
|
201
|
+
|
202
|
+
def each(&blk)
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
|
206
|
+
def or_nil
|
207
|
+
nil
|
208
|
+
end
|
209
|
+
|
210
|
+
def present?
|
211
|
+
false
|
212
|
+
end
|
213
|
+
|
214
|
+
def empty?
|
215
|
+
true
|
216
|
+
end
|
217
|
+
|
218
|
+
def map(&blk)
|
219
|
+
flat_map(&blk)
|
220
|
+
end
|
221
|
+
|
222
|
+
def flat_map(&blk)
|
223
|
+
self
|
224
|
+
end
|
225
|
+
|
226
|
+
def fold(if_empty, &blk)
|
227
|
+
if_empty.call
|
228
|
+
end
|
229
|
+
|
230
|
+
def exists?(&blk)
|
231
|
+
false
|
232
|
+
end
|
233
|
+
|
234
|
+
def include?(value)
|
235
|
+
false
|
236
|
+
end
|
237
|
+
|
238
|
+
def filter(&blk)
|
239
|
+
self
|
240
|
+
end
|
241
|
+
|
242
|
+
def inside(&blk)
|
243
|
+
self
|
244
|
+
end
|
245
|
+
|
246
|
+
def or_else(&blk)
|
247
|
+
assert_option(blk.call)
|
248
|
+
end
|
249
|
+
|
250
|
+
def flatten
|
251
|
+
self
|
252
|
+
end
|
253
|
+
|
254
|
+
def error(*argv)
|
255
|
+
argv.empty? ? raise : raise(*argv)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
None = NoneClass.new
|
260
|
+
Some = SomeClass
|
261
|
+
|
262
|
+
def Some(value)
|
263
|
+
SomeClass.new(value)
|
264
|
+
end
|
265
|
+
|
266
|
+
def Option(value=nil)
|
267
|
+
value.nil? ? None : Some(value)
|
268
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
some = Some("test")
|
4
|
+
upcase = lambda { |v| Some(v.upcase) }
|
5
|
+
empty = lambda { |v| None }
|
6
|
+
|
7
|
+
describe "Option" do
|
8
|
+
|
9
|
+
describe "when applying the 3 monadic axioms" do
|
10
|
+
|
11
|
+
describe "left identity" do
|
12
|
+
|
13
|
+
it "obeys (return x) >>= f == f x" do
|
14
|
+
some.flat_map(&upcase).must_equal(upcase.call(some.get))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "right identity" do
|
19
|
+
|
20
|
+
it "obeys m >>= return == m" do
|
21
|
+
some.flat_map { |v| Some(v) }.must_equal(some)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "associative composition" do
|
26
|
+
|
27
|
+
it "obeys (m >>= f) >>= g == m >>= (\\x -> f x >>= g)" do
|
28
|
+
some.flat_map(&upcase).flat_map(&empty).
|
29
|
+
must_equal(upcase.call(some.get).flat_map(&empty))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/spec/option_spec.rb
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe NoneClass do
|
4
|
+
|
5
|
+
it "#dup results in a TypeError" do
|
6
|
+
lambda { None.dup }.must_raise TypeError
|
7
|
+
end
|
8
|
+
|
9
|
+
it "#clone results in a TypeError" do
|
10
|
+
lambda { None.clone }.must_raise TypeError
|
11
|
+
end
|
12
|
+
|
13
|
+
it "#to_a returns an empty array" do
|
14
|
+
None.to_a.must_equal([])
|
15
|
+
end
|
16
|
+
|
17
|
+
it "#get raises IndexError" do
|
18
|
+
lambda { None.get }.must_raise IndexError
|
19
|
+
end
|
20
|
+
|
21
|
+
it "#get_or_else executes the block" do
|
22
|
+
None.get_or_else { "Some" }.must_equal "Some"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "#each does not execute the block" do
|
26
|
+
expected = nil
|
27
|
+
None.each { |v| expected = v }
|
28
|
+
|
29
|
+
expected.must_be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "#or_nil should return nil" do
|
33
|
+
None.or_nil.must_be_nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "#present? should be false" do
|
37
|
+
None.present?.must_equal(false)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "#empty? should be true" do
|
41
|
+
None.empty?.must_equal(true)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "#map should return itself" do
|
45
|
+
None.map {}.must_be_none
|
46
|
+
end
|
47
|
+
|
48
|
+
it "#flat_map should return itself" do
|
49
|
+
None.flat_map {}.must_be_none
|
50
|
+
end
|
51
|
+
|
52
|
+
it "#exists? should return false" do
|
53
|
+
None.exists? {}.must_equal(false)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "#include? should return false" do
|
57
|
+
None.include?(value).must_equal(false)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "#fold should invoke the default proc" do
|
61
|
+
None.fold(proc { value }) { |v| v.to_f }.must_equal(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "#filter with a true predicate returns itself" do
|
65
|
+
Option(value).filter { |i| i == 12 }.must_be_some(value)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "#filter with a false predicate returns None" do
|
69
|
+
Option(value).filter { |i| i == 1 }.must_be_none
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should be aliased to None" do
|
73
|
+
None.must_be_instance_of(NoneClass)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "#inside should return itself without invoking the block" do
|
77
|
+
expected = nil
|
78
|
+
None.inside { |v| expected = value }
|
79
|
+
expected.must_be_nil
|
80
|
+
end
|
81
|
+
|
82
|
+
it "#or_else should invoke the block and return an Option" do
|
83
|
+
None.or_else { Some(value) }.must_be_some(value)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "#or_else should raise a TypeError if an Option is not returned" do
|
87
|
+
lambda { None.or_else { value } }.must_raise TypeError
|
88
|
+
end
|
89
|
+
|
90
|
+
it "#flatten should return itself" do
|
91
|
+
None.flatten.must_be_none
|
92
|
+
end
|
93
|
+
|
94
|
+
it "#error should raise a RuntimeError with the given message" do
|
95
|
+
lambda { None.error("error") }.must_raise RuntimeError, "error"
|
96
|
+
end
|
97
|
+
|
98
|
+
it "#error should raise the error passed to it" do
|
99
|
+
lambda { None.error(ArgumentError.new("name")) }.must_raise ArgumentError, "name"
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should assemble an Error from the arguments passed in" do
|
103
|
+
lambda { None.error(StandardError, "this is a problem") }.must_raise StandardError, "this is a problem"
|
104
|
+
end
|
105
|
+
|
106
|
+
it "cannot be typed" do
|
107
|
+
lambda { None[String] }.must_raise TypeError, "can't specify a type of NoneClass"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe SomeClass do
|
112
|
+
|
113
|
+
it "#to_a returns the value wrapped in an array" do
|
114
|
+
Some(value).to_a.must_equal([value])
|
115
|
+
end
|
116
|
+
|
117
|
+
it "#get returns the inner value" do
|
118
|
+
Some(value).get.must_equal(value)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "#get_or_else does not execute the block;" do
|
122
|
+
expected = nil
|
123
|
+
Some(value).get_or_else { expected = true }
|
124
|
+
|
125
|
+
expected.must_be_nil
|
126
|
+
end
|
127
|
+
|
128
|
+
it "#get_or_else returns the value" do
|
129
|
+
Some(value).get_or_else { }.must_equal(value)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "#each executes the block passing the inner value" do
|
133
|
+
expected = nil
|
134
|
+
Some(value).each { |v| expected = v }
|
135
|
+
|
136
|
+
expected.must_equal(value)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "#or_nil should return the inner value" do
|
140
|
+
Some(value).or_nil.must_equal(value)
|
141
|
+
end
|
142
|
+
|
143
|
+
it "#present? should be true" do
|
144
|
+
Some(value).present?.must_equal(true)
|
145
|
+
end
|
146
|
+
|
147
|
+
it "#empty? should be false" do
|
148
|
+
Some(value).empty?.must_equal(false)
|
149
|
+
end
|
150
|
+
|
151
|
+
it "#map should return the result of the proc over the value in a Some" do
|
152
|
+
Some(value).map { |v| v * 2 }.must_be_some(24)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "#flat_map should raise TypeError if the returned value is not an Option" do
|
156
|
+
lambda { Some(value).flat_map { |v| v * 2 } }.must_raise TypeError
|
157
|
+
end
|
158
|
+
|
159
|
+
it "#flat_map should return an Option value from the block" do
|
160
|
+
Some(value).flat_map { |v| Option(v * 2) }.must_be_some(24)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "#flat_map can return None from the block" do
|
164
|
+
Some(value).flat_map { |_| None }.must_be_none
|
165
|
+
end
|
166
|
+
|
167
|
+
it "#exists? should return true when the block evaluates true" do
|
168
|
+
Some(value).exists? { |v| v % 2 == 0 }.must_equal(true)
|
169
|
+
end
|
170
|
+
|
171
|
+
it "#exists? should return false when the block evaluates false" do
|
172
|
+
Some(value).exists? { |v| v % 2 != 0 }.must_equal(false)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "#include? should return true when the passed value and the boxed value are the same" do
|
176
|
+
Some(value).include?(value).must_equal(true)
|
177
|
+
end
|
178
|
+
|
179
|
+
it "#include? should return false when the passed value and the boxed value are not the same" do
|
180
|
+
Some(value).include?(value + 1).must_equal(false)
|
181
|
+
end
|
182
|
+
|
183
|
+
it "#fold should map the proc over the value and return it" do
|
184
|
+
Some(value).fold(proc { value * 2 }) { |v| v * 3 }.must_equal(36)
|
185
|
+
end
|
186
|
+
|
187
|
+
it "#filter should return itself" do
|
188
|
+
None.filter { |i| i == 0 }.must_be_none
|
189
|
+
end
|
190
|
+
|
191
|
+
it "#inside should invoke the proc and return itself" do
|
192
|
+
expected = nil
|
193
|
+
Some(value).inside { |v| expected = v }.must_be_some(value)
|
194
|
+
expected.must_equal(value)
|
195
|
+
end
|
196
|
+
|
197
|
+
it "#or_else should return itself" do
|
198
|
+
Some(value).or_else { None }.must_be_some(value)
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should wrap the creation of a Some" do
|
202
|
+
Some(value).must_be_instance_of(SomeClass)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should be aliased to Some" do
|
206
|
+
Some.new(value).must_be_some(value)
|
207
|
+
end
|
208
|
+
|
209
|
+
it "#flatten" do
|
210
|
+
inner_value = Some(Some(Some(value))).flatten
|
211
|
+
inner_value.must_be_some(value)
|
212
|
+
inner_value.or_nil.must_equal(value)
|
213
|
+
end
|
214
|
+
|
215
|
+
it "#error should return the Some" do
|
216
|
+
value = !!(Some(value).error("error") rescue false)
|
217
|
+
value.must_equal true
|
218
|
+
end
|
219
|
+
|
220
|
+
it "can be typed" do
|
221
|
+
opt = Some[String]
|
222
|
+
opt.class.must_equal OptionHelpers::OptionType
|
223
|
+
opt.type.must_equal String
|
224
|
+
end
|
225
|
+
|
226
|
+
it "must be typed using a class, not a value" do
|
227
|
+
lambda { Some[5] }.must_raise TypeError, "Must be a Class"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe OptionClass do
|
232
|
+
|
233
|
+
it "must return a some if the passed value is not nil" do
|
234
|
+
Option(value).must_be_some(value)
|
235
|
+
end
|
236
|
+
|
237
|
+
it "must return a None if the passed value is nil" do
|
238
|
+
Option(nil).must_be_none
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should do equality checks against the boxed value" do
|
242
|
+
Option(value).must_equal(value)
|
243
|
+
end
|
244
|
+
|
245
|
+
it "allows matching typed options" do
|
246
|
+
Option(5).match do |matcher|
|
247
|
+
matcher.case(Some[Fixnum]) { |val| val * 2 }
|
248
|
+
matcher.case(Some[String]) { |val| "bar" }
|
249
|
+
end.must_equal 10
|
250
|
+
|
251
|
+
Option("foo").match do |matcher|
|
252
|
+
matcher.case(Some[Fixnum]) { |val| val * 2 }
|
253
|
+
matcher.case(Some[String]) { |val| "bar" }
|
254
|
+
end.must_equal "bar"
|
255
|
+
end
|
256
|
+
|
257
|
+
it "allows matching untyped options" do
|
258
|
+
Option(5).match do |matcher|
|
259
|
+
matcher.case(Some) { |val| val * 2 }
|
260
|
+
end.must_equal 10
|
261
|
+
end
|
262
|
+
|
263
|
+
it "allows matching on none" do
|
264
|
+
None.match do |matcher|
|
265
|
+
matcher.case(None) { "none" }
|
266
|
+
end.must_equal "none"
|
267
|
+
end
|
268
|
+
|
269
|
+
it "allows matching with an else block" do
|
270
|
+
Option(5).match do |matcher|
|
271
|
+
matcher.case(Some[String]) { |val| "bar" }
|
272
|
+
matcher.else { |val| val.get * 2}
|
273
|
+
end.must_equal 10
|
274
|
+
end
|
275
|
+
|
276
|
+
it "does not allow matching against non-option types" do
|
277
|
+
lambda do
|
278
|
+
Option(5).match do |matcher|
|
279
|
+
matcher.case(Fixnum) { 1 }
|
280
|
+
end
|
281
|
+
end.must_raise TypeError, "can't match an Option against a Fixnum"
|
282
|
+
end
|
283
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
require "minitest/pride"
|
3
|
+
require "minitest/spec"
|
4
|
+
|
5
|
+
require "option"
|
6
|
+
|
7
|
+
module MiniTest::Assertions
|
8
|
+
|
9
|
+
def assert_some(value, option, msg = nil)
|
10
|
+
assert (option.is_a?(Some) && option.or_nil == value), "Expected Some(#{value})"
|
11
|
+
end
|
12
|
+
|
13
|
+
def assert_none(value, option, msg = nil)
|
14
|
+
assert option == None, "Expected None"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
OptionClass.infect_an_assertion :assert_some, :must_be_some
|
19
|
+
OptionClass.infect_an_assertion :assert_none, :must_be_none
|
20
|
+
|
21
|
+
def value
|
22
|
+
12
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: camertron-option
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rob Ares
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.2.2
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.9.2.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.4.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.4.0
|
41
|
+
description: Ruby port of Scala's Option Monad
|
42
|
+
email:
|
43
|
+
- rob.ares@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- .travis.yml
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- camertron-option.gemspec
|
55
|
+
- ci/option.gemspec
|
56
|
+
- gem-public_cert.pem
|
57
|
+
- lib/option.rb
|
58
|
+
- lib/option/version.rb
|
59
|
+
- spec/acceptance_spec.rb
|
60
|
+
- spec/option_spec.rb
|
61
|
+
- spec/spec_helper.rb
|
62
|
+
homepage: http://www.github.com/rares/option
|
63
|
+
licenses: []
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 2.1.11
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: Option attempts to be faithful to the useful parts of the scala api. We lose
|
85
|
+
the type safety but still is quite useful when dealing with optional values.
|
86
|
+
test_files:
|
87
|
+
- spec/acceptance_spec.rb
|
88
|
+
- spec/option_spec.rb
|
89
|
+
- spec/spec_helper.rb
|