monadic 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +29 -3
- data/README.md +42 -22
- data/lib/monadic/either.rb +3 -1
- data/lib/monadic/maybe.rb +23 -6
- data/lib/monadic/try.rb +7 -3
- data/lib/monadic/version.rb +1 -1
- data/spec/either_spec.rb +12 -0
- data/spec/examples/builder_spec.rb +64 -0
- data/spec/examples/hotel_booking_spec.rb +52 -59
- data/spec/maybe_spec.rb +21 -6
- data/spec/try_spec.rb +14 -3
- metadata +6 -4
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.6 (unreleased)
|
4
|
+
|
5
|
+
Extend `Try` to also support lambdas.
|
6
|
+
|
7
|
+
The first parameter of `Try` can be used as a predicate and the block as formatter
|
8
|
+
|
9
|
+
Try(true) == Success(true)
|
10
|
+
Try(false) { "string" } == Failure("string")
|
11
|
+
Try(false) { "success" }.else("fail") == Failure("fail")
|
12
|
+
|
13
|
+
`Maybe` supports `#proxy` to avoid naming clashes between the underlying value and `Maybe` itself.
|
14
|
+
|
15
|
+
Maybe({a: 1}).proxy.fetch(:a) == Maybe(1)
|
16
|
+
# this is in effect syntactic sugar for
|
17
|
+
Maybe({a: 1}).map {|e| e.fetch(:a) }
|
18
|
+
|
19
|
+
`Nothing#or` coerces the or value into a Maybe, thus
|
20
|
+
|
21
|
+
Maybe(nil).or(1) == Just(1)
|
22
|
+
Maybe(nil).or(nil) == Nothing
|
23
|
+
|
24
|
+
`Either` coerces now `Just` and `Nothing` into `Success` and `Failure`
|
25
|
+
|
26
|
+
Either(Just.new(1)) == Success(1)
|
27
|
+
Either(Nothing) == Failure(Nothing)
|
28
|
+
|
3
29
|
## v0.5.0
|
4
30
|
|
5
31
|
Add the `Try` helper which works similar to Either, but takes a block
|
@@ -38,7 +64,7 @@ Instead the new `Monad#flat_map` function operates on the underlying value as on
|
|
38
64
|
## v0.1.1
|
39
65
|
|
40
66
|
`Either()` coerces only `StandardError` to `Failure`, other exceptions higher in the hierarchy are will break the flow.
|
41
|
-
Thanks to @pithyless for the suggestion.
|
67
|
+
Thanks to @pithyless for the suggestion.
|
42
68
|
|
43
69
|
Monad is now a Module instead of a Class as previously. This fits the Monad metaphor better. Each monad must now implement the unit method itself, which is the correct way to do anyway.
|
44
70
|
|
@@ -60,7 +86,7 @@ See [examples/validation.rb](https://github.com/pzol/monadic/blob/master/example
|
|
60
86
|
and [examples/validation_module](https://github.com/pzol/monadic/blob/master/examples/validation_module.rb)
|
61
87
|
|
62
88
|
|
63
|
-
## v0.0.7
|
89
|
+
## v0.0.7
|
64
90
|
|
65
91
|
Implements the #map method for all Monads. It works on value types and on Enumerable collections.
|
66
92
|
Provide a proc or a block and it will return a transformed value or collection boxed back in the monad.
|
@@ -88,7 +114,7 @@ Removed `Maybe#fetch` call with block`
|
|
88
114
|
Added Travis-Ci integration, [![Build Status](https://secure.travis-ci.org/pzol/monadic.png?branch=master)](http://travis-ci.org/pzol/monadic)
|
89
115
|
|
90
116
|
|
91
|
-
## v0.0.5
|
117
|
+
## v0.0.5
|
92
118
|
|
93
119
|
Removed the `#chain` method alias for bind in `Either`.
|
94
120
|
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in [Scala](http://www.scala-lang.org/) and [Haskell](http://www.haskell.org/) to my ruby projects.
|
5
5
|
|
6
|
-
My motivation to create this gem was that I often work with nested Hashes and need to reach deeply inside of them so my code is sprinkled with things like some_hash.fetch(:one, {}).fetch(:two, {}).fetch(:three, "unknown").
|
6
|
+
My motivation to create this gem was that I often work with nested Hashes and need to reach deeply inside of them so my code is sprinkled with things like some_hash.fetch(:one, {}).fetch(:two, {}).fetch(:three, "unknown").
|
7
7
|
|
8
8
|
We have the following monadics (monads, functors, applicatives and variations):
|
9
9
|
|
@@ -21,11 +21,11 @@ A monad is most effectively described as a computation that eventually returns a
|
|
21
21
|
### Maybe
|
22
22
|
Most people probably will be interested in the Maybe monad, as it solves the problem with nil invocations, similar to [andand](https://github.com/raganwald/andand) and others.
|
23
23
|
|
24
|
-
Maybe is an optional type, which helps to handle error conditions gracefully. The one thing to remember about option is: 'What goes into the Maybe, stays in the Maybe'.
|
24
|
+
Maybe is an optional type, which helps to handle error conditions gracefully. The one thing to remember about option is: 'What goes into the Maybe, stays in the Maybe'.
|
25
25
|
|
26
26
|
|
27
27
|
```ruby
|
28
|
-
Maybe(User.find(123)).name._ # ._ is a shortcut for .fetch
|
28
|
+
Maybe(User.find(123)).name._ # ._ is a shortcut for .fetch
|
29
29
|
|
30
30
|
# if you prefer the alias Maybe instead of option
|
31
31
|
Maybe(User.find(123)).name._
|
@@ -52,7 +52,7 @@ Maybe(nil).truly? == false
|
|
52
52
|
# Just stays Just, unless you unbox it
|
53
53
|
Maybe('FOO').downcase == Just('foo')
|
54
54
|
Maybe('FOO').downcase.fetch == "foo" # unboxing the value
|
55
|
-
Maybe('FOO').downcase._ == "foo"
|
55
|
+
Maybe('FOO').downcase._ == "foo"
|
56
56
|
Maybe('foo').empty? == false # always non-empty
|
57
57
|
Maybe('foo').truly? == true # depends on the boxed value
|
58
58
|
Maybe(false).empty? == false
|
@@ -60,8 +60,8 @@ Maybe(false).truly? == false
|
|
60
60
|
```
|
61
61
|
|
62
62
|
Map, select:
|
63
|
-
|
64
|
-
```ruby
|
63
|
+
|
64
|
+
```ruby
|
65
65
|
Maybe(123).map { |value| User.find(value) } == Just(someUser) # if user found
|
66
66
|
Maybe(0).map { |value| User.find(value) } == Nothing # if user not found
|
67
67
|
Maybe([1,2]).map { |value| value.to_s } == Just(["1, 2"]) # for all Enumerables
|
@@ -90,9 +90,10 @@ Maybe(1).to_s == '1'
|
|
90
90
|
```
|
91
91
|
|
92
92
|
`#or`
|
93
|
-
Maybe(nil).or(1) == 1
|
94
|
-
Maybe(1).or(2) == 1
|
95
|
-
Maybe(nil).or(
|
93
|
+
Maybe(nil).or(1) == Just(1)
|
94
|
+
Maybe(1).or(2) == Just(1)
|
95
|
+
Maybe(nil).or(1) == Just(1)
|
96
|
+
Maybe(nil).or(nil) == Nothing
|
96
97
|
|
97
98
|
Falsey values (kind-of) examples:
|
98
99
|
|
@@ -110,6 +111,14 @@ Remember! a Maybe is never false (in Ruby terms), if you want to know if it is f
|
|
110
111
|
|
111
112
|
`#truly?` will return true or false, always.
|
112
113
|
|
114
|
+
`Maybe` supports `#proxy` to avoid naming clashes between the underlying value and `Maybe` itself.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
Maybe({a: 1}).proxy.fetch(:a) == Maybe(1)
|
118
|
+
# this is in effect syntactic sugar for
|
119
|
+
Maybe({a: 1}).map {|e| e.fetch(:a) }
|
120
|
+
```
|
121
|
+
|
113
122
|
Slug example
|
114
123
|
|
115
124
|
```ruby
|
@@ -158,11 +167,11 @@ The `Either()` wrapper works like a coercon. It will treat all falsey values `ni
|
|
158
167
|
```ruby
|
159
168
|
result = parse_and_validate_params(params). # must return a Success or Failure inside
|
160
169
|
bind ->(user_id) { User.find(user_id) }. # if #find returns null it will become a Failure
|
161
|
-
bind ->(user) { authorized?(user); user }. # if authorized? raises an Exception, it will be a Failure
|
170
|
+
bind ->(user) { authorized?(user); user }. # if authorized? raises an Exception, it will be a Failure
|
162
171
|
bind ->(user) { UserDecorator(user) }
|
163
172
|
|
164
173
|
if result.success?
|
165
|
-
@user = result.fetch # result.fetch or result._ contains the
|
174
|
+
@user = result.fetch # result.fetch or result._ contains the inner value
|
166
175
|
render 'page'
|
167
176
|
else
|
168
177
|
@error = result.fetch
|
@@ -205,7 +214,7 @@ Another example:
|
|
205
214
|
```ruby
|
206
215
|
Success(params).
|
207
216
|
bind ->(params) { Either(params.fetch(:path)) } # fails if params does not contain :path
|
208
|
-
bind ->(path) { load_stuff(params) } #
|
217
|
+
bind ->(path) { load_stuff(params) } #
|
209
218
|
```
|
210
219
|
|
211
220
|
`Either#else` allows to provide alternate values in case of `Failure`:
|
@@ -231,18 +240,29 @@ result = Either.chain do
|
|
231
240
|
end
|
232
241
|
|
233
242
|
result == Success(101)
|
234
|
-
```
|
243
|
+
```
|
235
244
|
|
236
245
|
#### Try
|
237
246
|
|
238
|
-
`Try` helper which works similar to Either, but takes a block
|
247
|
+
`Try` helper which works similar to Either, but takes a block. Think of it as a secure if-then-else.
|
239
248
|
|
240
249
|
```ruby
|
241
250
|
Try { Date.parse('2012-02-30') } == Failure
|
242
251
|
Try { Date.parse('2012-02-28') } == Success
|
243
|
-
Try { Date.parse('2012-02-30') }.else {|e| "Exception: #{e.message}" } == Failure("Exception: invalid date")
|
244
|
-
```
|
245
252
|
|
253
|
+
date_s = '2012-02-30'
|
254
|
+
Try { Date.parse(date_s) }.else {|e| "#{e.message} #{date_s}" } == Failure("invalid date 2012-02-30")
|
255
|
+
|
256
|
+
# with a predicate
|
257
|
+
Try(true) == Success(true)
|
258
|
+
Try(false) { "string" } == Failure("string")
|
259
|
+
Try(false) { "success"}.else("fail") == Failure("fail")
|
260
|
+
|
261
|
+
VALID_TITLES = %w[MR MRS]
|
262
|
+
title = 'MS'
|
263
|
+
Try(VALID_TITLES.inlude?(title)) { title }.else { "title must be on of '#{VALID_TITLES.join(', ')}'' but was '#{title}'"}
|
264
|
+
== "title must be on of 'MR, MR' but was 'MS'"
|
265
|
+
```
|
246
266
|
|
247
267
|
### Validation
|
248
268
|
The Validation applicative functor, takes a list of checks within a block. Each check must return either Success of Failure.
|
@@ -267,13 +287,13 @@ def validate(person)
|
|
267
287
|
when :sober, :tipsy; Success(sobriety)
|
268
288
|
when :drunk ; Failure('No drunks allowed')
|
269
289
|
else Failure("Sobriety state '#{sobriety}' is not allowed")
|
270
|
-
end
|
290
|
+
end
|
271
291
|
}
|
272
292
|
|
273
293
|
check_gender = ->(gender) {
|
274
294
|
gender == :male || gender == :female ? Success(gender) : Failure("Invalid gender #{gender}")
|
275
295
|
}
|
276
|
-
|
296
|
+
|
277
297
|
Validation() do
|
278
298
|
check { check_age.(person.age); }
|
279
299
|
check { check_sobriety.(person.sobriety) }
|
@@ -306,8 +326,8 @@ Identity.unit([1,2]).map {|v| v + 1} == Identity([2,
|
|
306
326
|
|
307
327
|
__#bind__ allows (priviledged) access to the boxed value. This is the traditional _no-magic_ `#bind` as found in Haskell,
|
308
328
|
You` are responsible for re-wrapping the value into a Monad again.
|
309
|
-
|
310
|
-
```ruby
|
329
|
+
|
330
|
+
```ruby
|
311
331
|
# due to the way it works, it will simply return the value, don't rely on this though, different Monads may
|
312
332
|
# implement bind differently (e.g. Maybe involves some _magic_)
|
313
333
|
Identity.unit('foo').bind(&:capitalize) == Foo
|
@@ -356,8 +376,8 @@ Or install it yourself as:
|
|
356
376
|
$ gem install monadic
|
357
377
|
|
358
378
|
## Compatibility
|
359
|
-
Monadic is tested under ruby MRI 1.9.2, 1.9.3
|
360
|
-
|
379
|
+
Monadic is tested under ruby MRI 1.9.2, 1.9.3
|
380
|
+
jruby 1.9 mode, rbx 1.9 mode are currently not passing the tests on travis
|
361
381
|
|
362
382
|
## Contributing
|
363
383
|
|
data/lib/monadic/either.rb
CHANGED
@@ -8,7 +8,9 @@ module Monadic
|
|
8
8
|
|
9
9
|
def self.unit(value)
|
10
10
|
return value if value.is_a? Either
|
11
|
-
return
|
11
|
+
return Nothing if value.is_a? Nothing
|
12
|
+
return Success.new(value.fetch) if value.is_a? Just
|
13
|
+
return Failure.new(value) if value.nil? || (value.respond_to?(:empty?) && value.empty?) || !value
|
12
14
|
return Success.new(value)
|
13
15
|
end
|
14
16
|
|
data/lib/monadic/maybe.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
module Monadic
|
2
|
-
class Maybe
|
2
|
+
class Maybe
|
3
3
|
include Monadic::Monad
|
4
|
-
|
4
|
+
|
5
5
|
def self.unit(value)
|
6
6
|
return Nothing if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
7
7
|
return Just.new(value)
|
8
8
|
end
|
9
9
|
|
10
|
-
# Initialize is private, because it always would return an instance of Maybe, but Just or Nothing
|
10
|
+
# Initialize is private, because it always would return an instance of Maybe, but Just or Nothing
|
11
11
|
# are required (Maybe is abstract).
|
12
12
|
private_class_method :new
|
13
13
|
|
@@ -44,6 +44,20 @@ module Monadic
|
|
44
44
|
Maybe(@value.__send__(m, *args))
|
45
45
|
end
|
46
46
|
|
47
|
+
class Proxy < BasicObject
|
48
|
+
def initialize(maybe)
|
49
|
+
@maybe = maybe
|
50
|
+
end
|
51
|
+
|
52
|
+
def method_missing(m, *args)
|
53
|
+
@maybe.map { |e| e.__send__(m, *args) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def proxy
|
58
|
+
@proxy ||= Proxy.new(self)
|
59
|
+
end
|
60
|
+
|
47
61
|
# @return always self for Just
|
48
62
|
def or(other)
|
49
63
|
self
|
@@ -66,13 +80,12 @@ module Monadic
|
|
66
80
|
self
|
67
81
|
end
|
68
82
|
|
69
|
-
# @return an alternative value, the passed value is
|
83
|
+
# @return an alternative value, the passed value is coerced into Maybe, thus Nothing.or(1) will be Just(1)
|
70
84
|
def or(other)
|
71
|
-
|
85
|
+
Maybe.unit(other)
|
72
86
|
end
|
73
87
|
|
74
88
|
# def respond_to?
|
75
|
-
|
76
89
|
# end
|
77
90
|
|
78
91
|
def to_ary
|
@@ -87,6 +100,10 @@ module Monadic
|
|
87
100
|
def truly?
|
88
101
|
false
|
89
102
|
end
|
103
|
+
|
104
|
+
def empty?
|
105
|
+
true
|
106
|
+
end
|
90
107
|
end
|
91
108
|
end
|
92
109
|
|
data/lib/monadic/try.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
module Monadic
|
2
|
-
|
2
|
+
## Wrap a block or a predicate to always return Success or Failure.
|
3
|
+
## It will catch StandardError and return as a Failure.
|
4
|
+
def Try(arg = nil, &block)
|
3
5
|
begin
|
4
|
-
|
5
|
-
return Either(
|
6
|
+
predicate = arg.is_a?(Proc) ? arg.call : arg
|
7
|
+
return Either(block.call) if block_given? && predicate.nil?
|
8
|
+
return Either(predicate).class.unit(block.call) if block_given? # use predicate for Success/Failure and block for value
|
9
|
+
return Either(predicate)
|
6
10
|
rescue => error
|
7
11
|
Failure(error)
|
8
12
|
end
|
data/lib/monadic/version.rb
CHANGED
data/spec/either_spec.rb
CHANGED
@@ -39,6 +39,10 @@ describe Monadic::Either do
|
|
39
39
|
Failure(1).should_not == Failure(2)
|
40
40
|
end
|
41
41
|
|
42
|
+
it 'Either(Nothing) is a Failure' do
|
43
|
+
Either(Nothing).should == Failure(Nothing)
|
44
|
+
end
|
45
|
+
|
42
46
|
it 'wraps a nil result to Failure' do
|
43
47
|
Success(nil).bind { nil }.should == Failure(nil)
|
44
48
|
end
|
@@ -241,6 +245,14 @@ describe Monadic::Either do
|
|
241
245
|
either.fetch.should be_a KeyError
|
242
246
|
end
|
243
247
|
|
248
|
+
it 'Either(Nothing) returns a Failure' do
|
249
|
+
Either(Nothing).should == Failure(Nothing)
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'Either(Just) returns a Success' do
|
253
|
+
Either(Just.new(1)).should == Success(1)
|
254
|
+
end
|
255
|
+
|
244
256
|
it 'instance variables' do
|
245
257
|
result = Either.chain do
|
246
258
|
bind { @map = { one: 1, two: 2 } }
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class BSONTestConverter
|
4
|
+
class InvalidBSONString < StandardError; end
|
5
|
+
def self.from_string(s)
|
6
|
+
raise InvalidBSONString unless s =~ /^[0-9a-z]{24}$/
|
7
|
+
return self.new(s)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(s)
|
11
|
+
@bson = s
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@bson
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
return 0 unless other.is_a? BSONTestConverter
|
20
|
+
return to_s <=> other.to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class DatabaseReader
|
25
|
+
def self.find(id)
|
26
|
+
return { 'request_id' => '197412101130' } if id.to_s == '4fe48bcfcf79520644088180'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Transaction
|
31
|
+
def self.fetch(params)
|
32
|
+
return Failure('params must be a Hash') unless params.is_a? Hash
|
33
|
+
|
34
|
+
Either(Maybe(params)['id']).else('id is missing').
|
35
|
+
>= {|v| Try { BSONTestConverter.from_string(v) }.else("id '#{v}' is not a valid BSON id") }.
|
36
|
+
>= {|v| Try { DatabaseReader.find(v) }.else("'#{v}' not found") }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.logs(request_id)
|
40
|
+
Success([request_id])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'builder' do
|
45
|
+
# id = BSON::ObjectId.from_string(params[:id])
|
46
|
+
# @tr = MongoMapper.database.collection("log").find_one(id)
|
47
|
+
# @rid = @tr['request_id']
|
48
|
+
# @logs = LogReader.new.links(@rid)
|
49
|
+
it 'builds' do
|
50
|
+
Transaction.fetch({}).should == Failure('id is missing')
|
51
|
+
Transaction.fetch({'id' => '123' }).should == Failure("id '123' is not a valid BSON id")
|
52
|
+
Transaction.fetch({'id' => '000000000000000000000000'}).should == Failure("'000000000000000000000000' not found")
|
53
|
+
|
54
|
+
doc = Transaction.fetch({'id' => '4fe48bcfcf79520644088180'})
|
55
|
+
doc.should == Success({"request_id"=>"197412101130"})
|
56
|
+
|
57
|
+
doc.bind { Failure('logs not found') }.should == Failure('logs not found')
|
58
|
+
|
59
|
+
logs = doc.
|
60
|
+
>= {|v| Transaction.logs(v['request_id']) }
|
61
|
+
|
62
|
+
logs.should == Success(['197412101130'])
|
63
|
+
end
|
64
|
+
end
|
@@ -1,16 +1,15 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'active_support/time'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
class Request < Struct
|
4
|
+
module Monadic
|
5
|
+
class Struct < ::Struct
|
7
6
|
def self.create(params, params_module)
|
8
|
-
|
7
|
+
properties = self.create_properties(params || {}, params_module)
|
9
8
|
|
10
|
-
if
|
11
|
-
Success(
|
9
|
+
if properties.values.all?(&:success?)
|
10
|
+
Success(properties)
|
12
11
|
else
|
13
|
-
Failure(
|
12
|
+
Failure(properties)
|
14
13
|
end
|
15
14
|
end
|
16
15
|
|
@@ -19,12 +18,11 @@ describe 'Hotel Booking Example' do
|
|
19
18
|
end
|
20
19
|
|
21
20
|
def unwrap
|
22
|
-
Struct.new(*members).new(*values.map(&:fetch))
|
23
|
-
# self.class.new(*values.map(&:fetch))
|
21
|
+
::Struct.new(*members).new(*values.map(&:fetch))
|
24
22
|
end
|
25
23
|
|
26
24
|
private
|
27
|
-
def self.
|
25
|
+
def self.create_properties(params, params_module)
|
28
26
|
properties = params_module.methods - Module.methods
|
29
27
|
request_klas = self.new(*properties)
|
30
28
|
properties.reduce(request_klas.new) do |request, property|
|
@@ -33,57 +31,42 @@ describe 'Hotel Booking Example' do
|
|
33
31
|
end
|
34
32
|
end
|
35
33
|
end
|
34
|
+
end
|
36
35
|
|
36
|
+
describe 'Hotel Booking Example' do
|
37
37
|
module HotelBookingRequestParams
|
38
38
|
extend self
|
39
39
|
|
40
40
|
def hotel_code(params)
|
41
|
-
|
42
|
-
|
43
|
-
when value.nil?
|
44
|
-
Failure('hotel_code must not be empty')
|
45
|
-
when not(value =~ /^[A-Z]{3}[A-Z0-9]{4}$/)
|
46
|
-
Failure('hotel_code must be of pattern XXX0001')
|
47
|
-
else
|
48
|
-
Success(value)
|
49
|
-
end
|
41
|
+
param = params['hotel_code']
|
42
|
+
Try(param =~ /^[A-Z]{3}[A-Z0-9]{4}$/) { param }.else "hotel_code must be of pattern XXX0001, got '#{param}'"
|
50
43
|
end
|
51
44
|
|
52
45
|
def nights(params)
|
53
46
|
value = params.fetch('nights', 0).to_i
|
54
|
-
|
55
|
-
Failure("nights must be a number greater than 0 (got '#{params['nights']}')")
|
56
|
-
else
|
57
|
-
Success(value)
|
58
|
-
end
|
47
|
+
Try(value > 0) { value }.else "nights must be a number greater than 0, got '#{params['nights']}'"
|
59
48
|
end
|
60
49
|
|
61
|
-
|
50
|
+
ROOM_OCCUPANCIES = {'SR' => 1, 'DR' => 2, 'TR' => 3, 'QR' => 4 }
|
51
|
+
VALID_ROOM_TYPES = ROOM_OCCUPANCIES.keys
|
62
52
|
def room_type(params)
|
63
|
-
|
64
|
-
|
65
|
-
Failure('room_type must not be empty')
|
66
|
-
when is_valid_room_type
|
67
|
-
Success(value)
|
68
|
-
else
|
69
|
-
Failure("room_type '#{value}' must be one of #{VALID_ROOM_TYPE.join(', ')}")
|
70
|
-
end
|
53
|
+
param = params['room_type']
|
54
|
+
Try(VALID_ROOM_TYPES.include?(param)) { param }.else "room_type must be one of '#{VALID_ROOM_TYPES.join(', ')}', got '#{param}'"
|
71
55
|
end
|
72
56
|
|
73
57
|
def check_in(params)
|
74
58
|
param = params['check_in']
|
75
|
-
|
76
|
-
return Try { Date.parse(param) }.else {|e| "check_in #{e.message} '#{param}'" }
|
59
|
+
Try { Date.parse(param) }.else {|e| "check_in #{e.message}, got '#{param}'" }
|
77
60
|
end
|
78
61
|
|
79
|
-
|
80
|
-
def
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
}
|
62
|
+
OCCUPANCIES = [1, 2, 3, 4]
|
63
|
+
def guests(params)
|
64
|
+
guests = params.select {|p| p =~ /^guest/ }.values.compact
|
65
|
+
rt = params['room_type']
|
66
|
+
occupancy = ROOM_OCCUPANCIES.fetch(rt, 99)
|
67
|
+
Try(guests.count == occupancy) { guests }.else "guests number must match the room_type '#{rt}':#{occupancy}, got #{guests.count}: '#{guests.join(', ')}'"
|
86
68
|
end
|
69
|
+
|
87
70
|
end
|
88
71
|
|
89
72
|
let(:params_proto) do
|
@@ -96,38 +79,48 @@ describe 'Hotel Booking Example' do
|
|
96
79
|
end
|
97
80
|
|
98
81
|
it 'builds a valid request' do
|
99
|
-
result =
|
82
|
+
result = prepare params_proto
|
100
83
|
result.should be_a Success
|
101
|
-
request = result.fetch.unwrap
|
102
|
-
request.hotel_code.should
|
103
|
-
request.
|
84
|
+
request = result.fetch.unwrap # the valid object, ready to use
|
85
|
+
request.hotel_code.should == "STO0001"
|
86
|
+
request.check_in.should == Date.new(2012, 06, 15)
|
87
|
+
request.nights.should == 3
|
88
|
+
request.room_type.should == 'DR'
|
89
|
+
request.guests.should == ['Max Payne', 'Jenny Payne']
|
90
|
+
end
|
91
|
+
|
92
|
+
def prepare(params)
|
93
|
+
Monadic::Struct.create(params, HotelBookingRequestParams)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'reports too few guests' do
|
97
|
+
result = prepare params_proto.reject {|key| key == 'guest2'}
|
98
|
+
result.should be_a Failure
|
99
|
+
result.fetch.guests.should == Failure("guests number must match the room_type 'DR':2, got 1: 'Max Payne'")
|
104
100
|
end
|
105
101
|
|
106
102
|
it 'reports an invalid room_type' do
|
107
|
-
|
108
|
-
result = Request.create(params, HotelBookingRequestParams)
|
103
|
+
result = prepare params_proto.merge 'room_type' => 'XX'
|
109
104
|
result.should be_a Failure
|
110
|
-
|
111
|
-
request.room_type.should be_a Failure
|
105
|
+
result.fetch.room_type.should be_a Failure
|
112
106
|
end
|
113
107
|
|
114
108
|
it 'reports an invalid check_in time' do
|
115
|
-
|
116
|
-
result = Request.create(params, HotelBookingRequestParams)
|
109
|
+
result = prepare params_proto.merge 'check_in' => '2012-11-31'
|
117
110
|
result.should be_a Failure
|
118
111
|
request = result.fetch
|
119
|
-
request.check_in.should
|
120
|
-
request.check_in.should == Failure("check_in invalid date '2012-11-31'")
|
112
|
+
request.check_in.should == Failure("check_in invalid date, got '2012-11-31'")
|
121
113
|
end
|
122
114
|
|
123
115
|
it 'builds a failure request with no params' do
|
124
|
-
result =
|
116
|
+
result = prepare(nil)
|
125
117
|
result.should be_a Failure
|
126
118
|
request = result.fetch
|
127
|
-
request.hotel_code.should
|
128
|
-
request.nights.should
|
129
|
-
request.room_type.should
|
130
|
-
request.check_in.should
|
119
|
+
request.hotel_code.should == Failure("hotel_code must be of pattern XXX0001, got ''")
|
120
|
+
request.nights.should == Failure("nights must be a number greater than 0, got ''")
|
121
|
+
request.room_type.should == Failure("room_type must be one of 'SR, DR, TR, QR', got ''")
|
122
|
+
request.check_in.should == Failure("check_in can't convert nil into String, got ''")
|
123
|
+
request.guests.should == Failure("guests number must match the room_type '':99, got 0: ''")
|
131
124
|
end
|
132
125
|
|
133
126
|
end
|
data/spec/maybe_spec.rb
CHANGED
@@ -7,12 +7,17 @@ describe Monadic::Maybe do
|
|
7
7
|
|
8
8
|
it 'Maybe cannot be created using #new, use #unit instead' do
|
9
9
|
expect { Maybe.new(1) }.to raise_error NoMethodError
|
10
|
-
end
|
10
|
+
end
|
11
11
|
|
12
12
|
it 'nil as value always returns Nothing()' do
|
13
13
|
Maybe(nil).a.b.c.should == Nothing
|
14
14
|
end
|
15
15
|
|
16
|
+
it 'Maybe(Just) should return Just' do
|
17
|
+
Maybe(Just(nil)).should == Just(nil)
|
18
|
+
Maybe(Nothing).should == Nothing
|
19
|
+
end
|
20
|
+
|
16
21
|
it 'on non-existant methods, returns Nothing' do
|
17
22
|
Maybe({}).a.b.c.should == Nothing
|
18
23
|
end
|
@@ -26,7 +31,7 @@ describe Monadic::Maybe do
|
|
26
31
|
Maybe(nil).fetch.should == Nothing
|
27
32
|
Maybe(nil)._.should == Nothing
|
28
33
|
Maybe(nil).empty?.should be_true
|
29
|
-
end
|
34
|
+
end
|
30
35
|
|
31
36
|
it 'Nothing#to_s is "Nothing"' do
|
32
37
|
option = Maybe(nil)
|
@@ -55,6 +60,10 @@ describe Monadic::Maybe do
|
|
55
60
|
|
56
61
|
it 'Nothing#or returns the alternative' do
|
57
62
|
Maybe(nil).or(1).should == Just(1)
|
63
|
+
Nothing.or(1).should == Just(1)
|
64
|
+
Nothing.or(Just(nil)).should == Just(nil)
|
65
|
+
Nothing.something.or(1).should == Just(1)
|
66
|
+
Nothing.something.or(Just(nil)).should == Just(nil)
|
58
67
|
end
|
59
68
|
end
|
60
69
|
|
@@ -64,18 +73,19 @@ describe Monadic::Maybe do
|
|
64
73
|
end
|
65
74
|
|
66
75
|
it 'Just stays Just' do
|
67
|
-
Maybe('foo').should be_kind_of(Just)
|
76
|
+
Maybe('foo').should be_kind_of(Just)
|
68
77
|
Maybe('foo').empty?.should be_false
|
69
78
|
end
|
70
79
|
|
71
80
|
it 'Just#to_s is "value"' do
|
72
81
|
Just.unit(123).to_s.should == "Just(123)"
|
82
|
+
Just("4fe48bcfcf79520644088180").to_s.should == 'Just("4fe48bcfcf79520644088180")'
|
73
83
|
end
|
74
84
|
|
75
85
|
it 'Just#or return self' do
|
76
86
|
Maybe(1).or(2).should == Just(1)
|
77
|
-
Maybe(nil).or(nil).should ==
|
78
|
-
Maybe(nil).something.or('').fetch.should == ''
|
87
|
+
Maybe(nil).or(nil).should == Nothing
|
88
|
+
Maybe(nil).something.or(Just('')).fetch.should == Just('')
|
79
89
|
end
|
80
90
|
|
81
91
|
it 'Just#inspect returns Just(value)' do
|
@@ -163,7 +173,12 @@ describe Monadic::Maybe do
|
|
163
173
|
|
164
174
|
format_date_in_march(nil).should == "not in march!"
|
165
175
|
format_date_in_march(Time.parse('2009-01-01 01:02')).should == "not in march!"
|
166
|
-
format_date_in_march(Time.parse('2011-03-21 12:34')).should == "20110321"
|
176
|
+
format_date_in_march(Time.parse('2011-03-21 12:34')).should == "20110321"
|
167
177
|
end
|
168
178
|
|
179
|
+
it 'delegates method calls with #proxy' do
|
180
|
+
Maybe({a: 1}).proxy.fetch(:a).should == Maybe(1)
|
181
|
+
Maybe("FOO").proxy.downcase.capitalize.should == Maybe("Foo")
|
182
|
+
Maybe(nil).proxy.downcase.capitalize.bar.should == Nothing
|
183
|
+
end
|
169
184
|
end
|
data/spec/try_spec.rb
CHANGED
@@ -14,14 +14,25 @@ describe 'Monadic::Try' do
|
|
14
14
|
Try { [] }.should be_a Failure
|
15
15
|
end
|
16
16
|
|
17
|
-
it 'without a block it
|
17
|
+
it 'with a param, without a block it returns the predicate' do
|
18
18
|
Try(true).should be_a Success
|
19
19
|
Try(false).should be_a Failure
|
20
20
|
Try(nil).should be_a Failure
|
21
21
|
end
|
22
22
|
|
23
|
-
it 'with a
|
24
|
-
|
23
|
+
it 'with a param and with a block it returns the block result' do
|
24
|
+
Try(true) { 1 }.should == Success(1)
|
25
|
+
Try(false) { 2 }.should == Failure(2)
|
26
|
+
Try(false) { 1 }.else { 2 }.should == Failure(2)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'with a proc and no block it evaluates the proc' do
|
30
|
+
Try(-> { 1 }).should == Success(1)
|
31
|
+
Try(-> { 1 }) { 2 }.should == Success(2)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'no params is the same as passing nil as the predicatea' do
|
35
|
+
Try().should == Failure(nil)
|
25
36
|
end
|
26
37
|
|
27
38
|
it 'returns Success for non-nil values' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: monadic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-06-
|
12
|
+
date: 2012-06-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -154,6 +154,7 @@ files:
|
|
154
154
|
- spec/core_ext/object_spec.rb
|
155
155
|
- spec/either_spec.rb
|
156
156
|
- spec/examples/applicative_spec.rb
|
157
|
+
- spec/examples/builder_spec.rb
|
157
158
|
- spec/examples/hotel_booking_spec.rb
|
158
159
|
- spec/jruby_fixes.rb
|
159
160
|
- spec/maybe_spec.rb
|
@@ -176,7 +177,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
176
177
|
version: '0'
|
177
178
|
segments:
|
178
179
|
- 0
|
179
|
-
hash:
|
180
|
+
hash: 2246458514498594220
|
180
181
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
182
|
none: false
|
182
183
|
requirements:
|
@@ -185,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
185
186
|
version: '0'
|
186
187
|
segments:
|
187
188
|
- 0
|
188
|
-
hash:
|
189
|
+
hash: 2246458514498594220
|
189
190
|
requirements: []
|
190
191
|
rubyforge_project:
|
191
192
|
rubygems_version: 1.8.24
|
@@ -196,6 +197,7 @@ test_files:
|
|
196
197
|
- spec/core_ext/object_spec.rb
|
197
198
|
- spec/either_spec.rb
|
198
199
|
- spec/examples/applicative_spec.rb
|
200
|
+
- spec/examples/builder_spec.rb
|
199
201
|
- spec/examples/hotel_booking_spec.rb
|
200
202
|
- spec/jruby_fixes.rb
|
201
203
|
- spec/maybe_spec.rb
|