couchrest 0.12.4 → 0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/README.md +33 -8
  2. data/Rakefile +11 -2
  3. data/examples/model/example.rb +19 -13
  4. data/lib/couchrest.rb +70 -11
  5. data/lib/couchrest/core/database.rb +121 -62
  6. data/lib/couchrest/core/design.rb +7 -17
  7. data/lib/couchrest/core/document.rb +42 -30
  8. data/lib/couchrest/core/response.rb +16 -0
  9. data/lib/couchrest/core/server.rb +47 -10
  10. data/lib/couchrest/helper/upgrade.rb +51 -0
  11. data/lib/couchrest/mixins.rb +4 -0
  12. data/lib/couchrest/mixins/attachments.rb +31 -0
  13. data/lib/couchrest/mixins/callbacks.rb +483 -0
  14. data/lib/couchrest/mixins/class_proxy.rb +108 -0
  15. data/lib/couchrest/mixins/design_doc.rb +90 -0
  16. data/lib/couchrest/mixins/document_queries.rb +44 -0
  17. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  18. data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
  19. data/lib/couchrest/mixins/properties.rb +129 -0
  20. data/lib/couchrest/mixins/validation.rb +242 -0
  21. data/lib/couchrest/mixins/views.rb +169 -0
  22. data/lib/couchrest/monkeypatches.rb +81 -6
  23. data/lib/couchrest/more/casted_model.rb +28 -0
  24. data/lib/couchrest/more/extended_document.rb +215 -0
  25. data/lib/couchrest/more/property.rb +40 -0
  26. data/lib/couchrest/support/blank.rb +42 -0
  27. data/lib/couchrest/support/class.rb +176 -0
  28. data/lib/couchrest/validation/auto_validate.rb +163 -0
  29. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  30. data/lib/couchrest/validation/validation_errors.rb +118 -0
  31. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  32. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  33. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  34. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  35. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  36. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  37. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  38. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  39. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  40. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  41. data/spec/couchrest/core/database_spec.rb +189 -124
  42. data/spec/couchrest/core/design_spec.rb +13 -6
  43. data/spec/couchrest/core/document_spec.rb +231 -177
  44. data/spec/couchrest/core/server_spec.rb +35 -0
  45. data/spec/couchrest/helpers/pager_spec.rb +1 -1
  46. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  47. data/spec/couchrest/more/casted_model_spec.rb +98 -0
  48. data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
  49. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  50. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  51. data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
  52. data/spec/couchrest/more/property_spec.rb +136 -0
  53. data/spec/fixtures/more/article.rb +34 -0
  54. data/spec/fixtures/more/card.rb +20 -0
  55. data/spec/fixtures/more/course.rb +14 -0
  56. data/spec/fixtures/more/event.rb +6 -0
  57. data/spec/fixtures/more/invoice.rb +17 -0
  58. data/spec/fixtures/more/person.rb +8 -0
  59. data/spec/fixtures/more/question.rb +6 -0
  60. data/spec/fixtures/more/service.rb +12 -0
  61. data/spec/spec_helper.rb +13 -7
  62. metadata +58 -4
  63. data/lib/couchrest/core/model.rb +0 -613
  64. data/spec/couchrest/core/model_spec.rb +0 -855
