smart_properties 1.1.0 → 1.2.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.
@@ -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