couchrest_extended_document 1.0.0.beta5

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 (71) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +68 -0
  3. data/Rakefile +68 -0
  4. data/THANKS.md +19 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +159 -0
  7. data/lib/couchrest/casted_array.rb +25 -0
  8. data/lib/couchrest/casted_model.rb +55 -0
  9. data/lib/couchrest/extended_document.rb +323 -0
  10. data/lib/couchrest/mixins/attribute_protection.rb +74 -0
  11. data/lib/couchrest/mixins/callbacks.rb +532 -0
  12. data/lib/couchrest/mixins/class_proxy.rb +120 -0
  13. data/lib/couchrest/mixins/collection.rb +260 -0
  14. data/lib/couchrest/mixins/design_doc.rb +127 -0
  15. data/lib/couchrest/mixins/document_queries.rb +82 -0
  16. data/lib/couchrest/mixins/extended_attachments.rb +73 -0
  17. data/lib/couchrest/mixins/properties.rb +162 -0
  18. data/lib/couchrest/mixins/validation.rb +245 -0
  19. data/lib/couchrest/mixins/views.rb +148 -0
  20. data/lib/couchrest/mixins.rb +11 -0
  21. data/lib/couchrest/property.rb +50 -0
  22. data/lib/couchrest/support/couchrest.rb +19 -0
  23. data/lib/couchrest/support/rails.rb +42 -0
  24. data/lib/couchrest/typecast.rb +175 -0
  25. data/lib/couchrest/validation/auto_validate.rb +156 -0
  26. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  27. data/lib/couchrest/validation/validation_errors.rb +125 -0
  28. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  29. data/lib/couchrest/validation/validators/confirmation_validator.rb +107 -0
  30. data/lib/couchrest/validation/validators/format_validator.rb +122 -0
  31. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  32. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  33. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  34. data/lib/couchrest/validation/validators/length_validator.rb +139 -0
  35. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  36. data/lib/couchrest/validation/validators/numeric_validator.rb +109 -0
  37. data/lib/couchrest/validation/validators/required_field_validator.rb +114 -0
  38. data/lib/couchrest/validation.rb +245 -0
  39. data/lib/couchrest_extended_document.rb +21 -0
  40. data/spec/couchrest/attribute_protection_spec.rb +150 -0
  41. data/spec/couchrest/casted_extended_doc_spec.rb +79 -0
  42. data/spec/couchrest/casted_model_spec.rb +406 -0
  43. data/spec/couchrest/extended_doc_attachment_spec.rb +148 -0
  44. data/spec/couchrest/extended_doc_inherited_spec.rb +40 -0
  45. data/spec/couchrest/extended_doc_spec.rb +868 -0
  46. data/spec/couchrest/extended_doc_subclass_spec.rb +99 -0
  47. data/spec/couchrest/extended_doc_view_spec.rb +529 -0
  48. data/spec/couchrest/property_spec.rb +648 -0
  49. data/spec/fixtures/attachments/README +3 -0
  50. data/spec/fixtures/attachments/couchdb.png +0 -0
  51. data/spec/fixtures/attachments/test.html +11 -0
  52. data/spec/fixtures/more/article.rb +35 -0
  53. data/spec/fixtures/more/card.rb +22 -0
  54. data/spec/fixtures/more/cat.rb +22 -0
  55. data/spec/fixtures/more/course.rb +25 -0
  56. data/spec/fixtures/more/event.rb +8 -0
  57. data/spec/fixtures/more/invoice.rb +17 -0
  58. data/spec/fixtures/more/person.rb +9 -0
  59. data/spec/fixtures/more/question.rb +6 -0
  60. data/spec/fixtures/more/service.rb +12 -0
  61. data/spec/fixtures/more/user.rb +22 -0
  62. data/spec/fixtures/views/lib.js +3 -0
  63. data/spec/fixtures/views/test_view/lib.js +3 -0
  64. data/spec/fixtures/views/test_view/only-map.js +4 -0
  65. data/spec/fixtures/views/test_view/test-map.js +3 -0
  66. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  67. data/spec/spec.opts +5 -0
  68. data/spec/spec_helper.rb +49 -0
  69. data/utils/remap.rb +27 -0
  70. data/utils/subset.rb +30 -0
  71. metadata +200 -0
