couchrest_extended_document 1.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
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