smart_properties 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,16 +1,16 @@
1
1
  ##
2
- # {SmartProperties} can be used to easily build more full-fledged accessors
3
- # for standard Ruby classes. In contrast to regular accessors,
4
- # {SmartProperties} support validation and conversion of input data, as well
5
- # as, the specification of default values. Additionally, individual
2
+ # {SmartProperties} can be used to easily build more full-fledged accessors
3
+ # for standard Ruby classes. In contrast to regular accessors,
4
+ # {SmartProperties} support validation and conversion of input data, as well
5
+ # as, the specification of default values. Additionally, individual
6
6
  # {SmartProperties} can be marked as required. This causes the runtime to
7
7
  # throw an +ArgumentError+ whenever a required property has not been
8
8
  # specified.
9
9
  #
10
- # In order to use {SmartProperties}, simply include the {SmartProperties}
10
+ # In order to use {SmartProperties}, simply include the {SmartProperties}
11
11
  # module and use the {ClassMethods#property} method to define properties.
12
12
  #
13
- # @see ClassMethods#property
13
+ # @see ClassMethods#property
14
14
  # More information on how to configure properties
15
15
  #
16
16
  # @example Definition of a property that makes use of all {SmartProperties} features.
@@ -21,9 +21,9 @@
21
21
  # :required => true
22
22
  #
23
23
  module SmartProperties
24
-
25
- VERSION = "1.1.0"
26
-
24
+
25
+ VERSION = "1.2.0"
26
+
27
27
  class Property
28
28
 
29
29
  attr_reader :name
@@ -32,13 +32,13 @@ module SmartProperties
32
32
 
33
33
  def initialize(name, attrs = {})
34
34
  attrs = attrs.dup
35
-
35
+
36
36
  @name = name.to_sym
37
37
  @default = attrs.delete(:default)
38
38
  @converter = attrs.delete(:converts)
39
39
  @accepter = attrs.delete(:accepts)
40
40
  @required = !!attrs.delete(:required)
41
-
41
+
42
42
  unless attrs.empty?
43
43
  raise ArgumentError, "SmartProperties do not support the following configuration options: #{attrs.keys.join(', ')}."
44
44
  end
@@ -65,7 +65,7 @@ module SmartProperties
65
65
  end
66
66
  end
67
67
  end
68
-
68
+
69
69
  def default(scope)
70
70
  @default.kind_of?(Proc) ? scope.instance_exec(&@default) : @default
71
71
  end
@@ -73,7 +73,7 @@ module SmartProperties
73
73
  def accepts?(value, scope)
74
74
  return true unless value
75
75
  return true unless accepter
76
-
76
+
77
77
  if accepter.kind_of?(Enumerable)
78
78
  accepter.include?(value)
79
79
  elsif !accepter.kind_of?(Proc)
@@ -82,7 +82,7 @@ module SmartProperties
82
82
  !!scope.instance_exec(value, &accepter)
83
83
  end
84
84
  end
85
-
85
+
86
86
  def prepare(value, scope)
87
87
  if required? && value.nil?
88
88
  raise ArgumentError, "#{scope.class.name} requires the property #{self.name} to be set"
@@ -96,10 +96,10 @@ module SmartProperties
96
96
 
97
97
  @value = value
98
98
  end
99
-
99
+
100
100
  def define(klass)
101
101
  property = self
102
-
102
+
103
103
  scope = klass.instance_variable_get(:"@_smart_properties_method_scope") || begin
104
104
  m = Module.new
105
105
  klass.send(:include, m)
@@ -114,21 +114,88 @@ module SmartProperties
114
114
  end
115
115
 
116
116
  end
