monadic 0.5.0 → 0.6.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.
- 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, [](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
|