functional-ruby 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,8 @@ 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
6
8
  module TypeCheck
7
9
 
8
10
  # Performs an `is_a?` check of the given value object against the
@@ -52,6 +52,8 @@ module Functional
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
+ #
55
57
  # @!macro thread_safe_immutable_object
56
58
  module Union
57
59
  extend self
@@ -0,0 +1,144 @@
1
+ module Functional
2
+
3
+ # A variation on Ruby's `OpenStruct` in which all fields are immutable and
4
+ # set at instantiation. For compatibility with {Functional::FinalStruct},
5
+ # predicate methods exist for all potential fields and these predicates
6
+ # indicate if the field has been set. Calling a predicate method for a field
7
+ # that does not exist on the struct will return false.
8
+ #
9
+ # Unlike {Functional::Record}, which returns a new class which can be used to
10
+ # create immutable objects, `ValueStruct` creates simple immutable objects.
11
+ #
12
+ # @example Instanciation
13
+ # name = Functional::ValueStruct.new(first: 'Douglas', last: 'Adams')
14
+ #
15
+ # name.first #=> 'Douglas'
16
+ # name.last #=> 'Adams'
17
+ # name.first? #=> true
18
+ # name.last? #=> true
19
+ # name.middle? #=> false
20
+ #
21
+ # @since 1.1.0
22
+ #
23
+ # @see Functional::Record
24
+ # @see Functional::FinalStruct
25
+ # @see http://www.ruby-doc.org/stdlib-2.1.2/libdoc/ostruct/rdoc/OpenStruct.html
26
+ #
27
+ # @!macro thread_safe_immutable_object
28
+ class ValueStruct
29
+
30
+ def initialize(attributes)
31
+ raise ArgumentError.new('attributes must be given as a hash') unless attributes.respond_to?(:each_pair)
32
+ @attribute_hash = {}
33
+ attributes.each_pair do |field, value|
34
+ set_attribute(field, value)
35
+ end
36
+ @attribute_hash.freeze
37
+ self.freeze
38
+ end
39
+
40
+ # Get the value of the given field.
41
+ #
42
+ # @param [Symbol] field the field to retrieve the value for
43
+ # @return [Object] the value of the field is set else nil
44
+ def get(field)
45
+ @attribute_hash[field.to_sym]
46
+ end
47
+ alias_method :[], :get
48
+
49
+ # Check the internal hash to unambiguously verify that the given
50
+ # attribute has been set.
51
+ #
52
+ # @param [Symbol] field the field to get the value for
53
+ # @return [Boolean] true if the field has been set else false
54
+ def set?(field)
55
+ @attribute_hash.has_key?(field.to_sym)
56
+ end
57
+
58
+ # Get the current value of the given field if already set else return the given
59
+ # default value.
60
+ #
61
+ # @param [Symbol] field the field to get the value for
62
+ # @param [Object] default the value to return if the field has not been set
63
+ # @return [Object] the value of the given field else the given default value
64
+ def fetch(field, default)
65
+ @attribute_hash.fetch(field.to_sym, default)
66
+ end
67
+
68
+ # Calls the block once for each attribute, passing the key/value pair as parameters.
69
+ # If no block is given, an enumerator is returned instead.
70
+ #
71
+ # @yieldparam [Symbol] field the struct field for the current iteration
72
+ # @yieldparam [Object] value the value of the current field
73
+ #
74
+ # @return [Enumerable] when no block is given
75
+ def each_pair
76
+ return enum_for(:each_pair) unless block_given?
77
+ @attribute_hash.each do |field, value|
78
+ yield(field, value)
79
+ end
80
+ end
81
+
82
+ # Converts the `ValueStruct` to a `Hash` with keys representing each attribute
83
+ # (as symbols) and their corresponding values.
84
+ #
85
+ # @return [Hash] a `Hash` representing this struct
86
+ def to_h
87
+ @attribute_hash.dup # dup removes the frozen flag
88
+ end
89
+
90
+ # Compares this object and other for equality. A `ValueStruct` is `eql?` to
91
+ # other when other is a `ValueStruct` and the two objects have identical
92
+ # fields and values.
93
+ #
94
+ # @param [Object] other the other record to compare for equality
95
+ # @return [Boolean] true when equal else false
96
+ def eql?(other)
97
+ other.is_a?(self.class) && @attribute_hash == other.to_h
98
+ end
99
+ alias_method :==, :eql?
100
+
101
+ # Describe the contents of this object in a string.
102
+ #
103
+ # @return [String] the string representation of this object
104
+ #
105
+ # @!visibility private
106
+ def inspect
107
+ state = @attribute_hash.to_s.gsub(/^{/, '').gsub(/}$/, '')
108
+ "#<#{self.class} #{state}>"
109
+ end
110
+ alias_method :to_s, :inspect
111
+
112
+ protected
113
+
114
+ # Set the value of the give field to the given value.
115
+ #
116
+ # @param [Symbol] field the field to set the value for
117
+ # @param [Object] value the value to set the field to
118
+ # @return [Object] the final value of the given field
119
+ #
120
+ # @!visibility private
121
+ def set_attribute(field, value)
122
+ @attribute_hash[field.to_sym] = value
123
+ end
124
+
125
+ # Check the method name and args for signatures matching potential
126
+ # final predicate methods. If the signature matches call the appropriate
127
+ # method
128
+ #
129
+ # @param [Symbol] symbol the name of the called function
130
+ # @param [Array] args zero or more arguments
131
+ # @return [Object] the result of the proxied method or the `super` call
132
+ #
133
+ # @!visibility private
134
+ def method_missing(symbol, *args)
135
+ if args.length == 0 && (match = /([^\?]+)\?$/.match(symbol))
136
+ set?(match[1])
137
+ elsif args.length == 0 && set?(symbol)
138
+ get(symbol)
139
+ else
140
+ super
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,5 +1,5 @@
1
1
  module Functional