117
-
117
+
118
+ ##
119
+ # {AttributeBuilder} is a singleton object that builds and keeps track of
120
+ # annonymous class that can be used to build attribute hashes for smart
121
+ # property enabled classes.
122
+ #
123
+ class << (AttributeBuilder = Object.new)
124
+
125
+ ##
126
+ # Returns an attribute builder for a given klass. If the attribute builder
127
+ # does not exist yet, it is created.
128
+ #
129
+ # @return [Class]
130
+ #
131
+ def [](klass)
132
+ store.fetch(klass) { new(property_names_for(klass)) }
133
+ end
134
+
135
+ ##
136
+ # Constructs a hash of attributes for a smart property enabled class using
137
+ # the instructions provided by the block.
138
+ #
139
+ # @return [Hash<String, Object>] Hash of attributes
140
+ #
141
+ def build_attributes_for(klass, &block)
142
+ block.nil? ? {} : self[klass].new(&block).to_hash
143
+ end
144
+
145
+ ##
146
+ # Returns the name of this singleton object.
147
+ #
148
+ # @return String
149
+ #
150
+ def inspect
151
+ "AttributeBuilder"
152
+ end
153
+ alias :to_s :inspect
154
+
155
+ ##
156
+ # Creates a new anonymous class that can act as an attribute builder.
157
+ #
158
+ # @return [Class]
159
+ #
160
+ def new(fields)
161
+ Struct.new(*fields) do
162
+ def initialize(*args, &block)
163
+ super
164
+ yield(self) if block
165
+ end
166
+
167
+ def to_hash
168
+ Hash[members.zip(entries)]
169
+ end
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def property_names_for(klass)
176
+ klass.properties.keys
177
+ end
178
+
179
+ def store
180
+ @store ||= {}
181
+ end
182
+
183
+ end
184
+
118
185
  module ClassMethods
119
186
 
120
187
  ##
121
- # Returns the list of smart properties that for this class. This
122
- # includes the properties that have been defined in the parent classes.
188
+ # Returns a class's smart properties. This includes the properties that
189
+ # have been defined in the parent classes.
123
190
  #
124
- # @return [Array<Property>] The list of properties.
191
+ # @return [Hash<String, Property>] A map of property names to property instances.
125
192
  #
126
193
  def properties
127
- @_smart_properties ||= begin
194
+ @_smart_properties ||= begin
128
195
  parent = if self != SmartProperties
129
196
  (ancestors[1..-1].find { |klass| klass.ancestors.include?(SmartProperties) && klass != SmartProperties })
130
197
  end
131
-
198
+
132
199
  parent ? parent.properties.dup : {}
133
200
  end
134
201
  end
@@ -189,13 +256,13 @@ module SmartProperties
189
256
  protected :property
190
257
 
191
258
  end
192
-
259
+
193
260
  class << self
194
-
261
+
195
262
  private
196
-
263
+
197
264
  ##
198
- # Extends the class, which this module is included in, with a property
265
+ # Extends the class, which this module is included in, with a property
199
266
  # method to define properties.
200
267
  #
201
268
  # @param [Class] base the class this module is included in
@@ -203,17 +270,18 @@ module SmartProperties
203
270
  def included(base)
204
271
  base.extend(ClassMethods)
205
272
  end
206
-
273
+
207
274
  end
208
-
275
+
209
276
  ##
210
277
  # Implements a key-value enabled constructor that acts as default
211
278
  # constructor for all {SmartProperties}-enabled classes.
212
279
  #
213
280
  # @param [Hash] attrs the set of attributes that is used for initialization
214
281
  #
215
- def initialize(attrs = {})
282
+ def initialize(attrs = {}, &block)
216
283
  attrs ||= {}
284
+ attrs = attrs.merge(AttributeBuilder.build_attributes_for(self.class, &block)) if block
217
285
 
218
286
  self.class.properties.each do |_, property|
219
287
  value = attrs.key?(property.name) ? attrs.delete(property.name) : property.default(self)
@@ -13,9 +13,9 @@ describe SmartProperties do
13
13
  it "should add a .property method" do
14
14
  subject.should respond_to(:property)
15
15
  end
16
-
16
+
17
17
  context "and defining a property with invalid configuration options" do
18
-
18
+
19
19
  it "should raise an error reporting one invalid option when one invalid option was given" do
20
20
  expect {
21
21
  subject.tap do |c|
@@ -25,7 +25,7 @@ describe SmartProperties do
25
25
  end
26
26
  }.to raise_error(ArgumentError, "SmartProperties do not support the following configuration options: invalid_option.")
27
27
  end
28
-
28
+
29
29
  it "should raise an error reporting three invalid options when three invalid options were given" do
30
30
  expect {
31
31
  subject.tap do |c|
@@ -35,30 +35,30 @@ describe SmartProperties do
35
35
  end
36
36
  }.to raise_error(ArgumentError, "SmartProperties do not support the following configuration options: invalid_option_1, invalid_option_2, invalid_option_3.")
37
37
  end
38
-
38
+
39
39
  end
40
40
 
41
41
  end
42
42
 
43
- context "when used to build a class that has a property called :title on a class" do
44
-
43
+ context "when used to build a class that has a property called :title" do
44
+
45
45
  subject do
46
46
  title = Object.new.tap do |o|
47
47
  def o.to_title; 'chunky'; end
48
48
  end
49
-
49
+
50
50
  klass = Class.new.tap do |c|
