couchrest_model 1.0.0 → 1.1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +1 -1
  2. data/Gemfile.lock +19 -20
  3. data/README.md +145 -20
  4. data/VERSION +1 -1
  5. data/couchrest_model.gemspec +2 -3
  6. data/history.txt +14 -0
  7. data/lib/couchrest/model/associations.rb +4 -4
  8. data/lib/couchrest/model/base.rb +5 -0
  9. data/lib/couchrest/model/callbacks.rb +1 -2
  10. data/lib/couchrest/model/collection.rb +1 -1
  11. data/lib/couchrest/model/designs/view.rb +486 -0
  12. data/lib/couchrest/model/designs.rb +81 -0
  13. data/lib/couchrest/model/document_queries.rb +1 -1
  14. data/lib/couchrest/model/persistence.rb +25 -16
  15. data/lib/couchrest/model/properties.rb +5 -1
  16. data/lib/couchrest/model/property.rb +2 -2
  17. data/lib/couchrest/model/proxyable.rb +152 -0
  18. data/lib/couchrest/model/typecast.rb +1 -1
  19. data/lib/couchrest/model/validations/casted_model.rb +3 -1
  20. data/lib/couchrest/model/validations/locale/en.yml +1 -1
  21. data/lib/couchrest/model/validations/uniqueness.rb +6 -7
  22. data/lib/couchrest/model/validations.rb +1 -0
  23. data/lib/couchrest/model/views.rb +11 -9
  24. data/lib/couchrest_model.rb +3 -0
  25. data/spec/couchrest/assocations_spec.rb +2 -2
  26. data/spec/couchrest/base_spec.rb +15 -1
  27. data/spec/couchrest/casted_model_spec.rb +30 -12
  28. data/spec/couchrest/class_proxy_spec.rb +2 -2
  29. data/spec/couchrest/collection_spec.rb +89 -0
  30. data/spec/couchrest/designs/view_spec.rb +766 -0
  31. data/spec/couchrest/designs_spec.rb +110 -0
  32. data/spec/couchrest/persistence_spec.rb +36 -7
  33. data/spec/couchrest/property_spec.rb +15 -0
  34. data/spec/couchrest/proxyable_spec.rb +329 -0
  35. data/spec/couchrest/{validations.rb → validations_spec.rb} +1 -3
  36. data/spec/couchrest/view_spec.rb +19 -91
  37. data/spec/fixtures/base.rb +8 -6
  38. data/spec/fixtures/more/article.rb +1 -1
  39. data/spec/fixtures/more/course.rb +4 -2
  40. metadata +21 -76
  41. data/lib/couchrest/model/view.rb +0 -190
