monadic 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +21 -1
- data/README.md +26 -8
- data/lib/monadic/either.rb +9 -0
- data/lib/monadic/validation.rb +12 -0
- data/lib/monadic/version.rb +1 -1
- data/spec/either_spec.rb +9 -9
- data/spec/examples/hotel_booking_spec.rb +12 -41
- data/spec/maybe_spec.rb +1 -0
- data/spec/try_spec.rb +4 -4
- data/spec/validation_spec.rb +28 -0
- metadata +4 -4
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,26 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## v0.
|
3
|
+
## v0.7
|
4
|
+
**BREAKING CHANGES**
|
5
|
+
|
6
|
+
Rename `Either#else` to `Either#or` to be consistent with `Maybe`
|
7
|
+
Using `Either#else` will give you a deprecation warning for now, it will be removed in v1.0
|
8
|
+
|
9
|
+
New `Validation#fill` method for validating filling Structs.
|
10
|
+
|
11
|
+
ExampleStruct = Struct.new(:a, :b)
|
12
|
+
module ExampleValidator
|
13
|
+
extend self
|
14
|
+
def a(params); Try { params[0] }.or 'a cannot be empty'; end
|
15
|
+
def b(params); Try { params[1] }.or 'b cannot be empty'; end
|
16
|
+
end
|
17
|
+
|
18
|
+
result = Validation.fill(ExampleStruct, [1, 2], ExampleValidator) == Sucess
|
19
|
+
example = result.fetch
|
20
|
+
example.a == 1
|
21
|
+
example.b == 2
|
22
|
+
|
23
|
+
## v0.6
|
4
24
|
|
5
25
|
Extend `Try` to also support lambdas.
|
6
26
|
|
data/README.md
CHANGED
@@ -90,10 +90,12 @@ Maybe(1).to_s == '1'
|
|
90
90
|
```
|
91
91
|
|
92
92
|
`#or`
|
93
|
+
```ruby
|
93
94
|
Maybe(nil).or(1) == Just(1)
|
94
95
|
Maybe(1).or(2) == Just(1)
|
95
96
|
Maybe(nil).or(1) == Just(1)
|
96
97
|
Maybe(nil).or(nil) == Nothing
|
98
|
+
```
|
97
99
|
|
98
100
|
Falsey values (kind-of) examples:
|
99
101
|
|
@@ -217,17 +219,17 @@ Success(params).
|
|
217
219
|
bind ->(path) { load_stuff(params) } #
|
218
220
|
```
|
219
221
|
|
220
|
-
`Either#
|
222
|
+
`Either#or` allows to provide alternate values in case of `Failure`:
|
221
223
|
|
222
224
|
```ruby
|
223
|
-
Either(false == true).
|
224
|
-
Success('truth needs no sugar coating').
|
225
|
+
Either(false == true).or('false was not true') == Failure(false was not true)
|
226
|
+
Success('truth needs no sugar coating').or('all lies') == Success('truth needs no sugar coating')
|
225
227
|
```
|
226
228
|
|
227
|
-
`Either#
|
229
|
+
`Either#or` supports also a block
|
228
230
|
|
229
231
|
```ruby
|
230
|
-
Failure(1).
|
232
|
+
Failure(1).or {|other| 1 + 2 } == Failure(3)
|
231
233
|
```
|
232
234
|
|
233
235
|
Storing intermediate results in instance variables is possible, although it is not very elegant:
|
@@ -251,16 +253,16 @@ Try { Date.parse('2012-02-30') } == Failu
|
|
251
253
|
Try { Date.parse('2012-02-28') } == Success
|
252
254
|
|
253
255
|
date_s = '2012-02-30'
|
254
|
-
Try { Date.parse(date_s) }.
|
256
|
+
Try { Date.parse(date_s) }.or {|e| "#{e.message} #{date_s}" } == Failure("invalid date 2012-02-30")
|
255
257
|
|
256
258
|
# with a predicate
|
257
259
|
Try(true) == Success(true)
|
258
260
|
Try(false) { "string" } == Failure("string")
|
259
|
-
Try(false) { "success"}.
|
261
|
+
Try(false) { "success"}.or("fail") == Failure("fail")
|
260
262
|
|
261
263
|
VALID_TITLES = %w[MR MRS]
|
262
264
|
title = 'MS'
|
263
|
-
Try(VALID_TITLES.inlude?(title)) { title }.
|
265
|
+
Try(VALID_TITLES.inlude?(title)) { title }.or { "title must be on of '#{VALID_TITLES.join(', ')}'' but was '#{title}'"}
|
264
266
|
== "title must be on of 'MR, MR' but was 'MS'"
|
265
267
|
```
|
266
268
|
|
@@ -306,6 +308,22 @@ The above example, returns either `Success([32, :sober, :male])` or `Failure(['A
|
|
306
308
|
|
307
309
|
See also [examples/validation.rb](https://github.com/pzol/monadic/blob/master/examples/validation.rb) and [examples/validation_module](https://github.com/pzol/monadic/blob/master/examples/validation_module.rb)
|
308
310
|
|
311
|
+
`Validation#fill` method for validating filling Structs:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
ExampleStruct = Struct.new(:a, :b)
|
315
|
+
module ExampleValidator
|
316
|
+
extend self
|
317
|
+
def a(params); Try { params[0] }.or 'a cannot be empty'; end
|
318
|
+
def b(params); Try { params[1] }.or 'b cannot be empty'; end
|
319
|
+
end
|
320
|
+
|
321
|
+
result = Validation.fill(ExampleStruct, [1, 2], ExampleValidator) == Sucess
|
322
|
+
example = result.fetch
|
323
|
+
example.a == 1
|
324
|
+
example.b == 2
|
325
|
+
```
|
326
|
+
|
309
327
|
### Monad
|
310
328
|
All Monads include this module. Standalone it is an Identity monad. Not useful on its own. It's methods are usable on all its descendants.
|
311
329
|
|
data/lib/monadic/either.rb
CHANGED
@@ -36,7 +36,15 @@ module Monadic
|
|
36
36
|
|
37
37
|
# If it is a Failure it will return a new Failure with the provided value
|
38
38
|
# @return [Success, Failure]
|
39
|
+
def or(value=nil, &block)
|
40
|
+
return Failure(block.call(@value)) if failure? && block_given?
|
41
|
+
return Failure(value) if failure?
|
42
|
+
return self
|
43
|
+
end
|
44
|
+
|
45
|
+
#@deprecated
|
39
46
|
def else(value=nil, &block)
|
47
|
+
warn 'Either#else is deprecated and will be removed in a future version, use Either#or instead'
|
40
48
|
return Failure(block.call(@value)) if failure? && block_given?
|
41
49
|
return Failure(value) if failure?
|
42
50
|
return self
|
@@ -83,6 +91,7 @@ module Monadic
|
|
83
91
|
def bind(proc=nil, &block)
|
84
92
|
@chain << (proc || block)
|
85
93
|
end
|
94
|
+
alias :>= :bind
|
86
95
|
end
|
87
96
|
|
88
97
|
# @private instance and class methods for +Success+ and +Failure+
|
data/lib/monadic/validation.rb
CHANGED
@@ -29,5 +29,17 @@ module Monadic
|
|
29
29
|
# @failure.fetch << result.fetch) if result.failure?
|
30
30
|
@result << result
|
31
31
|
end
|
32
|
+
|
33
|
+
# Useful to populate a data structure like a Struct, which takes values in its constructor
|
34
|
+
def self.fill(obj, params, validator)
|
35
|
+
properties = validator.methods - Module.methods
|
36
|
+
values = properties.collect {|property| validator.send(property, params) }
|
37
|
+
|
38
|
+
if values.all?(&:success?)
|
39
|
+
Success(obj.new(*values.collect(&:fetch)))
|
40
|
+
else
|
41
|
+
Failure(obj.new(*values))
|
42
|
+
end
|
43
|
+
end
|
32
44
|
end
|
33
45
|
end
|
data/lib/monadic/version.rb
CHANGED
data/spec/either_spec.rb
CHANGED
@@ -57,17 +57,17 @@ describe Monadic::Either do
|
|
57
57
|
error.message.should == 'error'
|
58
58
|
end
|
59
59
|
|
60
|
-
it '#
|
61
|
-
Failure(false).
|
62
|
-
Either(nil).
|
63
|
-
Failure(1).
|
64
|
-
Success(true).
|
65
|
-
Either(true).
|
66
|
-
Success(false).
|
60
|
+
it '#or returns an alternative value considered Success if it is Nothing' do
|
61
|
+
Failure(false).or(true).should == Failure(true)
|
62
|
+
Either(nil).or(true).should == Failure(true)
|
63
|
+
Failure(1).or(nil).should == Failure(nil)
|
64
|
+
Success(true).or(false).should == Success(true)
|
65
|
+
Either(true).or(false).should == Success(true)
|
66
|
+
Success(false).or(true).should == Success(false)
|
67
67
|
end
|
68
68
|
|
69
|
-
it '#
|
70
|
-
(Failure(1).
|
69
|
+
it '#or with a block gets the original value passed' do
|
70
|
+
(Failure(1).or { |other| other + 1 }).should == Failure(2)
|
71
71
|
end
|
72
72
|
|
73
73
|
class User
|
@@ -1,62 +1,30 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'active_support/time'
|
3
3
|
|
4
|
-
module Monadic
|
5
|
-
class Struct < ::Struct
|
6
|
-
def self.create(params, params_module)
|
7
|
-
properties = self.create_properties(params || {}, params_module)
|
8
|
-
|
9
|
-
if properties.values.all?(&:success?)
|
10
|
-
Success(properties)
|
11
|
-
else
|
12
|
-
Failure(properties)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def to_hash
|
17
|
-
Hash[*members.zip(values).flatten]
|
18
|
-
end
|
19
|
-
|
20
|
-
def unwrap
|
21
|
-
::Struct.new(*members).new(*values.map(&:fetch))
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
def self.create_properties(params, params_module)
|
26
|
-
properties = params_module.methods - Module.methods
|
27
|
-
request_klas = self.new(*properties)
|
28
|
-
properties.reduce(request_klas.new) do |request, property|
|
29
|
-
request[property] = params_module.send(property, params)
|
30
|
-
request
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
4
|
describe 'Hotel Booking Example' do
|
37
5
|
module HotelBookingRequestParams
|
38
6
|
extend self
|
39
7
|
|
40
8
|
def hotel_code(params)
|
41
9
|
param = params['hotel_code']
|
42
|
-
Try(param =~ /^[A-Z]{3}[A-Z0-9]{4}$/) { param }.
|
10
|
+
Try(param =~ /^[A-Z]{3}[A-Z0-9]{4}$/) { param }.or "hotel_code must be of pattern XXX0001, got '#{param}'"
|
43
11
|
end
|
44
12
|
|
45
13
|
def nights(params)
|
46
14
|
value = params.fetch('nights', 0).to_i
|
47
|
-
Try(value > 0) { value }.
|
15
|
+
Try(value > 0) { value }.or "nights must be a number greater than 0, got '#{params['nights']}'"
|
48
16
|
end
|
49
17
|
|
50
18
|
ROOM_OCCUPANCIES = {'SR' => 1, 'DR' => 2, 'TR' => 3, 'QR' => 4 }
|
51
19
|
VALID_ROOM_TYPES = ROOM_OCCUPANCIES.keys
|
52
20
|
def room_type(params)
|
53
21
|
param = params['room_type']
|
54
|
-
Try(VALID_ROOM_TYPES.include?(param)) { param }.
|
22
|
+
Try(VALID_ROOM_TYPES.include?(param)) { param }.or "room_type must be one of '#{VALID_ROOM_TYPES.join(', ')}', got '#{param}'"
|
55
23
|
end
|
56
24
|
|
57
25
|
def check_in(params)
|
58
26
|
param = params['check_in']
|
59
|
-
Try { Date.parse(param) }.
|
27
|
+
Try { Date.parse(param) }.or {|e| "check_in #{e.message}, got '#{param}'" }
|
60
28
|
end
|
61
29
|
|
62
30
|
OCCUPANCIES = [1, 2, 3, 4]
|
@@ -64,7 +32,7 @@ describe 'Hotel Booking Example' do
|
|
64
32
|
guests = params.select {|p| p =~ /^guest/ }.values.compact
|
65
33
|
rt = params['room_type']
|
66
34
|
occupancy = ROOM_OCCUPANCIES.fetch(rt, 99)
|
67
|
-
Try(guests.count == occupancy) { guests }.
|
35
|
+
Try(guests.count == occupancy) { guests }.or "guests number must match the room_type '#{rt}':#{occupancy}, got #{guests.count}: '#{guests.join(', ')}'"
|
68
36
|
end
|
69
37
|
|
70
38
|
end
|
@@ -81,7 +49,7 @@ describe 'Hotel Booking Example' do
|
|
81
49
|
it 'builds a valid request' do
|
82
50
|
result = prepare params_proto
|
83
51
|
result.should be_a Success
|
84
|
-
request = result.fetch
|
52
|
+
request = result.fetch # the valid object, ready to use
|
85
53
|
request.hotel_code.should == "STO0001"
|
86
54
|
request.check_in.should == Date.new(2012, 06, 15)
|
87
55
|
request.nights.should == 3
|
@@ -90,7 +58,9 @@ describe 'Hotel Booking Example' do
|
|
90
58
|
end
|
91
59
|
|
92
60
|
def prepare(params)
|
93
|
-
|
61
|
+
properties = HotelBookingRequestParams.methods - Module.methods
|
62
|
+
klas = Struct.new(*properties)
|
63
|
+
Monadic::Validation.fill(klas, params, HotelBookingRequestParams)
|
94
64
|
end
|
95
65
|
|
96
66
|
it 'reports too few guests' do
|
@@ -113,13 +83,14 @@ describe 'Hotel Booking Example' do
|
|
113
83
|
end
|
114
84
|
|
115
85
|
it 'builds a failure request with no params' do
|
116
|
-
result = prepare(
|
86
|
+
result = prepare({})
|
117
87
|
result.should be_a Failure
|
118
88
|
request = result.fetch
|
119
89
|
request.hotel_code.should == Failure("hotel_code must be of pattern XXX0001, got ''")
|
120
90
|
request.nights.should == Failure("nights must be a number greater than 0, got ''")
|
121
91
|
request.room_type.should == Failure("room_type must be one of 'SR, DR, TR, QR', got ''")
|
122
|
-
request.check_in.should
|
92
|
+
request.check_in.should be_a Failure
|
93
|
+
request.check_in.fetch.should =~ /check_in.*, got ''/
|
123
94
|
request.guests.should == Failure("guests number must match the room_type '':99, got 0: ''")
|
124
95
|
end
|
125
96
|
|
data/spec/maybe_spec.rb
CHANGED
data/spec/try_spec.rb
CHANGED
@@ -23,7 +23,7 @@ describe 'Monadic::Try' do
|
|
23
23
|
it 'with a param and with a block it returns the block result' do
|
24
24
|
Try(true) { 1 }.should == Success(1)
|
25
25
|
Try(false) { 2 }.should == Failure(2)
|
26
|
-
Try(false) { 1 }.
|
26
|
+
Try(false) { 1 }.or { 2 }.should == Failure(2)
|
27
27
|
end
|
28
28
|
|
29
29
|
it 'with a proc and no block it evaluates the proc' do
|
@@ -40,8 +40,8 @@ describe 'Monadic::Try' do
|
|
40
40
|
Try { "string" }.should == Success("string")
|
41
41
|
end
|
42
42
|
|
43
|
-
it 'combined with
|
44
|
-
Try { Date.parse('2012-02-30') }.
|
45
|
-
Try { Date.parse('2012-02-28') }.
|
43
|
+
it 'combined with or and a block' do
|
44
|
+
Try { Date.parse('2012-02-30') }.or {|e| "Exception: #{e.message}" }.should == Failure("Exception: invalid date")
|
45
|
+
Try { Date.parse('2012-02-28') }.or {|e| "Exception: #{e.message}" }.should == Success(Date.parse('2012-02-28'))
|
46
46
|
end
|
47
47
|
end
|
data/spec/validation_spec.rb
CHANGED
@@ -44,4 +44,32 @@ describe Monadic::Validation do
|
|
44
44
|
success.should be_a Success
|
45
45
|
success.should == Success([30, :sober, :male])
|
46
46
|
end
|
47
|
+
|
48
|
+
describe '#fill' do
|
49
|
+
ExampleStruct = Struct.new(:a, :b)
|
50
|
+
module ExampleValidator
|
51
|
+
extend self
|
52
|
+
def a(params); Try { params[0] }.or 'a cannot be empty'; end
|
53
|
+
def b(params); Try { params[1] }.or 'b cannot be empty'; end
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'works with a struct, returns the object wrapped in Success' do
|
57
|
+
params = [1, 2]
|
58
|
+
result = Validation.fill(ExampleStruct, params, ExampleValidator)
|
59
|
+
result.should be_a Success
|
60
|
+
example = result.fetch
|
61
|
+
|
62
|
+
example.a.should == 1
|
63
|
+
example.b.should == 2
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'on Failure, each individual field contains a Success or Failure' do
|
67
|
+
params = [1]
|
68
|
+
result = Validation.fill(ExampleStruct, params, ExampleValidator)
|
69
|
+
result.should be_a Failure
|
70
|
+
example = result.fetch
|
71
|
+
example.a.should == Success(1)
|
72
|
+
example.b.should == Failure('b cannot be empty')
|
73
|
+
end
|
74
|
+
end
|
47
75
|
end
|
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.7.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-
|
12
|
+
date: 2012-07-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -177,7 +177,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
177
177
|
version: '0'
|
178
178
|
segments:
|
179
179
|
- 0
|
180
|
-
hash:
|
180
|
+
hash: 494077126848857519
|
181
181
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
182
182
|
none: false
|
183
183
|
requirements:
|
@@ -186,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
186
186
|
version: '0'
|
187
187
|
segments:
|
188
188
|
- 0
|
189
|
-
hash:
|
189
|
+
hash: 494077126848857519
|
190
190
|
requirements: []
|
191
191
|
rubyforge_project:
|
192
192
|
rubygems_version: 1.8.24
|