51
51
  c.send(:include, described_class)
52
52
  c.instance_eval do
53
53
  def name; "TestDummy"; end
54
-
54
+
55
55
  property :title, :accepts => String,
56
56
  :converts => :to_title,
57
57
  :required => true,
58
58
  :default => title
59
59
  end
60
60
  end
61
-
61
+
62
62
  klass
63
63
  end
64
64
 
@@ -66,40 +66,54 @@ describe SmartProperties do
66
66
  its(:properties) { should have_key(:title) }
67
67
 
68
68
  context "instances of this class" do
69
-
69
+
70
70
  klass = subject.call
71
-
71
+
72
72
  subject do
73
73
  klass.new
74
74
  end
75
-
75
+
76
76
  it { should respond_to(:title) }
77
77
  it { should respond_to(:title=) }
78
-
78
+
79
79
  it "should have 'chucky' as default value for title" do
80
80
  subject.title.should be == 'chunky'
81
81
  end
82
-
82
+
83
83
  it "should convert all values that are assigned to title into strings" do
84
84
  subject.title = double(:to_title => 'bacon')
85
85
  subject.title.should be == 'bacon'
86
86
  end
87
-
87
+
88
88
  it "should not allow to set nil as title" do
89
89
  expect { subject.title = nil }.to raise_error(ArgumentError, "TestDummy requires the property title to be set")
90
90
  end
91
-
91
+
92
92
  it "should not allow to set objects as title that do not respond to #to_title" do
93
93
  expect { subject.title = Object.new }.to raise_error(ArgumentError, "Object does not respond to #to_title")
94
94
  end
95
-
95
+
96
96
  it "should not influence other instances that have been initialized with different attributes" do
97
97
  other = klass.new :title => double(:to_title => 'Lorem ipsum')
98
-
98
+
99
99
  subject.title.should be == 'chunky'
100
100
  other.title.should be == 'Lorem ipsum'
101
101
  end
102
102
 
103
+ context "when initialized with a block" do
104
+
105
+ subject do
106
+ klass.new do |c|
107
+ c.title = double(:to_title => 'bacon')
108
+ end
109
+ end
110
+
111
+ it "should have the title specified in the block" do
112
+ subject.title.should be == 'bacon'
113
+ end
114
+
115
+ end
116
+
103
117
  end
104
118
 
105
119
  context "when subclassed" do
@@ -127,25 +141,25 @@ describe SmartProperties do
127
141
  end
128
142
 
129
143
  context "instances of this subclass that have been intialized from a set of attributes" do
130
-
144
+
131
145
  klass = subject.call
132
-
146
+
133
147
  subject do
134
148
  klass.new :title => stub(:to_title => 'Message')
135
149
  end
136
-
150
+
137
151
  it "should have the correct title" do
138
152
  subject.title.should be == 'Message'
139
153
  end
140
-
154
+
141
155
  end
142
-
156
+
143
157
  end
144
158
 
145
159
  context "when subclassed and extended with a property called text" do
146
-
160
+
147
161
  superklass = subject.call
148
-
162
+
149
163
  subject do
150
164
  Class.new(superklass).tap do |c|
151
165
  c.instance_eval do
@@ -159,55 +173,78 @@ describe SmartProperties do
159
173
  its(:properties) { should have_key(:text) }
160
174
 
161
175
  context "instances of this subclass" do
162
-
176
+
163
177
  klass = subject.call
164
-
178
+
165
179
  subject do
166
180
  klass.new
167
181
  end
168
-
182
+
169
183
  it { should respond_to(:title) }
170
184
  it { should respond_to(:title=) }
171
185
  it { should respond_to(:text) }
172
186
  it { should respond_to(:text=) }
173
-
187
+
174
188
  end
175
-
189
+
176
190
  context "instances of the super class" do
177
-
191
+
178
192
  subject do
179
193
  superklass.new
180
194
  end
181
-
195
+
182
196
  it { should_not respond_to(:text) }
183
197
  it { should_not respond_to(:text=) }
184
-
198
+
185
199
  end
186
-
187
- context "instances of this subclass that have been intialized from a set of attributes" do
188
-
200
+
201
+ context "instances of this subclass" do
202
+
189
203
  klass = subject.call
