functional-ruby 1.0.0 → 1.1.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.
@@ -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