2
2
 
3
3
  # The current gem version.
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -0,0 +1,266 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ module Functional
5
+
6
+ describe FinalStruct do
7
+
8
+ context 'instanciation' do
9
+
10
+ specify 'with no args defines no fields' do
11
+ subject = FinalStruct.new
12
+ expect(subject.to_h).to be_empty
13
+ end
14
+
15
+ specify 'with a hash sets fields using has values' do
16
+ subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
17
+ expect(subject.foo).to eq 1
18
+ expect(subject.bar).to eq :two
19
+ expect(subject.baz).to eq 'three'
20
+ end
21
+
22
+ specify 'with a hash creates true predicates for has keys' do
23
+ subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
24
+ expect(subject.foo?).to be true
25
+ expect(subject.bar?).to be true
26
+ expect(subject.baz?).to be true
27
+ end
28
+
29
+ specify 'can be created from any object that responds to #to_h' do
30
+ clazz = Class.new do
31
+ def to_h; {answer: 42, harmless: 'mostly'}; end
32
+ end
33
+ struct = clazz.new
34
+ subject = FinalStruct.new(struct)
35
+ expect(subject.answer).to eq 42
36
+ expect(subject.harmless).to eq 'mostly'
37
+ end
38
+
39
+ specify 'raises an exception if given a non-hash argument' do
40
+ expect {
41
+ FinalStruct.new(:bogus)
42
+ }.to raise_error(ArgumentError)
43
+ end
44
+ end
45
+
46
+ context 'set fields' do
47
+
48
+ subject do
49
+ struct = FinalStruct.new
50
+ struct.foo = 42
51
+ struct.bar = "Don't Panic"
52
+ struct
53
+ end
54
+
55
+ specify 'have a reader which returns the value' do
56
+ expect(subject.foo).to eq 42
57
+ expect(subject.bar).to eq "Don't Panic"
58
+ end
59
+
60
+ specify 'have a predicate which returns true' do
61
+ expect(subject.foo?).to be true
62
+ expect(subject.bar?).to be true
63
+ end
64
+
65
+ specify 'raise an exception when written to again' do
66
+ expect {subject.foo = 0}.to raise_error(Functional::FinalityError)
67
+ expect {subject.bar = 0}.to raise_error(Functional::FinalityError)
68
+ end
69
+ end
70
+
71
+ context 'unset fields' do
72
+
73
+ subject { FinalStruct.new }
74
+
75
+ specify 'have a magic reader that always returns nil' do
76
+ expect(subject.foo).to be nil
77
+ expect(subject.bar).to be nil
78
+ expect(subject.baz).to be nil
79
+ end
80
+
81
+ specify 'have a magic predicate that always returns false' do
82
+ expect(subject.foo?).to be false
83
+ expect(subject.bar?).to be false
84
+ expect(subject.baz?).to be false
85
+ end
86
+
87
+ specify 'have a magic writer that sets the field' do
88
+ expect(subject.foo = 42).to eq 42
89
+ expect(subject.bar = :towel).to eq :towel
90
+ expect(subject.baz = "Don't Panic").to eq "Don't Panic"
91
+ end
92
+ end
93
+
94
+ context 'accessors' do
95
+
96
+ let!(:field_value_pairs) { {foo: 1, bar: :two, baz: 'three'} }
97
+
98
+ subject { FinalStruct.new(field_value_pairs) }
99
+
100
+ specify '#get returns the value of a set field' do
101
+ expect(subject.get(:foo)).to eq 1
102
+ end
103
+
104
+ specify '#get returns nil for an unset field' do
105
+ expect(subject.get(:bogus)).to be nil
106
+ end
107
+
108
+ specify '#[] is an alias for #get' do
109
+ expect(subject[:foo]).to eq 1
110
+ expect(subject[:bogus]).to be nil
111
+ end
112
+
113
+ specify '#set sets the value of an unset field' do
114
+ subject.set(:harmless, 'mostly')
115
+ expect(subject.harmless).to eq 'mostly'
116
+ expect(subject.harmless?).to be true
117
+ end
118
+
119
+ specify '#set raises an exception if the field has already been set' do
120
+ subject.set(:harmless, 'mostly')
121
+ expect {
122
+ subject.set(:harmless, 'extremely')
123
+ }.to raise_error(Functional::FinalityError)
124
+ end
125
+
126
+ specify '#[]= is an alias for set' do
127
+ subject[:harmless] = 'mostly'
128
+ expect(subject.harmless).to eq 'mostly'
129
+ expect {
130
+ subject[:harmless] = 'extremely'
131
+ }.to raise_error(Functional::FinalityError)
132
+ end
133
+
134
+ specify '#set? returns false for an unset field' do
135
+ expect(subject.set?(:harmless)).to be false
136
+ end
137
+
138
+ specify '#set? returns true for a field that has been set' do
139
+ subject.set(:harmless, 'mostly')
140
+ expect(subject.set?(:harmless)).to be true
141
+ end
142
+
143
+ specify '#get_or_set returns the value of a set field' do
144
+ subject.answer = 42
145
+ expect(subject.get_or_set(:answer, 100)).to eq 42
146
+ end
147
+
148
+ specify '#get_or_set sets the value of an unset field' do
149
+ subject.get_or_set(:answer, 42)
150
+ expect(subject.answer).to eq 42
151
+ expect(subject.answer?).to be true
152
+ end
153
+
154
+ specify '#get_or_set returns the value of a newly set field' do
155
+ expect(subject.get_or_set(:answer, 42)).to eq 42
156
+ end
157
+
158
+ specify '#fetch gets the value of a set field' do
159
+ subject.harmless = 'mostly'
160
+ expect(subject.fetch(:harmless, 'extremely')).to eq 'mostly'
161
+ end
162
+
163
+ specify '#fetch returns the given value when the field is unset' do
164
+ expect(subject.fetch(:harmless, 'extremely')).to eq 'extremely'
165
+ end
166
+
167
+ specify '#fetch does not set an unset field' do
168
+ subject.fetch(:answer, 42)
169
+ expect(subject.answer).to be_nil
170
+ expect(subject.answer?).to be false
171
+ end
172
+
173
+ specify '#to_h returns the key/value pairs for all set values' do
174
+ subject = FinalStruct.new(field_value_pairs)
175
+ expect(subject.to_h).to eq field_value_pairs
176
+ end
177
+
178
+ specify '#to_h is updated when new fields are added' do
179
+ subject = FinalStruct.new
180
+ field_value_pairs.each_pair do |field, value|
181
+ subject.set(field, value)
182
+ end
183
+ expect(subject.to_h).to eq field_value_pairs
184
+ end
185
+
186
+ specify '#each_pair returns an Enumerable when no block given' do
187
+ subject = FinalStruct.new(field_value_pairs)
188
+ expect(subject.each_pair).to be_a Enumerable
189
+ end
190
+
191
+ specify '#each_pair enumerates over each field/value pair' do
192
+ subject = FinalStruct.new(field_value_pairs)
193
+ result = {}
194
+
195
+ subject.each_pair do |field, value|
196
+ result[field] = value
197
+ end
198
+
199
+ expect(result).to eq field_value_pairs
200
+ end
201
+ end
202
+
203
+ context 'reflection' do
204
+
205
+ specify '#eql? returns true when both define the same fields with the same values' do
206
+ first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
207
+ second = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
208
+
209
+ expect(first.eql?(second)).to be true
210
+ expect(first == second).to be true
211
+ end
212
+
213
+ specify '#eql? returns false when other has different fields defined' do
214
+ first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
215
+ second = FinalStruct.new(foo: 1, 'bar' => :two)
216
+
217
+ expect(first.eql?(second)).to be false
218
+ expect(first == second).to be false
219
+ end
220
+
221
+ specify '#eql? returns false when other has different field values' do
222
+ first = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
223
+ second = FinalStruct.new(foo: 1, 'bar' => :two, baz: 3)
224
+
225
+ expect(first.eql?(second)).to be false
226
+ expect(first == second).to be false
227
+ end
228
+
229
+ specify '#eql? returns false when other is not a FinalStruct' do
230
+ attributes = {answer: 42, harmless: 'mostly'}
231
+ clazz = Class.new do
232
+ def to_h; {answer: 42, harmless: 'mostly'}; end
233
+ end
234
+ other = clazz.new
235
+ subject = FinalStruct.new(attributes)
236
+ expect(subject.eql?(other)).to be false
237
+ expect(subject == other).to be false
238
+ end
239
+
240
+ specify '#inspect begins with the class name' do
241
+ subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
242
+ expect(subject.inspect).to match(/^#<#{described_class}\s+/)
243
+ end
244
+
245
+ specify '#inspect includes all field/value pairs' do
246
+ field_value_pairs = {foo: 1, 'bar' => :two, baz: 'three'}
247
+ subject = FinalStruct.new(field_value_pairs)
248
+
249
+ field_value_pairs.each do |field, value|
250
+ expect(subject.inspect).to match(/:#{field}=>"?:?#{value}"?/)
251
+ end
252
+ end
253
+
254
+ specify '#to_s returns the same value as #inspect' do
255
+ subject = FinalStruct.new(foo: 1, 'bar' => :two, baz: 'three')
256
+ expect(subject.to_s).to eq subject.inspect
257
+ end
258
+
259
+ specify '#method_missing raises an exception for methods with unrecognized signatures' do
260
+ expect {
261
+ subject.foo(1, 2, 3)
262
+ }.to raise_error(NoMethodError)
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,169 @@
1
+ require 'spec_helper'
2
+
3
+ module Functional
4
+
5
+ describe FinalVar do
6
+
7
+ context 'instanciation' do
8
+
9
+ it 'is unset when no arguments given' do
10
+ expect(FinalVar.new).to_not be_set
11
+ end
12
+
13
+ it 'is set with the given argument' do
14
+ expect(FinalVar.new(41)).to be_set
15
+ end
16
+ end
17
+
18
+ context '#get' do
19
+
20
+ subject { FinalVar.new }
21
+
22
+ it 'returns nil when unset' do
23
+ expect(subject.get).to be nil
24
+ end
25
+
26
+ it 'returns the value when set' do
27
+ expect(FinalVar.new(42).get).to eq 42
28
+ end
29
+
30
+ it 'is aliased as #value' do
31
+ expect(subject.value).to be nil
32
+ subject.set(42)
33
+ expect(subject.value).to eq 42
34
+ end
35
+ end
36
+
37
+ context '#set' do
38
+
39
+ subject { FinalVar.new }
40
+
41
+ it 'sets the value when unset' do
42
+ subject.set(42)
43
+ expect(subject.get).to eq 42
44
+ end
45
+
46
+ it 'returns the new value when unset' do
47
+ expect(subject.set(42)).to eq 42
48
+ end
49
+
50
+ it 'raises an exception when already set' do
51
+ subject.set(42)
52
+ expect {
53
+ subject.set(42)
54
+ }.to raise_error(Functional::FinalityError)
55
+ end
56
+
57
+ it 'is aliased as #value=' do
58
+ subject.value = 42
59
+ expect(subject.get).to eq 42
60
+ end
61
+ end
62
+
63
+ context '#set?' do
64
+
65
+ it 'returns false when unset' do
66
+ expect(FinalVar.new).to_not be_set
67
+ end
68
+
69
+ it 'returns true when set' do
70
+ expect(FinalVar.new(42)).to be_set
71
+ end
72
+
73
+ it 'is aliased as value?' do
74
+ expect(FinalVar.new.value?).to be false
75
+ expect(FinalVar.new(42).value?).to be true
76
+ end
77
+ end
78
+
79
+ context '#get_or_set' do
80
+
81
+ it 'sets the value when unset' do
82
+ subject = FinalVar.new
83
+ subject.get_or_set(42)
84
+ expect(subject.get).to eq 42
85
+ end
86
+
87
+ it 'returns the new value when previously unset' do
88
+ subject = FinalVar.new
89
+ expect(subject.get_or_set(42)).to eq 42
90
+ end
91
+
92
+ it 'returns the current value when already set' do
93
+ subject = FinalVar.new(100)
94
+ expect(subject.get_or_set(42)).to eq 100
95
+ end
96
+ end
97
+
98
+ context '#fetch' do
99
+
100
+ it 'returns the given default value when unset' do
101
+ subject = FinalVar.new
102
+ expect(subject.fetch(42)).to eq 42
103
+ end
104
+
105
+ it 'does not change the current value when unset' do
106
+ subject = FinalVar.new
107
+ subject.fetch(42)
108
+ expect(subject.get).to be nil
109
+ end
110
+
111
+ it 'returns the current value when already set' do
112
+ subject = FinalVar.new(100)
113
+ expect(subject.get_or_set(42)).to eq 100
114
+ end
115
+ end
116
+
117
+ context 'reflection' do
118
+
119
+ specify '#eql? returns false when unset' do
120
+ expect(FinalVar.new.eql?(nil)).to be false
121
+ expect(FinalVar.new.eql?(42)).to be false
122
+ expect(FinalVar.new.eql?(FinalVar.new.value)).to be false
123
+ end
124
+
125
+ specify '#eql? returns false when set and the value does not match other' do
126
+ subject = FinalVar.new(42)
127
+ expect(subject.eql?(100)).to be false
128
+ end
129
+
130
+ specify '#eql? returns true when set and the value matches other' do
131
+ subject = FinalVar.new(42)
132
+ expect(subject.eql?(42)).to be true
133
+ end
134
+
135
+ specify '#eql? returns true when set and other is a FinalVar with the same value' do
136
+ subject = FinalVar.new(42)
137
+ other = FinalVar.new(42)
138
+ expect(subject.eql?(other)).to be true
139
+ end
140
+
141
+ specify 'aliases #== as #eql?' do
142
+ expect(FinalVar.new == nil).to be false
143
+ expect(FinalVar.new == 42).to be false
144
+ expect(FinalVar.new == FinalVar.new).to be false
145
+ expect(FinalVar.new(42) == 42).to be true
146
+ expect(FinalVar.new(42) == FinalVar.new(42)).to be true
147
+ end
148
+
149
+ specify '#inspect includes the word "value" and the value when set' do
150
+ subject = FinalVar.new(42)
151
+ expect(subject.inspect).to match(/value\s?=\s?42\s*>$/)
152
+ end
153
+
154
+ specify '#inspect include the word "unset" when unset' do
155
+ subject = FinalVar.new
156
+ expect(subject.inspect).to match(/unset\s*>$/i)
157
+ end
158
+
159
+ specify '#to_s returns nil as a string when unset' do
160
+ expect(FinalVar.new.to_s).to eq nil.to_s
161
+ end
162
+
163
+ specify '#to_s returns the value as a string when set' do
164
+ expect(FinalVar.new(42).to_s).to eq 42.to_s
165
+ expect(FinalVar.new('42').to_s).to eq '42'
166
+ end
167
+ end
168
+ end
169
+ end