190
-
191
- subject do
192
- klass.new :title => stub(:to_title => 'Message'), :text => "Hello"
193
- end
194
-
195
- it "should have the correct title" do
196
- subject.title.should be == 'Message'
204
+
205
+ context "when initialized with a set of attributes" do
206
+
207
+ subject do
208
+ klass.new :title => stub(:to_title => 'Message'), :text => "Hello"
209
+ end
210
+
211
+ it "should have the correct title" do
212
+ subject.title.should be == 'Message'
213
+ end
214
+
215
+ it "should have the correct text" do
216
+ subject.text.should be == 'Hello'
217
+ end
218
+
197
219
  end
198
-
199
- it "should have the correct text" do
200
- subject.text.should be == 'Hello'
220
+
221
+ context "when initialized with a block" do
222
+
223
+ subject do
224
+ klass.new do |c|
225
+ c.title = stub(:to_title => 'Message')
226
+ c.text = "Hello"
227
+ end
228
+ end
229
+
230
+ it "should have the title specified in the block" do
231
+ subject.title.should be == 'Message'
232
+ end
233
+
234
+ it "should have the text specified in the block" do
235
+ subject.text.should be == 'Hello'
236
+ end
237
+
201
238
  end
202
-
239
+
203
240
  end
204
-
241
+
205
242
  end
206
-
243
+
207
244
  context "when extended with a :type property at runtime" do
208
-
245
+
209
246
  klass = subject.call
210
-
247
+
211
248
  subject do
212
249
  klass.tap do |c|
213
250
  c.instance_eval do
@@ -221,50 +258,50 @@ describe SmartProperties do
221
258
  its(:properties) { should have_key(:type) }
222
259
 
223
260
  context "instances of this class" do
224
-
261
+
225
262
  klass = subject.call
226
-
263
+
227
264
  subject do
228
265
  klass.new :title => double(:to_title => 'Lorem ipsum')
229
266
  end
230
-
267
+
231
268
  it { should respond_to(:type) }
232
269
  it { should respond_to(:type=) }
233
-
270
+
234
271
  end
235
-
272
+
236
273
  context "when subclassing this class" do
237
-
274
+
238
275
  superklass = subject.call
239
-
276
+
240
277
  subject do
241
278
  Class.new(superklass)
242
279
  end
243
-
280
+
244
281
  context "instances of this class" do
245
-
282
+
246
283
  klass = subject.call
247
-
284
+
248
285
  subject do
249
286
  klass.new :title => double(:to_title => 'Lorem ipsum')
250
287
  end
251
-
288
+
252
289
  it { should respond_to :title }
253
290
  it { should respond_to :title= }
254
-
291
+
255
292
  it { should respond_to :type }
256
293
  it { should respond_to :type= }
257
-
294
+
258
295
  end
259
-
296
+
260
297
  end
261
-
298
+
262
299
  end
263
-
300
+
264
301
  end
265
-
302
+
266
303
  context "when used to build a class that has a property called :title which a lambda statement for conversion" do
267
-
304
+
268
305
  subject do
269
306
  Class.new.tap do |c|
270
307
  c.send(:include, described_class)
@@ -273,106 +310,106 @@ describe SmartProperties do
273
310
  end
274
311
  end
275
312
  end
276
-
313
+
277
314
  context "instances of this class" do
278
-
315
+
279
316
  klass = subject.call
280
-
317
+
281
318
  subject do
282
319
  klass.new
283
320
  end
284
-
321
+
285
322
  it "should convert the property title as specified the lambda statement" do
286
323
  subject.title = "Lorem ipsum"
287
324
  subject.title.should be == "<title>Lorem ipsum</title>"
288
325
  end
289
-
326
+
290
327
  end
291
-
328
+
292
329
  end
293
-
330
+
294
331
  context "when used to build a class that has a property called :visible which uses an array of valid values for acceptance checking" do
295
-
332
+
296
333
  subject do
297
334
  Class.new.tap do |c|
298
335
  def c.name; "TestDummy"; end
299
-
336
+
300
337
  c.send(:include, described_class)
301
-
338
+
302
339
  c.instance_eval do
303
340
  property :visible, :accepts => [true, false]
304
341
  end
305
342
  end
306
343
  end
307
-
344
+
308
345
  context "instances of this class" do
309
-
346
+
310
347
  klass = subject.call
311
348
 
312
349
  subject do
313
350
  klass.new
314
351
  end
315
-
352
+
316
353
  it "should allow to set true as value for visible" do
317
354
  expect { subject.visible = true }.to_not raise_error
318
355
  end
319
-
356
+
320
357
  it "should allow to set false as value for visible" do
321
358
  expect { subject.visible = false }.to_not raise_error
322
359
  end
323
-
360
+
324
361
  it "should not allow to set :maybe as value for visible" do
