eigindir 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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