eigindir 0.0.1

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.
data/lib/eigindir.rb ADDED
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative "eigindir/patches"
4
+ require_relative "eigindir/coercer"
5
+ require_relative "eigindir/api"
6
+
7
+ # Module Eigindir provides PORO attributes declaration and coersion
8
+ #
9
+ # @example
10
+ # class Foo
11
+ # include Eigindir
12
+ #
13
+ # attribute(
14
+ # :foo,
15
+ # writer: proc { |val| val.to_i + 1 },
16
+ # reader: proc { |val| val.to_s }
17
+ # )
18
+ # end
19
+ #
20
+ # @author Andrew Kozin <Andrew.Kozin@gmail.com>
21
+ #
22
+ # @api public
23
+ module Eigindir
24
+ using Patches
25
+
26
+ # Returns the hash of the object's attributes
27
+ #
28
+ # @return [Hash]
29
+ def attributes
30
+ __readers.zip(__readers.map(&method(:public_send))).to_h
31
+ end
32
+
33
+ # Assigns attributes from hash
34
+ #
35
+ # Unknown attributes are ignored
36
+ #
37
+ # @param [Hash] options
38
+ #
39
+ # @return [self] itself
40
+ def attributes=(options)
41
+ options
42
+ .normalize
43
+ .slice(*__writers)
44
+ .each { |key, value| public_send :"#{ key }=", value }
45
+ end
46
+
47
+ # @!parse extend Eigindir::API
48
+ # @private
49
+ def self.included(klass)
50
+ klass.extend API
51
+ end
52
+
53
+ private
54
+
55
+ def __readers
56
+ self.class.__send__ :__readers
57
+ end
58
+
59
+ def __writers
60
+ self.class.__send__ :__writers
61
+ end
62
+
63
+ end # module Eigindir
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+
3
+ module Eigindir
4
+
5
+ # The module provides class-level API
6
+ #
7
+ # @author Andrew Kozin <Andrew.Kozin@gmail.com>
8
+ module API
9
+
10
+ # Declares the attribute
11
+ #
12
+ # @param (see #attribute_reader)
13
+ # @option (see #attribute_reader)
14
+ # @option [Proc, Symbol, String] :reader
15
+ # The coercer to be used by method getter
16
+ # @option [Proc, Symbol, String] :writer
17
+ # The coercer to be used by method writer
18
+ #
19
+ # @return [undefined]
20
+ def attribute(name, coerce: nil, reader: nil, writer: nil, strict: nil)
21
+ attribute_reader name, coerce: (reader || coerce), strict: strict
22
+ attribute_writer name, coerce: (writer || coerce), strict: strict
23
+ end
24
+
25
+ # @!method attribute_reader(name, coerce: nil, strict: nil)
26
+ # Declares the attribute getter
27
+ #
28
+ # @param [Symbol, String] name
29
+ # The name of the attribute
30
+ # @option [Proc, Symbol, String] :coerce
31
+ # The coercer for the attribute
32
+ # @option [Boolean] :strict
33
+ # Whether +nil+ should be coerced
34
+ #
35
+ # @return [undefined]
36
+ def attribute_reader(name, **options)
37
+ __declare_reader name, Coercer.new(options)
38
+ __readers << name.to_sym
39
+ end
40
+
41
+ # @!method attribute_writer(name, coerce: nil, strict: nil)
42
+ # Declares the attribute writer
43
+ #
44
+ # @param (see #attribute_reader)
45
+ # @option (see #attribute_reader)
46
+ #
47
+ # @return [undefined]
48
+ def attribute_writer(name, **options)
49
+ __declare_writer name, Coercer.new(options)
50
+ __writers << name.to_sym
51
+ end
52
+
53
+ private
54
+
55
+ def __readers
56
+ @__readers ||= []
57
+ end
58
+
59
+ def __writers
60
+ @__writers ||= []
61
+ end
62
+
63
+ def __declare_reader(name, coercer)
64
+ return attr_reader(name) unless coercer
65
+ define_method(name) do
66
+ coercer.call self, instance_variable_get(:"@#{ name }")
67
+ end
68
+ end
69
+
70
+ def __declare_writer(name, coercer)
71
+ return attr_writer(name) unless coercer
72
+ define_method("#{ name }=") do |value|
73
+ instance_variable_set :"@#{ name }", coercer.call(self, value)
74
+ end
75
+ end
76
+
77
+ end # module API
78
+
79
+ end # module Eigindir
@@ -0,0 +1,70 @@
1
+ # encoding: utf-8
2
+
3
+ module Eigindir
4
+
5
+ # Class Coercer creates the proc to be called by attribute getter and setter
6
+ #
7
+ # @author Andrew Kozin <Andrew.Kozin@gmail.com>
8
+ #
9
+ # @api private
10
+ class Coercer
11
+
12
+ # Checks the coercer validity and creates its istance
13
+ #
14
+ # @option [Proc, Symbol, String, NilClass] :coerce
15
+ # @option [Boolean] :strict
16
+ #
17
+ # @return [Eigindir::Coercer]
18
+ def self.new(coerce: nil, strict: nil)
19
+ super if coerce
20
+ end
21
+
22
+ # @private
23
+ def initialize(coerce: nil, strict: nil)
24
+ @coerce = coerce
25
+ @strict = strict
26
+ check_type
27
+ check_arity
28
+ end
29
+
30
+ # Coerces a value in a context of some instance
31
+ #
32
+ # @param [Object] instance
33
+ # The object whose method is used to coerce value
34
+ # @param [Object] value
35
+ # The value to coerce
36
+ #
37
+ # @return [Object] the coerced value
38
+ def call(instance, value)
39
+ proc.call(instance, value) if value || strict
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :coerce, :strict, :proc
45
+
46
+ def proc
47
+ coerce.instance_of?(Proc) ? proc_coercer : method_coercer
48
+ end
49
+
50
+ def method_coercer
51
+ ->(instance, value) { instance.__send__ coerce, value }
52
+ end
53
+
54
+ def proc_coercer
55
+ ->(_, value) { coerce.call value }
56
+ end
57
+
58
+ def check_type
59
+ return if [Proc, String, Symbol].include? coerce.class
60
+ fail TypeError.new "#{ coerce } is not a Proc, Symbol, or String"
61
+ end
62
+
63
+ def check_arity
64
+ return unless coerce.instance_of?(Proc) && coerce.arity != 1
65
+ fail ArgumentError.new "Coercer should take exactly one argument"
66
+ end
67
+
68
+ end # class Coercer
69
+
70
+ end # module Eigindir
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+
3
+ module Eigindir
4
+
5
+ # @api private
6
+ module Patches
7
+
8
+ refine Hash do
9
+
10
+ # Returns a new hash whose keys are symbolized
11
+ #
12
+ # @example
13
+ # { "foo" => "foo", "bar" => { "baz" => "baz" } }.normalize
14
+ # # => { foo: "foo", bar: { "baz" => "baz" } }
15
+ #
16
+ # @return [Hash]
17
+ def normalize
18
+ keys.map(&:to_sym).zip(values).to_h
19
+ end
20
+
21
+ # Returns a new hash with given keys only
22
+ #
23
+ # @example
24
+ # { foo: "foo", bar: "bar" }.slice(:foo, :baz)
25
+ # # => { foo: "foo" }
26
+ #
27
+ # @param [Array] list The keys to slice from the hash
28
+ #
29
+ # @return [Hash]
30
+ def slice(*list)
31
+ sliced_keys = keys & list
32
+ sliced_keys.zip(values_at(*sliced_keys)).to_h
33
+ end
34
+
35
+ end # refine Hash
36
+
37
+ end # module Patches
38
+
39
+ end # module Eigindir
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ module Eigindir
4
+
5
+ # The semantic version of the module.
6
+ # @see http://semver.org/ Semantic versioning 2.0
7
+ VERSION = "0.0.1".freeze
8
+
9
+ end # module Eigindir
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ begin
4
+ require "hexx-suit"
5
+ Hexx::Suit.load_metrics_for(self)
6
+ rescue LoadError
7
+ require "hexx-rspec"
8
+ Hexx::RSpec.load_metrics_for(self)
9
+ end
10
+
11
+ # Loads the code under test
12
+ require "eigindir"
@@ -0,0 +1,395 @@
1
+ # encoding: utf-8
2
+
3
+ describe Eigindir::API do
4
+
5
+ let(:coercer) { ->(value) { value.to_s } }
6
+ let(:klass) do
7
+ Class.new do
8
+ extend Eigindir::API
9
+
10
+ private
11
+
12
+ def coercer(value)
13
+ value.to_s
14
+ end
15
+ end
16
+ end
17
+
18
+ subject { klass.new }
19
+
20
+ shared_examples "adding readonly attribute :foo" do
21
+
22
+ it "declares the getter" do
23
+ expect(subject).to respond_to :foo
24
+ end
25
+
26
+ it "doesn't declare the setter" do
27
+ expect(subject).not_to respond_to :foo=
28
+ end
29
+
30
+ end # shared example
31
+
32
+ shared_examples "adding writeonly attribute :foo" do
33
+
34
+ it "declares the setter" do
35
+ expect(subject).to respond_to :foo=
36
+ end
37
+
38
+ it "doesn't declare the getter" do
39
+ expect(subject).not_to respond_to :foo
40
+ end
41
+
42
+ end # shared example
43
+
44
+ shared_examples "making :foo to coerce something" do
45
+
46
+ it "coerces the getter" do
47
+ subject.instance_eval "@foo = 1"
48
+ expect(subject.foo).to eq "1"
49
+ end
50
+
51
+ it "doesn't coerce nil" do
52
+ expect(subject.foo).to be_nil
53
+ end
54
+
55
+ end # shared example
56
+
57
+ shared_examples "making :foo to coerce anything" do
58
+
59
+ it "coerces the getter" do
60
+ subject.instance_eval "@foo = 1"
61
+ expect(subject.foo).to eq "1"
62
+ end
63
+
64
+ it "doesn't coerce nil" do
65
+ expect(subject.foo).to eq ""
66
+ end
67
+
68
+ end # shared example
69
+
70
+ shared_examples "making :foo= to coerce something" do
71
+
72
+ it "coerces the setter" do
73
+ subject.foo = 1
74
+ expect(subject.instance_eval "@foo").to eq "1"
75
+ end
76
+
77
+ it "doesn't coerce nil" do
78
+ subject.foo = nil
79
+ expect(subject.instance_eval "@foo").to be_nil
80
+ end
81
+
82
+ end # shared example
83
+
84
+ shared_examples "making :foo= to coerce anything" do
85
+
86
+ it "coerces the setter" do
87
+ subject.foo = 1
88
+ expect(subject.instance_eval "@foo").to eq "1"
89
+ end
90
+
91
+ it "coerces nil" do
92
+ subject.foo = nil
93
+ expect(subject.instance_eval "@foo").to eq ""
94
+ end
95
+
96
+ end # shared example
97
+
98
+ shared_examples "reporting a wrong coercer arity" do
99
+
100
+ it "fails with ArgumentError" do
101
+ expect { subject }.to raise_error ArgumentError
102
+ end
103
+
104
+ it "returns a proper error message" do
105
+ begin
106
+ subject
107
+ rescue => error
108
+ expect(error.message).to eq "Coercer should take exactly one argument"
109
+ end
110
+ end
111
+
112
+ end # shared example
113
+
114
+ shared_examples "reporting a wrong coercer type" do
115
+
116
+ it "fails with TypeError" do
117
+ expect { subject }.to raise_error TypeError
118
+ end
119
+
120
+ it "returns a proper error message" do
121
+ begin
122
+ subject
123
+ rescue => error
124
+ expect(error.message).to eq "1 is not a Proc, Symbol, or String"
125
+ end
126
+ end
127
+
128
+ end # shared example
129
+
130
+ describe ".attribute_reader" do
131
+
132
+ context "without coercer" do
133
+
134
+ before { klass.attribute_reader :foo }
135
+
136
+ it_behaves_like "adding readonly attribute :foo"
137
+
138
+ end # context
139
+
140
+ context "with proc coercer" do
141
+
142
+ before { klass.attribute_reader :foo, coerce: coercer.to_proc }
143
+
144
+ it_behaves_like "adding readonly attribute :foo"
145
+ it_behaves_like "making :foo to coerce something"
146
+
147
+ end # context
148
+
149
+ context "with lambda coercer" do
150
+
151
+ before { klass.attribute_reader :foo, coerce: coercer }
152
+
153
+ it_behaves_like "adding readonly attribute :foo"
154
+ it_behaves_like "making :foo to coerce something"
155
+
156
+ end # context
157
+
158
+ context "with a symbolic coercer" do
159
+
160
+ before { klass.attribute_reader :foo, coerce: :coercer }
161
+
162
+ it_behaves_like "adding readonly attribute :foo"
163
+ it_behaves_like "making :foo to coerce something"
164
+
165
+ end # context
166
+
167
+ context "with a string coercer" do
168
+
169
+ before { klass.attribute_reader :foo, coerce: "coercer" }
170
+
171
+ it_behaves_like "adding readonly attribute :foo"
172
+ it_behaves_like "making :foo to coerce something"
173
+
174
+ end # context
175
+
176
+ context "in a strict mode" do
177
+
178
+ before { klass.attribute_reader :foo, coerce: coercer, strict: true }
179
+
180
+ it_behaves_like "adding readonly attribute :foo"
181
+ it_behaves_like "making :foo to coerce anything"
182
+
183
+ end # context
184
+
185
+ context "with coercer that has a wrong type" do
186
+
187
+ subject { klass.attribute_reader :foo, coerce: 1 }
188
+
189
+ it_behaves_like "reporting a wrong coercer type"
190
+
191
+ end # context
192
+
193
+ context "with coercer that takes no attributes" do
194
+
195
+ subject { klass.attribute_reader :foo, coerce: proc { 1 } }
196
+
197
+ it_behaves_like "reporting a wrong coercer arity"
198
+
199
+ end # context
200
+
201
+ context "with coercer that takes more than one attribute" do
202
+
203
+ subject { klass.attribute_reader :foo, coerce: proc { |_, _| 1 } }
204
+
205
+ it_behaves_like "reporting a wrong coercer arity"
206
+
207
+ end # context
208
+
209
+ context "without method name" do
210
+
211
+ it "fails with ArgumentError" do
212
+ expect { klass.attribute_reader }.to raise_error ArgumentError
213
+ end
214
+
215
+ end # context
216
+
217
+ end # describe .attribute_reader
218
+
219
+ describe ".attribute_writer" do
220
+
221
+ context "without coercer" do
222
+
223
+ before { klass.attribute_writer :foo }
224
+
225
+ it_behaves_like "adding writeonly attribute :foo"
226
+
227
+ end # context
228
+
229
+ context "with proc coercer" do
230
+
231
+ before { klass.attribute_writer :foo, coerce: coercer.to_proc }
232
+
233
+ it_behaves_like "adding writeonly attribute :foo"
234
+ it_behaves_like "making :foo= to coerce something"
235
+
236
+ end # context
237
+
238
+ context "with lambda coercer" do
239
+
240
+ before { klass.attribute_writer :foo, coerce: coercer }
241
+
242
+ it_behaves_like "adding writeonly attribute :foo"
243
+ it_behaves_like "making :foo= to coerce something"
244
+
245
+ end # context
246
+
247
+ context "with a symbolic coercer" do
248
+
249
+ before { klass.attribute_writer :foo, coerce: :coercer }
250
+
251
+ it_behaves_like "adding writeonly attribute :foo"
252
+ it_behaves_like "making :foo= to coerce something"
253
+
254
+ end # context
255
+
256
+ context "with a string coercer" do
257
+
258
+ before { klass.attribute_writer :foo, coerce: "coercer" }
259
+
260
+ it_behaves_like "adding writeonly attribute :foo"
261
+ it_behaves_like "making :foo= to coerce something"
262
+
263
+ end # context
264
+
265
+ context "in a strict mode" do
266
+
267
+ before { klass.attribute_writer :foo, coerce: coercer, strict: true }
268
+
269
+ it_behaves_like "adding writeonly attribute :foo"
270
+ it_behaves_like "making :foo= to coerce anything"
271
+
272
+ end # context
273
+
274
+ context "with coercer of a wrong type" do
275
+
276
+ subject { klass.attribute_writer :foo, coerce: 1 }
277
+
278
+ it_behaves_like "reporting a wrong coercer type"
279
+
280
+ end # context
281
+
282
+ context "with coercer that takes no attributes" do
283
+
284
+ subject { klass.attribute_writer :foo, coerce: proc { 1 } }
285
+
286
+ it_behaves_like "reporting a wrong coercer arity"
287
+
288
+ end # context
289
+
290
+ context "with coercer that takes more than one attribute" do
291
+
292
+ subject { klass.attribute_writer :foo, coerce: proc { |_, _| 1 } }
293
+
294
+ it_behaves_like "reporting a wrong coercer arity"
295
+
296
+ end # context
297
+
298
+ context "without method name" do
299
+
300
+ it "fails with ArgumentError" do
301
+ expect { klass.attribute_writer }.to raise_error ArgumentError
302
+ end
303
+
304
+ end # context
305
+
306
+ end # describe .attribute_reader
307
+
308
+ describe ".attribute" do
309
+
310
+ let(:other) { proc { |value| value.to_i } }
311
+
312
+ context "without coercers" do
313
+
314
+ after { klass.attribute :foo }
315
+
316
+ it "calls .attribute_writer without coercer" do
317
+ expect(klass).to receive(:attribute_writer)
318
+ .with(:foo, coerce: nil, strict: nil)
319
+ .once
320
+ end
321
+
322
+ it "calls .attribute_reader without coercer" do
323
+ expect(klass).to receive(:attribute_reader)
324
+ .with(:foo, coerce: nil, strict: nil)
325
+ .once
326
+ end
327
+
328
+ end # context
329
+
330
+ context "with :coerce" do
331
+
332
+ after { klass.attribute :foo, coerce: coercer, strict: true }
333
+
334
+ it "calls .attribute_writer with coercer" do
335
+ expect(klass)
336
+ .to receive(:attribute_writer)
337
+ .with(:foo, coerce: coercer, strict: true)
338
+ .once
339
+ end
340
+
341
+ it "calls .attribute_reader with coercer" do
342
+ expect(klass)
343
+ .to receive(:attribute_reader)
344
+ .with(:foo, coerce: coercer, strict: true)
345
+ .once
346
+ end
347
+
348
+ end # context
349
+
350
+ context "with :writer" do
351
+
352
+ after do
353
+ klass.attribute :foo, coerce: coercer, writer: other, strict: true
354
+ end
355
+
356
+ it "calls .attribute_writer with specific coercer" do
357
+ expect(klass)
358
+ .to receive(:attribute_writer)
359
+ .with(:foo, coerce: other, strict: true)
360
+ .once
361
+ end
362
+
363
+ it "calls .attribute_reader with common coercer" do
364
+ expect(klass).to receive(:attribute_reader)
365
+ .with(:foo, coerce: coercer, strict: true)
366
+ .once
367
+ end
368
+
369
+ end # context
370
+
371
+ context "with :reader" do
372
+
373
+ after do
374
+ klass.attribute :foo, coerce: coercer, reader: other, strict: true
375
+ end
376
+
377
+ it "calls .attribute_writer with common coercer" do
378
+ expect(klass)
379
+ .to receive(:attribute_writer)
380
+ .with(:foo, coerce: coercer, strict: true)
381
+ .once
382
+ end
383
+
384
+ it "calls .attribute_reader with specific coercer" do
385
+ expect(klass)
386
+ .to receive(:attribute_reader)
387
+ .with(:foo, coerce: other, strict: true)
388
+ .once
389
+ end
390
+
391
+ end # context
392
+
393
+ end # describe .attribute
394
+
395
+ end # describe Eigindir::API