325
362
  expect { subject.visible = :maybe }.to raise_error(ArgumentError, "TestDummy does not accept :maybe as value for the property visible")
326
363
  end
327
-
364
+
328
365
  end
329
-
366
+
330
367
  end
331
-
368
+
332
369
  context 'when used to build a class that has a property called :license_plate which uses a lambda statement for accpetance checking' do
333
-
370
+
334
371
  subject do
335
372
  Class.new.tap do |c|
336
373
  def c.name; 'TestDummy'; end
337
-
374
+
338
375
  c.send(:include, described_class)
339
-
376
+
340
377
  c.instance_eval do
341
378
  property :license_plate, :accepts => lambda { |v| /\w{1,2} \w{1,2} \d{1,4}/.match(v) }
342
379
  end
343
380
  end
344
381
  end
345
-
382
+
346
383
  context 'instances of this class' do
347
-
384
+
348
385
  klass = subject.call
349
-
386
+
350
387
  subject do
351
388
  klass.new
352
389
  end
353
-
390
+
354
391
  it 'should not a accept "invalid" as value for license_plate' do
355
392
  expect { subject.license_plate = "invalid" }.to raise_error(ArgumentError, 'TestDummy does not accept "invalid" as value for the property license_plate')
356
393
  end
357
-
394
+
358
395
  it 'should accept "NE RD 1337" as license plate' do
359
396
  expect { subject.license_plate = "NE RD 1337" }.to_not raise_error
360
397
  end
361
-
398
+
362
399
  end
363
-
400
+
364
401
  end
365
-
402
+
366
403
  context 'when used to build a class that has a property called :text whose getter is overriden' do
367
-
404
+
368
405
  subject do
369
406
  Class.new.tap do |c|
370
407
  c.send(:include, described_class)
371
-
408
+
372
409
  c.instance_eval do
373
410
  property :text, :default => 'Hello'
374
411
  end
375
-
412
+
376
413
  c.class_eval do
377
414
  def text
378
415
  "<em>#{super}</em>"
@@ -380,59 +417,59 @@ describe SmartProperties do
380
417
  end
381
418
  end
382
419
  end
383
-
420
+
384
421
  context "instances of this class" do
385
-
422
+
386
423
  klass = subject.call
387
-
424
+
388
425
  subject do
389
426
  klass.new
390
427
  end
391
-
428
+
392
429
  it "should return the accepted value for the property called :text" do
393
430
  subject.text.should be == '<em>Hello</em>'
394
431
  end
395
-
432
+
396
433
  end
397
-
434
+
398
435
  end
399
-
436
+
400
437
  context 'when used to build a class that has a property called :id whose default value is a lambda statement' do
401
-
438
+
402
439
  subject do
403
440
  counter = Class.new.tap do |c|
404
-
441
+
405
442
  c.class_eval do
406
443
  def next
407
444
  @counter ||= 0
408
445
  @counter += 1
409
446
  end
410
447
  end
411
-
448
+
412
449
  end.new
413
-
450
+
414
451
  Class.new.tap do |c|
415
452
  c.send(:include, described_class)
416
-
453
+
417
454
  c.instance_eval do
418
455
  property :id, :default => lambda { counter.next }
419
456
  end
420
457
  end
421
458
  end
422
-
459
+
423
460
  context "instances of this class" do
424
-
461
+
425
462
  klass = subject.call
426
-
463
+
427
464
  it "should have auto-incrementing ids" do
428
465
  first_instance = klass.new
429
466
  second_instance = klass.new
430
-
467
+
431
468
  (second_instance.id - first_instance.id).should be == 1
432
469
  end
433
-
470
+
434
471
  end
435
-
472
+
436
473
  end
437
-
474
+
438
475
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_properties
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-03 00:00:00.000000000 Z
12
+ date: 2012-11-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -126,7 +126,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
126
126
  version: '0'
127
127
  segments:
128
128
  - 0
129
- hash: 2267066308945748271
129
+ hash: 2972832392315398871
130
130
  required_rubygems_version: !ruby/object:Gem::Requirement
131
131
  none: false
132
132
  requirements:
@@ -135,10 +135,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
135
  version: '0'
136
136
  segments:
137
137
  - 0
138
- hash: 2267066308945748271
138
+ hash: 2972832392315398871
139
139
  requirements: []
140
140
  rubyforge_project:
141
- rubygems_version: 1.8.19
141
+ rubygems_version: 1.8.24
142
142
  signing_key:
143
143
  specification_version: 3
144
144
  summary: SmartProperties – Ruby accessors on steroids