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 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(nil) == Just(nil)
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, jruby 1.9 mode, rbx 1.9 mode.
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
 
@@ -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 Failure.new(value) if value.nil? || (value.respond_to?(:empty?) && value.empty?) || !value
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 NOT coerced into Maybe, thus Nothing.or(nil) will be Just(nil)
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
- Just.new(other)
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
- def Try(proc = nil, &block)
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
- return Either(proc) unless proc.nil? || proc.is_a?(Proc)
5
- return Either((proc || block).call)
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
@@ -1,3 +1,3 @@
1
1
  module Monadic
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
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
- describe 'Hotel Booking Example' do
5
-
6
- class Request < Struct
4
+ module Monadic
5
+ class Struct < ::Struct
7
6
  def self.create(params, params_module)
8
- value = self.create_value(params || {}, params_module)
7
+ properties = self.create_properties(params || {}, params_module)
9
8
 
10
- if value.values.all?(&:success?)
11
- Success(value)
9
+ if properties.values.all?(&:success?)
10
+ Success(properties)
12
11
  else
13
- Failure(value)
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.create_value(params, params_module)
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
- value = params['hotel_code']
42
- case
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
- if value <= 0
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
- VALID_ROOM_TYPE = %w[SR DR TR QR]
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
- case value = params['room_type']
64
- when nil
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
- return Failure("check_in must not be empty") if param.nil?
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
- private
80
- def is_valid_room_type
81
- Object.new.tap {|matcher|
82
- def matcher.===(value)
83
- VALID_ROOM_TYPE.include? value
84
- end
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 = Request.create(params_proto, HotelBookingRequestParams)
82
+ result = prepare params_proto
100
83
  result.should be_a Success
101
- request = result.fetch.unwrap
102
- request.hotel_code.should eq "STO0001"
103
- request.nights.should eq 3
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
- params = params_proto.merge 'room_type' => 'XX'
108
- result = Request.create(params, HotelBookingRequestParams)
103
+ result = prepare params_proto.merge 'room_type' => 'XX'
109
104
  result.should be_a Failure
110
- request = result.fetch
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
- params = params_proto.merge 'check_in' => '2012-11-31'
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 be_a Failure
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 = Request.create(nil, HotelBookingRequestParams)
116
+ result = prepare(nil)
125
117
  result.should be_a Failure
126
118
  request = result.fetch
127
- request.hotel_code.should be_a Failure
128
- request.nights.should be_a Failure
129
- request.room_type.should be_a Failure
130
- request.check_in.should be_a Failure
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 == Just.new(nil)
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 works like Either' do
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 proc' do
24
- pending
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.5.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-17 00:00:00.000000000 Z
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: 44748107420070269
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: 44748107420070269
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