functional-ruby 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +14 -12
- data/doc/memo.md +192 -0
- data/doc/pattern_matching.md +481 -0
- data/doc/protocol.md +219 -0
- data/doc/record.md +247 -0
- data/lib/functional/abstract_struct.rb +8 -8
- data/lib/functional/delay.rb +31 -38
- data/lib/functional/either.rb +48 -45
- data/lib/functional/final_struct.rb +23 -34
- data/lib/functional/final_var.rb +20 -21
- data/lib/functional/memo.rb +33 -24
- data/lib/functional/method_signature.rb +1 -2
- data/lib/functional/option.rb +7 -7
- data/lib/functional/pattern_matching.rb +12 -10
- data/lib/functional/protocol.rb +2 -4
- data/lib/functional/protocol_info.rb +5 -3
- data/lib/functional/record.rb +82 -16
- data/lib/functional/synchronization.rb +88 -0
- data/lib/functional/tuple.rb +14 -4
- data/lib/functional/type_check.rb +0 -2
- data/lib/functional/union.rb +5 -4
- data/lib/functional/value_struct.rb +5 -3
- data/lib/functional/version.rb +1 -1
- data/spec/functional/complex_pattern_matching_spec.rb +1 -2
- data/spec/functional/configuration_spec.rb +0 -2
- data/spec/functional/delay_spec.rb +0 -2
- data/spec/functional/either_spec.rb +0 -1
- data/spec/functional/final_struct_spec.rb +0 -1
- data/spec/functional/final_var_spec.rb +0 -2
- data/spec/functional/memo_spec.rb +7 -10
- data/spec/functional/option_spec.rb +0 -1
- data/spec/functional/pattern_matching_spec.rb +0 -1
- data/spec/functional/protocol_info_spec.rb +0 -2
- data/spec/functional/protocol_spec.rb +1 -3
- data/spec/functional/record_spec.rb +170 -87
- data/spec/functional/tuple_spec.rb +0 -1
- data/spec/functional/type_check_spec.rb +0 -2
- data/spec/functional/union_spec.rb +0 -1
- data/spec/functional/value_struct_spec.rb +0 -1
- metadata +14 -29
- data/doc/memo.txt +0 -192
- data/doc/pattern_matching.txt +0 -485
- data/doc/protocol.txt +0 -221
- data/doc/record.txt +0 -207
- data/doc/thread_safety.txt +0 -17
@@ -0,0 +1,88 @@
|
|
1
|
+
module Functional
|
2
|
+
|
3
|
+
# @!visibility private
|
4
|
+
#
|
5
|
+
# Based on work originally done by Petr Chalupa (@pitr-ch) in Concurrent Ruby.
|
6
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/synchronization/object.rb
|
7
|
+
module Synchronization
|
8
|
+
|
9
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
10
|
+
|
11
|
+
require 'jruby'
|
12
|
+
|
13
|
+
# @!visibility private
|
14
|
+
class Object
|
15
|
+
|
16
|
+
# @!visibility private
|
17
|
+
def initialize(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
# @!visibility private
|
23
|
+
def synchronize
|
24
|
+
JRuby.reference0(self).synchronized { yield }
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
def ensure_ivar_visibility!
|
29
|
+
# relying on undocumented behavior of JRuby, ivar access is volatile
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
|
34
|
+
|
35
|
+
# @!visibility private
|
36
|
+
class Object
|
37
|
+
|
38
|
+
# @!visibility private
|
39
|
+
def initialize(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
# @!visibility private
|
45
|
+
def synchronize(&block)
|
46
|
+
Rubinius.synchronize(self, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @!visibility private
|
50
|
+
def ensure_ivar_visibility!
|
51
|
+
# Rubinius instance variables are not volatile so we need to insert barrier
|
52
|
+
Rubinius.memory_barrier
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
else
|
57
|
+
|
58
|
+
require 'thread'
|
59
|
+
|
60
|
+
# @!visibility private
|
61
|
+
class Object
|
62
|
+
|
63
|
+
# @!visibility private
|
64
|
+
def initialize(*args)
|
65
|
+
@__lock__ = ::Mutex.new
|
66
|
+
@__condition__ = ::ConditionVariable.new
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
# @!visibility private
|
72
|
+
def synchronize
|
73
|
+
if @__lock__.owned?
|
74
|
+
yield
|
75
|
+
else
|
76
|
+
@__lock__.synchronize { yield }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @!visibility private
|
81
|
+
def ensure_ivar_visibility!
|
82
|
+
# relying on undocumented behavior of CRuby, GVL acquire has lock which ensures visibility of ivars
|
83
|
+
# https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L204-L211
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/functional/tuple.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'functional/synchronization'
|
2
|
+
|
1
3
|
module Functional
|
2
4
|
|
3
5
|
# A tuple is a pure functional data strcture that is similar to an array but is
|
@@ -9,8 +11,6 @@ module Functional
|
|
9
11
|
# contains, the less efficient it will become. A future version will use a fast,
|
10
12
|
# immutable, persistent data structure such as a finger tree or a trie.
|
11
13
|
#
|
12
|
-
# @since 1.1.0
|
13
|
-
#
|
14
14
|
# @see http://en.wikipedia.org/wiki/Tuple
|
15
15
|
# @see http://msdn.microsoft.com/en-us/library/system.tuple.aspx
|
16
16
|
# @see http://www.tutorialspoint.com/python/python_tuples.htm
|
@@ -20,8 +20,16 @@ module Functional
|
|
20
20
|
# @see http://www.erlang.org/doc/man/erlang.html#make_tuple-2
|
21
21
|
# @see http://en.wikibooks.org/wiki/Haskell/Lists_and_tuples#Tuples
|
22
22
|
#
|
23
|
-
# @!macro thread_safe_immutable_object
|
24
|
-
|
23
|
+
# @!macro [new] thread_safe_immutable_object
|
24
|
+
#
|
25
|
+
# @note This is a write-once, read-many, thread safe object that can be
|
26
|
+
# used in concurrent systems. Thread safety guarantees *cannot* be made
|
27
|
+
# about objects contained *within* this object, however. Ruby variables
|
28
|
+
# are mutable references to mutable objects. This cannot be changed. The
|
29
|
+
# best practice it to only encapsulate immutable, frozen, or thread safe
|
30
|
+
# objects. Ultimately, thread safety is the responsibility of the
|
31
|
+
# programmer.
|
32
|
+
class Tuple < Synchronization::Object
|
25
33
|
|
26
34
|
# Create a new tuple with the given data items in the given order.
|
27
35
|
#
|
@@ -29,8 +37,10 @@ module Functional
|
|
29
37
|
# @raise [ArgumentError] if data is not an array or does not implement `to_a`
|
30
38
|
def initialize(data = [])
|
31
39
|
raise ArgumentError.new('data is not an array') unless data.respond_to?(:to_a)
|
40
|
+
super
|
32
41
|
@data = data.to_a.dup.freeze
|
33
42
|
self.freeze
|
43
|
+
ensure_ivar_visibility!
|
34
44
|
end
|
35
45
|
|
36
46
|
# Retrieve the item at the given index. Indices begin at zero and increment
|
@@ -3,8 +3,6 @@ module Functional
|
|
3
3
|
# Supplies type-checking helpers whenever included.
|
4
4
|
#
|
5
5
|
# @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Actor/TypeCheck.html TypeCheck in Concurrent Ruby
|
6
|
-
#
|
7
|
-
# @since 1.0.0
|
8
6
|
module TypeCheck
|
9
7
|
|
10
8
|
# Performs an `is_a?` check of the given value object against the
|
data/lib/functional/union.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
require 'functional/abstract_struct'
|
2
|
+
require 'functional/synchronization'
|
2
3
|
|
3
4
|
module Functional
|
4
5
|
|
@@ -47,13 +48,10 @@ module Functional
|
|
47
48
|
# Functional::Union::Suit.hearts('Queen')
|
48
49
|
# #=> #<union Functional::Union::Suit :clubs=>nil, :diamonds=>nil, :hearts=>"Queen", :spades=>nil>
|
49
50
|
#
|
50
|
-
# @see Functional::AbstractStruct
|
51
51
|
# @see Functional::Union
|
52
52
|
# @see http://www.ruby-doc.org/core-2.1.2/Struct.html Ruby `Struct` class
|
53
53
|
# @see http://en.wikipedia.org/wiki/Union_type "Union type" on Wikipedia
|
54
54
|
#
|
55
|
-
# @since 1.0.0
|
56
|
-
#
|
57
55
|
# @!macro thread_safe_immutable_object
|
58
56
|
module Union
|
59
57
|
extend self
|
@@ -127,6 +125,7 @@ module Functional
|
|
127
125
|
# @return [Functional::AbstractStruct] the union class
|
128
126
|
def define_initializer(union)
|
129
127
|
union.send(:define_method, :initialize) do |field, value|
|
128
|
+
super()
|
130
129
|
@field = field
|
131
130
|
@value = value
|
132
131
|
data = fields.reduce({}) do |memo, field|
|
@@ -135,6 +134,8 @@ module Functional
|
|
135
134
|
end
|
136
135
|
set_data_hash(data)
|
137
136
|
set_values_array(data.values)
|
137
|
+
ensure_ivar_visibility!
|
138
|
+
self.freeze
|
138
139
|
end
|
139
140
|
union
|
140
141
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'functional/synchronization'
|
2
|
+
|
1
3
|
module Functional
|
2
4
|
|
3
5
|
# A variation on Ruby's `OpenStruct` in which all fields are immutable and
|
@@ -18,22 +20,22 @@ module Functional
|
|
18
20
|
# name.last? #=> true
|
19
21
|
# name.middle? #=> false
|
20
22
|
#
|
21
|
-
# @since 1.1.0
|
22
|
-
#
|
23
23
|
# @see Functional::Record
|
24
24
|
# @see Functional::FinalStruct
|
25
25
|
# @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/ostruct/rdoc/OpenStruct.html
|
26
26
|
#
|
27
27
|
# @!macro thread_safe_immutable_object
|
28
|
-
class ValueStruct
|
28
|
+
class ValueStruct < Synchronization::Object
|
29
29
|
|
30
30
|
def initialize(attributes)
|
31
31
|
raise ArgumentError.new('attributes must be given as a hash') unless attributes.respond_to?(:each_pair)
|
32
|
+
super
|
32
33
|
@attribute_hash = {}
|
33
34
|
attributes.each_pair do |field, value|
|
34
35
|
set_attribute(field, value)
|
35
36
|
end
|
36
37
|
@attribute_hash.freeze
|
38
|
+
ensure_ivar_visibility!
|
37
39
|
self.freeze
|
38
40
|
end
|
39
41
|
|
data/lib/functional/version.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'spec_helper'
|
2
1
|
require 'ostruct'
|
3
2
|
|
4
3
|
class Bar
|
@@ -194,7 +193,7 @@ describe 'complex pattern matching' do
|
|
194
193
|
|
195
194
|
specify { expect(Fizzbuzz.new.who(5)).to eq 15 }
|
196
195
|
specify { expect(Fizzbuzz.new.who()).to eq 0 }
|
197
|
-
specify {
|
196
|
+
specify {
|
198
197
|
expect {
|
199
198
|
Fizzbuzz.new.who('Jerry', 'secret middle name', "D'Antonio")
|
200
199
|
}.to raise_error(NoMethodError)
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
1
|
module Functional
|
4
2
|
|
5
3
|
describe Memo do
|
@@ -163,30 +161,29 @@ module Functional
|
|
163
161
|
|
164
162
|
context 'thread safety' do
|
165
163
|
|
166
|
-
let(:
|
164
|
+
let(:memoizer_factory){ Functional::Memo::ClassMethods.const_get(:Memoizer) }
|
165
|
+
let(:memoizer){ memoizer_factory.new(:func, 0) }
|
167
166
|
|
168
167
|
before(:each) do
|
169
|
-
allow(
|
170
|
-
allow(mutex).to receive(:lock).with(no_args).and_return(mutex)
|
171
|
-
allow(mutex).to receive(:unlock).with(no_args).and_return(mutex)
|
168
|
+
allow(memoizer_factory).to receive(:new).with(any_args).and_return(memoizer)
|
172
169
|
end
|
173
170
|
|
174
171
|
it 'locks a mutex whenever a memoized function is called' do
|
175
|
-
expect(
|
172
|
+
expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args)
|
176
173
|
|
177
174
|
subject.memoize(:increment)
|
178
175
|
subject.increment(0)
|
179
176
|
end
|
180
177
|
|
181
178
|
it 'unlocks the mutex whenever a memoized function is called' do
|
182
|
-
expect(
|
179
|
+
expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args)
|
183
180
|
|
184
181
|
subject.memoize(:increment)
|
185
182
|
subject.increment(0)
|
186
183
|
end
|
187
184
|
|
188
185
|
it 'unlocks the mutex when the method call raises an exception' do
|
189
|
-
expect(
|
186
|
+
expect(memoizer).to receive(:synchronize).exactly(:once).with(no_args)
|
190
187
|
|
191
188
|
subject.memoize(:exception)
|
192
189
|
begin
|
@@ -197,7 +194,7 @@ module Functional
|
|
197
194
|
end
|
198
195
|
|
199
196
|
it 'uses different mutexes for different functions' do
|
200
|
-
expect(
|
197
|
+
expect(memoizer_factory).to receive(:new).with(any_args).exactly(3).times.and_return(memoizer)
|
201
198
|
# once for memoize(:add) in the definition
|
202
199
|
subject.memoize(:increment)
|
203
200
|
subject.memoize(:exception)
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
1
|
describe 'protocol specification' do
|
4
2
|
|
5
3
|
before(:each) do
|
@@ -96,7 +94,7 @@ describe 'protocol specification' do
|
|
96
94
|
end
|
97
95
|
|
98
96
|
expect(
|
99
|
-
Functional::Protocol.Satisfy?(
|
97
|
+
Functional::Protocol.Satisfy?(clazz.new, :foo)
|
100
98
|
).to be false
|
101
99
|
end
|
102
100
|
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require 'spec_helper'
|
2
1
|
require_relative 'abstract_struct_shared'
|
2
|
+
require 'securerandom'
|
3
3
|
|
4
4
|
module Functional
|
5
5
|
|
@@ -16,12 +16,49 @@ module Functional
|
|
16
16
|
|
17
17
|
context 'definition' do
|
18
18
|
|
19
|
-
it '
|
20
|
-
Record.new(
|
21
|
-
expect(defined?(Record::Foo)).to
|
19
|
+
it 'does not register a new class when no name is given' do
|
20
|
+
Record.new(:foo, :bar, :baz)
|
21
|
+
expect(defined?(Record::Foo)).to be_falsey
|
22
22
|
end
|
23
23
|
|
24
|
-
it '
|
24
|
+
it 'creates a new class when given an array of field names' do
|
25
|
+
clazz = Record.new(:foo, :bar, :baz)
|
26
|
+
expect(clazz).to be_a Class
|
27
|
+
expect(clazz.ancestors).to include(Functional::AbstractStruct)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'registers the new class with Record when given a string name and an array' do
|
31
|
+
Record.new('Bar', :foo, :bar, :baz)
|
32
|
+
expect(defined?(Record::Bar)).to eq 'constant'
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'creates a new class when given a hash of field names and types/protocols' do
|
36
|
+
clazz = Record.new(foo: String, bar: String, baz: String)
|
37
|
+
expect(clazz).to be_a Class
|
38
|
+
expect(clazz.ancestors).to include(Functional::AbstractStruct)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'registers the new class with Record when given a string name and a hash' do
|
42
|
+
Record.new('Boom', foo: String, bar: String, baz: String)
|
43
|
+
expect(defined?(Record::Boom)).to eq 'constant'
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'raises an exception when given a hash with an invalid type/protocol' do
|
47
|
+
expect {
|
48
|
+
Record.new(foo: 'String', bar: String, baz: String)
|
49
|
+
}.to raise_error(ArgumentError)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'raises an exception when given an invalid definition' do
|
53
|
+
expect {
|
54
|
+
Record.new(:foo, bar: String, baz: String)
|
55
|
+
}.to raise_error(ArgumentError)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'initialization' do
|
60
|
+
|
61
|
+
it 'sets all fields values to nil' do
|
25
62
|
fields = [:foo, :bar, :baz]
|
26
63
|
clazz = Record.new(*fields)
|
27
64
|
|
@@ -41,74 +78,156 @@ module Functional
|
|
41
78
|
expect(record.baz).to eq 3
|
42
79
|
end
|
43
80
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
81
|
+
context 'with default values' do
|
82
|
+
|
83
|
+
it 'defaults fields to values given during class creation' do
|
84
|
+
clazz = Record.new(:foo, :bar, :baz) do
|
85
|
+
default :foo, 42
|
86
|
+
default :bar, 'w00t!'
|
87
|
+
end
|
88
|
+
|
89
|
+
record = clazz.new
|
90
|
+
expect(record.foo).to eq 42
|
91
|
+
expect(record.bar).to eq 'w00t!'
|
92
|
+
expect(record.baz).to be_nil
|
48
93
|
end
|
49
94
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
95
|
+
it 'overrides default values with values provided at object construction' do
|
96
|
+
clazz = Record.new(:foo, :bar, :baz) do
|
97
|
+
default :foo, 42
|
98
|
+
default :bar, 'w00t!'
|
99
|
+
default :baz, :bogus
|
100
|
+
end
|
55
101
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
102
|
+
record = clazz.new(foo: 1, bar: 2)
|
103
|
+
|
104
|
+
expect(record.foo).to eq 1
|
105
|
+
expect(record.bar).to eq 2
|
106
|
+
expect(record.baz).to eq :bogus
|
61
107
|
end
|
62
108
|
|
63
|
-
|
109
|
+
it 'duplicates default values when assigning to a new object' do
|
110
|
+
original = 'Foo'
|
111
|
+
clazz = Record.new(:foo, :bar, :baz) do
|
112
|
+
default :foo, original
|
113
|
+
end
|
64
114
|
|
65
|
-
|
66
|
-
|
67
|
-
|
115
|
+
record = clazz.new
|
116
|
+
expect(record.foo).to eq original
|
117
|
+
expect(record.foo.object_id).to_not eql original.object_id
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'does not conflate defaults across record classes' do
|
121
|
+
clazz_foo = Record.new(:foo, :bar, :baz) do
|
122
|
+
default :foo, 42
|
123
|
+
end
|
124
|
+
|
125
|
+
clazz_matz = Record.new(:foo, :bar, :baz) do
|
126
|
+
default :foo, 'Matsumoto'
|
127
|
+
end
|
128
|
+
|
129
|
+
expect(clazz_foo.new.foo).to eq 42
|
130
|
+
expect(clazz_matz.new.foo).to eq 'Matsumoto'
|
131
|
+
end
|
68
132
|
end
|
69
133
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
134
|
+
context 'with mandatory fields' do
|
135
|
+
|
136
|
+
it 'raises an exception when values for requred field are not provided' do
|
137
|
+
clazz = Record.new(:foo, :bar, :baz) do
|
138
|
+
mandatory :foo
|
139
|
+
end
|
140
|
+
|
141
|
+
expect {
|
142
|
+
clazz.new(bar: 1)
|
143
|
+
}.to raise_exception(ArgumentError)
|
74
144
|
end
|
75
145
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
146
|
+
it 'raises an exception when required values are nil' do
|
147
|
+
clazz = Record.new(:foo, :bar, :baz) do
|
148
|
+
mandatory :foo
|
149
|
+
end
|
80
150
|
|
81
|
-
|
82
|
-
|
83
|
-
|
151
|
+
expect {
|
152
|
+
clazz.new(foo: nil, bar: 1)
|
153
|
+
}.to raise_exception(ArgumentError)
|
84
154
|
end
|
85
155
|
|
86
|
-
|
87
|
-
|
156
|
+
it 'allows multiple required fields to be specified together' do
|
157
|
+
clazz = Record.new(:foo, :bar, :baz) do
|
158
|
+
mandatory :foo, :bar, :baz
|
159
|
+
end
|
160
|
+
|
161
|
+
expect {
|
162
|
+
clazz.new(foo: 1, bar: 2)
|
163
|
+
}.to raise_exception(ArgumentError)
|
164
|
+
|
165
|
+
expect {
|
166
|
+
clazz.new(bar: 2, baz: 3)
|
167
|
+
}.to raise_exception(ArgumentError)
|
168
|
+
|
169
|
+
expect {
|
170
|
+
clazz.new(foo: 1, bar: 2, baz: 3)
|
171
|
+
}.to_not raise_exception
|
88
172
|
end
|
89
173
|
|
90
|
-
|
91
|
-
|
174
|
+
it 'does not conflate default values across record classes' do
|
175
|
+
clazz_foo = Record.new(:foo, :bar, :baz) do
|
176
|
+
mandatory :foo
|
177
|
+
end
|
178
|
+
|
179
|
+
clazz_baz = Record.new(:foo, :bar, :baz) do
|
180
|
+
mandatory :baz
|
181
|
+
end
|
182
|
+
|
183
|
+
expect {
|
184
|
+
clazz_foo.new(foo: 42)
|
185
|
+
}.to_not raise_error
|
186
|
+
|
187
|
+
expect {
|
188
|
+
clazz_baz.new(baz: 42)
|
189
|
+
}.to_not raise_error
|
190
|
+
end
|
92
191
|
end
|
93
192
|
|
94
|
-
|
95
|
-
|
96
|
-
|
193
|
+
context 'with field type specification' do
|
194
|
+
|
195
|
+
let(:type_safe_definition) do
|
196
|
+
{foo: String, bar: Fixnum, baz: protocol}
|
97
197
|
end
|
98
198
|
|
99
|
-
|
100
|
-
clazz.new(bar: 1)
|
101
|
-
}.to raise_exception(ArgumentError)
|
102
|
-
end
|
199
|
+
let(:protocol){ SecureRandom.uuid.to_sym }
|
103
200
|
|
104
|
-
|
105
|
-
|
106
|
-
|
201
|
+
let(:clazz_with_protocol) do
|
202
|
+
Class.new do
|
203
|
+
def foo() nil end
|
204
|
+
end
|
107
205
|
end
|
108
206
|
|
109
|
-
|
110
|
-
|
111
|
-
|
207
|
+
let(:record_clazz) do
|
208
|
+
Record.new(type_safe_definition)
|
209
|
+
end
|
210
|
+
|
211
|
+
before(:each) do
|
212
|
+
Functional::SpecifyProtocol(protocol){ instance_method(:foo) }
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'raises an exception for a value with an invalid type' do
|
216
|
+
expect {
|
217
|
+
record_clazz.new(foo: 'foo', bar: 'bar', baz: clazz_with_protocol.new)
|
218
|
+
}.to raise_error(ArgumentError)
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'raises an exception for a value that does not satisfy a protocol' do
|
222
|
+
expect {
|
223
|
+
record_clazz.new(foo: 'foo', bar: 42, baz: 'baz')
|
224
|
+
}.to raise_error(ArgumentError)
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'creates the object when all values match the appropriate types and protocols' do
|
228
|
+
record = record_clazz.new(foo: 'foo', bar: 42, baz: clazz_with_protocol.new)
|
229
|
+
expect(record).to be_a record_clazz
|
230
|
+
end
|
112
231
|
end
|
113
232
|
|
114
233
|
it 'allows a field to be required and have a default value' do
|
@@ -124,24 +243,6 @@ module Functional
|
|
124
243
|
expect(clazz.new.foo).to eq 42
|
125
244
|
end
|
126
245
|
|
127
|
-
it 'allows multiple required fields to be specified together' do
|
128
|
-
clazz = Record.new(:foo, :bar, :baz) do
|
129
|
-
mandatory :foo, :bar, :baz
|
130
|
-
end
|
131
|
-
|
132
|
-
expect {
|
133
|
-
clazz.new(foo: 1, bar: 2)
|
134
|
-
}.to raise_exception(ArgumentError)
|
135
|
-
|
136
|
-
expect {
|
137
|
-
clazz.new(bar: 2, baz: 3)
|
138
|
-
}.to raise_exception(ArgumentError)
|
139
|
-
|
140
|
-
expect {
|
141
|
-
clazz.new(foo: 1, bar: 2, baz: 3)
|
142
|
-
}.to_not raise_exception
|
143
|
-
end
|
144
|
-
|
145
246
|
it 'raises an exception if the default value for a require field is nil' do
|
146
247
|
clazz = Record.new(:foo, :bar, :baz) do
|
147
248
|
mandatory :foo
|
@@ -152,24 +253,6 @@ module Functional
|
|
152
253
|
clazz.new
|
153
254
|
}.to raise_exception(ArgumentError)
|
154
255
|
end
|
155
|
-
|
156
|
-
it 'does not conflate default values across record classes' do
|
157
|
-
clazz_foo = Record.new(:foo, :bar, :baz) do
|
158
|
-
mandatory :foo
|
159
|
-
end
|
160
|
-
|
161
|
-
clazz_baz = Record.new(:foo, :bar, :baz) do
|
162
|
-
mandatory :baz
|
163
|
-
end
|
164
|
-
|
165
|
-
expect {
|
166
|
-
clazz_foo.new(foo: 42)
|
167
|
-
}.to_not raise_error
|
168
|
-
|
169
|
-
expect {
|
170
|
-
clazz_baz.new(baz: 42)
|
171
|
-
}.to_not raise_error
|
172
|
-
end
|
173
256
|
end
|
174
257
|
|
175
258
|
context 'subclassing' do
|