@@ -0,0 +1,28 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'mixins', 'properties')
2
+
3
+ module CouchRest
4
+ module CastedModel
5
+
6
+ def self.included(base)
7
+ base.send(:include, CouchRest::Mixins::Properties)
8
+ base.send(:attr_accessor, :casted_by)
9
+ end
10
+
11
+ def initialize(keys={})
12
+ super
13
+ keys.each do |k,v|
14
+ self[k.to_s] = v
15
+ end if keys
16
+ apply_defaults # defined in CouchRest::Mixins::Properties
17
+ # cast_keys # defined in CouchRest::Mixins::Properties
18
+ end
19
+
20
+ def []= key, value
21
+ super(key.to_s, value)
22
+ end
23
+
24
+ def [] key
25
+ super(key.to_s)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,215 @@
1
+ require 'mime/types'
2
+ require File.join(File.dirname(__FILE__), "property")
3
+ require File.join(File.dirname(__FILE__), '..', 'mixins', 'extended_document_mixins')
4
+
5
+ module CouchRest
6
+
7
+ # Same as CouchRest::Document but with properties and validations
8
+ class ExtendedDocument < Document
9
+ include CouchRest::Callbacks
10
+ include CouchRest::Mixins::DocumentQueries
11
+ include CouchRest::Mixins::Views
12
+ include CouchRest::Mixins::DesignDoc
13
+ include CouchRest::Mixins::ExtendedAttachments
14
+ include CouchRest::Mixins::ClassProxy
15
+
16
+ def self.inherited(subklass)
17
+ subklass.send(:include, CouchRest::Mixins::Properties)
18
+ subklass.class_eval <<-EOS, __FILE__, __LINE__
19
+ def self.inherited(subklass)
20
+ subklass.properties = self.properties.dup
21
+ end
22
+ EOS
23
+ end
24
+
25
+ # Accessors
26
+ attr_accessor :casted_by
27
+
28
+ # Callbacks
29
+ define_callbacks :create
30
+ define_callbacks :save
31
+ define_callbacks :update
32
+ define_callbacks :destroy
33
+
34
+ def initialize(passed_keys={})
35
+ apply_defaults # defined in CouchRest::Mixins::Properties
36
+ super
37
+ cast_keys # defined in CouchRest::Mixins::Properties
38
+ unless self['_id'] && self['_rev']
39
+ self['couchrest-type'] = self.class.to_s
40
+ end
41
+ end
42
+
43
+
44
+ # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
45
+ # on the document whenever saving occurs. CouchRest uses a pretty
46
+ # decent time format by default. See Time#to_json
47
+ def self.timestamps!
48
+ class_eval <<-EOS, __FILE__, __LINE__
49
+ property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
50
+ property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
51
+
52
+ save_callback :before do |object|
53
+ object['updated_at'] = Time.now
54
+ object['created_at'] = object['updated_at'] if object.new_document?
55
+ end
56
+ EOS
57
+ end
58
+
59
+ # Name a method that will be called before the document is first saved,
60
+ # which returns a string to be used for the document's <tt>_id</tt>.
61
+ # Because CouchDB enforces a constraint that each id must be unique,
62
+ # this can be used to enforce eg: uniq usernames. Note that this id
63
+ # must be globally unique across all document types which share a
64
+ # database, so if you'd like to scope uniqueness to this class, you
65
+ # should use the class name as part of the unique id.
66
+ def self.unique_id method = nil, &block
67
+ if method
68
+ define_method :set_unique_id do
69
+ self['_id'] ||= self.send(method)
70
+ end
71
+ elsif block
72
+ define_method :set_unique_id do
73
+ uniqid = block.call(self)
74
+ raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
75
+ self['_id'] ||= uniqid
76
+ end
77
+ end
78
+ end
79
+
80
+ # Temp solution to make the view_by methods available
81
+ def self.method_missing(m, *args, &block)
82
+ if has_view?(m)
83
+ query = args.shift || {}
84
+ view(m, query, *args, &block)
85
+ else
86
+ super
87
+ end
88
+ end
89
+
90
+ ### instance methods
91
+
92
+ # Returns the Class properties
93
+ #
94
+ # ==== Returns
95
+ # Array:: the list of properties for the instance
96
+ def properties
97
+ self.class.properties
98
+ end
99
+
100
+ # Takes a hash as argument, and applies the values by using writer methods
101
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
102
+ # missing. In case of error, no attributes are changed.
103
+ def update_attributes_without_saving(hash)
104
+ hash.each do |k, v|
105
+ raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
106
+ end
107
+ hash.each do |k, v|
108
+ self.send("#{k}=",v)
109
+ end
110
+ end
111
+
112
+ # Takes a hash as argument, and applies the values by using writer methods
113
+ # for each key. Raises a NoMethodError if the corresponding methods are
114
+ # missing. In case of error, no attributes are changed.
115
+ def update_attributes(hash)
116
+ update_attributes_without_saving hash
117
+ save
118
+ end
119
+
120
+ # for compatibility with old-school frameworks
121
+ alias :new_record? :new_document?
122
+
123
+ # Trigger the callbacks (before, after, around)
124
+ # and create the document
125
+ # It's important to have a create callback since you can't check if a document
126
+ # was new after you saved it
127
+ #
128
+ # When creating a document, both the create and the save callbacks will be triggered.
129
+ def create(bulk = false)
130
+ caught = catch(:halt) do
131
+ _run_create_callbacks do
132
+ _run_save_callbacks do
133
+ create_without_callbacks(bulk)
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ # unlike save, create returns the newly created document
140
+ def create_without_callbacks(bulk =false)
141
+ raise ArgumentError, "a document requires a database to be created to (The document or the #{self.class} default database were not set)" unless database
142
+ set_unique_id if new_document? && self.respond_to?(:set_unique_id)
143
+ result = database.save_doc(self, bulk)
144
+ (result["ok"] == true) ? self : false
145
+ end
146
+
147
+ # Creates the document in the db. Raises an exception
148
+ # if the document is not created properly.
149
+ def create!
150
+ raise "#{self.inspect} failed to save" unless self.create
151
+ end
152
+
153
+ # Trigger the callbacks (before, after, around)
154
+ # only if the document isn't new
155
+ def update(bulk = false)
156
+ caught = catch(:halt) do
157
+ if self.new_document?
158
+ save(bulk)
159
+ else
160
+ _run_update_callbacks do
161
+ _run_save_callbacks do
162
+ save_without_callbacks(bulk)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ # Trigger the callbacks (before, after, around)
170
+ # and save the document
171
+ def save(bulk = false)
172
+ caught = catch(:halt) do
173
+ if self.new_document?
174
+ _run_save_callbacks do
175
+ save_without_callbacks(bulk)
176
+ end
177
+ else
178
+ update(bulk)
179
+ end
180
+ end
181
+ end
182
+
183
+ # Overridden to set the unique ID.
184
+ # Returns a boolean value
185
+ def save_without_callbacks(bulk = false)
186
+ raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database
187
+ set_unique_id if new_document? && self.respond_to?(:set_unique_id)
188
+ result = database.save_doc(self, bulk)
189
+ result["ok"] == true
190
+ end
191
+
192
+ # Saves the document to the db using save. Raises an exception
193
+ # if the document is not saved properly.
194
+ def save!
195
+ raise "#{self.inspect} failed to save" unless self.save
196
+ end
197
+
198
+ # Deletes the document from the database. Runs the :destroy callbacks.
199
+ # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
200
+ # document to be saved to a new <tt>_id</tt>.
201
+ def destroy(bulk=false)
202
+ caught = catch(:halt) do
203
+ _run_destroy_callbacks do
204
+ result = database.delete_doc(self, bulk)
205
+ if result['ok']
206
+ self.delete('_rev')
207
+ self.delete('_id')
208
+ end
209
+ result['ok']
210
+ end
211
+ end
212
+ end
213
+
214
+ end
215
+ end
@@ -0,0 +1,40 @@
1
+ module CouchRest
2
+
3
+ # Basic attribute support for adding getter/setter + validation
4
+ class Property
5
+ attr_reader :name, :type, :read_only, :alias, :default, :casted, :init_method, :options
6
+
7
+ # attribute to define
8
+ def initialize(name, type = nil, options = {})
9
+ @name = name.to_s
10
+ parse_type(type)
11
+ parse_options(options)
12
+ self
13
+ end
14
+
15
+
16
+ private
17
+
18
+ def parse_type(type)
19
+ if type.nil?
20
+ @type = 'String'
21
+ elsif type.is_a?(Array) && type.empty?
22
+ @type = 'Array'
23
+ else
24
+ @type = type.is_a?(Array) ? [type.first.to_s] : type.to_s
25
+ end
26
+ end
27
+
28
+ def parse_options(options)
29
+ return if options.empty?
30
+ @validation_format = options.delete(:format) if options[:format]
31
+ @read_only = options.delete(:read_only) if options[:read_only]
32
+ @alias = options.delete(:alias) if options[:alias]
33
+ @default = options.delete(:default) if options[:default]
34
+ @casted = options[:casted] ? true : false
35
+ @init_method = options[:send] ? options.delete(:send) : 'new'
36
+ @options = options
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # blank? methods for several different class types
2
+ class Object
3
+ # Returns true if the object is nil or empty (if applicable)
4
+ def blank?
5
+ nil? || (respond_to?(:empty?) && empty?)
6
+ end
7
+ end # class Object
8
+
9
+ class Numeric
10
+ # Numerics can't be blank
11
+ def blank?
12
+ false
13
+ end
14
+ end # class Numeric
15
+
16
+ class NilClass
17
+ # Nils are always blank
18
+ def blank?
19
+ true
20
+ end
21
+ end # class NilClass
22
+
23
+ class TrueClass
24
+ # True is not blank.
25
+ def blank?
26
+ false
27
+ end
28
+ end # class TrueClass
29
+
30
+ class FalseClass
31
+ # False is always blank.
32
+ def blank?
33
+ true
34
+ end
35
+ end # class FalseClass
36
+
37
+ class String
38
+ # Strips out whitespace then tests if the string is empty.
39
+ def blank?
40
+ strip.empty?
41
+ end
42
+ end # class String
@@ -0,0 +1,176 @@
1
+ # Copyright (c) 2004-2008 David Heinemeier Hansson
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ # Allows attributes to be shared within an inheritance hierarchy, but where
23
+ # each descendant gets a copy of their parents' attributes, instead of just a
24
+ # pointer to the same. This means that the child can add elements to, for
25
+ # example, an array without those additions being shared with either their
26
+ # parent, siblings, or children, which is unlike the regular class-level
27
+ # attributes that are shared across the entire hierarchy.
28
+ class Class
29
+ # Defines class-level and instance-level attribute reader.
30
+ #
31
+ # @param *syms<Array> Array of attributes to define reader for.
32
+ # @return <Array[#to_s]> List of attributes that were made into cattr_readers
33
+ #
34
+ # @api public
35
+ #
36
+ # @todo Is this inconsistent in that it does not allow you to prevent
37
+ # an instance_reader via :instance_reader => false
38
+ def cattr_reader(*syms)
39
+ syms.flatten.each do |sym|
40
+ next if sym.is_a?(Hash)
41
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
42
+ unless defined? @@#{sym}
43
+ @@#{sym} = nil
44
+ end
45
+
46
+ def self.#{sym}
47
+ @@#{sym}
48
+ end
49
+
50
+ def #{sym}
51
+ @@#{sym}
52
+ end
53
+ RUBY
54
+ end
55
+ end unless Class.respond_to?(:cattr_reader)
56
+
57
+ # Defines class-level (and optionally instance-level) attribute writer.
58
+ #
59
+ # @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
60
+ # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
61
+ # @return <Array[#to_s]> List of attributes that were made into cattr_writers
62
+ #
63
+ # @api public
64
+ def cattr_writer(*syms)
65
+ options = syms.last.is_a?(Hash) ? syms.pop : {}
66
+ syms.flatten.each do |sym|
67
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
68
+ unless defined? @@#{sym}
69
+ @@#{sym} = nil
70
+ end
71
+
72
+ def self.#{sym}=(obj)
73
+ @@#{sym} = obj
74
+ end
75
+ RUBY
76
+
77
+ unless options[:instance_writer] == false
78
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
79
+ def #{sym}=(obj)
80
+ @@#{sym} = obj
81
+ end
82
+ RUBY
83
+ end
84
+ end
85
+ end unless Class.respond_to?(:cattr_writer)
86
+
87
+ # Defines class-level (and optionally instance-level) attribute accessor.
88
+ #
89
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
90
+ # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
91
+ # @return <Array[#to_s]> List of attributes that were made into accessors
92
+ #
93
+ # @api public
94
+ def cattr_accessor(*syms)
95
+ cattr_reader(*syms)
96
+ cattr_writer(*syms)
97
+ end unless Class.respond_to?(:cattr_accessor)
98
+
99
+ # Defines class-level inheritable attribute reader. Attributes are available to subclasses,
100
+ # each subclass has a copy of parent's attribute.
101
+ #
102
+ # @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
103
+ # @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
104
+ #
105
+ # @api public
106
+ #
107
+ # @todo Do we want to block instance_reader via :instance_reader => false
108
+ # @todo It would be preferable that we do something with a Hash passed in
109
+ # (error out or do the same as other methods above) instead of silently
110
+ # moving on). In particular, this makes the return value of this function
111
+ # less useful.
112
+ def extlib_inheritable_reader(*ivars)
113
+ instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
114
+
115
+ ivars.each do |ivar|
116
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
117
+ def self.#{ivar}
118
+ return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
119
+ ivar = superclass.#{ivar}
120
+ return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
121
+ @#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) && !ivar.is_a?(TrueClass) && !ivar.is_a?(FalseClass) ? ivar.dup : ivar
122
+ end
123
+ RUBY
124
+ unless instance_reader == false
125
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
126
+ def #{ivar}
127
+ self.class.#{ivar}
128
+ end
129
+ RUBY
130
+ end
131
+ end
132
+ end unless Class.respond_to?(:extlib_inheritable_reader)
133
+
134
+ # Defines class-level inheritable attribute writer. Attributes are available to subclasses,
135
+ # each subclass has a copy of parent's attribute.
136
+ #
137
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
138
+ # define inheritable writer for.
139
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
140
+ # @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
141
+ #
142
+ # @api public
143
+ #
144
+ # @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
145
+ # class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
146
+ def extlib_inheritable_writer(*ivars)
147
+ instance_writer = ivars.pop[:writer] if ivars.last.is_a?(Hash)
148
+ ivars.each do |ivar|
149
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
150
+ def self.#{ivar}=(obj)
151
+ @#{ivar} = obj
152
+ end
153
+ RUBY
154
+ unless instance_writer == false
155
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
156
+ def #{ivar}=(obj) self.class.#{ivar} = obj end
157
+ RUBY
158
+ end
159
+ end
160
+ end unless Class.respond_to?(:extlib_inheritable_writer)
161
+
162
+ # Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
163
+ # each subclass has a copy of parent's attribute.
164
+ #
165
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
166
+ # define inheritable accessor for.
167
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
168
+ # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
169
+ #
170
+ # @api public
171
+ def extlib_inheritable_accessor(*syms)
172
+ extlib_inheritable_reader(*syms)
173
+ extlib_inheritable_writer(*syms)
174
+ end unless Class.respond_to?(:extlib_inheritable_accessor)
175
+ end
176
+