couchrest_model 1.0.0 → 1.1.0.beta

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.
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)