@@ -0,0 +1,55 @@
1
+ module CouchRest
2
+ module CastedModel
3
+
4
+ def self.included(base)
5
+ base.send(:include, ::CouchRest::Mixins::Callbacks)
6
+ base.send(:include, ::CouchRest::Mixins::Properties)
7
+ base.send(:attr_accessor, :casted_by)
8
+ base.send(:attr_accessor, :document_saved)
9
+ end
10
+
11
+ def initialize(keys={})
12
+ raise StandardError unless self.is_a? Hash
13
+ apply_defaults # defined in CouchRest::Mixins::Properties
14
+ super()
15
+ keys.each do |k,v|
16
+ self[k.to_s] = v
17
+ end if keys
18
+ cast_keys # defined in CouchRest::Mixins::Properties
19
+ end
20
+
21
+ def []= key, value
22
+ super(key.to_s, value)
23
+ end
24
+
25
+ def [] key
26
+ super(key.to_s)
27
+ end
28
+
29
+ # Gets a reference to the top level extended
30
+ # document that a model is saved inside of
31
+ def base_doc
32
+ return nil unless @casted_by
33
+ @casted_by.base_doc
34
+ end
35
+
36
+ # False if the casted model has already
37
+ # been saved in the containing document
38
+ def new?
39
+ !@document_saved
40
+ end
41
+ alias :new_record? :new?
42
+
43
+ # Sets the attributes from a hash
44
+ def update_attributes_without_saving(hash)
45
+ hash.each do |k, v|
46
+ raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
47
+ end
48
+ hash.each do |k, v|
49
+ self.send("#{k}=",v)
50
+ end
51
+ end
52
+ alias :attributes= :update_attributes_without_saving
53
+
54
+ end
55
+ end
@@ -0,0 +1,323 @@
1
+
2
+ require File.join(File.dirname(__FILE__), "property")
3
+ require File.join(File.dirname(__FILE__), "validation")
4
+ require File.join(File.dirname(__FILE__), 'mixins')
5
+
6
+ module CouchRest
7
+
8
+ # Same as CouchRest::Document but with properties and validations
9
+ class ExtendedDocument < Document
10
+
11
+ VERSION = "1.0.0.beta5"
12
+
13
+ include CouchRest::Mixins::Callbacks
14
+ include CouchRest::Mixins::DocumentQueries
15
+ include CouchRest::Mixins::Views
16
+ include CouchRest::Mixins::DesignDoc
17
+ include CouchRest::Mixins::ExtendedAttachments
18
+ include CouchRest::Mixins::ClassProxy
19
+ include CouchRest::Mixins::Collection
20
+ include CouchRest::Mixins::AttributeProtection
21
+
22
+ # Including validation here does not work due to the way inheritance is handled.
23
+ #include CouchRest::Validation
24
+
25
+ def self.subclasses
26
+ @subclasses ||= []
27
+ end
28
+
29
+ def self.inherited(subklass)
30
+ super
31
+ subklass.send(:include, CouchRest::Mixins::Properties)
32
+ subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
33
+ def self.inherited(subklass)
34
+ super
35
+ subklass.properties = self.properties.dup
36
+ end
37
+ EOS
38
+ subclasses << subklass
39
+ end
40
+
41
+ # Accessors
42
+ attr_accessor :casted_by
43
+
44
+ # Callbacks
45
+ define_callbacks :create, "result == :halt"
46
+ define_callbacks :save, "result == :halt"
47
+ define_callbacks :update, "result == :halt"
48
+ define_callbacks :destroy, "result == :halt"
49
+
50
+ # Creates a new instance, bypassing attribute protection
51
+ #
52
+ #
53
+ # ==== Returns
54
+ # a document instance
55
+ def self.create_from_database(doc = {})
56
+ base = (doc['couchrest-type'].blank? || doc['couchrest-type'] == self.to_s) ? self : doc['couchrest-type'].constantize
57
+ base.new(doc, :directly_set_attributes => true)
58
+ end
59
+
60
+ def initialize(doc = {}, options = {})
61
+ apply_defaults # defined in CouchRest::Mixins::Properties
62
+ remove_protected_attributes(doc) unless options[:directly_set_attributes]
63
+ directly_set_attributes(doc) unless doc.nil?
64
+ super(doc)
65
+ cast_keys # defined in CouchRest::Mixins::Properties
66
+ unless self['_id'] && self['_rev']
67
+ self['couchrest-type'] = self.class.to_s
68
+ end
69
+ after_initialize if respond_to?(:after_initialize)
70
+ end
71
+
72
+ # Defines an instance and save it directly to the database
73
+ #
74
+ # ==== Returns
75
+ # returns the reloaded document
76
+ def self.create(options)
77
+ instance = new(options)
78
+ instance.create
79
+ instance
80
+ end
81
+
82
+ # Defines an instance and save it directly to the database
83
+ #
84
+ # ==== Returns
85
+ # returns the reloaded document or raises an exception
86
+ def self.create!(options)
87
+ instance = new(options)
88
+ instance.create!
89
+ instance
90
+ end
91
+
92
+ # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
93
+ # on the document whenever saving occurs. CouchRest uses a pretty
94
+ # decent time format by default. See Time#to_json
95
+ def self.timestamps!
96
+ class_eval <<-EOS, __FILE__, __LINE__
97
+ property(:updated_at, :read_only => true, :type => 'Time', :auto_validation => false)
98
+ property(:created_at, :read_only => true, :type => 'Time', :auto_validation => false)
99
+
100
+ set_callback :save, :before do |object|
101
+ object['updated_at'] = Time.now
102
+ object['created_at'] = object['updated_at'] if object.new?
103
+ end
104
+ EOS
105
+ end
106
+
107
+ # Name a method that will be called before the document is first saved,
108
+ # which returns a string to be used for the document's <tt>_id</tt>.
109
+ # Because CouchDB enforces a constraint that each id must be unique,
110
+ # this can be used to enforce eg: uniq usernames. Note that this id
111
+ # must be globally unique across all document types which share a
112
+ # database, so if you'd like to scope uniqueness to this class, you
113
+ # should use the class name as part of the unique id.
114
+ def self.unique_id method = nil, &block
115
+ if method
116
+ define_method :set_unique_id do
117
+ self['_id'] ||= self.send(method)
118
+ end
119
+ elsif block
120
+ define_method :set_unique_id do
121
+ uniqid = block.call(self)
122
+ raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
123
+ self['_id'] ||= uniqid
124
+ end
125
+ end
126
+ end
127
+
128
+ # Temp solution to make the view_by methods available
129
+ def self.method_missing(m, *args, &block)
130
+ if has_view?(m)
131
+ query = args.shift || {}
132
+ return view(m, query, *args, &block)
133
+ elsif m.to_s =~ /^find_(by_.+)/
134
+ view_name = $1
135
+ if has_view?(view_name)
136
+ query = {:key => args.first, :limit => 1}
137
+ return view(view_name, query).first
138
+ end
139
+ end
140
+ super
141
+ end
142
+
143
+ ### instance methods
144
+
145
+ # Returns the Class properties
146
+ #
147
+ # ==== Returns
148
+ # Array:: the list of properties for the instance
149
+ def properties
150
+ self.class.properties
151
+ end
152
+
153
+ # Gets a reference to the actual document in the DB
154
+ # Calls up to the next document if there is one,
155
+ # Otherwise we're at the top and we return self
156
+ def base_doc
157
+ return self if base_doc?
158
+ @casted_by.base_doc
159
+ end
160
+
161
+ # Checks if we're the top document
162
+ def base_doc?
163
+ !@casted_by
164
+ end
165
+
166
+ # Takes a hash as argument, and applies the values by using writer methods
167
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
168
+ # missing. In case of error, no attributes are changed.
169
+ def update_attributes_without_saving(hash)
170
+ # remove attributes that cannot be updated, silently ignoring them
171
+ # which matches Rails behavior when, for instance, setting created_at.
172
+ # make a copy, we don't want to change arguments
173
+ attrs = hash.dup
174
+ %w[_id _rev created_at updated_at].each {|attr| attrs.delete(attr)}
175
+ check_properties_exist(attrs)
176
+ set_attributes(attrs)
177
+ end
178
+ alias :attributes= :update_attributes_without_saving
179
+
180
+ # Takes a hash as argument, and applies the values by using writer methods
181
+ # for each key. Raises a NoMethodError if the corresponding methods are
182
+ # missing. In case of error, no attributes are changed.
183
+ def update_attributes(hash)
184
+ update_attributes_without_saving hash
185
+ save
186
+ end
187
+
188
+ # for compatibility with old-school frameworks
189
+ alias :new_record? :new?
190
+ alias :new_document? :new?
191
+
192
+ # Trigger the callbacks (before, after, around)
193
+ # and create the document
194
+ # It's important to have a create callback since you can't check if a document
195
+ # was new after you saved it
196
+ #
197
+ # When creating a document, both the create and the save callbacks will be triggered.
198
+ def create(bulk = false)
199
+ caught = catch(:halt) do
200
+ _run_create_callbacks do
201
+ _run_save_callbacks do
202
+ create_without_callbacks(bulk)
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ # unlike save, create returns the newly created document
209
+ def create_without_callbacks(bulk =false)
210
+ raise ArgumentError, "a document requires a database to be created to (The document or the #{self.class} default database were not set)" unless database
211
+ set_unique_id if new? && self.respond_to?(:set_unique_id)
212
+ result = database.save_doc(self, bulk)
213
+ (result["ok"] == true) ? self : false
214
+ end
215
+
216
+ # Creates the document in the db. Raises an exception
217
+ # if the document is not created properly.
218
+ def create!
219
+ raise "#{self.inspect} failed to save" unless self.create
220
+ end
221
+
222
+ # Trigger the callbacks (before, after, around)
223
+ # only if the document isn't new
224
+ def update(bulk = false)
225
+ caught = catch(:halt) do
226
+ if self.new?
227
+ save(bulk)
228
+ else
229
+ _run_update_callbacks do
230
+ _run_save_callbacks do
231
+ save_without_callbacks(bulk)
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ # Trigger the callbacks (before, after, around)
239
+ # and save the document
240
+ def save(bulk = false)
241
+ caught = catch(:halt) do
242
+ if self.new?
243
+ _run_save_callbacks do
244
+ save_without_callbacks(bulk)
245
+ end
246
+ else
247
+ update(bulk)
248
+ end
249
+ end
250
+ end
251
+
252
+ # Overridden to set the unique ID.
253
+ # Returns a boolean value
254
+ def save_without_callbacks(bulk = false)
255
+ raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database
256
+ set_unique_id if new? && self.respond_to?(:set_unique_id)
257
+ result = database.save_doc(self, bulk)
258
+ mark_as_saved
259
+ result["ok"] == true
260
+ end
261
+
262
+ # Saves the document to the db using save. Raises an exception
263
+ # if the document is not saved properly.
264
+ def save!
265
+ raise "#{self.inspect} failed to save" unless self.save
266
+ true
267
+ end
268
+
269
+ # Deletes the document from the database. Runs the :destroy callbacks.
270
+ # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
271
+ # document to be saved to a new <tt>_id</tt>.
272
+ def destroy(bulk=false)
273
+ caught = catch(:halt) do
274
+ _run_destroy_callbacks do
275
+ result = database.delete_doc(self, bulk)
276
+ if result['ok']
277
+ self.delete('_rev')
278
+ self.delete('_id')
279
+ end
280
+ result['ok']
281
+ end
282
+ end
283
+ end
284
+
285
+ protected
286
+
287
+ # Set document_saved flag on all casted models to true
288
+ def mark_as_saved
289
+ self.each do |key, prop|
290
+ if prop.is_a?(Array)
291
+ prop.each do |item|
292
+ if item.respond_to?(:document_saved)
293
+ item.send(:document_saved=, true)
294
+ end
295
+ end
296
+ elsif prop.respond_to?(:document_saved)
297
+ prop.send(:document_saved=, true)
298
+ end
299
+ end
300
+ end
301
+
302
+ private
303
+
304
+ def check_properties_exist(attrs)
305
+ attrs.each do |attribute_name, attribute_value|
306
+ raise NoMethodError, "#{attribute_name}= method not available, use property :#{attribute_name}" unless self.respond_to?("#{attribute_name}=")
307
+ end
308
+ end
309
+
310
+ def directly_set_attributes(hash)
311
+ hash.each do |attribute_name, attribute_value|
312
+ if self.respond_to?("#{attribute_name}=")
313
+ self.send("#{attribute_name}=", hash.delete(attribute_name))
314
+ end
315
+ end
316
+ end
317
+
318
+ def set_attributes(hash)
319
+ attrs = remove_protected_attributes(hash)
320
+ directly_set_attributes(attrs)
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,74 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module AttributeProtection
4
+ # Attribute protection from mass assignment to CouchRest properties
5
+ #
6
+ # Protected methods will be removed from
7
+ # * new
8
+ # * update_attributes
9
+ # * upate_attributes_without_saving
10
+ # * attributes=
11
+ #
12
+ # There are two modes of protection
13
+ # 1) Declare accessible poperties, assume all the rest are protected
14
+ # property :name, :accessible => true
15
+ # property :admin # this will be automatically protected
16
+ #
17
+ # 2) Declare protected properties, assume all the rest are accessible
18
+ # property :name # this will not be protected
19
+ # property :admin, :protected => true
20
+ #
21
+ # Note: you cannot set both flags in a single class
22
+
23
+ def self.included(base)
24
+ base.extend(ClassMethods)
25
+ end
26
+
27
+ module ClassMethods
28
+ def accessible_properties
29
+ properties.select { |prop| prop.options[:accessible] }
30
+ end
31
+
32
+ def protected_properties
33
+ properties.select { |prop| prop.options[:protected] }
34
+ end
35
+ end
36
+
37
+ def accessible_properties
38
+ self.class.accessible_properties
39
+ end
40
+
41
+ def protected_properties
42
+ self.class.protected_properties
43
+ end
44
+
45
+ def remove_protected_attributes(attributes)
46
+ protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name }
47
+ return attributes if protected_names.empty?
48
+
49
+ attributes.reject! do |property_name, property_value|
50
+ protected_names.include?(property_name.to_s)
51
+ end
52
+
53
+ attributes || {}
54
+ end
55
+
56
+ private
57
+
58
+ def properties_to_remove_from_mass_assignment
59
+ has_protected = !protected_properties.empty?
60
+ has_accessible = !accessible_properties.empty?
61
+
62
+ if !has_protected && !has_accessible
63
+ []
64
+ elsif has_protected && !has_accessible
65
+ protected_properties
66
+ elsif has_accessible && !has_protected
67
+ properties.reject { |prop| prop.options[:accessible] }
68
+ else
69
+ raise "Set either :accessible or :protected for #{self.class}, but not both"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end