@@ -0,0 +1,486 @@
1
+ module CouchRest
2
+ module Model
3
+ module Designs
4
+
5
+ #
6
+ # A proxy class that allows view queries to be created using
7
+ # chained method calls. After each call a new instance of the method
8
+ # is created based on the original in a similar fashion to ruby's Sequel
9
+ # library, or Rails 3's Arel.
10
+ #
11
+ # CouchDB views have inherent limitations, so joins and filters as used in
12
+ # a normal relational database are not possible.
13
+ #
14
+ class View
15
+ include Enumerable
16
+
17
+ attr_accessor :owner, :model, :name, :query, :result
18
+
19
+ # Initialize a new View object. This method should not be called from
20
+ # outside CouchRest Model.
21
+ def initialize(parent, new_query = {}, name = nil)
22
+ if parent.is_a?(Class) && parent < CouchRest::Model::Base
23
+ raise "Name must be provided for view to be initialized" if name.nil?
24
+ self.model = parent
25
+ self.owner = parent
26
+ self.name = name.to_s
27
+ # Default options:
28
+ self.query = { :reduce => false }
29
+ elsif parent.is_a?(self.class)
30
+ self.model = (new_query.delete(:proxy) || parent.model)
31
+ self.owner = parent.owner
32
+ self.name = parent.name
33
+ self.query = parent.query.dup
34
+ else
35
+ raise "View cannot be initialized without a parent Model or View"
36
+ end
37
+ query.update(new_query)
38
+ super()
39
+ end
40
+
41
+
42
+ # == View Execution Methods
43
+ #
44
+ # Request to the CouchDB database using the current query values.
45
+
46
+ # Return each row wrapped in a ViewRow object. Unlike the raw
47
+ # CouchDB request, this will provide an empty array if there
48
+ # are no results.
49
+ def rows
50
+ return @rows if @rows
51
+ if execute && result['rows']
52
+ @rows ||= result['rows'].map{|v| ViewRow.new(v, model)}
53
+ else
54
+ [ ]
55
+ end
56
+ end
57
+
58
+ # Fetch all the documents the view can access. If the view has
59
+ # not already been prepared for including documents in the query,
60
+ # it will be added automatically and reset any previously cached
61
+ # results.
62
+ def all
63
+ include_docs!
64
+ docs
65
+ end
66
+
67
+ # Provide all the documents from the view. If the view has not been
68
+ # prepared with the +include_docs+ option, each document will be
69
+ # loaded individually.
70
+ def docs
71
+ @docs ||= rows.map{|r| r.doc}
72
+ end
73
+
74
+ # If another request has been made on the view, this will return
75
+ # the first document in the set. If not, a new query object will be
76
+ # generated with a limit of 1 so that only the first document is
77
+ # loaded.
78
+ def first
79
+ result ? all.first : limit(1).all.first
80
+ end
81
+
82
+ # Same as first but will order the view in descending order. This
83
+ # does not however reverse the search keys or the offset, so if you
84
+ # are using a +startkey+ and +endkey+ you might end up with
85
+ # unexpected results.
86
+ #
87
+ # If in doubt, don't use this method!
88
+ #
89
+ def last
90
+ result ? all.last : limit(1).descending.all.last
91
+ end
92
+
93
+ # Perform a count operation based on the current view. If the view
94
+ # can be reduced, the reduce will be performed and return the first
95
+ # value. This is okay for most simple queries, but may provide
96
+ # unexpected results if your reduce method does not calculate
97
+ # the total number of documents in a result set.
98
+ #
99
+ # Trying to use this method with the group option will raise an error.
100
+ #
101
+ # If no reduce function is defined, a query will be performed
102
+ # to return the total number of rows, this is the equivalant of:
103
+ #
104
+ # view.limit(0).total_rows
105
+ #
106
+ def count
107
+ raise "View#count cannot be used with group options" if query[:group]
108
+ if can_reduce?
109
+ row = reduce.rows.first
110
+ row.nil? ? 0 : row.value
111
+ else
112
+ limit(0).total_rows
113
+ end
114
+ end
115
+
116
+ # Check to see if the array of documents is empty. This *will*
117
+ # perform the query and return all documents ready to use, if you don't
118
+ # want to load anything, use +#total_rows+ or +#count+ instead.
119
+ def empty?
120
+ all.empty?
121
+ end
122
+
123
+ # Run through each document provided by the +#all+ method.
124
+ # This is also used by the Enumerator mixin to provide all the standard
125
+ # ruby collection directly on the view.
126
+ def each(&block)
127
+ all.each(&block)
128
+ end
129
+
130
+ # Wrapper for the results offset. As per the CouchDB API,
131
+ # this may be nil if groups are used.
132
+ def offset
133
+ execute['offset']
134
+ end
135
+
136
+ # Wrapper for the total_rows value provided by the query. As per the
137
+ # CouchDB API, this may be nil if groups are used.
138
+ def total_rows
139
+ execute['total_rows']
140
+ end
141
+
142
+ # Convenience wrapper around the rows result set. This will provide
143
+ # and array of keys.
144
+ def keys
145
+ rows.map{|r| r.key}
146
+ end
147
+
148
+ # Convenience wrapper to provide all the values from the route
149
+ # set without having to go through +rows+.
150
+ def values
151
+ rows.map{|r| r.value}
152
+ end
153
+
154
+ # Accept requests as if the view was an array. Used for backwards compatibity
155
+ # with older queries:
156
+ #
157
+ # Model.all(:raw => true, :limit => 0)['total_rows']
158
+ #
159
+ # In this example, the raw option will be ignored, and the total rows
160
+ # will still be accessible.
161
+ #
162
+ def [](value)
163
+ execute[value]
164
+ end
165
+
166
+ # No yet implemented. Eventually this will provide a raw hash
167
+ # of the information CouchDB holds about the view.
168
+ def info
169
+ raise "Not yet implemented"
170
+ end
171
+
172
+
173
+ # == View Filter Methods
174
+ #
175
+ # View filters return a copy of the view instance with the query
176
+ # modified appropriatly. Errors will be raised if the methods
177
+ # are combined in an incorrect fashion.
178
+ #
179
+
180
+ # Find all entries in the index whose key matches the value provided.
181
+ #
182
+ # Cannot be used when the +#startkey+ or +#endkey+ have been set.
183
+ def key(value)
184
+ raise "View#key cannot be used when startkey or endkey have been set" unless query[:startkey].nil? && query[:endkey].nil?
185
+ update_query(:key => value)
186
+ end
187
+
188
+ # Find all index keys that start with the value provided. May or may
189
+ # not be used in conjunction with the +endkey+ option.
190
+ #
191
+ # When the +#descending+ option is used (not the default), the start
192
+ # and end keys should be reversed, as per the CouchDB API.
193
+ #
194
+ # Cannot be used if the key has been set.
195
+ def startkey(value)
196
+ raise "View#startkey cannot be used when key has been set" unless query[:key].nil?
197
+ update_query(:startkey => value)
198
+ end
199
+
200
+ # The result set should start from the position of the provided document.
201
+ # The value may be provided as an object that responds to the +#id+ call
202
+ # or a string.
203
+ def startkey_doc(value)
204
+ update_query(:startkey_docid => value.is_a?(String) ? value : value.id)
205
+ end
206
+
207
+ # The opposite of +#startkey+, finds all index entries whose key is before
208
+ # the value specified.
209
+ #
210
+ # See the +#startkey+ method for more details and the +#inclusive_end+
211
+ # option.
212
+ def endkey(value)
213
+ raise "View#endkey cannot be used when key has been set" unless query[:key].nil?
214
+ update_query(:endkey => value)
215
+ end
216
+
217
+ # The result set should end at the position of the provided document.
218
+ # The value may be provided as an object that responds to the +#id+
219
+ # call or a string.
220
+ def endkey_doc(value)
221
+ update_query(:endkey_docid => value.is_a?(String) ? value : value.id)
222
+ end
223
+
224
+
225
+ # The results should be provided in descending order.
226
+ #
227
+ # Descending is false by default, this method will enable it and cannot
228
+ # be undone.
229
+ def descending
230
+ update_query(:descending => true)
231
+ end
232
+
233
+ # Limit the result set to the value supplied.
234
+ def limit(value)
235
+ update_query(:limit => value)
236
+ end
237
+
238
+ # Skip the number of entries in the index specified by value. This would be
239
+ # the equivilent of an offset in SQL.
240
+ #
241
+ # The CouchDB documentation states that the skip option should not be used
242
+ # with large data sets as it is inefficient. Use the +startkey_doc+ method
243
+ # instead to skip ranges efficiently.
244
+ def skip(value = 0)
245
+ update_query(:skip => value)
246
+ end
247
+
248
+ # Use the reduce function on the view. If none is available this method
249
+ # will fail.
250
+ def reduce
251
+ raise "Cannot reduce a view without a reduce method" unless can_reduce?
252
+ update_query(:reduce => true, :include_docs => nil)
253
+ end
254
+
255
+ # Control whether the reduce function reduces to a set of distinct keys
256
+ # or to a single result row.
257
+ #
258
+ # By default the value is false, and can only be set when the view's
259
+ # +#reduce+ option has been set.
260
+ def group
261
+ raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
262
+ update_query(:group => true)
263
+ end
264
+
265
+ # Will set the level the grouping should be performed to. As per the
266
+ # CouchDB API, it only makes sense when the index key is an array.
267
+ #
268
+ # This will automatically set the group option.
269
+ def group_level(value)
270
+ group.update_query(:group_level => value.to_i)
271
+ end
272
+
273
+ def include_docs
274
+ update_query.include_docs!
275
+ end
276
+
277
+ ### Special View Filter Methods
278
+
279
+ # Specify the database the view should use. If not defined,
280
+ # an attempt will be made to load its value from the model.
281
+ def database(value)
282
+ update_query(:database => value)
283
+ end
284
+
285
+ # Set the view's proxy that will be used instead of the model
286
+ # for any future searches. As soon as this enters the
287
+ # new object's initializer it will be removed and replace
288
+ # the model object.
289
+ #
290
+ # See the Proxyable mixin for more details.
291
+ #
292
+ def proxy(value)
293
+ update_query(:proxy => value)
294
+ end
295
+
296
+ # Return any cached values to their nil state so that any queries
297
+ # requested later will have a fresh set of data.
298
+ def reset!
299
+ self.result = nil
300
+ @rows = nil
301
+ @docs = nil
302
+ end
303
+
304
+ # == Kaminari compatible pagination support
305
+ #
306
+ # Based on the really simple support for scoped pagination in the
307
+ # the Kaminari gem, we provide compatible methods here to perform
308
+ # the same actions you'd expect.
309
+ #
310
+
311
+ def page(page)
312
+ limit(owner.default_per_page).skip(owner.default_per_page * ([page.to_i, 1].max - 1))
313
+ end
314
+
315
+ def per(num)
316
+ raise "View#page must be called before #per!" if limit_value.nil? || offset_value.nil?
317
+ if (n = num.to_i) <= 0
318
+ self
319
+ else
320
+ limit(num).skip(offset_value / limit_value * n)
321
+ end
322
+ end
323
+
324
+ def total_count
325
+ @total_count ||= limit(nil).skip(nil).count
326
+ end
327
+
328
+ def offset_value
329
+ query[:skip]
330
+ end
331
+
332
+ def limit_value
333
+ query[:limit]
334
+ end
335
+
336
+ def num_pages
337
+ (total_count.to_f / limit_value).ceil
338
+ end
339
+
340
+ def current_page
341
+ (offset_value / limit_value) + 1
342
+ end
343
+
344
+
345
+
346
+ protected
347
+
348
+ def include_docs!
349
+ raise "Cannot include documents in view that has been reduced!" if query[:reduce]
350
+ reset! if result && !include_docs?
351
+ query[:include_docs] = true
352
+ self
353
+ end
354
+
355
+ def include_docs?
356
+ !!query[:include_docs]
357
+ end
358
+
359
+ def update_query(new_query = {})
360
+ self.class.new(self, new_query)
361
+ end
362
+
363
+ def design_doc
364
+ model.design_doc
365
+ end
366
+
367
+ def can_reduce?
368
+ !design_doc['views'][name]['reduce'].blank?
369
+ end
370
+
371
+ def use_database
372
+ query[:database] || model.database
373
+ end
374
+
375
+ def execute
376
+ return self.result if result
377
+ raise "Database must be defined in model or view!" if use_database.nil?
378
+ retryable = true
379
+ # Remove the reduce value if its not needed
380
+ query.delete(:reduce) unless can_reduce?
381
+ begin
382
+ self.result = model.design_doc.view_on(use_database, name, query.reject{|k,v| v.nil?})
383
+ rescue RestClient::ResourceNotFound => e
384
+ if retryable
385
+ model.save_design_doc(use_database)
386
+ retryable = false
387
+ retry
388
+ else
389
+ raise e
390
+ end
391
+ end
392
+ end
393
+
394
+ # Class Methods
395
+ class << self
396
+
397
+ # Simplified view creation. A new view will be added to the
398
+ # provided model's design document using the name and options.
399
+ #
400
+ # If the view name starts with "by_" and +:by+ is not provided in
401
+ # the options, the new view's map method will be interpretted and
402
+ # generated automatically. For example:
403
+ #
404
+ # View.create(Meeting, "by_date_and_name")
405
+ #
406
+ # Will create a view that searches by the date and name properties.
407
+ # Explicity setting the attributes to use is possible using the
408
+ # +:by+ option. For example:
409
+ #
410
+ # View.create(Meeting, "by_date_and_name", :by => [:date, :firstname, :lastname])
411
+ #
412
+ # The view name is the same, but three keys would be used in the
413
+ # subsecuent index.
414
+ #
415
+ def create(model, name, opts = {})
416
+
417
+ unless opts[:map]
418
+ if opts[:by].nil? && name.to_s =~ /^by_(.+)/
419
+ opts[:by] = $1.split(/_and_/)
420
+ end
421
+
422
+ raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
423
+
424
+ opts[:guards] ||= []
425
+ opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.to_s}')"
426
+
427
+ keys = opts[:by].map{|o| "doc['#{o}']"}
428
+ emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
429
+ opts[:guards] += keys.map{|k| "(#{k} != null)"}
430
+ opts[:map] = <<-EOF
431
+ function(doc) {
432
+ if (#{opts[:guards].join(' && ')}) {
433
+ emit(#{emit}, 1);
434
+ }
435
+ }
436
+ EOF
437
+ opts[:reduce] = <<-EOF
438
+ function(key, values, rereduce) {
439
+ return sum(values);
440
+ }
441
+ EOF
442
+ end
443
+
444
+ model.design_doc['views'] ||= {}
445
+ view = model.design_doc['views'][name.to_s] = { }
446
+ view['map'] = opts[:map]
447
+ view['reduce'] = opts[:reduce] if opts[:reduce]
448
+ view
449
+ end
450
+
451
+ end
452
+
453
+ end
454
+
455
+ # A special wrapper class that provides easy access to the key
456
+ # fields in a result row.
457
+ class ViewRow < Hash
458
+ attr_reader :model
459
+ def initialize(hash, model)
460
+ @model = model
461
+ replace(hash)
462
+ end
463
+ def id
464
+ self["id"]
465
+ end
466
+ def key
467
+ self["key"]
468
+ end
469
+ def value
470
+ self['value']
471
+ end
472
+ def raw_doc
473
+ self['doc']
474
+ end
475
+ # Send a request for the linked document either using the "id" field's
476
+ # value, or the ["value"]["_id"] used for linked documents.
477
+ def doc
478
+ return model.build_from_database(self['doc']) if self['doc']
479
+ doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
480
+ doc_id ? model.get(doc_id) : nil
481
+ end
482
+ end
483
+
484
+ end
485
+ end
486
+ end
@@ -0,0 +1,81 @@
1
+
2
+ #### NOTE Work in progress! Not yet used!
3
+
4
+ module CouchRest
5
+ module Model
6
+
7
+ # A design block in CouchRest Model groups together the functionality of CouchDB's
8
+ # design documents in a simple block definition.
9
+ #
10
+ # class Person < CouchRest::Model::Base
11
+ # property :name
12
+ # timestamps!
13
+ #
14
+ # design do
15
+ # view :by_name
16
+ # end
17
+ # end
18
+ #
19
+ module Designs
20
+ extend ActiveSupport::Concern
21
+
22
+ module ClassMethods
23
+
24
+ # Add views and other design document features
25
+ # to the current model.
26
+ def design(*args, &block)
27
+ mapper = DesignMapper.new(self)
28
+ mapper.create_view_method(:all)
29
+
30
+ mapper.instance_eval(&block) if block_given?
31
+
32
+ req_design_doc_refresh
33
+ end
34
+
35
+ # Override the default page pagination value:
36
+ #
37
+ # class Person < CouchRest::Model::Base
38
+ # paginates_per 10
39
+ # end
40
+ #
41
+ def paginates_per(val)
42
+ @_default_per_page = val
43
+ end
44
+
45
+ # The models number of documents to return
46
+ # by default when performing pagination.
47
+ # Returns 25 unless explicitly overridden via <tt>paginates_per</tt>
48
+ def default_per_page
49
+ @_default_per_page || 25
50
+ end
51
+
52
+ end
53
+
54
+ #
55
+ class DesignMapper
56
+
57
+ attr_accessor :model
58
+
59
+ def initialize(model)
60
+ self.model = model
61
+ end
62
+
63
+ # Define a view and generate a method that will provide a new
64
+ # View instance when requested.
65
+ def view(name, opts = {})
66
+ View.create(model, name, opts)
67
+ create_view_method(name)
68
+ end
69
+
70
+ def create_view_method(name)
71
+ model.class_eval <<-EOS, __FILE__, __LINE__ + 1
72
+ def self.#{name}(opts = {})
73
+ CouchRest::Model::Designs::View.new(self, opts, '#{name}')
74
+ end
75
+ EOS
76
+ end
77
+
78
+ end
79
+ end
80
+ end
81
+ end
@@ -88,7 +88,7 @@ module CouchRest
88
88
  def get!(id, db = database)
89
89
  raise "Missing or empty document ID" if id.to_s.empty?
90
90
  doc = db.get id
91
- create_from_database(doc)
91
+ build_from_database(doc)
92
92
  end
93
93
  alias :find! :get!
94
94
 
@@ -3,7 +3,7 @@ module CouchRest
3
3
  module Persistence
4
4
  extend ActiveSupport::Concern
5
5
 
6
- # Create the document. Validation is enabled by default and will return
6
+ # Create the document. Validation is enabled by default and will return
7
7
  # false if the document is not valid. If all goes well, the document will
8
8
  # be returned.
9
9
  def create(options = {})
@@ -16,13 +16,13 @@ module CouchRest
16
16
  end
17
17
  end
18
18
  end
19
-
19
+
20
20
  # Creates the document in the db. Raises an exception
21
21
  # if the document is not created properly.
22
22
  def create!
23
23
  self.class.fail_validate!(self) unless self.create
24
24
  end
25
-
25
+
26
26
  # Trigger the callbacks (before, after, around)
27
27
  # only if the document isn't new
28
28
  def update(options = {})
@@ -35,12 +35,12 @@ module CouchRest
35
35
  end
36
36
  end
37
37
  end
38
-
38
+
39
39
  # Trigger the callbacks (before, after, around) and save the document
40
40
  def save(options = {})
41
41
  self.new? ? create(options) : update(options)
42
42
  end
43
-
43
+
44
44
  # Saves the document to the db using save. Raises an exception
45
45
  # if the document is not saved properly.
46
46
  def save!
@@ -65,7 +65,6 @@ module CouchRest
65
65
  # Update the document's attributes and save. For example:
66
66
  #
67
67
  # doc.update_attributes :name => "Fred"
68
- #
69
68
  # Is the equivilent of doing the following:
70
69
  #
71
70
  # doc.attributes = { :name => "Fred" }
@@ -76,7 +75,17 @@ module CouchRest
76
75
  save
77
76
  end
78
77
 
79
- protected
78
+ # Reloads the attributes of this object from the database.
79
+ # It doesn't override custom instance variables.
80
+ #
81
+ # Returns self.
82
+ def reload
83
+ merge!(self.class.get(id))
84
+ self
85
+ end
86
+
87
+
88
+ protected
80
89
 
81
90
  def perform_validations(options = {})
82
91
  perform_validation = case options
@@ -93,16 +102,16 @@ module CouchRest
93
102
 
94
103
  # Creates a new instance, bypassing attribute protection
95
104
  #
96
- #
97
105
  # ==== Returns
98
106
  # a document instance
99
- def create_from_database(doc = {})
107
+ #
108
+ def build_from_database(doc = {})
100
109
  base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
101
- base.new(doc, :directly_set_attributes => true)
110
+ base.new(doc, :directly_set_attributes => true)
102
111
  end
103
112
 
104
- # Defines an instance and save it directly to the database
105
- #
113
+ # Defines an instance and save it directly to the database
114
+ #
106
115
  # ==== Returns
107
116
  # returns the reloaded document
108
117
  def create(attributes = {})
@@ -110,9 +119,9 @@ module CouchRest
110
119
  instance.create
111
120
  instance
112
121
  end
113
-
114
- # Defines an instance and save it directly to the database
115
- #
122
+
123
+ # Defines an instance and save it directly to the database
124
+ #
116
125
  # ==== Returns
117
126
  # returns the reloaded document or raises an exception
118
127
  def create!(attributes = {})
@@ -148,7 +157,7 @@ module CouchRest
148
157
  raise Errors::Validations.new(document)
149
158
  end
150
159
  end
151
-
160
+
152
161
 
153
162
  end
154
163
  end
@@ -156,7 +156,11 @@ module CouchRest
156
156
  type = Class.new(Hash) do
157
157
  include CastedModel
158
158
  end
159
- type.class_eval { yield type }
159
+ if block.arity == 1 # Traditional, with options
160
+ type.class_eval { yield type }
161
+ else
162
+ type.instance_exec(&block)
163
+ end
160
164
  type = [type] # inject as an array
161
165
  end
162
166
  property = Property.new(name, type, options)